TPCTF 2025 wp[复现]

没打,只是复现一下逆向

chase

.nes 文件是 NES(Nintendo Entertainment System)游戏ROM 文件,是一种经典的游戏格式,在CTF中偶尔用于考察你对模拟器、ROM格式、6502汇编指令集等的理解。逆向 .nes 文件的过程和普通程序逆向不完全一样,但也有方法可循。

FCEUX:强大的 NES 模拟器,内置调试器(断点、内存查看器、trace log)。

Mesen:现代 NES 模拟器,图形界面友好,支持调试。

IDAPython + 6502 插件Ghidra + 6502 支持插件:静态分析。

image-20250505104305440

image-20250505104332873

直接打不开,出现 Truncated input file 通常说明 IDA 解析 .nes 文件时 没有正确加载 PRG ROM 段,这是因为 .nes 文件包含一个 16 字节的 iNES 头,而 IDA 不是专门为 NES ROM 设计的,它会把整个 .nes 当作纯裸机器码处理,导致分析失败。

打开了还是看不了,算了,玩了下是个闯关游戏,既然不能IDA直接逆向分析,那么可以试试CE去开挂看看

不知道为啥CE搜不到,只能用FECXU自带的

image-20250505110204785

然后看了下0x83是要吃的数量,用cheat改成0x1就能通关了

image-20250505111028854

只拿到了第一部分的flag

第三部分,点击PPU viewer即可

image-20250505111532373

这里的右边是符号表,在 NES 中,屏幕显示的文字和图形并不是 ASCII 字符,而是 图块(Tile)编号

  • 图块编号是一个 字节(0x00~0xFF)
  • 每个编号对应 CHR ROM 中的某个 8x8 图像
  • 游戏用 Tile 编号 + 属性表 组合成一个完整的背景/界面

我们可以通过PPU Viewer查看

image-20250505112740116

搜索到了第二部分:

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
def parse_tile_string(hex_string):
# 清洗输入,按两个字符一组切分
hex_bytes = [hex_string[i:i+2] for i in range(0, len(hex_string), 2)]
result = []

for byte_str in hex_bytes:
try:
tile = int(byte_str, 16)
except ValueError:
continue # 忽略非法 hex
# 0x10 - 0x19 → '0' - '9'
if tile == 0x20:
result.append('@')
elif tile == 0x3d:
result.append('_')
elif 0xD0 <= tile <= 0xD9:
result.append(chr(ord('0') + (tile - 0xD0)))
# 0x21 - 0x3A → 'A' - 'Z'
elif 0x21 <= tile <= 0x3A:
result.append(chr(ord('A') + (tile - 0x21)))
else:
result.append('.') # 用 '.' 表示未知/非可识别字符

return ''.join(result)

# 输入的 hex 字符串(你提供的)
hex_input = '34282500262C21270030340ED200262F3200392F35002933000112A4000118302C2139D12ED93DD6202DD3333D00019ABDADAD6D000103B9ADAD6E000103B9ADAD6E0001030D01030001100001000F0F0F0F0F1C2C3C0F1222320F1424340F1132300F1C2C3C0F0927380F1121310F1132300F1121310F0727380F1323330F1132300F1525350F0527380F1323330F1132300F1929390F0B27380F1727370F1132300F1626360F0727380F1828380F0F29300F0F26300F0F24300F0F213000FF490008FF4A0000074B0008074C008000FF4D0108FF4E0100074F01080750018000FF4D0208FF4E0200074F02080750028000FF4D0308FF4E0300074F03080750038077F488F499F4AAF4B0E717F4E1E827F4ACEA37F4C8EC37F448EF37F49BF217F4280044280044280044280044204F10205010205110FF2C2536252C1A000027252D331A0000000F000000002C293625331A000304037A7B7C7D7C7D7E7F80819A9B867B8788898A8B8C8D7D807D909192939495967F94959C9500000000000058E5ADE40BE7FEE48D1C038E1D038D23038E240388B9FFFF8D2D0388B9FFFF8D2C038C2F0320FFFFA0FFD0E860000000000000'

# 执行
output = parse_tile_string(hex_input)
print(output)

三部分都有了

linuxpdf

image-20250505131734905

image-20250505150102246

其实运行后我们发现,Flag字样出来时是与root/files/0a9这个文件同步的,猜测可能相关

下面标了源码,跟进看看

image-20250505150731691

先base64解码再解压

image-20250505154036160

直接对这部分解压也行,或者直接写个批量脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import json
import base64
import zlib
from pathlib import Path

with open("files.js", "r") as f:
js_content = f.read()

json_str = js_content.split("var embedded_files = ")[1].split(";")[0]
embedded_files = json.loads(json_str)
for rel_path, data_b64 in embedded_files.items():
output_path = Path("extracted_files") / rel_path
output_path.parent.mkdir(parents=True, exist_ok=True)
# Base64 解码
data_compressed = base64.b64decode(data_b64)
# zlib 解压
data_original = zlib.decompress(data_compressed)
# 写入文件
with open(output_path, "wb") as f:
f.write(data_original)

直接ida打开(8.3好像不能F5,用9.0)

字符串里看到有MD5,跟踪下

image-20250505155905127

主函数读取29个字符的输入,调用sub_2948函数。sub_2948函数处理当前字符串的MD5哈希,并与预存哈希比较。unk_4008 是预存的一批哈希值。

这里只有28个md5,由于每个都是单字母的,因此爆破很容易

image-20250505164712256

突然找到了

image-20250505164754884

可以发现规律了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from hashlib import md5

enc = [0x38,0xF8,0x8A,0x3B,0xC5,0x70,0x21,0xF,0x8A,0x8D,0x95,0x58,0x5B,0x46,0xB0,0x65,0x83,0x5,0x5A,0xE8,0xC,0xDC,0x8B,0xD5,0x93,0x78,0xB8,0x62,0x8D,0x73,0x3F,0xCB,0xFA,0x7D,0xAF,0xFB,0xD7,0xAC,0xEC,0x13,0xB0,0x69,0x5D,0x93,0x5A,0x4,0xBC,0xF,0xC2,0x9C,0xC0,0xFD,0x38,0x1,0xC7,0xFD,0xD3,0x15,0xC7,0x82,0x99,0x9B,0xD4,0xCB,0x2B,0xA2,0xD0,0x1A,0xF1,0x2D,0x9B,0xE3,0x1A,0x2B,0x44,0x32,0x3C,0x1A,0x4F,0x47,0xDD,0xEE,0xBA,0xF0,0x2,0x52,0x7A,0x9E,0xAD,0x78,0xBD,0x16,0x68,0x45,0x73,0xCC,0xBF,0x95,0xB8,0x99,0x34,0xA1,0xB5,0x55,0xE1,0x9,0xF,0xEC,0xDF,0xD3,0xDA,0x9F,0xB6,0x42,0x2C,0x30,0xB0,0x29,0x38,0x53,0x5F,0x8E,0x64,0x8D,0x60,0xA8,0x7B,0x94,0x8,0xC1,0xB7,0x66,0x43,0xAF,0x8D,0xD5,0xC,0xB0,0x6D,0x7F,0xDD,0x3C,0xF8,0xED,0x42,0xD6,0x97,0x19,0xF9,0x70,0x88,0xF0,0x65,0x40,0xF4,0x12,0xDC,0x17,0x6,0xFB,0xA1,0xF2,0x3D,0xA6,0x16,0x15,0x40,0xE,0x7B,0xD9,0xEA,0x72,0xD6,0x35,0x67,0xEB,0x4E,0x24,0x6F,0xA,0x5D,0xD3,0xCE,0x59,0x46,0x5F,0xF3,0xD0,0x2E,0xC4,0xF9,0x84,0xB8,0xCF,0x25,0xF9,0x63,0xE8,0xE9,0xF4,0xC3,0xFD,0xDA,0x34,0xF6,0xF0,0x1A,0x35,0x2D,0x98,0xD8,0x20,0x83,0x5C,0x75,0xA9,0xF9,0x81,0xAD,0x4D,0xB8,0x26,0xBF,0x8E,0x70,0x2E,0xAD,0x8,0xA3,0xDD,0x56,0xB3,0x13,0x4C,0x7C,0x38,0x41,0xA6,0x52,0xAA,0xD2,0xD5,0x57,0xB6,0x13,0x66,0x2B,0x92,0xF3,0x99,0xD6,0x12,0xFB,0x91,0x59,0x1E,0xE4,0x42,0x2B,0x63,0x20,0xED,0x98,0x9E,0x7E,0x3C,0xB9,0x7F,0x36,0x9C,0xBA,0x38,0x71,0x80,0x35,0x86,0xC6,0x70,0x59,0xDD,0xA3,0x25,0x25,0xCE,0x84,0x4C,0x50,0x79,0x83,0xB3,0x71,0x80,0x1D,0xA,0xDE,0x7,0xB5,0xC4,0xF5,0x1E,0x8C,0x62,0x15,0xE2,0xB0,0xD1,0xB4,0x88,0x5B,0xC2,0xFD,0xC5,0xA6,0x65,0x26,0x69,0x24,0x48,0x6C,0x5F,0x79,0x2C,0x9E,0x7F,0x5,0xC4,0x7,0xC5,0x6F,0x3B,0xEC,0x4C,0xA7,0xE5,0xC1,0x71,0x38,0x55,0xE5,0xA5,0xBB,0xC1,0xCB,0xE1,0x8A,0x6E,0xAB,0x5D,0xD9,0x7C,0x6,0x3C,0x88,0x6D,0x45,0xE0,0x45,0x1B,0xBB,0xA7,0xC0,0x34,0x1F,0xE9,0xA,0x95,0x4F,0x34,0x3A,0x43,0x7C,0xBE,0x65,0x91,0xEA,0x34,0x89,0x64,0x25,0x85,0x6E,0xAE,0x7B,0x65,0x34,0x30,0x49,0x67,0xA0,0x67,0x30,0x8A,0x76,0x70,0x1F,0x5,0xC0,0x66,0x85,0x51,0xD6,0xAF,0x7C,0x4F,0xED,0xCF,0x2B,0x67,0x77,0xDF,0x8E,0x83,0xC9,0x32,0xF8,0x83,0xDF,0x88,0x93,0x1E,0x7E,0xEF,0xDF,0xCC,0x2B,0xB8,0xD,0x4A,0x4F,0x57,0x10,0xFB,0xCB,0xF,0xC8,0x13,0x75,0x5A,0x45,0xCE,0x59,0x84,0xBF,0xBA,0x15,0x84,0x7C,0x1E]

enc_list = []

for i in range(0, len(enc), 16):
chunk = enc[i:i+16]
hex_str = ''.join(f'{b:02x}' for b in chunk)
enc_list.append(hex_str)

str = 'DF}'

for i in range(25,-1,-1):
for j in range(32, 127):
if md5((chr(j) + str).encode()).hexdigest() == enc_list[i]:
str = chr(j) + str
print(str)

TPCTF{mag1c_RISC-V_linux-PDF}

stone-game

玩不了了..

题目还是挺简单的,简单来说就是给了七个部分,每个部分都有1-100个石头,每次可以取每个部分的若干个石头,看最后谁取到最后一个石头谁就获胜,总共100轮,赢了90轮+就可以获得flag。

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

第一轮:
x1 x2 x3 x4 x5 x6 x7
user: x1 x2 x3 x4 0 0 0 -> 使得前四部分数量=0 -> 0 0 0 0 x5 x6 x7
ai: 会从剩下三部分中取完其中一个部分 -> 0 0 0 0 0 x6 x7

第二轮:
0 0 0 0 0 x6 x7
user: 0 0 0 0 0 x6-1 x7-1 -> 剩下两部分就只剩一个 -> 0 0 0 0 0 1 1
ai: 只能从剩下两部分取1个了 -> 0 0 0 0 0 0 1

第三轮:
直接取剩下的1个即可,就赢噜

TPCTF{M0nt3_C4rl0_S34rch_1s_4w3s0m3_f0r_g4m3s}

portable

无法交叉引用就直接找立即数

image-20250505201355165

输入函数:

image-20250505202054099

后面调了半天,估计还有长度的判断在下面

image-20250505202923090

往下翻发现几个可疑的byte

试了下直接出了

image-20250505204434415

image-20250505204424821

记得用github的厨子,不然显示不出来

该题使用的不是传统的 libc 而是静态链接的 cosmopolitan libc

而我们手上没有它的符号信息,所以库函数是几乎没法分析的

image-20250505204915133

这个 file 命令的输出表明你分析的文件是一个MBR(主引导记录)引导扇区格式的镜像文件

该文件:

  • 是一个以 MBR 格式构造的镜像;
  • 指定了一个从第 0 扇区开始、大小为 0xFFFFFFFF 的分区;
  • 分区类型 0x7f 是不常见的值;
  • 极可能用于某种反取证、混淆、CTF题目或者特殊系统用途(比如隐藏或加密文件系统

image-20250505205041339

0x9C000 ELF 64-bit LSB executable (FreeBSD)

1
2
3
4
5
6
7
8
9
f = open("portable_ef3c5f3a7155c5a4c6f0b08606dea575","rb").read()
exe_data= f[0x9C000:]
open("dump","wb").write(exe_data)
print("end!!!!!!!")


exe_data= f[0x10000:]
open("dump.exe","wb").write(exe_data)
print("end!!!!!!!")

再导出的dump文件就能交叉引用了

magicfile

又不能交叉引用

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
size_t v4; // rax
char *s; // [rsp+8h] [rbp-18h]
void *ptr; // [rsp+10h] [rbp-10h]
char *s_1; // [rsp+18h] [rbp-8h]

s = (char *)malloc(0x64uLL);
printf("Please input the flag: ");
__isoc99_scanf("%100s", s);
if ( strlen(s) == 48 )
{
ptr = (void *)sub_5890(0LL);
sub_58E0(ptr, &off_119B018, &off_119B010, 1LL);
v4 = strlen(s);
s_1 = (char *)sub_59A0(ptr, s, v4);
puts(s_1);
sub_58A0(ptr);
}
else
{
puts("Try again.");
}
return 0LL;
}
1
sub_58E0(ptr, &off_119B018, &off_119B010, 1LL);

从程序提取出mgc文件 第二个参数是地址 第三个参数是size

不如直接010打开

image-20250505211357330

image-20250505211448655

TPCTF{YoU_AR3_SO_5m@R7_TO_cRACk_Th1$_m@9iC_f1le}

往上找规律

obfuscator

image-20250506105237996

XCTF-TPCTF 2025 WP | CN-SEC 中文网