CVE-2020-14364 Qemu越界读写漏洞复现与分析
前言
该漏洞是长亭的师傅发现的,被称为是qemu历史上最严重的漏洞,本篇文章旨在复现该漏洞,理解漏洞产生原因及利用链,最终达成逃逸,由于涉及到的东西太多,不能和以前一样把所有东西都弄好再总结成博客,打算边做边记录,康康这样对整理思路是不是更好一点。
环境搭建
在虚拟机中连了一个真实的USB2.0设备,并且根据这篇文章CVE-2020-14364-Qemu逃逸漏洞分析及两种利用思路制造了一个.img文件模拟USB的文件系统,启动脚本如下:
1 | qemu-system-x86_64 \ |
漏洞分析
直接看下CVE的官方描述,漏洞产生于qemu中模拟USB的部分。
- An out-of-bounds read/write access flaw was found in the USB emulator of the QEMU in versions before 5.2.0. This issue occurs while processing USB packets from a guest when USBDevice ‘setup_len’ exceeds its ‘data_buf[4096]’ in the do_token_in, do_token_out routines. This flaw allows a guest user to crash the QEMU process, resulting in a denial of service, or the potential execution of arbitrary code with the privileges of the QEMU process on the host.
在 usb: fix setup_len init CVE-2020-14364看下patch的内容,setup_len
是在赋值后才判断的,即使超过了限制,该处return也不会回滚其原始值,而是继续向后运行,从而在后面的do_token_in/do_token_out
中产生越界读和越界写。
1 | --- |
1 | //USB数据包 |
读写函数
USB内存初始化
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
映射到usb 设备的内存。
usb_ehci_pci_init
将opreg的基址opregbase设置为了0x20,对这块内存读写即可对opreg的内容进行读写。
1 | static void usb_ehci_pci_init(Object *obj) |
opreg的内容如下:
1 | union { |
usb_ehci_init
函数里注册了对于opreg读写的函数
1 | void usb_ehci_init(EHCIState *s, DeviceState *dev) |
看一下这里的读写函数,读函数读取addr + s->opregbase
的内容到addr给用户,比如我们调用*((uint64_t*)(mmio_mem + 0x20))
实际上这里的addr为0。
1 | static uint64_t ehci_opreg_read(void *ptr, hwaddr addr, |
再来看下写函数,加入我们调用mmio_write(0x20,0x1234),实际上传入的addr=0,会对usbcmd进行赋值s->usbcmd = val
,在最后有一个*mmio = val;
,代换一下就是*(s->opreg + (addr >> 2)) = val
,因此我们可以控制opreg的其他成员。
1 | // |
漏洞利用
漏洞触发
首先一个朴素的问题是如何触发到漏洞,这也是前两天给我劝退的一个问题,这两天想了一下没有很多参考资料的情况应该是常态,应该hack to learn, not learn to hack
,边走边看,而不是等准备好了储备知识再上手。抱着这样的想法,捡起来之前断掉的部分继续看。
查看交叉引用,可以找到ehci_work_bh->ehci_advance_periodic_state->ehci_advance_state->ehci_state_execute->ehci_execute->usb_handle_packet->usb_process_one
,再往前找就没有调用函数了。因此我们从头开始一步步看需要满足那些条件才能触发越界读写。
ehci_work_bh
函数中,我们需要满足ehci_periodic_enabled(ehci)
或者ehci->pstate != EST_INACTIVE
,这里我们选择满足前者,具体到变量上需要赋值使得s->usbcmd & USBCMD_RUNSTOP
以及s->usbcmd & USBCMD_PSE
。ehci_update_frindex(ehci, 1);
的函数调用每次都会使得ehci->frindex++
,故其总会增加到8(最大循环数为24).
1 |
|
分析ehci_advance_periodic_state
函数如何调用到ehci_advance_state
,因为进这个函数的时候ehci->frindex & 7
已经成立了,所以下面的两个if都是必过的,ehci->periodiclistbase
对齐0x1000得到list,只要list不为0即可。之后list |= ((ehci->frindex & 0x1ff8) >> 1)
经过调试会对list+4。在开始的时候我们设置EHCIState的时候调用mmio_write(0x34,virt2phys(dma_buf))
将periodiclistbase
设置为dma_buf的物理地址,随后list=virt2phys(dma_buf)+4,再之后的get_dwords
从list中读取值赋值给entry,开始的时候我们在这里布置virt2phys(qh)+0x2
,最后将entry通过ehci_set_fetch_addr
赋值给ehci->p_fetch_addr
。
1 | static void ehci_advance_periodic_state(EHCIState *ehci) |
进入ehci_advance_state
函数查看如何调用到ehci_state_execute
函数。看注释该函数是个状态机。根据s->pstate
进行状态判断需要进入哪个分支,经过调试每次都会先进入EST_FETCHENTRY
,调用ehci_state_fetchentry
函数。该函数里通过NLPTR_TYPE_GET(entry)
的返回值进一步设置状态,这里以entry/2的单字节作为状态码,共有四种状态。回想一下刚刚我们将entry设置为virt2phys(qh)+0x2
,因为对齐的关系,最后1字节一定是2,故(2/2)&3=1,对应将状态设置为NLPTR_TYPE_QH
。
1 | /* |
状态机是个不断切换状态的循环,因此接下来会进入EST_FETCHQH
,调用ehci_state_fetchqh
,在这个函数中并没有直接将状态转换为EST_EXECUTE
的部分,我们需要先进入到EST_FETCHQTD
分支。为此我们需要设置q->qh.token & (1<<7)
,q->qh.current_qtd
最后一位为0,q->qh.current_qtd != 0
。qh是通过get_dwords(ehci, NLPTR_GET(q->qhaddr),(uint32_t *) &qh, sizeof(EHCIqh) >> 2)
函数根据entry来查找的,因此我们也可以控制qh的内容。ehci_find_device
函数里有一个检查ehci->portsc[i] & PORTSC_PED
(这里多谢Resery
师傅指教),而这个设置是通过ehci_port_write
来做的。
1 |
|
继续往下看EST_FETCHQTD
这个分支,会调用ehci_state_fetchqtd
函数,qtd的地址是根据qh.current_qtd
决定的,由于我们可以控制qh因此可以控制qtd。这里只需要设置qtd.token & QTD_TOKEN_ACTIVE(1<<7)
即可
1 |
|
查看ehci_state_execute
函数,这里会直接进入again = ehci_execute(p, "process");
,进而直接进入usb_handle_packet(p->queue->dev, &p->packet);
再进入usb_process_one(p)
,这里的pid是在ehci_execute
函数中的p->pid = ehci_get_pid(&p->qtd);
获取的,由于我们可以控制qtd,因此可以控制调用任何一个分支函数。
1 | static void usb_process_one(USBPacket *p) |
来看下漏洞函数函数do_token_setup
,p->iov.size
设置为8,它是由qtd->token
决定的,setup_buf的地址是由qtd的bufptr确定的,因此长度可控(这块待确定)。
1 | static void do_token_setup(USBDevice *s, USBPacket *p) |
越界读
根据ehci_get_pid
,我们需要设置(qtd->token & 0x00000300) >> 8
,设置为2>>8
。再使用s->setup_len = (s->setup_buf[7] << 8) | s->setup_buf[6];
设置setip_len。为了进入指定分支,我们需要提前设置s->setup_state=SETUP_STATE_DATA
,要达到这个状态需要在do_token_setup
函数中满足s->setup_buf[0] & 0x80(USB_DIR_IN)
。
设置qtd->token & (2 >> 8)
进入读函数,最后一个检查我们要提前设置p->iov.size
,它的值由qtd->token = size << QTD_TOKEN_TBYTES_SH
控制(这一点是参考文章说的,自己源码中没找到emmmm)
1 |
|
越界写
先进入do_token_setup函数设置越界长度以及setbuf[0] & USB_DIR_OUT(0))。设置qtd->token = (0 << 8)
进入到do_token_out
,将qtd->bufptr[0]复制到s->data_buf。
还有一个存疑的地方是参考文章说
- 这里需要注意的是经过几次调用后,s->setup_index >= s->setup_len 会满足条件,s->setup_state 会被设置成 SETUP_STATE_ACK,可以通过调用一次do_token_setup,设置正常长度,将s->setup_state重新设置成SETUP_STATE_DATA。
这部分等下动调看看
1 | static void do_token_out(USBDevice *s, USBPacket *p) |
任意读
- 通过
do_token_setup
设置越界长度setup_len为0x1010
- 越界写将
setup_len
设置为0x1010
,setup_index
设置为0xfffffff8-0x1010
,开始setup_index为0,因此第一次copy可以覆写到setup_len和setup_index,第二次copy的时候setup_index=-8
,len=0x1018
,故可以拷贝覆写setup_buf[8]
- 覆写
setup_buf[0]=USB_DIR_IN
,将setup_index
设置为(target_addr-s->data_buf)-0x1018
,len = s->setup_len - s->setup_index
得到0x1018,故实际拷贝时s->setup_index += len
得到target_addr-s->data_buf
- 进行越界读即可读取目标地址的内容
1 | usb_packet_copy(p, s->data_buf + s->setup_index, len); |
1 | struct USBDevice { |
任意写
- 通过
do_token_setup
设置越界长度为0x1010
- 越界写将
setup_len
设置为offset+8
,将setup_index
则offset-0x1010
,copy结束后setup_index=offset
,第二次copy的时候len=(offset+8-(offset-0x1010))=0x1018
,再拷贝的时候即可对target_qddr进行0x1018长度的拷贝。
1 | int len = s->setup_len - s->setup_index; |
整体利用思路
- 获取USBDevice对象的地址。越界读取dma_buf+0x2004可以得到
USBDevice->remote_wakeup
的内容,继续往下读可以读到USBEndpoint ep_ctl
,读取其中的dev
即可获取到对象的地址,计算偏移就可以获得data_buf
和USBPort
字段的地址
1 | struct USBEndpoint { |
- 在越界读出来的内容里有一个变量是USBDescDevice *device,可以根据这个变量得到system的地址
- USBDevice 会在 realize 时,调用usb_claim_port,将USBDevice中的port字段设置为指向
EHCIState中的ports的地址, 读取USBDevice->port的内容就能获得EHCIState->ports 的地址,减去偏移得到 EHCIState的地址。进而得到EHCIState->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
30
31
32
33
34
35
36
37
38
39
40
41void usb_claim_port(USBDevice *dev, Error **errp)
{
USBBus *bus = usb_bus_from_device(dev);
USBPort *port;
assert(dev->port == NULL);
if (dev->port_path) {
QTAILQ_FOREACH(port, &bus->free, next) {
if (strcmp(port->path, dev->port_path) == 0) {
break;
}
}
if (port == NULL) {
error_setg(errp, "usb port %s (bus %s) not found (in use?)",
dev->port_path, bus->qbus.name);
return;
}
} else {
if (bus->nfree == 1 && strcmp(object_get_typename(OBJECT(dev)), "usb-hub") != 0) {
/* Create a new hub and chain it on */
usb_try_create_simple(bus, "usb-hub", NULL);
}
if (bus->nfree == 0) {
error_setg(errp, "tried to attach usb device %s to a bus "
"with no free ports", dev->product_desc);
return;
}
port = QTAILQ_FIRST(&bus->free);
}
trace_usb_port_claim(bus->busnr, port->path);
QTAILQ_REMOVE(&bus->free, port, next);
bus->nfree--;
dev->port = port;
port->dev = dev;
QTAILQ_INSERT_TAIL(&bus->used, port, next);
bus->nused++;
}
- USBDevice 会在 realize 时,调用usb_claim_port,将USBDevice中的port字段设置为指向
- 利用任意写将EHCIState->irq内容填充为伪造的irq地址,将handler 填充成system@plt地址,opaque填充成payload的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct IRQState {
Object parent_obj;
qemu_irq_handler handler;
void *opaque;
int n;
};
void qemu_set_irq(qemu_irq irq, int level)
{
if (!irq)
return;
irq->handler(irq->opaque, irq->n, level);
}
- 利用任意写将EHCIState->irq内容填充为伪造的irq地址,将handler 填充成system@plt地址,opaque填充成payload的地址
调试记录
在Resery师傅的exp上调下233,我这里用的还是前几天的CVE-2019-6788
那个qemu的binary。
diretory /home/wz/qemu
导入一下代码文件夹,之后b core.c:204
断到do_token_in
的usb_packet_copy
函数处,第一次的越界读我们读的长度为0x1e00
,虽然设置了0x2000
的越界长度,但是因为p->iov.size < len
,所以我们最终可以读取的长度为0x1e00
,设置的qtd->bufptr[0] = virtuak_addr_to_physical_addr(data_buf)
使得我们读取的结果存放到data_buf=dmabuf + 0x1000
处,这里我们读取data_buf[4096]
后偏移为0x24的地方,进而计算得到port_addr和data_buf_addr
- 在越界读偏移为0x4fc处有个程序加载地址相关地址,据此算出
proc_base和system@plt
- 读取
port_addr
的内容得到port_ptr
,根据这个地址可以算出EHCIState_addr和irq_addr
,调试过程中比较重要的是偏移,我们可以用p &((struct EHCIState*)0)->ports
得到偏移,这里是0x530
,同理可以算出来irq
拿伪造的
fake_irq
覆写原irq的地址,上面布置函数指针和参数地址(在dma_buf上布置
)mmio_write读写触发ehci_update_irq->qemu_set_irq,执行system函数弹出计算器。
exp.c
基本就是resery师傅的exp,偏移略有不同,非常感谢师傅的分享以及解惑。
1 |
|