Joe1sn's Cabin

【CTF】2026 VNCTF Re 复现与WriteUp

好久没打CTF了,得快三年了吧。有一道安卓逆向就不写了,12月重装系统后没有环境

1. ez_maze

最短路径即为flag

image-20260202084931310

image-20260202084426729

MFC程序,常规IDA

image-20260202084520522

image-20260202084529626

一眼壳,上动态调试,还好是签到题,假设没有反调试

快速定位

直接运行到程序开始,定位字符串

image-20260202085011968

image-20260202085052446

下个断点,然后dump

image-20260202085217460

然后IDA

image-20260202085626673

发现验证算法的方向是反着来的。可以翻过去看看迷宫生成算法,其实在附件没更新之前的版本更好观察一点,但是直接在内存里面看更快些。

image-20260202090209093

image-20260202090154092

部分操作迷宫可能直接走死,没有到这个判断,跟一下rcx就行了的值,然后看rcx+0x260

image-20260202090546440

还原迷宫

1
2
3
4
5
6
7
8
9
from collections import deque
test = [0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0,0,0,1,0,1,0,1,0,1,0,0,0,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,0,1,1,1,0,1,1,1,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,1,1,1,1,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1,1,1,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,1,0,1,0,1,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,0,1,1,1,0,1,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,
]

for i in range(1, len(test)+1):
if i % 20 == 0:
print(test[i-1],)
else:
print(test[i-1], end="")

image-20260202090824687

解出flag

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
maze = [[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1],
[0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
[0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1],
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1],
[0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
[0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
[0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1],
[0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1],
[0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1],
[0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]]


def shortest_path_wasd(maze, start, end):
rows, cols = len(maze), len(maze[0])
visited = [[False] * cols for _ in range(rows)]
parent = [[None] * cols for _ in range(rows)]

queue = deque()
queue.append(start)
visited[start[0]][start[1]] = True

# 四个方向及对应 WASD
directions = [(-1, 0, 's'), (1, 0, 'w'), (0, -1, 'd'), (0, 1, 'a')]

while queue:
x, y = queue.popleft()
if (x, y) == end:
# 回溯路径生成 WASD
path = []
while parent[x][y] is not None:
px, py, move = parent[x][y]
path.append(move)
x, y = px, py
return ''.join(path[::-1]) # 反转得到正确顺序

for dx, dy, move in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < rows and 0 <= ny < cols:
if not visited[nx][ny] and maze[nx][ny] == 0:
visited[nx][ny] = True
parent[nx][ny] = (x, y, move)
queue.append((nx, ny))

return None # 无法到达终点

start = (0, 0)
end = (19, 19)
wasd_path = shortest_path_wasd(maze, start, end)
print("WASD 最短路径:", wasd_path)

得到flag, wwaaaaaaaawwwwddwwddwwaaaawwaaaaaassssaawwwwaawwwwwwwa

image-20260202091003793

其实这道题可以学的更多的东西

  • 在定位的时候可以了解MFC的Dialog机制,包括SendMessage等windows消息队列机制。虽然在比赛中太慢了。
  • 手动脱壳,然后逆向分析程序(还没试过)

2. delicious obf

能解出复杂的逆向题,却摸不透她的心。你能克服重重阻碍,找到隐藏在她内心深处的flag吗

flag长度为32

字符串找到主要逻辑

image-20260205180754535

跟一小段,到0x14000655+4

image-20260205180825850

image-20260205180904948

第一段是正常指令,后续的是跳转的混淆的花指令,尝试使用ida提取出主要汇编,有jmpcall我是手动跟进去看的

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
import ida_bytes
import ida_ua
import idaapi
import ida_lines
import idc

START_ADDR = 0x00140006560
MAX_STEPS = 200
def force_code(ea):
# 删除原有 item,强制当作 code
ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE)
return ida_ua.create_insn(ea)


def get_first_insn(ea, limit=0x40):
"""
从 ea 开始逐字节尝试 decode,找第一条有效指令
"""
insn = ida_ua.insn_t()
cur = ea
end = ea + limit

while cur < end:
if ida_ua.decode_insn(insn, cur):
return cur, insn
cur += 1

return idaapi.BADADDR, None


def format_insn(ea):
"""
打印标准反汇编(避免乱码)
"""
line = idc.generate_disasm_line(ea, 0)
if line:
print(f"--- 0x{ea:X}: {line}")
else:
print(f"--- 0x{ea:X}: <no disasm>")


def find_lea_r10(start_ea, limit=0x200):
insn = ida_ua.insn_t()
cur = start_ea
end = start_ea + limit

while cur < end:
ida_ua.create_insn(cur)

if not ida_ua.decode_insn(insn, cur):
cur += 1
continue

mnem = insn.get_canon_mnem().lower()

if mnem == "lea":
dis = idc.generate_disasm_line(cur, 0)
# print(f" [SCAN] 0x{cur:X}: {dis}")

# 直接用 print_operand,更稳
op0 = idc.print_operand(cur, 0)
if op0 and op0.lower() == "r10":
new_base = idc.get_operand_value(cur, 1)
return cur, new_base

if insn.size > 0:
cur += insn.size
else:
cur += 1

return idaapi.BADADDR, None


def main():
addr = START_ADDR
step = 0

print("[*] Follow trampoline chain start at 0x%X\n" % addr)

while step < MAX_STEPS:
step += 1
# 1. 从 addr 开始强制为 code
force_code(addr)

# 2. 找第一段有效汇编(第一条有效指令)
first_ea, first_insn = get_first_insn(addr)
if first_ea == idaapi.BADADDR:
print("[-] No valid instruction near 0x%X" % addr)
break
format_insn(first_ea)

# 3. 找第二段:lea r10, new_addr
lea_ea, new_base = find_lea_r10(first_ea)
if lea_ea == idaapi.BADADDR or not new_base:
print("[!] lea r10 not found, stop.")
break
addr = new_base + 4

print("[*] Done.")


if __name__ == "__main__":
main()

最后一步步得到汇编

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
--- 0x140006560: push    rbp
--- 0x1400060BE: mov rbp, rsp
--- 0x140006181: and rsp, 0FFFFFFFFFFFFFFF0h
--- 0x140006095: sub rsp, 240h
--- 0x140006323: mov [rbp+10h], rcx
--- 0x140006583: mov [rbp+18h], rdx
--- 0x140006046: mov rax, [rbp+10h]
--- 0x140005E03: mov rcx, rax
--- 0x140005E9C: call strlen
--- 0x14000615C: mov rcx, rax
--- 0x140005FAA: mov rdx, [rbp+10h]
--- 0x140005E72: lea rax, [rsp+1D0h]
--- 0x140005EEA: mov r8, rcx
--- 0x1400065F4: mov rcx, rax
--- 0x14000623F: call memcpy
--- 0x140005E28: mov rdx, [rbp+18h]
--- 0x14000601C: lea rax, [rsp+1D0h]
--- 0x1400064A2: mov rcx, rax
--- 0x140006218: call near ptr qword_1400043A8+0B6h
其中 RCX=输入,RDX==>00007FF60B845070 67 75 7C 73 7F 7D 75 30 64 7F 30 66 7E 73 64 76 gu|s.}u0d.0f~sdv
实际代码:
--- 0x140004A7C: push rbp
--- 0x140004C01: mov rbp, rsp
--- 0x1400048D5: push rbx
--- 0x140004E2D: sub rsp, 48h
--- 0x140005494: mov [rbp+10h], rcx
--- 0x14000511B: mov [rbp+18h], rdx
--- 0x140005340: mov dword ptr [rbp-14h], 0
--- 0x140004E01: jmp near ptr unk_140004489
----- jmp 后的实际代码
--- 0x140004F7B: mov eax, [rbp-14h]
--- 0x140005245: movsxd rbx, eax
--- 0x1400049D3: mov rax, [rbp+10h]
--- 0x140004D2A: mov rcx, rax
--- 0x1400045D2: call strlen
--- 0x140004B7B: popfq
--- 0x140004CD3: mov eax, 0
--- 0x1400048FD: mov rbx, [rbp-8]
--- 0x140004503: leave
--- 0x140004FF8: int 3; Trap to Debugger
--- 0x140004DD2: retn
--- 0x1400049A9: mov eax, [rbp-14h]
--- 0x140004855: cdqe
--- 0x140005398: mov rdx, [rbp+10h]
--- 0x1400051C6: add rax, rdx
--- 0x14000546A: movzx eax, byte ptr [rax]
--- 0x140004DD7: movzx eax, al
--- 0x14000536E: mov [rbp-18h], eax
--- 0x14000519C: mov eax, [rbp-14h]
--- 0x140005020: cdqe
--- 0x140004554: lea rdx, [rax+1]
--- 0x1400047AC: mov rax, [rbp+10h]
--- 0x140005416: add rax, rdx
--- 0x1400044D9: movzx eax, byte ptr [rax]
--- 0x140004D7E: movzx eax, al
--- 0x140004C2B: mov [rbp-1Ch], eax
--- 0x140004BD3: mov dword ptr [rbp-20h], 0
--- 0x1400048A7: mov dword ptr [rbp-28h], 9E3779B9h
--- 0x140004952: mov dword ptr [rbp-24h], 0
--- 0x140005170: jmp near ptr unk_140004AA8
-----jmp 后的实际代码
--- 0x140004AA8: cmp dword ptr [rbp-24h], 1Fh
--- 0x14000462C: popfq
--- 0x1400052C3: mov eax, [rbp-14h]
--- 0x140004F27: cdqe
--- 0x14000472C: mov rdx, [rbp+10h]
--- 0x140004782: add rax, rdx
--- 0x14000457F: mov edx, [rbp-18h]
--- 0x140004A28: mov [rax], dl
--- 0x1400050F1: mov eax, [rbp-14h]
--- 0x1400047D7: cdqe
--- 0x140004CFF: lea rdx, [rax+1]
--- 0x14000482A: mov rax, [rbp+10h]
--- 0x140004AD4: add rax, rdx
--- 0x140005146: mov edx, [rbp-1Ch]
--- 0x140004B52: mov [rax], dl
--- 0x140004485: add dword ptr [rbp-14h], 2
--- 0x140004F7B: mov eax, [rbp-14h]
--- 0x140005245: movsxd rbx, eax
--- 0x1400049D3: mov rax, [rbp+10h]
--- 0x140004D2A: mov rcx, rax
--- 0x1400045D2: call strlen
--- 0x140004B7B: popfq
--- 0x140004CD3: mov eax, 0
--- 0x1400048FD: mov rbx, [rbp-8]
--- 0x140004503: leave
--- 0x140004FF8: int 3; Trap to Debugger
--- 0x140004DD2: retn
--- 0x140004702: mov eax, [rbp-28h]
--- 0x1400049FE: add [rbp-20h], eax
--- 0x140004BA9: mov eax, [rbp-1Ch]
--- 0x140004928: shl eax, 4
--- 0x1400052ED: mov edx, eax
--- 0x1400051F0: mov rax, [rbp+18h]
--- 0x140004685: movzx eax, byte ptr [rax]
--- 0x140005316: movzx eax, al
--- 0x1400044B0: add edx, eax
--- 0x140004800: mov ecx, [rbp-1Ch]
--- 0x140004ED3: mov eax, [rbp-20h]
--- 0x140004980: add eax, ecx
--- 0x1400046D9: xor edx, eax
--- 0x140004CA9: mov eax, [rbp-1Ch]
--- 0x140005299: shr eax, 5
--- 0x140004B29: mov ecx, eax
--- 0x140004757: mov rax, [rbp+18h]
--- 0x140004A51: add rax, 1
--- 0x140004602: movzx eax, byte ptr [rax]
--- 0x140004DA8: movzx eax, al
--- 0x1400045A9: add eax, ecx
--- 0x1400053C3: xor eax, edx
--- 0x140004D54: add [rbp-18h], eax
--- 0x140004C7F: mov eax, [rbp-18h]
--- 0x140005440: shl eax, 4
--- 0x140004E81: mov edx, eax
--- 0x14000465A: mov rax, [rbp+18h]
--- 0x140004AFE: add rax, 2
--- 0x140004EFD: movzx eax, byte ptr [rax]
--- 0x1400053EC: movzx eax, al
--- 0x140004FCF: add edx, eax
--- 0x14000509D: mov ecx, [rbp-18h]
--- 0x14000526F: mov eax, [rbp-20h]
--- 0x140004EAA: add eax, ecx
--- 0x140005074: xor edx, eax
--- 0x14000521B: mov eax, [rbp-18h]
--- 0x1400046AF: shr eax, 5
--- 0x140004E58: mov ecx, eax
--- 0x140004F50: mov rax, [rbp+18h]
--- 0x140005049: add rax, 3
--- 0x140004FA5: movzx eax, byte ptr [rax]
--- 0x1400050C7: movzx eax, al
--- 0x14000452B: add eax, ecx
--- 0x14000487E: xor eax, edx
--- 0x140004C55: add [rbp-1Ch], eax
--- 0x140004AA4: add dword ptr [rbp-24h], 1
--- 0x14000462C: popfq
--- 0x1400052C3: mov eax, [rbp-14h]
--- 0x140004F27: cdqe
--- 0x14000472C: mov rdx, [rbp+10h]
--- 0x140004782: add rax, rdx
--- 0x14000457F: mov edx, [rbp-18h]
--- 0x140004A28: mov [rax], dl
--- 0x1400050F1: mov eax, [rbp-14h]
--- 0x1400047D7: cdqe
--- 0x140004CFF: lea rdx, [rax+1]
--- 0x14000482A: mov rax, [rbp+10h]
--- 0x140004AD4: add rax, rdx
--- 0x140005146: mov edx, [rbp-1Ch]
--- 0x140004B52: mov [rax], dl
--- 0x140004485: add dword ptr [rbp-14h], 2
--- 0x140004F7B: mov eax, [rbp-14h]
--- 0x140005245: movsxd rbx, eax
--- 0x1400049D3: mov rax, [rbp+10h]
--- 0x140004D2A: mov rcx, rax
--- 0x1400045D2: call strlen
--- 0x140004B7B: popfq
--- 0x140004CD3: mov eax, 0
--- 0x1400048FD: mov rbx, [rbp-8]
--- 0x140004503: leave
--- 0x140004FF8: int 3; Trap to Debugger
--- 0x140004DD2: retn
--- 0x140005F80: lea rax, [rsp+1D0h]
--- 0x1400064EC: mov rcx, rax
--- 0x140006451: call strlen
--- 0x1400065CF: mov rdx, rax
--- 0x140006478: lea rax, [rsp+1D0h]
--- 0x140005F0F: mov r8, rdx
--- 0x140006511: lea rdx, unk_140015000
--- 0x1400063E2: mov rcx, rax
--- 0x14000610C: call memcmp
--- 0x140006349: test eax, eax
--- 0x1400062B2: setz al
--- 0x140005FF7: movzx eax, al
--- 0x14000606C: mov [rsp+23Ch], eax
--- 0x1400061CE: lea r8, [rsp+20h]
--- 0x140005EC3: lea rcx, [rsp+40h]
--- 0x1400065A9: mov rdx, [rbp+10h]
--- 0x14000653A: mov rax, [rbp+18h]
--- 0x14000642C: mov r9, r8
--- 0x1400064C7: mov r8, rcx
--- 0x140006407: mov rcx, rax
--- 0x140005FD0: call sub_1400054BB
实际代码:
--- 0x1400054BB: lea r10, sub_140005C34
--- 0x140005C38: push rbp
--- 0x14000598C: mov rbp, rsp
--- 0x140005A34: push rbx
--- 0x140005907: sub rsp, 38h
--- 0x140005D13: mov [rbp+10h], rcx
--- 0x14000553C: mov [rbp+18h], rdx
--- 0x1400055BB: mov [rbp+20h], r8
--- 0x1400056B8: mov [rbp+28h], r9
--- 0x140005CE5: mov dword ptr [rbp-14h], 0
--- 0x140005C0A: mov dword ptr [rbp-18h], 0
--- 0x1400055E6: jmp loc_140005510
-----jmp 后的实际代码
--- 0x140005510 cmp dword ptr [rbp-18h], 0Fh
--- 0x140005514 pushfq
--- 0x140005BDC: popfq
--- 0x1400057DB: mov dword ptr [rbp-14h], 0
--- 0x140005833: mov dword ptr [rbp-1Ch], 0
--- 0x140005BB0: jmp loc_140005668
--- 0x140005D90: mov eax, [rbp-1Ch]
--- 0x14000588B: cdqe
--- 0x140005ADC: mov rdx, [rbp+18h]
--- 0x140005B86: add rax, rdx
--- 0x140005D3E: mov ecx, [rax]
--- 0x140005C60: mov r8, [rbp+20h]
--- 0x14000570D: mov eax, [rbp-14h]
--- 0x1400054E2: lea edx, [rax+1]
--- 0x140005809: mov [rbp-14h], edx
--- 0x14000578A: cdqe
--- 0x140005567: shl rax, 2
--- 0x140005A0A: add rax, r8
--- 0x140005592: mov [rax], ecx
--- 0x140005664: add dword ptr [rbp-1Ch], 4
--- 0x140005A84: mov eax, [rbp-1Ch]
--- 0x140005B31: movsxd rbx, eax
--- 0x1400058B4: mov rax, [rbp+18h]
--- 0x140005C8B: mov rcx, rax
--- 0x140005CB5: call strlen
--- 0x140005AAE: popfq
--- 0x140005937: mov rbx, [rbp-8]
--- 0x1400057B3: leave
--- 0x140005A5C: int 3; Trap to Debugger
--- 0x140005932: retn
--- 0x140005760: mov eax, [rbp-18h]
--- 0x1400059E1: cdqe
--- 0x1400059B6: mov rdx, [rbp+10h]
--- 0x140005861: add rax, rdx
--- 0x140005D67: mov ecx, [rax]
--- 0x140005B5B: mov r8, [rbp+28h]
--- 0x14000563A: mov eax, [rbp-14h]
--- 0x140005962: lea edx, [rax+1]
--- 0x140005B07: mov [rbp-14h], edx
--- 0x14000568F: cdqe
--- 0x140005DBA: shl rax, 2
--- 0x1400056E3: add rax, r8
--- 0x140005737: mov [rax], ecx
--- 0x14000550C: add dword ptr [rbp-18h], 4
--- 0x140005BDC: popfq
--- 0x1400057DB: mov dword ptr [rbp-14h], 0
--- 0x140005833: mov dword ptr [rbp-1Ch], 0
--- 0x140005BB0: jmp loc_140005668
-----jmp 后的实际代码
--- 0x140005A84: mov eax, [rbp-1Ch]
--- 0x140005B31: movsxd rbx, eax
--- 0x1400058B4: mov rax, [rbp+18h]
--- 0x140005C8B: mov rcx, rax
--- 0x140005CB5: call strlen
--- 0x140005AAE: popfq
--- 0x1400058DF: nop
--- 0x140005612: nop
--- 0x140005937: mov rbx, [rbp-8]
--- 0x1400057B3: leave
--- 0x140005A5C: int 3; Trap to Debugger
--- 0x140005932: retn
--- 0x140005D90: mov eax, [rbp-1Ch]
--- 0x14000588B: cdqe
--- 0x140005ADC: mov rdx, [rbp+18h]
--- 0x140005B86: add rax, rdx
--- 0x140005D3E: mov ecx, [rax]
--- 0x140005C60: mov r8, [rbp+20h]
--- 0x14000570D: mov eax, [rbp-14h]
--- 0x1400054E2: lea edx, [rax+1]
--- 0x140005809: mov [rbp-14h], edx
--- 0x14000578A: cdqe
--- 0x140005567: shl rax, 2
--- 0x140005A0A: add rax, r8
--- 0x140005592: mov [rax], ecx
--- 0x140005664: add dword ptr [rbp-1Ch], 4
--- 0x140005A84: mov eax, [rbp-1Ch]
--- 0x140005B31: movsxd rbx, eax
--- 0x1400058B4: mov rax, [rbp+18h]
--- 0x140005C8B: mov rcx, rax
--- 0x140005CB5: call strlen
--- 0x140005AAE: popfq
--- 0x1400058DF: nop
--- 0x140005612: nop
--- 0x140005937: mov rbx, [rbp-8]
--- 0x1400057B3: leave
--- 0x140005A5C: int 3; Trap to Debugger
--- 0x140005932: retn
--- 0x140005F5A: mov rax, [rbp+10h]
--- 0x140006372: mov rcx, rax
--- 0x1400061A7: call strlen
--- 0x140005F34: shr rax, 2
--- 0x140006397: mov ecx, eax
--- 0x1400062FC: lea rdx, [rsp+20h]
--- 0x1400063BB: lea rax, [rsp+40h]
--- 0x14000628D: mov r8, rdx
--- 0x140005E4E: mov edx, ecx
--- 0x1400062D7: mov rcx, rax
--- 0x140006266: call loc_140001977
实际代码:
--- 0x140001977 and [rcx+rax*8], ebp
--- 0x14000197A mov eax, ds:576526BE8120076Eh
--- 0x140001983 outsd
--- 0x140001984 pushfq
--- 0x140001986 push 0FFFFFFFFFFFFFFD7h
--- 0x140001988 push 79h ; 'y'
--- 0x14000198A db 46h
--- 0x14000198A cld
--- 0x14000198D outsb
--- 0x140006133: mov [rsp+238h], eax
--- 0x1400060E3: mov eax, [rsp+238h]
--- 0x1400061F5: leave
--- 0x14000636D: retn

这一步我就卡死了,在疑似tea的那段不管是调试还是写hook,都只能把加密算法拿出来,但是memcmp哪里我哪怕修改成为正确的结果也无法通过检验

后来看了 VNCTF2026 出题笔记 出题人是手动patch混淆的leajmp后如果ida识别为函数,可以使用ida的 Append function tail

image-20260205182911505

# Post author: Y&Y @保持好奇心
# Post link: http://example.com/2026/02/04/VNCTF2026出题笔记/
# Copyright Notice: All articles in this blog are licensed under (CC)BY-NC-SA unless stating additionally.

那么先 Edit -> Functions -> Delete function 删除函数,再选中这些汇编 Append function tail 把他们并入到函数 sub_1400054BB 中

image-20260205185419696

sub_14000445E

这个部分我通过自己的分析得到了加密的python函数

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
DELTA = 0x9E3779B9
ROUNDS = 32
KEY_BYTES = bytes([
0x67, 0x75, 0x7C, 0x73,
0x7F, 0x7D, 0x75, 0x30,
0x64, 0x7F, 0x30, 0x66,
0x7E, 0x73, 0x64, 0x76,
])


def u32(x):
return x & 0xFFFFFFFF


def encrypt_pair(v0, v1, key):
v0 = u32(v0)
v1 = u32(v1)

k0 = key[0]
k1 = key[1]
k2 = key[2]
k3 = key[3]

s = 0
for _ in range(ROUNDS):
s = u32(s + DELTA)
v0 = u32(v0 + (
((v1 << 4) + k0) ^
(v1 + s) ^
((v1 >> 5) + k1)
))
v1 = u32(v1 + (
((v0 << 4) + k2) ^
(v0 + s) ^
((v0 >> 5) + k3)
))
return v0 & 0xFF, v1 & 0xFF


def encrypt_bytes(data: bytes, key4):
out = bytearray()
i = 0
while i < len(data):
v0 = data[i]
v1 = data[i+1] if i+1 < len(data) else 0

c0, c1 = encrypt_pair(v0, v1, key4)
out.append(c0)
out.append(c1)

i += 2
return bytes(out)

这一点我用hook和调试确保代码是正确的,但是不知道为啥KEY_BYTES和ida中的预期不一样

sub_1400054BB

image-20260205191023437

没招了,直接问AI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall sub_140005907(
const char *src1, // a1
const char *src2, // a2
int *out1, // a3
int *out2 // a4
)
{
int count1 = 0;
int count2 = 0;

// Copy src1 bytes into out2 as ints
for (int i = 0; src1[i] != '\0'; i++)
{
out2[count1++] = (unsigned char)src1[i];
}

// Copy src2 bytes into out1 as ints
for (int i = 0; src2[i] != '\0'; i++)
{
out1[count2++] = (unsigned char)src2[i];
}

return sub_140005A5C();
}

loc_14000670B

image-20260205191208019

乱码,wp上写的是可以找到交叉引用,但是我的IDA在之前的去花指令后的效果有巨大不同。在我自己的ida脚本中也是的不出这一段的反汇编。不急hooksub_140006095后进行dump

image-20260205192152043

image-20260205192317865

测试发现函数长度0x15B,手动覆写回去

image-20260205194509264

还原的非常漂亮,解密算法直接喂给AI

wp中说这一段

img

# Post author: Y&Y @保持好奇心
# Post link: http://example.com/2026/02/04/VNCTF2026出题笔记/
# Copyright Notice: All articles in this blog are licensed under (CC)BY-NC-SA unless stating additionally.

发现如果有 int 3 中断,就对 loc_140001977 进行 smc,这里其实可以起一个反调试的作用,因为软件断点的原理就是把断点处汇编的第一个字节改为 0xCC。

但是我的ida一直都调不出来…

得到flag

根据程序直接拿到密文

image-20260205200320976

key有点麻烦,但是hook不麻烦。注意,这里不要直接Hook,估计是有SMC自解码,在解密之前都是这段乱码,如果你在解密之前hook,会造成解密失败

image-20260205201750285

image-20260205201813892

最后

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
#include<stdio.h>
#include <stdint.h>

void tea_decrypt(uint32_t* buf, int len, uint32_t* key)
{
uint32_t sum = 0;

// 先把 sum 走到加密结束时的位置
// 每个 block 32 轮,每轮 sum += delta
for (int i = 0; i < len; i += 2)
{
for (int j = 0; j <= 31; ++j)
{
sum += 0x61C88647;
}
}

// 现在倒序解密
for (int i = len - 2; i >= 0; i -= 2)
{
uint32_t v0 = buf[i];
uint32_t v1 = buf[i + 1];

for (int j = 0; j <= 31; ++j)
{
v0 += (v1 + ((v1 << 4) ^ (v1 >> 5)))
^ (key[sum & 3] + sum);

sum -= 0x61C88647;
v1 += (v0 + ((v0 << 4) ^ (v0 >> 5)))
^ (key[(sum >> 11) & 3] + sum);
}

buf[i] = v0;
buf[i + 1] = v1;
}
}

int main() {
uint32_t buf[] = { 0x738EA1B9, 0xF5B06584, 0xDCF952D5, 0x6FC28041,0x1DA40CF1, 0x07572A62, 0xB4C49903, 0x9BA536D8 };
uint32_t key[] = { 0xF9B2917F, 0x2A9D0847, 0x0C874A13, 0xA0253AD3 };

tea_decrypt(buf, 8, key);
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%c", (buf[i] >> (j * 8)) & 0xFF);
}
}
}
//VNCTF{N0w_Y0u_Kn0w_SMC_4nd_@bf!}

3. Shadow

说明:

  • 推荐在虚拟机Windows10-11运行该驱动。
  • 自行配备环境或签名用于加载驱动。
  • 请先运行.exe再加载驱动。
  • flag格式:VNCTF{你的输入}

谍影重重的迷宫,你能通过迷宫成功攻击宿主电脑吗?

还是第一次做这种驱动逆向,看了下题,不知道要干嘛,就去看obf那道题了

VNCTF2026-Shadow-WP的WP,是从exe的逆向开始

走迷宫

image-20260202092903432

然后开始分析驱动

image-20260202093501826

doSomeToPool,问问AI

image-20260202093819380

AES

静态+python脚本

sub_140001168AES加密过程,这个可以用插件找到S盒特征然后得出。拼接方式为ECB模式

7dce1c466ce04bea88c09a3f448c1bc2_720

image-20260202102702381

  1. 字节代换

    image-20260202101924064

    标准S盒

    image-20260202101907052

    image-20260202102001588

  2. 行移位

    image-20260202101513612

  3. 列混淆

    image-20260202101552132

编写脚本看看数据

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
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad


def aes_ecb_encrypt(plaintext: bytes, key: bytes) -> bytes:
"""
AES ECB 加密,自动 PKCS7 填充
key 长度必须是 16 / 24 / 32 字节
"""
cipher = AES.new(key, AES.MODE_ECB)
ct = cipher.encrypt(pad(plaintext, AES.block_size))
return ct


def aes_ecb_decrypt(ciphertext: bytes, key: bytes) -> bytes:
cipher = AES.new(key, AES.MODE_ECB)
pt = unpad(cipher.decrypt(ciphertext), AES.block_size)
return pt


if __name__ == "__main__":
key = bytes.fromhex("E9 BB 39 19 8B F5 CC BD E2 F0 55 93 11 18 98 CB")
data = bytes.fromhex("...")

enc = aes_ecb_encrypt(data, key)
dec = aes_ecb_decrypt(enc, key)

# print("Plain :", data)
# print("Cipher:", enc[:0x10])
# print("Decryp:", dec)
with open("mem_pe.exe", "b+a") as f:
f.write(enc)

image-20260202103709426

windbg

可以添加相关的符号类型便于逆向

image-20260202132703480

右键过后添加ntddk,wdk之类的

这里也可以尝试使用windbg调试

1
sxe ld Shadow

image-20260202111008159

image-20260202111825618

1
.writemem D:\CTF\vnctf\re\125431_Shadow\dump.exe ffffad82253aa000 ffffad82253aa000+5e00

image-20260202112148363

文件大小不一样,但是有效的PE文件大小是一样的

PE映射

由于我自己做过反射式dll注入的,这个也差不多(有AI其实并不难),只不过映射的是Sys PE,这里修改下类型变得好看一些

image-20260202114301781

导入IAT

image-20260202114210846

NewPE

上来就是个解密

image-20260202124259700

还原

1
2
3
4
5
6
7
cipher = [0xF1, 0xBB, 0xD9, 0xBD, 0xFA, 0xBF, 0xA5, 0xC1, 0xAE, 0xC3, 0xA5, 0xC5, 0xBF, 0xBA, 0xFE, 0xBC, 0xC5, 0xBE, 0xDA, 0xC0, 0xA2, 0xC2,
0xB6, 0xC4, 0xB1, 0xC6, 0xD3, 0xBB, 0xD3, 0xBD, 0xD0, 0xBF, 0x94, 0xC1, 0xAA, 0xC3, 0xB6, 0xC5, 0xA3, 0xBA, 0xDA, 0xBC, 0xD9, 0xBE, 0xBF, 0xC0]
plaintest = []
for i in range(0x2E):
plaintest.append(cipher[i] ^ (i % 0xD+0xBA))
print(bytes(plaintest).decode("utf-16-le").rstrip("\x00"))
#KeDelayExecutionThread

然后遍历Process找到宽字节Maze.exe程序

image-20260202130900416

PTE Hook

这个就是我的知识盲区了(已添加到TodoList),大致是通过修改物理页到虚拟页的映射来进行hook

1
2
3
4
5
原函数VA → 原物理页

修改PTE

原函数VA → Hook代码物理页

KeDelayExecutionThread 例程将当前线程置于指定间隔内可发出警报或不可更改的等待状态。

1
2
3
4
5
NTSTATUS KeDelayExecutionThread(
[in] KPROCESSOR_MODE WaitMode,
[in] BOOLEAN Alertable,
[in] PLARGE_INTEGER Interval
);

然后设置IRP类型,又解密Device的unicode名字。

image-20260202133339698

\Device\KeyboardClass0是由 kbdclass.sys 创建的键盘类设备对象。附加键盘驱动上,此时doIrpREAD就会接收到键盘相关消息。

doIrpREAD

image-20260202133747911

CompletionRoutine

先出两个加密

1
2
3
[LDriver] on input.

[LDriver] input end.

这是在 IRP_MJ_READ 完成后,从键盘输入缓冲区里解析扫描码,维护 Shift 状态,并把按键转换成 ASCII,然后存到内部缓冲区。

image-20260202135359536

相当于内核的键盘记录器,按下F12,输入flag,再按下F12,最后迷宫走到E判断flag

NewKeDelayExecutionThread

其实应该一开始就分析这段代码的,但是变量太杂、信息不全,看不出是干嘛的,这时候返回去看会发现引用了记录Key的数组

  1. 弄了一个很大的数

    image-20260202140714583

  2. 一堆自定义的加密

    image-20260202140740332

    enc2就是简单移位

  3. 最后一轮加密然后比较,应当有40字节成功,比较成功就蓝屏

    image-20260202140817824

动态调用

主要是 (v24)(KeylogBuffer, jj_2 + len, &MaybeKey);这一段经过了大段的加密,尝试动态调试

image-20260202143022637

image-20260202143353802

有其他的call,直接全部dump下来

1
.writemem D:\CTF\vnctf\re\125431_Shadow\dump_shellcode.exe ffffc98e`8b561210 ffffc98e`8b561a71

image-20260202145923299

精准戳中密码学功底,没招了,看上去像是RC4的结构。

image-20260202151733562

最后的解密

  1. 得到最后的Key

    image-20260202155503021

    image-20260202163624570

    1
    Key=0xe7d1cc85d2172c16

    后续的shellcode加密也可以检验

    image-20260202163939920

  2. 查看校验的答案

    image-20260202164014334

    image-20260202164244544

结合赛后看的wp的解密方法解密

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
#include <stdint.h>
#include <string.h>
#include <stdio.h>

#define ROR32(x, n) (((x) >> (n)) | ((x) << (32u - (n))))
#define ROL32(x, n) (((x) << (n)) | ((x) >> (32u - (n))))

static uint32_t xorshift32(uint32_t* s)
{
uint32_t x = *s;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
*s = x;
return x;
}

static void gen_sbox(const uint32_t k[2], uint8_t sbox[256], uint8_t inv[256])
{
for (int i = 0; i < 256; i++)
sbox[i] = (uint8_t)i;

uint32_t seed = k[0] ^ ROL32(k[1], 11) ^ 0xA5A5A5A5u ^ 0xB7E15163u;
for (int i = 255; i > 0; i--)
{
uint32_t r = xorshift32(&seed);
int j = (int)(r % (uint32_t)(i + 1));
uint8_t tmp = sbox[i];
sbox[i] = sbox[j];
sbox[j] = tmp;
}
for (int i = 0; i < 256; i++)
inv[sbox[i]] = (uint8_t)i;
}

static uint32_t inv_sub_bytes32(uint32_t x, const uint8_t inv[256])
{
return (uint32_t)inv[(x >> 0) & 0xFF] << 0 |
(uint32_t)inv[(x >> 8) & 0xFF] << 8 |
(uint32_t)inv[(x >> 16) & 0xFF] << 16 |
(uint32_t)inv[(x >> 24) & 0xFF] << 24;
}

static void expand_key(const uint32_t k[2], uint32_t rk[32])
{
uint32_t a = k[0] ^ 0xB7E15163u;
uint32_t b = k[1] + 0x9E3779B9u;

for (uint32_t i = 0; i < 32; i++)
{
uint32_t t = (a ^ ROL32(b, (a & 31u))) + (0xB7E15163u ^ (i * 0x9E3779B9u));
rk[i] = t ^ ROR32(a + b, (b & 31u));
a = b ^ rk[i];
b = t + ROL32(rk[i], (t & 31u));
}
}

static uint32_t tweak32(uint32_t idx, const uint32_t k[2])
{
uint32_t t = (idx + 1u) * 0x45D9F3Bu;
t ^= ROL32(k[0], (idx & 31u));
t += (k[1] ^ 0xDEADBEEFu);
t ^= t >> 16;
t *= 0x7FEB352Du;
t ^= t >> 15;
t *= 0x846CA68Bu;
t ^= t >> 16;
return t;
}

void decrypt_block(uint32_t v[2], const uint32_t k[2],
const uint32_t rk[32],
const uint8_t sbox[256],
const uint8_t inv[256],
uint32_t block_index)
{
uint32_t x = v[0], y = v[1];
uint32_t tw = tweak32(block_index, k);

x ^= (k[1] + ROL32(tw, 11));
y ^= (k[0] ^ tw);

uint32_t sum = 0;
for (uint32_t i = 0; i < 32; i++)
{
sum += (0xB7E15163u ^ rk[i]);
}

for (uint32_t r = 0; r < 32; r++)
{
uint32_t i = 32 - 1u - r;
if (sum & 1u)
{
uint32_t tmp = x;
x = y;
y = tmp;
}

uint32_t rotX = ((y ^ sum) + (rk[i] >> 1)) & 31u;
x = ROR32(x, rotX);

y -= (x ^ rk[i]);

uint32_t rotY = (uint32_t)sbox[x & 0xFFu] & 31u;
y = ROL32(y, rotY);

x ^= (y + ROR32(sum, 3)) ^ ROL32(rk[i], (y & 31u));

x -= (sum ^ rk[i]);

x = inv_sub_bytes32(x, inv);
y = inv_sub_bytes32(y, inv);

sum -= (0xB7E15163u ^ rk[i]);
}

x ^= (k[0] + tw);
y ^= (k[1] ^ ROR32(tw, 7));

v[0] = x;
v[1] = y;
}

void decrypt(uint8_t* buf, size_t len, const uint32_t k[2])
{
uint8_t sbox[256], inv[256];
uint32_t rk[32];
gen_sbox(k, sbox, inv);
expand_key(k, rk);

uint32_t idx = 0;
for (size_t off = 0; off + 8 <= len; off += 8, idx++)
{
uint32_t v[2];
memcpy(v, buf + off, 8);
decrypt_block(v, k, rk, sbox, inv, idx);
memcpy(buf + off, v, 8);
}
}

int main(void)
{
uint64_t Key = 0xe7d1cc85d2172c16;

unsigned char cipher[] =
{
0x51, 0xDA, 0xB8, 0x52, 0x73, 0xB9, 0x17, 0x00, 0xE0, 0x02, 0xF4, 0xB2, 0x2C, 0x5F, 0x22, 0x62,
0x33, 0x0C, 0x01, 0x44, 0xBB, 0x70, 0x9D, 0x92, 0x8A, 0x06, 0xF9, 0x2C, 0x1D, 0x8F, 0x0A, 0xA9,
0x22, 0x7B, 0x84, 0x30, 0x71, 0x13, 0xD0, 0xF9 };

decrypt(cipher, 40, (uint32_t*)&Key);

printf("%.40s\n", cipher);

return 0;
}

//ebbc8827-c040-4a7d-8bc7-0aeccb1ce094

从动态调试看,之前通过动态访问对密文和0x1c进行了异或

image-20260202152459537

image-20260202152542830

BSOD 😃

image-20260202170513575