好久没打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

还原迷宫

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

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我是手动跟进去看的

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()

最后一步步得到汇编

--- 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函数

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

__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

最后

#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

编写脚本看看数据

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调试

sxe ld Shadow

image-20260202111008159

image-20260202111825618

.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

还原

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

原函数VA → 原物理页

修改PTE

原函数VA → Hook代码物理页

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

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

先出两个加密

[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下来

.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

    Key=0xe7d1cc85d2172c16

    后续的shellcode加密也可以检验

    image-20260202163939920

  2. 查看校验的答案

    image-20260202164014334

    image-20260202164244544

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

#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