DASCTF 2024.8 wp re

Maze

三维迷宫,两个迷宫切换,TLS中塞了点东西,识别出来之后再写个路径算法即可

TLS中一段字节与程序的一段xor来恢复逻辑

image-20251027212356696

140001A60(watchdogThreadMain)前面的一段逻辑用于反调试

image-20251027212900766

我们可以修补到这:

image-20251027213334210

执行0x90 ^ key[i]

1
2
3
4
5
6
7
8
9
10
11
12
13
import ida_bytes

PATCH_ADDR = 0x140001B16
PATCH_HEX = (
"8b05184a000039050e4a000074648b05064a00008905044a0000"
"833d0949000001750ac744242002000000eb08c7442420010000"
"008b4424208905eb480000833de448000001750e488d059b4400"
"004889442428eb0c488d05cd4600004889442428488d05c14600"
"00488b4c2428488948c0"
)

ida_bytes.patch_bytes(PATCH_ADDR, bytes.fromhex(PATCH_HEX))
print("watchdogThreadMain decrypted at 0x{:X}".format(PATCH_ADDR))

image-20251027213928829

image-20251027214619250

这里其实就是在切换地图(迷宫)

之后我们理解下迷宫就知道走法可以写脚本了

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
from collections import deque
from pathlib import Path
import hashlib

def load_mazes(exe_path="Maze.exe"):
data = Path(exe_path).read_bytes()
base = 0x4200 # file offset for .data
a = data[base:base + 0x200]
b = data[base + 0x240:base + 0x240 + 0x200]
if len(a) != 0x200 or len(b) != 0x200:
raise ValueError("Unexpected maze size")
return [list(a), list(b)]

def slide(pos, maze, move):
if move == 'a':
while True:
if pos % 8 - 1 < 0:
return None
if maze[pos - 1]:
return pos
pos -= 1
elif move == 'd':
while True:
if pos % 8 + 1 >= 8:
return None
if maze[pos + 1]:
return pos
pos += 1
elif move == 'w':
while True:
if pos % 64 - 8 < 0:
return None
if maze[pos - 8]:
return pos
pos -= 8
elif move == 's':
while True:
if pos % 64 + 8 >= 64:
return None
if maze[pos + 8]:
return pos
pos += 8
elif move == 'n':
while True:
if pos - 64 < 0:
return None
if maze[pos - 64]:
return pos
pos -= 64
elif move == 'u':
while True:
if pos + 64 >= 512:
return None
if maze[pos + 64]:
return pos
pos += 64
else:
raise ValueError("Unknown move")

def find_path(mazes, start=0, goal=436):
queue = deque([(start, 0)]) # (cell index, 0 for maze A / 1 for maze B)
parents = {(start, 0): (None, None)}
moves = ['a', 'd', 'w', 's', 'n', 'u']
while queue:
pos, parity = queue.popleft()
if pos == goal:
path = []
state = (pos, parity)
while parents[state][0] is not None:
state, ch = parents[state]
path.append(ch)
return ''.join(reversed(path))
maze = mazes[parity]
for ch in moves:
new_pos = slide(pos, maze, ch)
if new_pos is None:
continue
state = (new_pos, 1 - parity)
if state not in parents:
parents[state] = ((pos, parity), ch)
queue.append(state)
raise RuntimeError("No path found")

def main():
mazes = load_mazes()
path = find_path(mazes)
flag = hashlib.md5(path.encode()).hexdigest()
print(f"path: {path}")
print(f"flag: {flag}")

if __name__ == "__main__":
main()

path: sdwusanwduawus

flag{1bb5fd78f2299f26ccc0630c5e7516b6}

ezCPP

通过 VirtualProtect 修改该派生类对象的 vtable 条目,使其指向两个自定义函数:sub_140001A60(抛出 runtime_error)和 sub_140001AA0(真正的“检查”函数)。异常路径 sub_140001A60 只在内存分配失败时使用,表面上属于反调试/反破坏保护。

1
2
3
4
_BOOL8 sub_140001AA0()
{
return 5 / 0 != 0;
}

sub_140001AA0 在进入时先执行一次 idiv 0;关键部分调用 sub_140001BA0,返回布尔值作为最终校验。

image-20251106223145670

sub_140001BA0 逻辑较复杂:

  1. VirtualAlloc 一块 0x130 大小的可执行内存,把 src__0(长度 0x130,位于 .rdata)拷贝过去。
  2. catch 块里把该内存逐字节 XOR 0xDA 解密,得到一段自修改后的代码;随后用 (BeingDebugged) 作为参数调用。我们已用脚本解密出这段代码,确认它是一个 64 位函数,实现了 32 轮 XTEA 风格的运算,常量 0xF3E56、0x42CA4455、0x8E0AE93B、0xA569C4D0 作为 128bit key,Delta 为 0x523A855B。
  3. 整体结构符合典型 XTEA,使用 key table 和 delta 做 32 轮更新。
  4. 函数结果传入 sub_140001820 -> sub_1400029D0(转换为 std::string)。随后 sub_140001DE0 拷贝 src__1(21 字节字符串)到一个缓冲,并通过 sub_140002290 在必要时执行 (i%5+3) XOR 的互斥解密;再与刚生成的字符串比较 (sub_140002DF0)。若匹配则返回 true。
  5. 若失败,再进行一次几乎相同的流程,换用 sub_140001E80 拷贝的另一串(“6?<82>967 4:1=??468$\a”),走 (i%10+7) XOR 的解码,再比较。

image-20251106223349436

这是还原后的代码,dump到了一个单独的二进制文件反编译分析

image-20251106223557522

字节异或

image-20251106223757160

image-20251106223928879

已经验证:

  • sub_140001D70 的 XOR 方案对 src__1 生成的明文是 12055721120662551337。
  • sub_140001E10 对第二串产生明文 17529248803287439874。
  • 对应到上一步 XTEA 输出必须等于这两种明文之一(先比第一串,失败再比第二串)。
1
2
3
4
5
6
7
8
9
10
11
src_1 = bytes.fromhex("32 36 35 33 32 34 36 34 37 35 33 32 33 34 32 36 35 36 35 30 03")
src_3 = bytes.fromhex("36 3F 3C 38 32 3E 39 36 37 20 34 3A 31 3D 3F 3F 34 36 38 24 07")
print(len(src_1))
def xor_decode(data, mod, offset):
return bytes(((i % mod) + offset) ^ b for i, b in enumerate(data))

goal1 = xor_decode(src_1, mod=5, offset=3).rstrip(b"\x00").decode()
goal2 = xor_decode(src_3, mod=10, offset=7).rstrip(b"\x00").decode()

print("target #1:", goal1)
print("target #2:", goal2)

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
#!/usr/bin/env python3
DELTA = 0xF3E56
KEY_BASE = [0x42CA4455, 0x8E0AE93B, 0xA569C4D0, 0x523A855B]
TARGETS = [
"12055721120662551337",
"17529248803287439874",
]

def tea_decrypt(result, being_debugged=0):
a1 = (result >> 32) & 0xFFFFFFFF
a2 = result & 0xFFFFFFFF
key = KEY_BASE.copy()
key[3] = being_debugged + 0x523A855B
sum_ = (DELTA * 0x20) & 0xFFFFFFFF
for _ in range(0x20):
idx = (sum_ >> 11) & 3
a2 = (a2 - ((key[idx] + sum_) ^ (a1 + ((a1 >> 5) ^ ((a1 << 4) & 0xFFFFFFFF))))) & 0xFFFFFFFF
sum_ = (sum_ - DELTA) & 0xFFFFFFFF
idx = sum_ & 3
a1 = (a1 - ((key[idx] + sum_) ^ (a2 + ((a2 >> 5) ^ ((a2 << 4) & 0xFFFFFFFF))))) & 0xFFFFFFFF
return (a1 << 32) | a2


def main():
for s in TARGETS:
value = int(s)
block = tea_decrypt(value)
candidate = block.to_bytes(8, "big")
print(f"target {s} -> {candidate!r}")
print("\nflag input:", tea_decrypt(int(TARGETS[1])).to_bytes(8, "big").decode())


if __name__ == "__main__":
main()

7up@fT3A

结果是错的,稍微短了点,应该有哪里漏了

忘记了 程序是根据是否被调试来进行解密的

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
#!/usr/bin/env python3
DELTA = 0xF3E56
KEY_BASE = [0x42CA4455, 0x8E0AE93B, 0xA569C4D0, 0x523A855B]
TARGETS = [
"12055721120662551337",
"17529248803287439874",
]

def tea_decrypt(result, being_debugged=0):
result ^= being_debugged
a1 = (result >> 32) & 0xFFFFFFFF
a2 = result & 0xFFFFFFFF
key = KEY_BASE.copy()
key[3] = being_debugged + 0x523A855B
sum_ = (DELTA * 0x20) & 0xFFFFFFFF
for _ in range(0x20):
idx = (sum_ >> 11) & 3
a2 = (a2 - ((key[idx] + sum_) ^ (a1 + ((a1 >> 5) ^ ((a1 << 4) & 0xFFFFFFFF))))) & 0xFFFFFFFF
sum_ = (sum_ - DELTA) & 0xFFFFFFFF
idx = sum_ & 3
a1 = (a1 - ((key[idx] + sum_) ^ (a2 + ((a2 >> 5) ^ ((a2 << 4) & 0xFFFFFFFF))))) & 0xFFFFFFFF
return (a1 << 32) | a2


def main():
print("\nflag input:", tea_decrypt(int(TARGETS[0]),1).to_bytes(8, "big").decode())
print("\nflag input:", tea_decrypt(int(TARGETS[1]),0).to_bytes(8, "big").decode())


if __name__ == "__main__":
main()

G0g3tTea7up@fT3A

DASCTF{G0g3tTea7up@fT3A}

FakeApple

TSCTF-J 2025 wp | Matriy’s blog

最后一题