首页
社区
课程
招聘
[原创]第八题:亥子合辰·塔影迷楼 解题writeup
发表于: 2天前 33

[原创]第八题:亥子合辰·塔影迷楼 解题writeup

2天前
33

kctf2026_CrackMe08.exe,Windows x86-64,16 字节输入,输出 correct 即通关。

flag:kanxue@2o26o8!@#

文件是个 PE32+(x86-64 console),5 个 section,一共 27136 字节。题目提示里塞了一堆意象:摩斯密码、16 层加密、16 个节点散落的钥匙、斐波那契、"子时通天塔"。我一开始没往深了想,先跑起来看行为。直接跑、随便输点东西,打印 fault。字符串里能直接搜到 correctfault 两个分支,说明校验逻辑集中在一个地方,找到它基本就找到了核心。后面慢慢发现,提示里那些"16""斐波那契"其实对应的是 16 位 base-94 输入、16 组轮密钥、两个 20 轮的 Feistel 阶梯,还有那个模数 2^127-39 的来源——不过这都是把题拆完了才反应过来,刚开始完全没头绪。

file 出来就是个普通 PE,跑起来也能正常打印 fault,看起来人畜无害。但拿原文件跟脱壳后的镜像做逐字节 diff 时发现,整个 .text 后半段全在变:

.text(RVA 0x1000,vsize 0x5672)前面只留了 0x800 字节明文桩,后面 20KB 全是密文。明文桩里没几样东西:0x1000 是 exit,0x1070 是个 FNV 哈希,0x1220 是 syscall 号解析器,真正的入口 0x1400012c0 里塞着解密循环。入口先拿一个 16 字节 key 做 XOR(64 字节一轮,尾巴按字节 XOR 0x5a),把 0x1800 那段解出来;解出来的又是一层 XMM 密码的解密器,负责把 0x1d10 往后 0x4962 字节全解开。两层套娃,手撸太累,干脆让程序自己解,我用 Unicorn 仿真到解密完成再 dump。

这一步踩了个挺深的坑。壳不走正常 CRT,printf/scanf 全换成了直接 syscall,号还不是写死的:0x140001220 先用 GetModuleHandleA+GetProcAddress 拿 ntdll 函数地址,再扫序言里的 0f 05 把号抠出来,就是 Hell's Gate 那套。我最早在 Unicorn 里只 hook 了 GetProcAddress,让它对每个 Nt* 函数名都返回同一个桩,结果这个解析器扫出来的号全是一个值——写和读的 syscall 号都被当成同一个,读输入时返回 0 字节,程序判定输入非法直接 exit,然后卡在一个死循环里出不来。折腾半天才搞明白:得把 0x140001220 这条链整段 hook 掉,按函数名直接给号(NtWriteFile→8、NtReadFile→6、NtQuerySystemTime→0x33、NtQueryInformationProcess→0x19、NtTerminateProcess→0x2c),再单独拦 syscall 指令分派——读输入就塞 16 字节进缓冲、查进程信息就返回"没被调试"、查时间清零。跑到校验函数入口 0x140004380 时把 .text 整段 dump 成 decrypted.exe(sha256 1a1f305d00e521c71942b36fd6f1e03346f974f8548ffb98d4559e52886647e9)。后面所有静态分析都基于这份镜像。

脱壳脚本就是附录 C 的 dump_decrypted.py

解密后的镜像里,correct/fault 两个字符串的交叉引用都指向同一个函数 FUN_140004380,校验逻辑全在这。反汇编存成 func4380.asm,三千多行。读下来整体是这么个流程:读 16 字节输入,先求和校验是不是 0x500(1280),过一遍 base-94 解码得到一个约 105 位的大整数 S,再把 S 塞进一个五次多项式输出 128 位结果,后面跟着两个 20 轮的 Feistel 阶梯,最后跟目标逐字比较,全等才 correct

这套东西全在栈上做 128 位大数运算,寄存器里全是 adcsetbcmovb 的进位链,纯肉眼硬啃不现实,所以下一步直接上动态仿真,把它当黑盒来拆。

程序会调一堆 Windows API(GetStdHandleVirtualProtectGetThreadContextNtQueryInformationProcess 这些),还带反调试(读线程上下文、检查 debug 标志)。Linux 上跑不起真机,就用 Unicorn 把整个 PE 仿真起来:映射 image base 0x140000000 和各 section,IAT 里那 8 个 API 全部指到一段假地址,hook 里按名字返回假值;syscall 指令(0f 05)单独 hook 掉;求和校验那条分支直接在 0x140004402 强制 jmp 过去,省得去改内存。

这个 harness 就是附录 D 的 solve.py,核心是个 F(inp),喂 16 字节进去返回 6 个输出字。有了它就能黑盒观察输入到输出的映射,后面逆向每个子步骤都是拿它当 oracle 对拍——猜一个结构,跑一遍,看对不对得上。

大数乘法那段最扎眼的是常数 0x4e(78)反复出现,跟在乘法后面做"乘 78 再加低位"的约简。反汇编里到处都是:

78 = 2×39,这暗示模数形如 2^127 - k,因为 2^128 ≡ 2k (mod 2^127 - k),当 k=39 时 2^128 ≡ 78,正好就是那个反复出现的约简因子。于是先猜模数 m = 2^127 - 39。验证不费事:从仿真器里取一轮的乘法结果 P 和两个乘数 A、B,算 D = A*B - P,看 D 能不能被 2^127-39 整除。整除,而且查了下 2^127-39 是个素数。就坐实了——所有 128 位"乘法"其实都是 mod (2^127-39) 的模乘。

阶梯部分反复出现 ror 0x2fror 0x33,还有 0x9e3779b97f4a7c15——黄金比例 φ 的倒数,TEA/XTEA 系常用。反汇编里:

我第一反应是 XXTEA,毕竟这常数太标志性了。照着 XXTEA 的结构套了半天对不上,才回头老老实实逐轮对拍寄存器。拍下来单轮函数长这样:

SBOX 是按字节查表,256 字节,但不是置换。我直接在 0x140004ee9 那里把 rbp+0x60 的 256 字节抓出来,开头长这样:

M = 0x4d8a9e9343d415cb83a0a488ec8523fe。整个阶梯是个标准两分支 Feistel,20 轮,每轮:

轮密钥 c_i = K_parity(i) ^ keys[i],偶数轮用 K_even=0x553d3c5ef6ee11ff、奇数轮用 K_odd=0x4aeee52b5738eb8f。两个阶梯的 keys 我直接从栈上 dump:阶梯 1 的 20 个 qword 在 rbp+0x160,阶梯 2 的在 rbp+0x200,写进 feistel.py 就是 keys1/keys2。这里有个容易漏的细节——第二个阶梯的奇偶性反过来了,反汇编里阶梯 1 用 cmove、阶梯 2 用 cmovne 选 K,不仔细看就把两个阶梯当成一模一样的了。

Feistel 的好处是可逆,反演只要把轮倒过来跑:

这部分写成 feistel.py(附录 A),正反都过了 roundtrip 验证。

反演两个阶梯,就得到"能让输出匹配目标"的阶梯输入:

其中 r11 是高 64 位 0x4be831b0ad3a2d36,r10 是低 64 位 0x1489375bba3fb8de。字节序是在 0x140005900 处确认的,那里有 mov r9,r11mov r12,r10,r11 是高 64 位、r10 是低 64 位。目标那 6 个字是 .tgt 段里躺着的 6 个 64 位大数,反演就是拿这 6 个目标当阶梯输出,把轮倒着跑一遍得到输入。具体过程我把正反两个方向的实现都写进了 feistel.py,对着 solve.pyF() 反复对拍才确认没搞反。

从 base-94 解码出来的 S,经过一段 Horner 求值,输出 128 位。这段我把 Horner 循环里反复 load 的那五个 128 位常数 dump 出来,预处理就是:

系数:

这里踩了全程最坑的一个坑。我一开始用测试输入 0x50×16 去对拍,dump 出来的 S 只有低 64 位 0xec3fb8af41531605,拿它算 P(S) 跟仿真器死活对不上。排查了半天才发现:S 的高 13 位还在 r15 寄存器里没写进内存——mov [rbp+0x7d8], r15 这条写内存的指令在我的 dump 点(0x140004ee9)之后才执行。补全高 64 位后:

多项式方向这才确认无误。

剩下就是解 P(S) ≡ T (mod m),一个 GF(2^127-39) 上的五次方程。用 sympy 在有限域上分解:

只有一个线性因子,所以根唯一:

代回去验证 P(S) == T 成立。求根脚本是附录 B 的 solve_poly.py

S 是按 base-94、低位在右拼出来的:S = Σ (byte_i - 33) * 94^(15-i)。倒过来把 S 逐次 mod 94 取位,得到 16 个数字:

每个加 33 转回字符:

前缀 kanxue@ 一看就对——看雪 KCTF 的题。求和 1280,正好过 0x500 那关。


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

上传的附件:
收藏
免费 1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回