首页
社区
课程
招聘
[原创] 看雪·2026 KCTF 第四题:未时·车流困城 WP
发表于: 2026-8-17 15:49 86

[原创] 看雪·2026 KCTF 第四题:未时·车流困城 WP

2026-8-17 15:49
86

本文做出来的时候, 使用的是网页端的 GPT-5.6-Sol xhigh, 之前使用 Opus 4.8 + GPT 5.5/5.6 Terra 的时候一直卡在动态调试导致的数据流错误上, 然后就一直卡在模拟和插桩上了(至于为什么用这两个老模型是因为这玩意不爆 Cyber,我又不想装破限提示词怕把我号爆破了), 这几天拿这两个叼毛的完整上下文对账去,顺便可以水水文章()

后面用 Codex 里面的 Sol 指定函数纯静态逆向的时候又不爆 Cyber 了,怪

下面的 WP 为网页端 GPT-5.6-Sol xhigh 生成, 整体解题过程用时约 3 小时(不算我被那两个老模型耗的时间)

样本:CrackMe.exe

目标:给定用户名,构造能够通过校验的超长 password。

最终结论:整个校验可以拆成三层:DayDayUp -> MengXinQiuFangGuo -> GoodGoodStudy。最麻烦的不是数学,而是 MengXin 中的控制流混淆、反调试状态字节,以及一处非常容易误判的 key schedule DWORD 符号扩展。

程序对 password 的实际处理链可以抽象成:

反向 keygen 则反过来:

公开成功向量:

可以完整重建出公开 serial。

MengXinQiuFangGuo 外面套了大量保护:

最初使用的 deobf3.py 还有一个非常重要的限制:它对所有条件跳转只走一条统一路径,默认 fall-through。因此 path2.txt 能帮助看结构,但不能被当成“真实动态执行轨迹”。

这也是前期出现“结构看起来像 AES,但怎么算都不匹配”的主要原因之一。

主程序读入 username 和 password 后,大体依次执行三层逻辑:

因此构造 password 时也只需要把这三层逐层求逆。

先对 username 做一个简单的 31-base hash:

然后使用一个 Murmur 风格的 fmix 更新 state:

每次取:

即从 0..16383 中选 row,直到得到 100 个不重复 row。

恢复出的逻辑:

每个 row 保存 11 个系数:

对应:

[
P(x)=C_0+C_1x+\cdots+C_{10}x^{10}
]

数据本身没有明文保存,而是通过 descriptor 指向编码 blob,再经一个 row / coefficient 相关的 keystream 解出十进制系数字符串。

恢复后发现选中的多项式都能分解为 10 个一次因子,因此每行可以得到 10 个整数根。

100 行 × 10 个根 = 1000 个整数。

最终 GoodGoodStudy 明文形态为:

当前恢复的验证模型对每组 10 个输入主要检查“值是否属于这一行多项式的可接受根集合”。因此至少从数学约束本身来看,根的排列并不天然唯一。

当前 keygen 为了得到稳定输出,主动执行了:

所以它只选了众多候选中的一个 canonical solution。

已经很可信:同一行 10 个不同根的排列存在多解。

至于“同一个根重复 10 次是否也能通过真实二进制”,目前还应保守处理:我们的高层验证器允许 membership-only,但是否存在隐藏的 distinctness 条件,最好再做一次真实程序实验后下最终结论。

这是后期才暴露出来的一个通用性坑。

得到十进制文本后,程序要求在尾部补 NUL,而且长度必须到“严格下一个 16-byte 边界”:

也就是说,即使原长度已经是 16 的倍数,也仍然补一个完整 16-byte block。

例如公开向量:

KCTF

之前错误地 padding 到 lcm(3,16)=48 的倍数,公开向量碰巧没有暴露问题,但 KCTF 立即失败。

这是整题最核心的一层。

它的干净数学结构可以看成:

AES-128 Equivalent Inverse Cipher 骨架 + 两张自定义 S-box + 特殊 key schedule + 环境状态字节。

块大小和 key 大小都是 16 bytes。

恢复出的 master key 为:

入口处直接从 R8 读取 4 个 DWORD,bswap 后写入 w[0..3],因此它就是 K0,而不是 Meng 内部再加工出来的某个中间 key。

这份 key 在样本中来自程序的完整性/self-check 逻辑,可理解为由多个区域的 CRC32 结果拼接而成。对写 solver 来说直接使用这 16 bytes 即可。

程序有两张独立 256-byte permutation:

原代码中表项不是直接存 byte,而是类似:

之后再混入保护状态字节。

一开始我们写成了:

这是错的。

原程序把 S-box 结果保留为 DWORD;当结果最高位为 1 时,值表现为 sign-extended DWORD:

随后直接移位、OR,没有重新 & 0xff

因此:

不能简单拼成:

而可能因为 ffffffb1 / ffffffda 的高位参与 OR 变成完全不同的 DWORD。

这正是最初大量 AES variant 穷举全部失败的关键原因。

原代码在 substitution 路径中出现:

最初把它理解为普通 BeingDebugged,只考虑 0/1,也是不对的。

保护代码会把这个位置作为完整 8-bit 状态使用。

通过公开成功 pair 和独立 Meng block 测量,目前成功路径等价于:

旧的坏路径 dump 则等价于:

这里要强调:0x40 的“成功路径有效值”已经被向量验证;但它在完整 VEH / protector 状态机中究竟由哪一组异常处理精确演化出来,目前没有必要为了 keygen 强行解释。

总体仍是 AES-128 的 44-word 结构:

i % 4 == 0

Rcon 仍然在 Rijndael GF(2^8) 中通过 0x1b reduction 生成。

round substitution 为:

接下来完全是标准 AES InvShiftRows。

机器代码中的:

正好对应三行右移 1、2、3。

这一层最终确认没有魔改,就是标准 AES InvMixColumns:

有限域仍是:

所以核心 xtime

早期动态追踪中,InvMixColumns 后面还出现了一组 16-byte XOR,看起来像额外 mask。

最后发现:

它不是随机 mask,而是 AES Equivalent Inverse Cipher 的标准变形。

利用线性性质:

[
IMC(S\oplus K)=IMC(S)\oplus IMC(K)
]

所以:

和:

是等价的。

成功路径可以干净地写成:

或者按原始 Equivalent-Inverse 的写法:

两者等价。

成功路径 peb_byte=0x40 可以重新命中最早独立测得的两个 block vector:

这也是判断 good-path 模型是否正确的非常强的 regression test。

DayDayUp 最终比 Meng 简单很多。


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

最后于 2026-8-17 15:54 被DNLINYJ编辑 ,原因:
收藏
免费 1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回