最近想学习一下关于qemu逃逸的pwn题目。学习资源来自于微信公众号“平凡路上”。感谢大佬整理。
目前qemu出问题比较多的地方以及比赛中出题目的形式都在在设备模拟中,因此后续也会将关注点主要放在设备模拟上。提供了例题:Blizzard CTF 2017 Strng:
qemu概述 每个qemu虚拟机对应一个qemu进程,虚拟机的线程对应qemu进程的线程。
关于qemu逃逸在phrack上有知识介绍:
qemu会通过mmap来分配虚拟机需要的内存。题目中的启动命令:
1 2 3 4 5 6 7 8 9 10 ./qemu-system-x86_64 \ -m 1G \ -device strng \ -hda my-disk.img \ -hdb my-seed.img \ -nographic \ -L pc-bios/ \ -enable-kvm \ -device e1000,netdev=net0 \ -netdev user,id=net0,hostfwd=tcp::5555-:22
加入一行-gdb tcp::1234
保存到start.sh,sudo启动,ubuntu@passw0rd登录进去,是个ubuntu1404的系统:
可以通过gdb remote链接,调试虚拟机内部。
调试宿主机内存,sudo gdb,然后attach qemu的pid即可。
vmmap找到1G的地方:
1 0x7f84b3e00000 0x7f84f3e00000 rw-p 40000000 0
内存定位 如果我们在虚拟机中申请一个内存,怎么定位在宿主机内存中的位置?
首先得到在虚拟机中的虚拟地址,然后转化为物理地址。这个物理地址是在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 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 #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){ return addr & ((1 << PAGE_SHIFT) - 1 ); } uint64_t gva_to_gfn(void *addr){ uint64_t pme, gfn; size_t offset; offset = ((uintptr_t )addr >> 9 ) & ~7 ; lseek(fd, offset, SEEK_SET); read(fd, &pme, 8 ); if (!(pme & PFN_PRESENT)) return -1 ; 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\n" , ptr); ptr_mem = gva_to_gpa(ptr); printf ("Your physical address is at 0x%" PRIx64"\n" , ptr_mem); getchar(); return 0 ; }
其中gva_to_gpa是虚拟地址转化为相应的物理地址的函数。
编译:gcc -m32 -O0 mmu.c -o mmu
传输:scp -P5555 mmu ubuntu@127.0.0.1:/home/ubuntu
(gdb连着别忘了c一下)
运行得到物理地址是0x32c2d008,而mmap的偏移是0x7f84b3e00000,这样在宿主机就可以找到字符串的地址:
PCI设备地址空间
PCI(Periheral Component Interconnect)有三种地址空间:PCI I/O空间、PCI内存地址空间和PCI配置空间。其中,PCI I/O空间和PCI内存地址空间由设备驱动程序(即上面提到的设备本身驱动)使用,而PCI配置空间由Linux PCI初始化代码使用,这些代码用于配置PCI设备,比如中断号以及I/O或内存基地址。
Ref: https://zhuanlan.zhihu.com/p/53957763
PCI配置空间记录了关于此设备的详细信息,大小256字节。其中头部64字节是PCI标准规定的,当然并非所有的项都必须填充,位置是固定了,没有用到可以填充0。前16个字节的格式是一定的,包含头部的类型、设备的总类、设备的性质以及制造商等,格式如下:
Ref: https://en.wikipedia.org/wiki/PCI_configuration_space
其中的BAR(Base Address Register)比较重要,共6个。BAR记录所需的地址空间的类型、基地址、和其他属性。PCI BAR分为两种,Memory BAR(最低位为0)和I/O BAR(最低位为1)。结构如图:
当BAR最后一位为0表示这是映射的I/O内存,为1是表示这是I/O端口,当是I/O内存的时候1-2位表示内存的类型,bit 2为1表示采用64位地址,为0表示采用32位地址。bit1为1表示区间大小超过1M,为0表示不超过1M。bit3表示是否支持可预取。
而相对于I/O内存,当最后一位为1时表示映射的I/O端口。I/O端口一般不支持预取,所以这里是29位的地址。
通过memory space访问设备I/O的方式称为memory mapped I/O,即MMIO,这种情况下,CPU直接使用普通访存指令即可访问设备I/O。【MMIO】
通过I/O space访问设备I/O的方式称为port I/O,或者port mapped I/O,这种情况下CPU需要使用专门的I/O指令如IN/OUT
访问I/O端口。 【PMIO】
后面是一些详细的解释,这里就不贴了。
qemu中查看PCI设备 指令:lspci
显示当前主机的所有PCI总线信息,以及所有已连接的PCI设备信息.
1 2 3 4 5 6 7 8 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)
这里的Unclassified device应该就是题目中加入的了。别的在我自己的虚拟机里差不多都找得到。
1 2 3 4 5 6 7 8 ubuntu@ubuntu:~$ lspci -t -v -[0000:00]-+-00.0 Intel Corporation 440FX - 82441FX PMC [Natoma] +-01.0 Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II] +-01.1 Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II] +-01.3 Intel Corporation 82371AB/EB/MB PIIX4 ACPI +-02.0 Device 1234:1111 +-03.0 Device 1234:11e9 \-04.0 Intel Corporation 82540EM Gigabit Ethernet Controller
其中[0000]
表示PCI的域,PCI域最多可以承载256条总线,每条总线最多32个设备,每个设备最多8个功能。
PCI设备通过VendorIDs
DeviceIDs
Class Codes
区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ubuntu@ubuntu:~$ lspci -vmns 00:03.0 Device: 00:03.0 Class: 00ff Vendor: 1234 Device: 11e9 SVendor: 1af4 SDevice: 1100 PhySlot: 3 Rev: 10 ubuntu@ubuntu:~$ lspci -vms 00:03.0 Device: 00:03.0 Class: Unclassified device [00ff] Vendor: Vendor 1234 Device: Device 11e9 SVendor: Red Hat, Inc SDevice: Device 1100 PhySlot: 3 Rev: 10
还有一些具体的命令查看信息,这里先不写。一般常用的就是lspci和lspci -v。
qemu中访问I/O空间 访问mmio与pmio都可以采用在内核态访问或在用户空间编程进行访问。
访问MMIO 编译内核/用户态编程
访问PMIO 编译内核/用户态编程
QOM编程模型
QEMU提供了一套面向对象编程的模型——QOM(QEMU Object Module),几乎所有的设备如CPU、内存、总线等都是利用这一面向对象的模型来实现的。
有几个比较关键的结构体,TypeInfo
、TypeImpl
、ObjectClass
以及Object
。其中ObjectClass、Object、TypeInfo定义在include/qom/object.h中,TypeImpl定义在qom/object.c中。
具体可以查看题目源码如何实现一个设备的。
例题 可以参考UAFIO大佬的wp: https://uaf.io/exploitation/2018/05/17/BlizzardCTF-2017-Strng.html
启动:sudo start.sh
。 IDA分析qemu二进制文件。
1 2 3 4 5 6 7 8 keenan@ubuntu:~/Desktop/strng$ checksec ./qemu-system-x86_64 [*] '/home/keenan/Desktop/strng/qemu-system-x86_64' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled
通过启动命令搜索strng找到相关函数:
lspci -v
可以看到:
1 2 3 4 5 6 00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10) Subsystem: Red Hat, Inc Device 1100 Physical Slot: 3 Flags: fast devsel Memory at febf1000 (32-bit, non-prefetchable) [size=256] I/O ports at c050 [size=8]
可以看到MMIO的地址在0xfebf1000,总共256字节。PMIO的端口从0xc050开始一共8个。
通过sysfs可以确认这一点:
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 ubuntu@ubuntu:~$ ls -la /sys/devices/pci0000\:00/0000\:00\:03.0/ total 0 drwxr-xr-x 3 root root 0 Feb 12 11:28 . drwxr-xr-x 11 root root 0 Feb 12 11:28 .. -rw-r--r-- 1 root root 4096 Feb 12 14:48 broken_parity_status -r--r--r-- 1 root root 4096 Feb 12 11:29 class -rw-r--r-- 1 root root 256 Feb 12 11:29 config -r--r--r-- 1 root root 4096 Feb 12 14:48 consistent_dma_mask_bits -rw-r--r-- 1 root root 4096 Feb 12 14:48 d3cold_allowed -r--r--r-- 1 root root 4096 Feb 12 11:29 device -r--r--r-- 1 root root 4096 Feb 12 14:48 dma_mask_bits -rw-r--r-- 1 root root 4096 Feb 12 14:48 enable lrwxrwxrwx 1 root root 0 Feb 12 14:48 firmware_node -> ../../LNXSYSTM:00/device:00/PNP0A03:00/device:06 -r--r--r-- 1 root root 4096 Feb 12 11:28 irq -r--r--r-- 1 root root 4096 Feb 12 14:48 local_cpulist -r--r--r-- 1 root root 4096 Feb 12 14:48 local_cpus -r--r--r-- 1 root root 4096 Feb 12 14:48 modalias -rw-r--r-- 1 root root 4096 Feb 12 14:48 msi_bus drwxr-xr-x 2 root root 0 Feb 12 14:48 power --w--w---- 1 root root 4096 Feb 12 14:48 remove --w--w---- 1 root root 4096 Feb 12 14:48 rescan -r--r--r-- 1 root root 4096 Feb 12 11:29 resource -rw------- 1 root root 256 Feb 12 14:48 resource0 <= 这里 -rw------- 1 root root 8 Feb 12 14:48 resource1 <= 这里 lrwxrwxrwx 1 root root 0 Feb 12 14:48 subsystem -> ../../../bus/pci -r--r--r-- 1 root root 4096 Feb 12 14:48 subsystem_device -r--r--r-- 1 root root 4096 Feb 12 14:48 subsystem_vendor -rw-r--r-- 1 root root 4096 Feb 12 11:28 uevent -r--r--r-- 1 root root 4096 Feb 12 11:29 vendor ubuntu@ubuntu:~$
这里的resource0和resource1就是MMIO和PMIO。
端口:
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 ubuntu@ubuntu:~$ sudo cat /proc/ioports sudo: unable to resolve host ubuntu 0000-0cf7 : PCI Bus 0000:00 0000-001f : dma1 0020-0021 : pic1 0040-0043 : timer0 0050-0053 : timer1 0060-0060 : keyboard 0064-0064 : keyboard 0070-0071 : rtc0 0080-008f : dma page reg 00a0-00a1 : pic2 00c0-00df : dma2 00f0-00ff : fpu 0170-0177 : 0000:00:01.1 0170-0177 : ata_piix 01f0-01f7 : 0000:00:01.1 01f0-01f7 : ata_piix 0376-0376 : 0000:00:01.1 0376-0376 : ata_piix 0378-037a : parport0 03c0-03df : vga+ 03f2-03f2 : floppy 03f4-03f5 : floppy 03f6-03f6 : 0000:00:01.1 03f6-03f6 : ata_piix 03f7-03f7 : floppy 03f8-03ff : serial 0600-063f : 0000:00:01.3 0600-0603 : ACPI PM1a_EVT_BLK 0604-0605 : ACPI PM1a_CNT_BLK 0608-060b : ACPI PM_TMR 0700-070f : 0000:00:01.3 0cf8-0cff : PCI conf1 0d00-ffff : PCI Bus 0000:00 afe0-afe3 : ACPI GPE0_BLK c000-c03f : 0000:00:04.0 c000-c03f : e1000 c040-c04f : 0000:00:01.1 c040-c04f : ata_piix c050-c057 : 0000:00:03.0 <= 这里 ubuntu@ubuntu:~$
反编译后,由于符号带着,找到STRNGState结构体,看一下结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 00000000 STRNGState struc ; (sizeof=0xC10, align=0x10, copyof_3815) 00000000 pdev PCIDevice_0 ? 000008F0 mmio MemoryRegion_0 ? 000009F0 pmio MemoryRegion_0 ? 00000AF0 addr dd ? 00000AF4 regs dd 64 dup(?) 00000BF4 db ? ; undefined 00000BF5 db ? ; undefined 00000BF6 db ? ; undefined 00000BF7 db ? ; undefined 00000BF8 srand dq ? ; offset 00000C00 rand dq ? ; offset 00000C08 rand_r dq ? ; offset 00000C10 STRNGState ends
对应源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #define STRNG_MMIO_REGS 64 #define STRNG_MMIO_SIZE (STRNG_MMIO_REGS * sizeof(uint32_t)) #define STRNG_PMIO_ADDR 0 #define STRNG_PMIO_DATA 4 #define STRNG_PMIO_REGS STRNG_MMIO_REGS #define STRNG_PMIO_SIZE 8 typedef struct { PCIDevice pdev; MemoryRegion mmio; MemoryRegion pmio; uint32_t addr; uint32_t regs[STRNG_MMIO_REGS]; <= 64 void (*srand)(unsigned int seed); int (*rand)(void ); int (*rand_r)(unsigned int *seed); } STRNGState;
strng_class_init 在strng_class_init函数里,修改指针类型为E1000BaseClass(搜索PCIDeviceClass
直接定位),可以看到对device_id等的赋值操作:
对应源码的:
strng_instance_init
源码:
赋值了函数指针。
pci_strng_realize 注册MMIO和PMIO的空间。
可以看到这里strng_mmio_ops
注册0x100空间,strng_pmio_ops
注册8字节空间。
源码:
ops有对应的read、write操作,一般就是漏洞函数。
MMIO相关函数 strng_mmio_read
源码:
将addr右移2位作为regs下标返回寄存器的值。
strng_mmio_write
源码:
当size=4的时候,addr右移2位作为索引saddr,提供4个功能:
saddr=0,调用srand,但是不在内存中赋值
saddr=1,调用rand,结果存储在regs[1]中
saddr=3,调用rand_r,regs[2]作为参数,结果存储在regs[3]中
其他,val存在regs[srand]中
这里addr和val是可控的,那么saddr也就可控,结合read和write岂不是可以越界读写?
看起来似乎是addr
可以由我们控制,可以使用addr
来越界读写regs
数组。即如果传入的addr大于regs的边界,那么我们就可以读写到后面的函数指针了。但是事实上是不可以的,前面已经知道了mmio
空间大小为256,我们传入的addr是不能大于mmio
的大小;因为pci设备内部会进行检查,而刚好regs
的大小为256,所以我们无法通过mmio
进行越界读写。
不过可以作为另一个出题思路。
编程访问MMIO 实现对MMIO空间的访问,比较便捷的方式就是使用mmap
函数将设备的resource0
文件映射到内存中,再进行相应的读写即可实现MMIO的读写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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[]) { 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" ); }
作为demo程序。