2025 TPCTF RE wp

linuxpdf

打开附件 下面有个github的链接 打开之后是一个网页的linux 观察一下原本的和附件的区别 发现这里多加载了很多东西

linuxpdf

应该是出题人加入的 多运行几次 可以发现最后的那个a9加载完毕之后 Flag出现 所以可以猜测那个a9就是主要的文件
扔010里 发现了很多base64加密的字符串 写个脚本解压保存文件

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
import base64  
import zlib
import os
from pathlib import Path
from typing import Dict, Union


def decode_and_inflate(encoded_data: str) -> bytes:
decoded = base64.b64decode(encoded_data)
return zlib.decompress(decoded, wbits=15)


def save_content(
output_path: Union[str, Path],
binary_data: bytes,
ensure_dir: bool = True
) -> None:
path = Path(output_path)
if ensure_dir:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(binary_data)


def extract_files(
file_map: Dict[str, str],
output_root: Union[str, Path] = ""
) -> None:
for filename, b64_data in file_map.items():
try:
uncompressed = decode_and_inflate(b64_data)
target_path = Path(output_root) / filename
save_content(target_path, uncompressed)
print(f"成功提取: {target_path}")
except (ValueError, zlib.error) as e:
print(f"解码/解压失败 {filename}: {str(e)}")
except OSError as e:
print(f"文件写入失败 {filename}: {str(e)}")


if __name__ == "__main__":
file_flag = {
"vm_64.cfg": "eNpNTtFugzAMfO..." #太长了不放了
}
extract_files(file_flag, "output_files")

随后直接找a9 然后扔ida9.0里 (实测ida8.3 7.7没办法F5 不知道什么原因┭┮﹏┭┮)

linuxpdf-1

跟进 发现md5初始化

linuxpdf-2

随后看unk_4008 发现是一堆md5值 中间用00分隔开

linuxpdf-3

提取出来爆破一下 发现最后三个爆破的很快 结果如下

CB0FC813755A45CE5984BFBA15847C1E——>F}

DF88931E7EEFDFCC2BB80D4A4F5710FB——>DF}

D6AF7C4FEDCF2B6777DF8E83C932F883——>PDF}

且有规律 爆破出来的字符在上一个的前面 所以可以猜到是从后往前依次爆破 直接把所有md5值提取出来 写脚本即可

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
import hashlib  
import itertools
import sys

chars = [chr(i) for i in range(32, 127)]

def crack_md5_chain(target_hashes):
cracked = []
previous_plain = None

for idx, target_hash in enumerate(target_hashes):
target_hash = target_hash.lower().strip()
if idx == 0:
for candidate in itertools.product(chars, repeat=2):
plain = ''.join(candidate)
if hashlib.md5(plain.encode()).hexdigest() == target_hash:
print(f"[+] 成功破解第1个哈希: {plain}")
previous_plain = plain
cracked.append(plain)
break
else:
print("[-] 无法破解第一个哈希,终止流程")
return None
else: # 后续哈希处理
found = False
for c in chars:
plain = c + previous_plain
if hashlib.md5(plain.encode()).hexdigest() == target_hash:
print(f"[+] 成功破解第{idx + 1}个哈希: {plain}")
previous_plain = plain
cracked.append(plain)
found = True
break if not found:
print(f"[-] 无法破解第{idx + 1}个哈希,终止流程")
return None
return cracked


def main():
if len(sys.argv) != 2:
print(f"用法: {sys.argv[0]} <哈希文件>")
return

with open(sys.argv[1]) as f:
hashes = [line.strip() for line in f if line.strip()]

if not hashes:
print("错误:哈希文件为空")
return

print("开始破解MD5链...")
result = crack_md5_chain(hashes)

if result:
print("\n破解结果链:")
for i, plain in enumerate(result):
print(f"哈希{i + 1}: {plain}")
else:
print("\n未能完整破解哈希链")

if __name__ == "__main__":
main()

linuxpdf-4

portable

ida打开附件 查看字符串 发现flag头

portable

随后在start函数开始 往下翻函数 在最后找到输入flag的函数 sub_407F30

portable-1

在下面找到一个异或 下面显示输入正确

portable-2

找一下密文跟密钥 扔赛博厨子里解一下得到flag

portable-3

ef8695a42a6fbbf1b93202dc9f7bb705

stone-game

python的exe 直接解包然后反编译 是个小游戏 pwntools直接交互打
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
from pwn import *

from functools import reduce

import random



# 定义服务器地址和端口

HOST = '1.1.1.1'

PORT = 0000



def calculate_nim_sum(stones):

"""计算所有非空段的 Nim-sum"""

return reduce(lambda x, y: x ^ y, [s for s in stones if s > 0], 0)



def get_nim_move(stones, tried_moves):

"""生成基于 Nim-sum 的移动"""

nim_sum = calculate_nim_sum(stones)

move = [0] * 7

non_zero_segments = [i for i, s in enumerate(stones) if s > 0]

if not non_zero_segments:

return move # 无可移除的石头

if nim_sum != 0:

# 尝试使 Nim-sum 变为 0

for seg in non_zero_segments:

target = stones[seg] ^ nim_sum

if target < stones[seg]:

move[seg] = stones[seg] - target

if tuple(move) not in tried_moves:

return move

# 如果无法直接使 Nim-sum 为 0,或移动无效,随机移除 1 个石头

seg = random.choice(non_zero_segments)

move[seg] = 1

while tuple(move) in tried_moves and len(tried_moves) < len(non_zero_segments):

seg = random.choice(non_zero_segments)

move = [0] * 7

move[seg] = 1

return move



def play_round(p, round_num):

"""在一轮比赛中进行交互,不关闭连接"""

log.info(f"开始第 {round_num} 轮")



# 等待轮次提示,例如 "=== Round 1/100 ==="

p.recvuntil(f'=== Round {round_num}/100 ==='.encode())

# 解析初始石头数量

p.recvuntil(b'Current stone count:')

stones = []

for i in range(7):

line = p.recvline().decode().strip()

while not line.startswith(f'Segment {i+1}:'):

log.warning(f"跳过意外行: '{line}'")

line = p.recvline().decode().strip()

count = int(line.split(': ')[1].split(' ')[0])

stones.append(count)

log.info(f"初始石头数量: {stones}")

# 检查当前玩家并发送第一次移动

p.recvuntil(b'Current player: ')

player = p.recvline().decode().strip()

assert player == 'You', "预期玩家先手"

p.recvuntil(b'Enter the number of stones to remove from each segment')

tried_moves = set()

move = get_nim_move(stones, tried_moves)

move_str = ' '.join(map(str, move))

log.info(f"发送初始移动: {move_str}")

p.sendline(move_str.encode())

tried_moves.add(tuple(move))

# 主交互循环

while True:

data = p.recvrepeat(timeout=1).decode()

log.info(f"收到数据: {data}")

if 'You won this round!' in data:

log.success(f"第 {round_num} 轮胜利!")

if 'Next round starting in 2 seconds...' in data:

log.info("等待下一轮开始...")

sleep(3) # 等待 3 秒,确保服务器发送新一轮提示

return True

elif 'AI won this round!' in data:

log.warning(f"第 {round_num} 轮失败!")

if 'Next round starting in 2 seconds...' in data:

log.info("等待下一轮开始...")

sleep(3) # 等待 3 秒,确保服务器发送新一轮提示

return False

if 'Invalid move! Remember you cannot form cycles.' in data:

log.warning("移动无效,重新尝试")

move = get_nim_move(stones, tried_moves)

move_str = ' '.join(map(str, move))

log.info(f"发送新移动: {move_str}")

p.sendline(move_str.encode())

tried_moves.add(tuple(move))

continue

if 'Current stone count:' in data:

lines = data.split('\n')

stones = []

for i in range(7, 0, -1):

for line in reversed(lines):

if line.startswith(f'Segment {i}:'):

count = int(line.split(': ')[1].split(' ')[0])

stones.insert(0, count)

break

log.info(f"更新石头数量: {stones}")

tried_moves.clear()

if 'AI decides to remove:' in data:

for line in data.split('\n'):

if 'AI decides to remove:' in line:

ai_move = line.split('AI decides to remove: ')[1].strip()

log.info(f"AI 移动: {ai_move}")

break

if 'Current player: You' in data and 'Enter the number of stones to remove from each segment' in data:

move = get_nim_move(stones, tried_moves)

move_str = ' '.join(map(str, move))

log.info(f"发送移动: {move_str}")

p.sendline(move_str.encode())

tried_moves.add(tuple(move))



# 主逻辑:使用单一连接运行所有轮次

p = remote(HOST, PORT)



# 跳过初始欢迎信息

p.recvuntil(b'Press Enter to start...')

p.sendline(b'')



player_wins = 0

for round_num in range(1, 101):

if play_round(p, round_num):

player_wins += 1

log.info(f"当前得分: {player_wins}/{round_num}")



# 在所有轮次结束后,接收服务器的最终输出

log.info("所有轮次完成,接收服务器最终输出...")

final_data = p.recvrepeat(timeout=2).decode() # 等待 2 秒,确保接收完整输出

log.info(f"服务器最终输出: {final_data}")



# 检查是否获得 Flag

if 'Congratulations!' in final_data:

log.success("成功获得 Flag!")

for line in final_data.split('\n'):

if line.strip() and not line.startswith('Final score') and not line.startswith('=== Challenge Complete ===') and not line.startswith('Congratulations'):

log.success(f"Flag: {line.strip()}")

elif 'Better luck next time!' in final_data:

log.error("未达到胜利阈值,未获得 Flag")



p.close() # 接收完输出后关闭连接



log.info(f"最终得分: {player_wins}/100")

if player_wins >= 90:

log.success("本地统计:赢得足够轮次,可能获得 Flag!")

else:

log.error(f"本地统计:未达到 90/100 的胜利要求,得分: {player_wins}/100")

chase

玩游戏通关得到第一段flag

67602d1264c4a5844789918e582322eb

在ppu查看器里看见第二段flag

chase

随后看ppu查看器 每个字符下面对应的Tile值都不一样 根据已经给定的flag格式 可以找FLAG对应的字符 扔010里找一下 发现有两段

chase-1

对应最上面字符的Tile值 打印出来即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
table = {0xd0: '0', 0xd1: '1', 0xd2: '2', 0xd3: '3', 0xd4: '4', 0xd5: '5', 0xd6: '6', 0xd7: '7', 0xd8: '8', 0xd9: '9',  
33: 'A', 34: 'B', 35: 'C', 36: 'D', 37: 'E', 38: 'F', 39: 'G', 40: 'H', 41: 'I', 42: 'J', 43: 'K', 44: 'L',
45: 'M', 46: 'N', 47: 'O', 48: 'P', 49: 'Q', 50: 'R', 51: 'S', 52: 'T', 53: 'U', 54: 'V', 55: 'W', 56: 'X',
57: 'Y', 58: 'Z', 0x3d: '_', 0x1a: ':', 0x1b: ';', 0x1c: '<', 0x1d: '=', 0x1e: '>', 0x20: '@', 0x00:' ',0x0e:'.',
0x3b:'{',0x3c:'}'}
enc = [
0x26, 0x2C, 0x21, 0x27, 0x00, 0x30, 0x34, 0x0E, 0xD2, 0x00, 0x26, 0x2F,
0x32, 0x00, 0x39, 0x2F, 0x35, 0x00, 0x29, 0x33, 0x00, 0x01, 0x12, 0xA4,
0x00, 0x01, 0x18, 0x30, 0x2C, 0x21, 0x39, 0xD1, 0x2E, 0xD9, 0x3D, 0xD6,
0x20, 0x2D, 0xD3, 0x33, 0x3D
]

flag2 = ''.join(table.get(i, '*') for i in enc)
print(flag2)
enc1 = [
0x26, 0x2C, 0x21, 0x27, 0x00, 0x30, 0x34, 0x0E, 0xD1, 0x00, 0x26, 0x2F,
0x32, 0x00, 0x39, 0x2F, 0x35, 0x00, 0x29, 0x33, 0x00, 0x02, 0x2A, 0x34,
0x30, 0x23, 0x34, 0x26, 0x3B, 0x24, 0xD0, 0x3D, 0x39, 0xD0, 0x35, 0x3D,
0x2C, 0xD1, 0x2B, 0x25, 0x3D
]
flag3 = ''.join(table.get(i, '*') for i in enc1)
print(flag3)
#FLAG PT.2 FOR YOU IS *** **PLAY1N9_6@M3S_
#FLAG PT.1 FOR YOU IS *JTPCTF{D0_Y0U_L1KE_

得到part2 拼接在一起即可
TPCTF{D0_Y0U_L1KE_PLAY1N9_6@M3S_ON_Y0UR_N3S?}