-
-
[原创]KCTF2026 第八题:亥子合辰·塔影迷楼
-
发表于: 2026-8-21 16:39 29
-
正确输入:
已经用原始未修改样本验证:
输入长度为 16 字节,字符和为:
主样本是:
最终使用的关键脚本:
程序要求输入长度为 16。
主 checker 入口在:
开头检查字符和:
所以必须有:
输入字符域是 printable:
随后按 base94 折叠成 128-bit 整数:
即:
最终校验位于:
clean timing 下,最终需要满足的四个核心状态是:
final sink 还计算两个 FNV-1a:
其中:
但 clean timing 下,如果前四个核心状态命中,上面的 FNV4 自动成立。
同时 FNV6 可以化成一个必要条件:
这个条件可以用来审计自洽性,但直接用 Z3 求这个前像很慢,不是最终突破口。
一开始遇到过一个很关键的假矛盾:
污染环境下 RHS 是:
这时:
看起来像程序无解或建模错了。
真正原因是 rdtsc timing 检测污染:
Unicorn 执行太慢,会让这个 timing flag 被置位。rbx 后续进入表生成逻辑,导致 final RHS 整体污染。
处理方式:
clean timing 后 RHS 变成:
并且:
这个修正非常关键。后续所有动态分析都必须使用 clean timing。
0x4EE9..0x5900 最终被化简为模有限域的 Horner 多项式。
模数:
常量:
语义:
也就是:
然后拆成第一段 20 轮入口:
这个模型用多个输入对拍通过,例如:
两段 20 轮都使用同一个核心结构。
先确认了大量看似复杂的乘法归约块其实是:
其中固定乘数:
核心函数可以写成:
虽然 [rbp+0x60] 的 256-byte S-box 不是置换,只有 166 个不同输出,但第一段整体是 Feistel 型可逆结构。
第一段每轮可以抽象为:
其中:
所以给定最终 (L', R') 可以倒推出上一轮 (L, R)。
final sink 要求第一段最终输出:
利用第一段 20 轮的可逆结构,从 (L20, R20) 反推第一段入口:
因此:
把这个入口再正向跑第二段,自动得到:
这说明 final 四个目标闭合,不需要再分别求第二段。
现在只剩一个有限域五次方程:
其中:
用 SymPy 在有限域 GF(p) 上分解:
得到一个合法线性根:
把 N 转回 16 位 base94 digit,再加回 0x21:
校验:
用原始 exe,通过 stdin 输入:
输出:
退出码:
这题耗时主要不在最终数学难度,而在被动态污染和复杂指令形态误导。
一开始过度相信静态视图里能直接看到完整校验逻辑,但真实 checker 是运行时解密出来的。仅靠静态反编译视图容易漏掉真实执行语义。
后面改成直接从原始 exe 解密 stage1/stage2,并用 Unicorn 跑真实路径,才避免继续被静态视图缺失误导。
sub_140001070 一度看起来像主校验的一部分。实际它只是另一个 16 字节 rolling hash,命中 0x1111111111111111 时改写 [0x140008000]。
曾经求到两个 prehash 命中样本:
其中第二个甚至满足 sum=0x500,但原始 exe 仍返回失败。原因是 [0x140008000] 和主 checker 使用的 [0x14000A000/0x14000A008/0x14000A010] 不是同一条关键链。
这个方向后来判定为诱导/环境 seed,不是最终约束。
这是最大坑。
Unicorn 下 rdtsc 时间差异常,导致:
被执行,污染后续表生成和 final RHS。
污染 RHS 下甚至出现:
这个矛盾容易让人误判为:
最终通过在 0x46ED 强制 rbx=0,clean RHS 变得自洽。
曾经尝试用 P*16 的 concrete block trace 引导符号执行,例如 second_phase_guided.py。
问题是不同输入会在中间分叉:
所以 P*16 trace 只是局部路径,不能当作全局路径约束。继续沿这个方向会让 solver 越来越大,而且语义假设不稳。
0x5930..0x646D 看起来是大量 ARX/MBA/查表/乘法/条件跳转。直接把指令级 AST 喂给 Z3,60 秒仍没有有效输出。
真正原因是这里有明显高层语义:
一旦识别出这些结构,问题从“大 SMT”变成:
复杂度直接降下来了。
[rbp+0x60] 的 256-byte 表不是置换:
这会让人以为整轮不可逆。实际第一段是 Feistel 型,内部函数不需要可逆,整体仍然可以反推。
[招生]科锐逆向工程师培训(2026年11月13日实地,远程教学同时开班, 第57期)