作者:raycp
原文鏈接:https://mp.weixin.qq.com/s/rJJYXIUWUh33G0KnvYT06w
終於到了這裡,把qwb2019的這兩題qemu逃逸題復現之後,qemu pwn的復現到這裡就告一段落,接下來將會去分析幾個qemu的cve。qwb初賽和決賽各有一道qemu逃逸題,初賽是qwct
,決賽是ExecChrome
。
因為通過前面的幾題分析,對這類pwn題有了一定的掌握。部分分析過程可以省略,所以此次也是將兩題寫在了一起。
qwct
描述
文件目錄:
$ ll -rwxrw-rw- 1 raycp raycp 179 Aug 26 06:01 launch.sh drwxr-xr-x 6 raycp raycp 4.0K Sep 6 2017 pc-bios -rwxr-xr-x 1 raycp raycp 53M May 25 18:07 QWCT_qemu-system-x86_64 -rw-rw-r-- 1 raycp raycp 3.1M Aug 28 04:42 rootfs.cpio -r-xr-xr-x 1 raycp raycp 8.2M Jun 3 23:37 vmlinuz-5.0.5-generic
launch.sh
1 2 |
#!/bin/bash ./qemu-system-x86_64 -initrd ./rootfs.cpio -nographic -kernel ./vmlinuz-5.0.5-generic -L pc-bios/ -append "priority=low console=ttyS0" -device qwb -monitor /dev/null |
漏洞應該會在qwb
設備中。
分析
解壓文件:
mkdir cpio cd cpio mv ../rootfs.cpio ./ cpio -idmv < rootfs.cpio
把qemu-system-x86_64
拖到IDA裡面,同時sudo ./launch.sh
運行起來。
程序報錯:
./qemu-system-x86_64: error while loading shared libraries: libncursesw.so.6: cannot open shared object file: No such file or directory
解決方法:
sudo wget -O /tmp/libtinfo6 http://mirrors.kernel.org/ubuntu/pool/main/n/ncurses/libtinfo6_6.1+20180210-4ubuntu1_amd64.deb sudo dpkg -i /tmp/libtinfo6 sudo rm /tmp/libtinfo6 sudo wget -O /tmp/libncursesw6 http://mirrors.kernel.org/ubuntu/pool/main/n/ncurses/libncursesw6_6.1+20180210-4ubuntu1_amd64.deb sudo dpkg -i /tmp/libncursesw6 sudo rm /tmp/libncursesw6
又報錯:
./qemu-system-x86_64: error while loading shared libraries: libgfapi.so.0: cannot open shared object file: No such file or directory
解決方法:
sudo wget -O /tmp/glusterfs-common http://mirrors.kernel.org/ubuntu/pool/universe/g/glusterfs/glusterfs-common_3.7.6-1ubuntu1_amd64.deb sudo dpkg -i /tmp/glusterfs-common sudo rm /tmp/glusterfs-common sudo apt-get install liblvm2app2.2 sudo apt --fix-broken install
IDA分析結束后,搜索qwb
相關函數。
看qwb_class_init
函數,知道了它的vendor_id
、device_id
以及realize
為pci_qwb_realize
。
k->revision = 0x10; k->class_id = 0xFF; k->realize = (void (__cdecl *)(PCIDevice_0 *, Error_0 **))pci_qwb_realize; k->exit = (PCIUnregisterFunc *)pci_qwb_uninit; k->vendor_id = 0x1234; k->device_id = 0x8848u; v2->categories[0] |= 0x80uLL;
去看pci_qwb_realize
函數,看到它只註冊了一個大小為0x100000
的mmio,結構體為qwb_mmio_ops
,其對應的IO函數為qwb_mmio_read
以及qwb_mmio_write
。
在分析函數前,看下它的QwbState
相關結構體,後續會分析會使用得到。
00000000 crypto_status struc ; (sizeof=0x1818, align=0x8, mappedto_4600) 00000000 ; XREF: QwbState/r 00000000 statu dq ? 00000008 crypt_key db 2048 dup(?) 00000808 input_buf db 2048 dup(?) 00001008 output_buf db 2048 dup(?) 00001808 encrypt_function dq ? ; offset 00001810 decrypt_function dq ? ; offset 00001818 crypto_status ends 00001818 00000000 ; --------------------------------------------------------------------------- 00000000 00000000 QwbState struc ; (sizeof=0x2250, align=0x10, copyof_4601) 00000000 pdev PCIDevice_0 ? 000008E0 mmio MemoryRegion_0 ? 000009D0 thread QemuThread_0 ? 000009D8 crypto_statu_mutex QemuMutex_0 ? 00000A08 crypto_buf_mutex QemuMutex_0 ? 00000A38 crypto crypto_status ? 00002250 QwbState ends
先看qwb_mmio_write
函數,該函數的主要功能為兩個:
- 當addr為0x1000至0x17ff時,且當
opaque->crypto.statu
為3時,設置opaque->crypto.crypt_key[addr-0x1000]
的值為value。 - 當addr為0x2000至0x27ff時,且當
opaque->crypto.statu
為1時,設置opaque->crypto.input_buf[addr-0x2000]
的值為value。
可以看到qwb_mmio_write
函數的主要功能就是設置input_buf
以及crypto_key
,且由於緩衝區空間大小都是0x800,輸入剛好可以填滿,不存在溢出。
接下來看qwb_mmio_read
函數,該函數功能較複雜,包括:
- 當addr為0時,且當
opaque->crypto.statu
不為5時,初始化所有的緩衝區空間,包括input_buf
、output_buf
以及crypt_key
- 當addr為1時,且當
opaque->crypto.statu
為2或者0時,設置statu為3。 - 當addr為2時,且當
opaque->crypto.statu
為4或者0時,設置statu為1。 - 當addr為3時,且當
opaque->crypto.statu
為3時,設置statu為4。 - 當addr為4時,且當
opaque->crypto.statu
為1時,設置statu為2。 - 當addr為5時,且當
opaque->crypto.statu
為2或者4時,設置opaque->crypto.encrypt_function
的值為aes_encrypt_function
函數。 - 當addr為6時,且當
opaque->crypto.statu
為2或者4時,設置opaque->crypto.decrypt_function
的值為aes_decrypto_function
函數。 - 當addr為7時,且當
opaque->crypto.statu
為2或者4時,設置opaque->crypto.encrypt_function
的值為stream_encrypto_function
函數。 - 當addr為8時,且當
opaque->crypto.statu
為2或者4時,設置opaque->crypto.decrypt_function
的值為stream_decrypto_function
函數。 - 當addr為9時,且當
opaque->crypto.statu
為2或者4時,且當opaque->crypto.encrypt_function
的值不為空時,創建線程qwb_encrypt_processing_thread
,並設置statu為5。 - 當addr為10時,且當
opaque->crypto.statu
為2或者4時,且當opaque->crypto.decrypt_function
的值不為空時,創建線程qwb_decrypt_processing_thread
,並設置statu為7。 - 其餘情況則可以根據addr的值讀取
input_buff
、crypto_key
以及output_buff
。
qwb_encrypt_processing_thread
線程以及qwb_decrypt_processing_thread
,則是在線程中調用相應的opaque->crypto.encrypt_function
函數以及opaque->crypto.decrypt_function
去實現加解密。
stream
相關的加解密函數則是實現了一個簡單的異或,而aes
相關的加解密函數則是對輸入進行aes加解密,並在最後附上了一個校驗值。
所以整個設備的功能主要是實現了一個加解密功能,算法可以選擇是流算法或aes算法,主要基於crypto_status
結構體來記錄關鍵數據。
經過分析該設備中存在兩個漏洞,一個是越界讀,一個是越界寫。
越界讀是在qwb_mmio_read
函數中,其對於output_buff
讀取的判斷條件為:只要小於strlen(output_buff)
,就可以讀取相應數據。乍一看沒有問題,可是當加解密的數據長度剛好填滿了output_buff
即長度為0x800時,調用strlen(output_buff)
時會導致獲得的長度大於0x800
,因為拼接上了後面的encrypt_function
指針的數據。使得越界讀到encrypt_function
指針的數據,實現程序地址的泄露。
越界寫在存在於aes_decrypto_function
以及aes_encrypto_function
函數中,兩個函數都在對輸入數據進行aes加密后,在output_buff
的末尾拼接了一個8字節的校驗值,該校驗值導致越界寫,關鍵代碼如下:
len = strlen((const char *)input); ... *(_QWORD *)crc = 0LL; v19 = 0; c = 0; for ( i = 0LL; ; c = crc[i & 7] ) { c ^= output[i]; idx = i++; crc[idx & 7] = c; if ( len == i ) break; } } else { *(_QWORD *)crc = 0LL; } *(_QWORD *)&output[len] = *(_QWORD *)crc;
如果len
長度剛好為0x800,則會導致最後的校驗值寫入到output_buff[0x800]處,導致越界覆蓋了encrypt_function
指針。
利用
如何利用上述的兩個漏洞拿到shell呢,大致也是分為四步。
第一步將input_buff
以及cyrpto_key
填滿,然後調用stream_encypt_function
將output_buff
填滿,再利用越界讀,讀出stream_encypt_function
函數的地址,根據偏移計算出system plt
的地址。
第二步構造能夠得到system plt
校驗值的input_buff
,因為是異或得到的校驗值,所以比較容易構造。然後將輸入以及key填進去,調用aes_encypt_function
函數加密,將output_buff
讀出來保存。
第三步是將上一步保存的output_buff
數據輸入到input_buff
中,再使用相同的key調用aes_decypt_function
函數進行解密,這樣解密出來的數據的校驗值就剛好會是system plt
,且會覆蓋至encrypt_function
指針。
第四步是將參數賦值到input_buff
中,最後調用encrypt_function
,實現system
函數的調用,拿到flag。
ExecChrome
qwb 2019 final的題,主辦方給了一個虛擬機,虛擬機的用戶名是qwb
,密碼是123456
。進去以後sudo ./launch.sh
啟動虛擬機,qemu虛擬機用戶名是ubuntu
,密碼是123456
,launch.sh
內容如下:
1 2 3 4 |
#!/bin/bash while true do ./qemu-system-x86_64 -m 1024 -smp 2 -boot c -cpu host -hda ubuntu_server.qcow2 --enable-kvm -drive file=./blknvme,if=none,id=D22 -device nvme,drive=D22,serial=1234 -net user,hostfwd=tcp::2222-:22 -net nic && sleep 5 done |
分析
根據參數-device nvme
,可以推斷應該主要是這個設備的問題,搜相關函數,看到有很多的函數。經過一番搜索以後發現是根據已有的設備改的代碼,目錄是hw/block/nvme.c
。
經過對比,發現主要是在nvme_mmio_read
以及nvme_mmio_write
裡面修改了部分代碼,研究相應代碼。
先看nvme_mmio_read
,原來的代碼是:
if (addr < sizeof(n->bar)) { memcpy(&val, ptr + addr, size); }
修改後的代碼是:
memcpy(&val, &ptr[addr], size);
可以看到少了對於size
的檢查,可能會存在越界讀。
再看nvme_mmio_write
中,該函數調用了nvme_write_bar
函數。經過對比,題目對nvme_write_bar
函數中添加了部分代碼,添加的代碼的內容為:
default: ... if ( size == 2 ) { *(_WORD *)((char *)&n->bar.cap + offset) = data; } else if ( size > 2 ) { if ( size == 4 ) { *(_DWORD *)((char *)&n->bar.cap + offset) = data; } else if ( size == 8 ) { *(uint64_t *)((char *)&n->bar.cap + offset) = data; } } else if ( size == 1 ) { *((_BYTE *)&n->bar.cap + offset) = data; } break; }
可以看到似乎也存在越界寫功能。
再去虛擬機中看mmio空間的大小:
lspci -vv -s 00:04.0 00:04.0 Non-Volatile memory controller: Intel Corporation QEMU NVM Express Controller (rev 02) (prog-if 02 [NVM Express]) Subsystem: Red Hat, Inc. QEMU Virtual Machine Physical Slot: 4 Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR+ FastB2B- DisINTx+ Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx- Latency: 0 Interrupt: pin A routed to IRQ 10 Region 0: Memory at febf0000 (64-bit, non-prefetchable) [size=8K] Region 4: Memory at febf3000 (32-bit, non-prefetchable) [size=4K]
可以看到mmio大小為8k,而NvmeCtrl->bar
大小卻只有0x40,結合上面的分析,確定該設備存在越界讀寫漏洞。
NvmeCtrl struc ; (sizeof=0x1C50, align=0x10, copyof_4151) 00000000 parent_obj PCIDevice_0 ? 000008E0 iomem MemoryRegion_0 ? 000009D0 ctrl_mem MemoryRegion_0 ? 00000AC0 bar NvmeBar_0 ? 00000B00 conf BlockConf_0 ? 00000B38 page_size dd ? 00000B3C page_bits dw ? 00000B3E max_prp_ents dw ? 00000B40 cqe_size dw ? 00000B42 sqe_size dw ? 00000B44 reg_size dd ? 00000B48 num_namespaces dd ? 00000B4C num_queues dd ? 00000B50 max_q_ents dd ? 00000B54 db ? ; undefined 00000B55 db ? ; undefined 00000B56 db ? ; undefined 00000B57 db ? ; undefined 00000B58 ns_size dq ? 00000B60 cmb_size_mb dd ? 00000B64 cmbsz dd ? 00000B68 cmbloc dd ? 00000B6C db ? ; undefined 00000B6D db ? ; undefined 00000B6E db ? ; undefined 00000B6F db ? ; undefined 00000B70 cmbuf dq ? ; offset 00000B78 irq_status dq ? 00000B80 serial dq ? ; offset 00000B88 namespaces dq ? ; offset 00000B90 sq dq ? ; offset 00000B98 cq dq ? ; offset 00000BA0 admin_sq NvmeSQueue_0 ? 00000C00 admin_cq NvmeCQueue_0 ? 00000C50 id_ctrl NvmeIdCtrl_0 ? 00001C50 NvmeCtrl ends
利用
要想成功利用,分為兩步:
- 利用越界讀,泄露程序基址與堆地址。
- 利用越界寫覆蓋
qemu timer
控制程序執行流
因為程序開啟了PIE,所以第一步需要先泄露地址。首先是得到system
地址,在與bar
地址偏移0x1ff0
的地方找到了存在程序地址的地方,利用mmio_read
越界讀出來,然後根據偏移計算出system
地址。其次是得到NvmeCtrl->bar
地址的空間以實現可以拿到最終傳參的地址,在與bar地址偏移0x1f98
的地方找到了存在堆地址的地方,根據偏移可以計算出NvmeCtrl->bar
地址。
關鍵的是如何控制程序執行流,主要原理是利用了NvmeCtrl
結構體中的admin_sq
,admin_sq
中存在一個timer
結構體,可以利用它來控制程序執行流。
00000000 NvmeSQueue_0 struc ; (sizeof=0x60, align=0x8, copyof_4154) 00000000 ; XREF: NvmeCtrl_0/r 00000000 ; NvmeCtrl/r 00000000 ctrl dq ? ; offset 00000008 sqid dw ? 0000000A cqid dw ? 0000000C head dd ? 00000010 tail dd ? 00000014 size dd ? 00000018 dma_addr dq ? 00000020 timer dq ? ; offset 00000028 io_req dq ? ; offset 00000030 req_list $FE468C6164B384978313660BA47FFEDA ? 00000040 out_req_list $FE468C6164B384978313660BA47FFEDA ? 00000050 entry $53C797D9CC370671B1F6BB504B4B2727 ? 00000060 NvmeSQueue_0 ends 00000000 ; --------------------------------------------------------------------------- 00000000 QEMUTimer struc ; (sizeof=0x30, align=0x8, copyof_729) 00000000 expire_time dq ? 00000008 timer_list dq ? ; offset 00000010 cb dq ? ; offset 00000018 opaque dq ? ; offset 00000020 next dq ? ; offset 00000028 attributes dd ? 0000002C scale dd ? 00000030 QEMUTimer ends 00000030
主要有兩種方式:
一種是偽造timer,利用虛擬機重啟或關機時會觸發時鐘timer
,調用cb(opaque)
控制程序執行流的方法,關鍵代碼如下所示:
void main_loop_wait(int nonblocking) { ... /* CPU thread can infinitely wait for event after missing the warp */ qemu_start_warp_timer(); qemu_clock_run_all_timers(); } bool timerlist_run_timers(QEMUTimerList *timer_list) { ... timer_list->active_timers = ts->next; ts->next = NULL; ts->expire_time = -1; cb = ts->cb; opaque = ts->opaque; /* run the callback (the timer list can be modified) */ qemu_mutex_unlock(&timer_list->active_timers_lock); cb(opaque); // we can hajack the control flow here qemu_mutex_lock(&timer_list->active_timers_lock); progress = true; } ... return progress; }
可以在堆中偽造好timer結構體,其cb
為system地址,opaque
為參數的地址。利用越界將admin_sq
中的timer
指針覆蓋成該偽造的結構體,當reboot時就可以成功控制程序的執行流。一個關鍵的點是timer
結構體中的timer_list
指針需要正確,因為之前泄露了堆地址,因此可以通過偏移計算得到原來的timer_list
結構體的值,將它覆蓋成原來的就好。但是由於結構體都是堆地址,會導致和泄漏的地址的偏移可能不固定。但是它的地址和堆基址的偏移時一致的,因為我們可以通過計算堆基址來得到timer_list
的地址,具體可以去看exp中的內容。
另一種方式則是在nvme_mmio_write
中存在一條調用鏈:nvme_mmio_write->nvme_process_db->timer_mod->timer_mod_ns->timerlist_rearm->timerlist_notify->(timer_list->notify_cb)(timer_list->notify_opaque,timer_list->clock->type)
,也可以成功控制程序執行流。
我的exp中使用的是第一種利用方式。
小結
qemu ctf pwn題分析到這就暫告一段落,接下來會分析一些qemu cve來進一步了解相關漏洞。
相關腳本以及文件鏈接