盒子
盒子
文章目录
  1. 0x01 TL;DR
  2. 0x02 Version
  3. 0x03 Trigger function
  4. 0x04 Exploit
  5. 0x05 Why EHCI?
  6. 0x06 Fuzz
  7. 0x07 Others
  8. 0x08 Reference

QEMU-CVE-2020-14364漏洞分析

0x01 TL;DR

这是一篇迟来的文章,该漏洞在19年天府杯上就有耳闻,四月份的时候得知了部分细节,只是匆匆看了一眼,这个漏洞的品相极好。网上也有比较多的该CVE的分析文章了,读者可以参考一下。在这篇文章中,我将从漏洞代码的角度出发,抽丝剥茧,逐步分析漏洞的原理,从poc到exp的整个过程。

0x02 Version

QEMU版本:5.1.0

0x03 Trigger function

话不多说,直接看漏洞代码出现在哪里,具体代码文件在hw\usb\core.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void do_token_setup(USBDevice *s, USBPacket *p)
{
int request, value, index;

if (p->iov.size != 8) {
p->status = USB_RET_STALL;
return;
}

usb_packet_copy(p, s->setup_buf, p->iov.size);
s->setup_index = 0;
p->actual_length = 0;
s->setup_len = (s->setup_buf[7] << 8) | s->setup_buf[6]; //直接赋值
if (s->setup_len > sizeof(s->data_buf)) {
fprintf(stderr,
"usb_generic_handle_packet: ctrl buffer too small (%d > %zu)\n",
s->setup_len, sizeof(s->data_buf));
p->status = USB_RET_STALL;
return;
}
............... //省略
}

我看第一眼的时候其实并看不出来这一块有漏洞点,甚至觉得有股说不出来的怪。

看上面注释的那一块,实际上可以称之为一个逻辑漏洞,首先对setup_len赋值,再去比较setup_lensizeof(data_buf),如果setup_len过大则报错并退出。但是这里的逻辑其实仔细想是错误的,因为正常情况下来说,要给一个结构体的参数赋值,先要判断传入的值是否在限制条件内,其次再真正赋值。这里的代码逻辑则恰恰相反,啥也不管,先给了再说,给完咱再来提需求。因此,如果传入的setup_len的值是过大(恶意)的话,很有可能会影响后续的代码。事实证明,就是如此。

所以我们第一件要做的事情就是解决如何才能到达这一段的代码的问题。那么,如何到达?

我们先来看do_token_setup函数的交叉引用。

交叉引用

确实太多了,这么多分支后续往哪看呢?我的想法是先搜集一下USB协议的相关资料。在简单阅读了一些USB协议资料后,我能够了解到USB协议的一些基础,以及最关键的USB Controller。USB协议主要实现在Controller中,Controller起着一个USB数据交互传输的作用,简单来说就是操作系统以及USB设备之间的数据交互传输,都要经过集线器这么一个东西,它负责数据的处理,队列以及输送等等。

USB协议中还分有控制传输、批量传输、同步传输、周期性传输等等,具体的内容就交给读者自行去翻阅资料查看了。我这里再简单说一下USB协议的迭代版本:

  1. USB 1.0 Open Host Controller Interface OHCI标准,low speeds
  2. USB 1.1 Universal Host Controller Interface UHCI标准,full speeds
  3. USB 2.0 Enhanced Host Controller Interface EHCI标准,high speeds
  4. USB 3.0Extensible Host Controller Interface xHCI标准,super speeds

可以看出来Controller是起着至关重要的作用的。再结合上图的交叉引用,可以将范围缩小在hcd-ohci.chcd-uhci.chcd-ehci.chcd-xhci.c这四个文件中。

我们先从hcd-ehci.c下手,先看第一条链,看看控制流程的整个过程:

ehci交叉引用

do_token_setup -> usb_process_one -> usb_handle_packet -> ehci_execute -> ehci_fill_queueehci_state_execute

这里有分叉,应该看哪一条?头铁的可以按顺序来一条一条的看。这里的话可以从两个函数的函数名称看起,并且还可以看到引用ehci_fill_queue的函数中就包含ehci_state_execute,结合execute的函数名,可以从ehci_state_execute函数分支看起。后续的分支还有两条,ehci_advance_asyn_stateehci_advance_periodic_state。从名称可以大致感觉到这两个分支会是两个“模式”。

具体EHCI Controller的设计规范建议去查看官方的手册,其中就包含这asyn同步传输和periodic周期性传输两种。手册中也包含了各个字节所定义的结构体、变量等等,以及数据的传输过程、数据走向等详细内容,结合QEMU中hcd-ehci.c代码来看会理解的更快、更深一些。

后续文章内容都在假定读者阅读过EHCI设计规范手册的前提下叙述。我这里就只简单介绍一下EHCI中最重要的两个结构体QH和qTD以及两种传输方式。

QH结构如下所示:

QH结构体

比较重要的就是Current qTD指针和Next qTD指针,分别指向当前所处理的qTD和下一个待处理的qTD结构体。Queue Head Horizontal Link Pointer则指向下一个待处理的QH结构体,当bit 0为1的时候代表该指针不可用,为0时代表可用。

qTD结构如下所示:

qTD结构体

Next qTD Pointer指向下一个qTD,bit 0的作用和QH相同。Buffer Pointer指向需要传输的USB数据内容。

Periodic Schedule:

periodic

周期性传输的方式为取USB寄存器中的两个寄存器,一个做Base Address,一个做index,相结合取出Frame List中的指针,该指针又指向了QH结构体,取出QH后再从QH中取出qTD结构体,最终取出传输数据。不过该传输方式是按照周期性来决定取出的Frame List中的指针地址的。也就是说按照时间来移动,在Guest用户角度来说具有不可控制性。

Asyn Schedule:

asyn schedule

同步传输根据USB寄存器中的ASYNCLISTADDR的地址来取出相应地址的QH结构体,从而取出qTD中的传输数据。由于地址可以由用户决定,因此该传输方式在Guest用户角度来说可控。

可以看出,qTD就是挂在QH下的一串串队列。因为qTD中存储着需要传输的数据内容,传输就利用了深度遍历的算法去遍历每一个qTD,取出所有需要传输的数据进行转发传送。两种传输方式还是有所不同的,具体不同部分就由读者自行阅读了解。

继续回到我们的主题,观察可以发现ehci_advance_asyn_stateehci_advance_periodic_state两者都被ehci_work_bh所调用。因此,我们可以从ehci_opreg_write函数来触发这个ehci_work_bh函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static void ehci_opreg_write(void *ptr, hwaddr addr,
uint64_t val, unsigned size)
{
EHCIState *s = ptr;
uint32_t *mmio = s->opreg + (addr >> 2);
uint32_t old = *mmio;
int i;

trace_usb_ehci_opreg_write(addr + s->opregbase, addr2str(addr), val);

switch (addr) {
case USBCMD:
if (val & USBCMD_HCRESET) {
ehci_reset(s);
val = s->usbcmd;
break;
}

/* not supporting dynamic frame list size at the moment */
if ((val & USBCMD_FLS) && !(s->usbcmd & USBCMD_FLS)) {
fprintf(stderr, "attempt to set frame list size -- value %d\n",
(int)val & USBCMD_FLS);
val &= ~USBCMD_FLS;
}

if (val & USBCMD_IAAD) {
/*
* Process IAAD immediately, otherwise the Linux IAAD watchdog may
* trigger and re-use a qh without us seeing the unlink.
*/
s->async_stepdown = 0;
qemu_bh_schedule(s->async_bh);
trace_usb_ehci_doorbell_ring();
}

if (((USBCMD_RUNSTOP | USBCMD_PSE | USBCMD_ASE) & val) !=
((USBCMD_RUNSTOP | USBCMD_PSE | USBCMD_ASE) & s->usbcmd)) {
if (s->pstate == EST_INACTIVE) {
SET_LAST_RUN_CLOCK(s);
}
s->usbcmd = val; /* Set usbcmd for ehci_update_halt() */
ehci_update_halt(s);
s->async_stepdown = 0;
qemu_bh_schedule(s->async_bh); //触发async_bh
}
break;

.................. //省略

再往下分析该ehci_work_bh函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static void ehci_work_bh(void *opaque)
{
EHCIState *ehci = opaque;
int need_timer = 0;
int64_t expire_time, t_now;
uint64_t ns_elapsed;
uint64_t uframes, skipped_uframes;
int i;

if (ehci->working) {
return;
}
ehci->working = true;

t_now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);//获取当前时间
ns_elapsed = t_now - ehci->last_run_ns;//当前时间减去上一次运行时的时间
uframes = ns_elapsed / UFRAME_TIMER_NS;//根据时间差得出具体的frame index

if (ehci_periodic_enabled(ehci) || ehci->pstate != EST_INACTIVE) {
need_timer++;

if (uframes > (ehci->maxframes * 8)) {//如果超出最大的index
skipped_uframes = uframes - (ehci->maxframes * 8);
ehci_update_frindex(ehci, skipped_uframes);//重新计算后更新index
ehci->last_run_ns += UFRAME_TIMER_NS * skipped_uframes;//更新last run的时间
uframes -= skipped_uframes;
DPRINTF("WARNING - EHCI skipped %d uframes\n", skipped_uframes);
}

for (i = 0; i < uframes; i++) {
/*
* If we're running behind schedule, we should not catch up
* too fast, as that will make some guests unhappy:
* 1) We must process a minimum of MIN_UFR_PER_TICK frames,
* otherwise we will never catch up
* 2) Process frames until the guest has requested an irq (IOC)
*/
if (i >= MIN_UFR_PER_TICK) {
ehci_commit_irq(ehci);
if ((ehci->usbsts & USBINTR_MASK) & ehci->usbintr) {
break;
}
}
if (ehci->periodic_sched_active) {
ehci->periodic_sched_active--;
}
ehci_update_frindex(ehci, 1);//从index为1开始
if ((ehci->frindex & 7) == 0) {//每8位index执行一次周期性传输
ehci_advance_periodic_state(ehci);
}
ehci->last_run_ns += UFRAME_TIMER_NS;
}
} else {
ehci->periodic_sched_active = 0;
ehci_update_frindex(ehci, uframes);
ehci->last_run_ns += UFRAME_TIMER_NS * uframes;
}
.............

从上面我写的注释中可以看出想进入ehci_advance_periodic_state函数的话需要根据时间来决定是否会进入分支,因此想次次都按我们的想法到达ehci_advance_periodic_state函数是有难度的。继续往下看看ehci_advance_asyn_state函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
............

if (ehci->periodic_sched_active) {
ehci->async_stepdown = 0;
} else if (ehci->async_stepdown < ehci->maxframes / 2) {
ehci->async_stepdown++;
}

/* Async is not inside loop since it executes everything it can once
* called
*/
if (ehci_async_enabled(ehci) || ehci->astate != EST_INACTIVE) {
need_timer++;
ehci_advance_async_state(ehci);
}

ehci_commit_irq(ehci);
if (ehci->usbsts_pending) {
need_timer++;
ehci->async_stepdown = 0;
}

.............

只需要满足ehci_async_enabled函数返回true或者ehci->astate != EST_INACTIVE两者之一即可进入ehci_advance_asyn_state函数。我们完全可以控制它进入ehci_advance_asyn_state。因此,后续我们就暂且抛弃周期性传输使用同步传输来进行后续步骤。

往下看ehci_advance_asyn_state函数具体做了哪些工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static void ehci_advance_async_state(EHCIState *ehci)
{
const int async = 1;

switch(ehci_get_state(ehci, async)) {
case EST_INACTIVE:
if (!ehci_async_enabled(ehci)) {
break;
}
ehci_set_state(ehci, async, EST_ACTIVE);//如果是INACTIVE则设置为ACTIVE,继续往下执行
// No break, fall through to ACTIVE

case EST_ACTIVE:
if (!ehci_async_enabled(ehci)) {
ehci_queues_rip_all(ehci, async);
ehci_set_state(ehci, async, EST_INACTIVE);
break;
}

/* make sure guest has acknowledged the doorbell interrupt */
/* TO-DO: is this really needed? */
if (ehci->usbsts & USBSTS_IAA) {
DPRINTF("IAA status bit still set.\n");
break;
}

/* check that address register has been set */
if (ehci->asynclistaddr == 0) {
break;
}

ehci_set_state(ehci, async, EST_WAITLISTHEAD);
ehci_advance_state(ehci, async);

/* If the doorbell is set, the guest wants to make a change to the
* schedule. The host controller needs to release cached data.
* (section 4.8.2)
*/
if (ehci->usbcmd & USBCMD_IAAD) {
/* Remove all unseen qhs from the async qhs queue */
ehci_queues_rip_unseen(ehci, async);
trace_usb_ehci_doorbell_ack();
ehci->usbcmd &= ~USBCMD_IAAD;
ehci_raise_irq(ehci, USBSTS_IAA);
}
break;

default:
/* this should only be due to a developer mistake */
fprintf(stderr, "ehci: Bad asynchronous state %d. "
"Resetting to active\n", ehci->astate);
g_assert_not_reached();
}
}

主要是部分变量的验证,asynclistaddr不为0,stateACTIVE等等。重点只需要关注ehci_advance_state即可。

粗略看ehci_advance_state的结构可以看出是一个复杂的循环处理函数,这块函数就是我上面所说的深度优先遍历qTD的流程。具体我就不细说了,读者自行去阅读源代码即可,我这里简单画了一个流程图来表明这一块函数大概做了哪些工作。

advance_state流程图

整个流程大概就是遍历QH和qTD,取出需要传输的数据进行传输。最关键的传输函数在ehci_state_execute中的ehci_execute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
static int ehci_execute(EHCIPacket *p, const char *action)
{
USBEndpoint *ep;
int endp;
bool spd;

//..........

p->pid = ehci_get_pid(&p->qtd);//取pid(TOKEN_IN、TOKEN_OUT、TOKEN_SETUP)
p->queue->last_pid = p->pid;
endp = get_field(p->queue->qh.epchar, QH_EPCHAR_EP);
ep = usb_ep_get(p->queue->dev, p->pid, endp);

if (p->async == EHCI_ASYNC_NONE) {
if (ehci_init_transfer(p) != 0) {//初始化传输数据的内存空间
return -1;
}

spd = (p->pid == USB_TOKEN_IN && NLPTR_TBIT(p->qtd.altnext) == 0);
usb_packet_setup(&p->packet, p->pid, ep, 0, p->qtdaddr, spd,
(p->qtd.token & QTD_TOKEN_IOC) != 0);
usb_packet_map(&p->packet, &p->sgl);
p->async = EHCI_ASYNC_INITIALIZED;//复制传输数据到sgl空间
}

trace_usb_ehci_packet_action(p->queue, p, action);
usb_handle_packet(p->queue->dev, &p->packet);//处理数据,important

//.........
}


void usb_handle_packet(USBDevice *dev, USBPacket *p)
{
//..........

if (QTAILQ_EMPTY(&p->ep->queue) || p->ep->pipeline || p->stream) {
usb_process_one(p);//触发

//...........
}


static void usb_process_one(USBPacket *p)
{
USBDevice *dev = p->ep->dev;

//...........

switch (p->pid) {
case USB_TOKEN_SETUP:
do_token_setup(dev, p); //触发!!!
break;
case USB_TOKEN_IN:
do_token_in(dev, p);
break;
case USB_TOKEN_OUT:
do_token_out(dev, p);
break;
default:
p->status = USB_RET_STALL;
}
} else {
/* data pipe */
usb_device_handle_data(dev, p);
}
}

按照上面的控制流程走,我们完全可以将执行流程控制到漏洞函数do_token_setup当中去。

整个漏洞触发流程到这里就结束了。

0x04 Exploit

由漏洞函数:

1
2
USBDevice *s;
s->setup_len = (s->setup_buf[7] << 8) | s->setup_buf[6];

可以知道最长的传输长度可以达到0xFFFF。再看看USBDevice的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct USBDevice {
//.............

int32_t state;
uint8_t setup_buf[8]; //设置传输变量buffer
uint8_t data_buf[4096]; //数据传输buffer
int32_t remote_wakeup;
int32_t setup_state; //指定传输的是ACK协议数据,还是普通数据
int32_t setup_len; //传输长度
int32_t setup_index; //传输的offset

USBEndpoint ep_ctl;
USBEndpoint ep_in[USB_MAX_ENDPOINTS];
USBEndpoint ep_out[USB_MAX_ENDPOINTS];

//.............
}

0xFFFF的长度或许对我们来说并没有多大的意义,在data_buf[4096]后的0xFFFF-4096个数据很有可能是没有有意义的数据的,我们必须想办法扩大溢出的范围。

在我标注的注释中,聪明的你应该能立即想到办法来扩大范围。我们只需要修改setup_len以及setup_index两个变量即可。可读写的范围会扩大很多,这时候就完全满足我们后续的利用了。

构造任意读写原语的方式想必读者在前面usb_process_one的函数时就已经知道了,USB的读和写操作分别存在于do_token_indo_token_out函数当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static void do_token_in(USBDevice *s, USBPacket *p)
{
//..........

switch(s->setup_state) {
case SETUP_STATE_ACK:
//..........
break;

case SETUP_STATE_DATA:
if (s->setup_buf[0] & USB_DIR_IN) {
int len = s->setup_len - s->setup_index;
if (len > p->iov.size) {//len最大为0xffff,但是setup_index可以任意构造
len = p->iov.size;
}
usb_packet_copy(p, s->data_buf + s->setup_index, len);//读数据

//..........
}


static void do_token_out(USBDevice *s, USBPacket *p)
{
//..........

case SETUP_STATE_DATA:
if (!(s->setup_buf[0] & USB_DIR_IN)) {
int len = s->setup_len - s->setup_index;
if (len > p->iov.size) {
len = p->iov.size;
}
usb_packet_copy(p, s->data_buf + s->setup_index, len);//同上

//...........
}

这里构造任意原语可能会遇到一些小小的困难,因为触发漏洞函数得到0xFFFF范围地址任意读写和写setup_lensetup_index构造更大范围任意读写这两个流程是要在一次USB数据传输的过程中完成的,而不是分两次发送数据。所以需要准备好一连串的子弹,到达“一次性发射”的目的。我简要描述一下我所分配的“弹夹”。

任意写结构:

任意写

任意读结构:

任意读

任意读写原语构造完,后续的事情就很顺了,信息泄露+写地址+触发恶意函数就可以完成整一套的利用。

信息泄露:

先看一下可以溢出到的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct USBDevice {
DeviceState qdev;
USBPort *port;//important struct
//...........
uint8_t setup_buf[8];
uint8_t data_buf[4096];//往下都可以泄漏到
int32_t remote_wakeup;
int32_t setup_state;
int32_t setup_len;
int32_t setup_index;

USBEndpoint ep_ctl;//important
USBEndpoint ep_in[USB_MAX_ENDPOINTS];
USBEndpoint ep_out[USB_MAX_ENDPOINTS];

QLIST_HEAD(, USBDescString) strings;
const USBDesc *usb_desc; /* Overrides class usb_desc if not NULL */
const USBDescDevice *device;

int configuration;
int ninterfaces;
int altsetting[USB_MAX_INTERFACES];
const USBDescConfig *config;
const USBDescIface *ifaces[USB_MAX_INTERFACES];
};

data_buf开始往后都可以泄漏到,既然要泄露那必定要泄露有价值的东西,譬如函数指针。注意到USBDevice中有这么一个结构体USBPort,结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct USBPort {
USBDevice *dev;
int speedmask;
int hubcount;
char path[16];
USBPortOps *ops;//important
void *opaque;
int index; /* internal port index, may be used with the opaque */
QTAILQ_ENTRY(USBPort) next;
};

typedef struct USBPortOps {//有着大量的函数指针,可以拿来泄露
void (*attach)(USBPort *port);
void (*detach)(USBPort *port);
/*
* This gets called when a device downstream from the device attached to
* the port (iow attached through a hub) gets detached.
*/
void (*child_detach)(USBPort *port, USBDevice *child);
void (*wakeup)(USBPort *port);
/*
* Note that port->dev will be different then the device from which
* the packet originated when a hub is involved.
*/
void (*complete)(USBPort *port, USBPacket *p);
} USBPortOps;

这个结构体中的USBPortOps结构体包含着许多函数指针,正是我们需要的。可是USBPortdata_buf的上方,没法泄露到,那么应该怎么办?USBDevice中还有这么一个结构体USBEndpoint

1
2
3
4
5
6
7
8
9
10
11
12
struct USBEndpoint {
uint8_t nr;
uint8_t pid;
uint8_t type;
uint8_t ifnum;
int max_packet_size;
int max_streams;
bool pipeline;
bool halted;
USBDevice *dev;//important!
QTAILQ_HEAD(, USBPacket) queue;
};

这个结构体本身包含着USBDevice的指针,而这个指针又恰恰指向了包含它的USBDevice结构体。并且该结构体处于data_buf的下方。

所以总的泄露方法如下:

  1. 先泄露USBEndpoint中的USBDevice的指针。
  2. 根据得到的指针泄露USBPort的指针。
  3. 再通过USBPort的指针泄露USBPortOps的指针。
  4. 再通过USBPortOps的指针泄露任意一个函数指针。
  5. 通过函数指针计算qemu-system-x86_64程序的基地址。
  6. 最后就可以通过程序基地址得到system函数的地址。

至此,整个泄露过程就完成了,得到了完成利用的关键函数system

函数覆盖&&触发:

既然得到了system函数的地址,那么该将他写到哪里才能够触发?这时候就需要仔细观察一下hcd-ehci中的用户接口函数ehci_opreg_write,其中的case USBSTS有一个函数ehci_update_irq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static inline void ehci_update_irq(EHCIState *s)
{
int level = 0;

if ((s->usbsts & USBINTR_MASK) & s->usbintr) {
level = 1;
}

trace_usb_ehci_irq(level, s->frindex, s->usbsts, s->usbintr);
qemu_set_irq(s->irq, level);//important!!!!!!!
}

void qemu_set_irq(qemu_irq irq, int level)
{
if (!irq)
return;

irq->handler(irq->opaque, irq->n, level);//important!!!!调用handler作函数,opaque作参数
}

typedef struct IRQState *qemu_irq;

struct IRQState {
Object parent_obj;

qemu_irq_handler handler;
void *opaque;
int n;
};

irq存在时,则触发irq->handler函数。这个操作我们只需要覆盖handlersystem函数,opaque覆盖为任意shell即可由用户任意触发。

观察一下qemu_irq结构体的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct EHCIState {
USBBus bus;
DeviceState *device;
qemu_irq irq;//here!!!
MemoryRegion mem;
AddressSpace *as;
MemoryRegion mem_caps;
MemoryRegion mem_opreg;
MemoryRegion mem_ports;

//..........

uint32_t astate;
uint32_t pstate;
USBPort ports[NB_PORTS]; //important!!!!

//..........
}

有的读者可能会问了,我们根本不知道EHCIState结构体的地址呀。你还记得前面提到过的USBDevice结构体中的USBPort指针吗?这个指针指向的就是EHCIState结构体中的USBPort,想知道原因的话,去查看一下USBDevice初始化函数或者直接下个硬件断点就明白了。同样的,按照上面所说的泄漏函数的操作,就能够泄漏出qemu_irq地址,最终利用任意写,即可把handler覆盖为system函数。

最终的shell写在哪呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct USBDevice {
DeviceState qdev;
//........
char product_desc[32];//写shell的buffer
int auto_attach;
bool attached;

int32_t state;
uint8_t setup_buf[8];
uint8_t data_buf[4096];
int32_t remote_wakeup;
int32_t setup_state;
int32_t setup_len;
int32_t setup_index;

//.........
};

USBDevice中有一个变量product_desc,这个变量是用来描述USB设备的信息的,可以将它作为shellbuffer,并不会产生影响,32个字节也足够使用了。

最后,只需要触发case USBSTS就可以执行system("shell")了。

0x05 Why EHCI?

回到最开始的地方,有hcd-ohci.chcd-uhci.chcd-ehci.chcd-xhci.c这四个分支,分析过的hcd-ehci.c可以触发,那么另外三个可以触发吗?

我尝试了hcd-uhci.chcd-xhci.c,答案是不可以。来一起看uhci_handle_td函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static int uhci_handle_td(UHCIState *s, UHCIQueue *q, uint32_t qh_addr,
UHCI_TD *td, uint32_t td_addr, uint32_t *int_mask)
{
int ret, max_len;
bool spd;
//.......
async = uhci_async_alloc(q, td_addr);

max_len = ((td->token >> 21) + 1) & 0x7ff;//max_len最大只有0x7ff,小于4096
spd = (pid == USB_TOKEN_IN && (td->ctrl & TD_CTRL_SPD) != 0);
usb_packet_setup(&async->packet, pid, q->ep, 0, td_addr, spd,
(td->ctrl & TD_CTRL_IOC) != 0);//初始化packet
if (max_len <= sizeof(async->static_buf)) {
async->buf = async->static_buf;
} else {
async->buf = g_malloc(max_len);//申请max_len长度的堆空间
}
usb_packet_addbuf(&async->packet, async->buf, max_len);//将分配的buf加入packet结构体中,max_len长度

switch(pid) {
case USB_TOKEN_OUT:
case USB_TOKEN_SETUP:
pci_dma_read(&s->dev, td->buffer, async->buf, max_len);//写入max_len长度数据
usb_handle_packet(q->ep->dev, &async->packet);//进入do_token_setup漏洞函数
//.........
}

这里可以基本确定没法构造越界读写了,max_len的最大长度只有0x7ff,但是data_buf最大有4096的长度。我最开始看的就是hcd-uhci.c,导致我浪费了很长时间在看设计手册和理清代码流程…相反的,再来看看hcd-ehci.c中的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int ehci_execute(EHCIPacket *p, const char *action)
{
USBEndpoint *ep;
int endp;
bool spd;

//.........

if (get_field(p->qtd.token, QTD_TOKEN_TBYTES) > BUFF_SIZE) {//最大可达BUFF_SIZE=5*4096
ehci_trace_guest_bug(p->queue->ehci,
"guest requested more bytes than allowed");
return -1;
}

//.........

if (p->async == EHCI_ASYNC_NONE) {
if (ehci_init_transfer(p) != 0) {//初始化传输空间p->sgl
return -1;
}

spd = (p->pid == USB_TOKEN_IN && NLPTR_TBIT(p->qtd.altnext) == 0);
usb_packet_setup(&p->packet, p->pid, ep, 0, p->qtdaddr, spd,
(p->qtd.token & QTD_TOKEN_IOC) != 0);//初始化packet
usb_packet_map(&p->packet, &p->sgl);//填充packet传输数据buffer important!!!!!
p->async = EHCI_ASYNC_INITIALIZED;
}

trace_usb_ehci_packet_action(p->queue, p, action);
usb_handle_packet(p->queue->dev, &p->packet);//进入do_token_setup漏洞函数

//...........
}

可见ehci中的最大size5*4096,是比data_buf长度大的。

再将眼光聚焦到hcd-xhci.c 中的xhci_setup_packet上来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static int xhci_setup_packet(XHCITransfer *xfer)
{
USBEndpoint *ep;
int dir;

dir = xfer->in_xfer ? USB_TOKEN_IN : USB_TOKEN_OUT;//dir只能为IN和OUT两者之一

//...........

xhci_xfer_create_sgl(xfer, dir == USB_TOKEN_IN); //初始化传输空间
usb_packet_setup(&xfer->packet, dir, ep, xfer->streamid,//初始化packet
xfer->trbs[0].addr, false, xfer->int_req);//dir参数会赋值给xfer->packet->pid
usb_packet_map(&xfer->packet, &xfer->sgl);//填充packet传输数据的buffer
DPRINTF("xhci: setup packet pid 0x%x addr %d ep %d\n",
xfer->packet.pid, ep->dev->addr, ep->nr);
return 0;
}

//后续需要通过usb_handle_packet->usb_process_one进入漏洞函数

static void usb_process_one(USBPacket *p)
{
USBDevice *dev = p->ep->dev;

//........

switch (p->pid) {//通过p->pid来选case,可是xfer->packet->pid只能为IN或OUT
case USB_TOKEN_SETUP:
do_token_setup(dev, p);
break;

//.........

可见xhci中的pid不可能为USB_TOKEN_SETUP,因此不可能到达漏洞函数,更不用说构造越界读写了。

至于ohci,就留给读者举一反三,自行分析可不可行了: )

0x06 Fuzz

经过对hcd-ehci.c源码的阅读、调试,读者应该对整个USB数据传输很熟悉了。这时候想要继续挖掘漏洞,fuzz当然是最好的选择。本文构造的fuzz并不像AFL那样强大,只是一个小巧而简单的demo。我只介绍一下我最简单编写fuzz的思路(都在hcd-ehci.c的基础上叙述)。

当读者自己独立写完exploit时候,想必就已经对于QHqTD结构体很熟悉了,我总结了了一下编写exploit的极简流程:

  1. 分配空间并初始化QHqTD结构体。
  2. 在限制条件下填充结构体数据内容。
  3. 喂数据,触发函数启动控制流程。

那么想要写fuzz脚本,无非是在流程2上下功夫,将原本特定的结构体数据转变为毫无根据的随机数即可。调整后的fuzz流程:

  1. 分配空间并初始化QHqTD结构体,用随机数填充。
  2. 根据想要的覆盖分支做不同的限制条件,改变相应的结构体数据内容。
  3. 喂数据给qemu进程,开始测试。

USB的作用主要是传输数据,因此我们可以只针对usb_handle_packet这个函数做测试即可,当然,只针对他显然覆盖率上是远远不够的。从Guest角度来说,ehci_opreg_write函数是主要的输入点,想要增大覆盖率,可以针对该函数的7个case来写脚本,想要更深入,可以继续往case中的各个分支以及函数去编写。我这里为了节省工作量,就只采用针对数据传输的关键函数为目标编写脚本。

我设置了一共两个QH以及三个qTD,并且设置了三种状态来测试:

状态一:

状态一

状态二:

状态二

状态三:

状态三

qhqtd的结构体中除了需要到达usb_handle_packet必须的字段,其余的全设置成随机数。

这就成了一个简单的fuzz demo。当然了,ehci中并不止有usb_handle_packet,还有其他的ehci_state_fetchqhehci_state_complete等等,都可以根据所需的限制条件来针对各个函数进行测试。

0x07 Others

本文至此就结束了,有问题欢迎指出!

exploit以及fuzz demo的脚本可以参考github.

0x08 Reference

  1. https://en.wikipedia.org/wiki/Host_controller_interface_(USB,_Firewire)
  2. https://isc.360.com/2020/detail.html?vid=108
支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫