一、内存模块:
1.可以看到在实际物理内存physical memory中存在qemu进程的内存空间
2.之后在qemu进程的内存空间中虚拟化出虚拟主机的实际内存空间Guest’s phy memory。
3.然后才是虚拟主机中各个进程的内存分配。
▲这里可以看到虚拟机中的进程地址都在实际内存地址中有映射,所以如果我们可以找到在虚拟主机中的某个进程对应的实际内存地址,那么一旦满足一定权限要求就可以执行实际内存中qemu进程加载的libc中的内容,从而调用实际内存中的system命令,实现qemu逃逸。
二、逃逸突破口:
▲qemu逃逸一般是从qemu虚拟化设备的代码中寻找漏洞,但是qemu会虚拟化很多设备,在ctf中一般需要关注的就是题目给的PCI设备。基本上都是把qemu-system-x86用IDA打开,然后从和该设备交互的函数中寻找漏洞。
例如分析题目后知道了某个设备可能就是漏洞设备,这里以BlizzardCTF2017-Strng题目为例,加载了strng设备,那么就用IDA打开qemu-system-x86,然后在函数列表搜索strng,找到对应函数进行分析。
寻找漏洞就需要更多的前置知识了:
1.寻找到设备的代号,方便查看其设备地址空间:
1 2 3 4 5 6 7 8 9 10 #注释头 ubuntu@ubuntu:~$ lspci 00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02) 00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II] 00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II] 00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03) 00:02.0 VGA compatible controller: Device 1234:1111 (rev 02) 00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10) 00:04.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)
这里可以看到00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10),一般标记为unclassified device就是qemu启动时的命令参数中加载进入的设备,也就是strng设备。那么其对应的代号就是00:03.0,之后通过这个代号查询更多内容。
2.查看PCI设备的地址空间,从而方便推断出设备实际申请的内存地址:
(1)通过设备代号,找到该设备的地址空间,可以用以下命令查看:
1 2 3 4 5 6 7 8 9 #注释头 lspci -v -m -n -s 00:03.0 ------------------------------------------------------------------------- lspci -v -m -s 00:03.0 ----------------------------------------------------------------------- lspci -v -s 00:03.0 -x ------------------------------------------------------------------------ hexdump /sys/devices/pci0000\:00/0000\:00\:03.0/config
(2)通过地址空间找到该设备申请的内存空间:
1 2 3 4 5 6 7 #注释头 ubuntu@ubuntu:~$ hexdump /sys/devices/pci0000\:00/0000\:00\:03.0/config 0000000 1234 11e9 0103 0000 0010 00ff 0000 0000 0000010 1000 febf c051 0000 0000 0000 0000 0000 0000020 0000 0000 0000 0000 0000 0000 1af4 1100 0000030 0000 0000 0000 0000 0000 0000 0000 0000
这里通过对照,可以知道以上各个参数的实际内容。
(3)然后寻找mmio和pmio的地址,这两个是PCI设备为了与qemu虚拟机进行I/O申请所映射的两块内存。由于qemu模拟设备最终都会与真实的设备进行交互,而qemu中的mmio就是相对于主机下的mmio的一段映射,所以访问这段空间就相当于访问主机下的mmio空间,那么就能跳出qemu,实现qemu逃逸。而PCI设备的大多的读写操作都在这两块内存上,所以针对这两块内存的操作函数通常也是最容易出现漏洞的地方。
①mmio:与内存共享一块地址空间,共用一定数量的地址总线,cpu对该地址空间的访问会直接转化为对设备的访问。
②pmio:与内存分开的,所用的地址总线和内存所用的地址总线不是一样的,cpu访问这块内存时需要使用特殊的指令进行访问。
这两块内存是在BAR(base address register)中,BAR总共6个register,BAR0,BAR1…,每个BAR占据4个字节。而mmio对应BAR0,pmio对应BAR1。
▲结合上图可以看到mmio的地址为0xfebf1000,pmio地址为0xc051。
3.查看PCI设备初始化的函数,寻找漏洞:
(1)首先查看注册strng设备的函数:这里分析可以知道strng_class_init即为初始化函数:
object_class_dynamic_cast_assert函数即创建了一个设备,后面的一些操作都是赋值语句,为了初始化该设备。
(2)然后查看strng_instance_init函数,该函数是为了实例化设备,初始化设备的结构体,这个结构体可以在IDA中看到:
函数:
这样可以看到创建了mmio和pmio空间,大小分别为0x100和0x8。
(3)现在查看对这两个空间的操作函数,这才是重点。但是需要注意的是,在这道题目中调用对mmio和pmio进行读写操作的函数时,并没有对mmio或者pmio空间进行读写操作,取而代之的是对reg数组进行读写操作。
▲原因是以下结构体的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 //注释头 typedef struct { PCIDevice pdev; MemoryRegion mmio; //mmio内存空间 ...... } STRNGState; static const MemoryRegionOps strng_mmio_ops = { //mmio空间对应的操作 .read = strng_mmio_read, //对mmio空间的read由strng_mmio_read实现 .write = strng_mmio_write, //对mmio空间的write由strng_mmio_write实现 .endianness = DEVICE_NATIVE_ENDIAN, };
原本在c语言中exp的接口函数中为:
1 2 3 4 5 #注释头 mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0); ----------------------------------------------------------------------------- *((uint32_t*)(mmio_mem + addr)) = value;
对mmio和pmio空间的操作函数就会执行.read和.write函数,但是结构体由于重新将.read函数和.write定位成了strng_mmio_read和strng_mmio_write函数,这个操作某种程度上相当于修改了两个函数的got表。之后就相当于正常的pwn题了,就是利用这几个读写函数中的漏洞,最终执行system(“cat /root/flag.txt”)即可。
三、调用mmio_read和mmio_write函数
在strng中,这两个函数就是strng_mmio_read和strng_mmio_write函数。
1.对mmio空间操作,进而调用strng_mmio_read和strng_mmio_write函数:
正常写c语言的exp编译之后,放到qemu中运行,就能通过PCI设备号从而得到mmio地址:/sys/devices/pci0000:00/0000:00:04.0/resource0。然后通过该地址和特定函数就能访问到mmio空间,跳出qemu到主机的mmio空间。
①用户态访问,正常pwn题:
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 //注释头 #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; 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)); } int main(int argc, char *argv[]) { // Open and map I/O memory for the strng device int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);//这里需要获取到mmio的地址 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_read(0x128); mmio_write(0x128, 1337); }
②内核态访问:不太知道有啥用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //注释头 #include <asm/io.h> #include <linux/ioport.h> long addr=ioremap(ioaddr,iomemsize); readb(addr); readw(addr); readl(addr); readq(addr);//qwords=8 btyes writeb(val,addr); writew(val,addr); writel(val,addr); writeq(val,addr); iounmap(addr);
2.对pmio空间操作,进而调用strng_pmio_read和strng_pmio_write函数:
①用户态访问:
1 2 3 4 5 6 7 8 9 10 11 12 //注释头 #include <sys/io.h > iopl(3);//需要先申请访问端口 inb(port); inw(port); inl(port); outb(val,port); outw(val,port); outl(val,port);
②内核态访问:也不太清楚有啥用
1 2 3 4 5 6 7 8 9 10 11 12 //注释头 #include <asm/io.h> #include <linux/ioport.h> inb(port); //读取一字节 inw(port); //读取两字节 inl(port); //读取四字节 outb(val,port); //写一字节 outw(val,port); //写两字节 outl(val,port); //写四字节
3.然后就能调用到qemu中关于mmio和pmio的read,write函数了,从而挖掘漏洞,跳出qemu。