最近想学习一下关于qemu逃逸的pwn题目。学习资源来自于微信公众号“平凡路上”。感谢大佬整理。

目前qemu出问题比较多的地方以及比赛中出题目的形式都在在设备模拟中,因此后续也会将关注点主要放在设备模拟上。提供了例题:Blizzard CTF 2017 Strng:

qemu概述

每个qemu虚拟机对应一个qemu进程,虚拟机的线程对应qemu进程的线程。

关于qemu逃逸在phrack上有知识介绍:

image-20200212135139446

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的系统:

image-20200212140945948

可以通过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,这样在宿主机就可以找到字符串的地址:

image-20200212160009899

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个字节的格式是一定的,包含头部的类型、设备的总类、设备的性质以及制造商等,格式如下:

image-20200212160610352

Ref: https://en.wikipedia.org/wiki/PCI_configuration_space

其中的BAR(Base Address Register)比较重要,共6个。BAR记录所需的地址空间的类型、基地址、和其他属性。PCI BAR分为两种,Memory BAR(最低位为0)和I/O BAR(最低位为1)。结构如图:

image-20200212182724229

当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、内存、总线等都是利用这一面向对象的模型来实现的。

有几个比较关键的结构体,TypeInfoTypeImplObjectClass以及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找到相关函数:

image-20200212205019619

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等的赋值操作:

image-20200212222922325

对应源码的:

image-20200212222946832

strng_instance_init

image-20200212230856790

源码:

image-20200212230913103

赋值了函数指针。

pci_strng_realize

注册MMIO和PMIO的空间。

image-20200212231311669

可以看到这里strng_mmio_ops注册0x100空间,strng_pmio_ops注册8字节空间。

源码:

image-20200212231436956

ops有对应的read、write操作,一般就是漏洞函数。

MMIO相关函数

strng_mmio_read

image-20200212235620963

源码:

image-20200212235640613

将addr右移2位作为regs下标返回寄存器的值。

strng_mmio_write

image-20200213000019613

源码:

image-20200213000508581

当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[])
{

// Open and map I/O memory for the strng device
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程序。