Windows x64 crackme,26 字符注册码。输入经 ARX 打包函数派生出 8 字种子,喂进一台指令动态解码的字节码 VM 跑 4 轮,每轮吐出一个 15×u32 的状态块,4 块过海绵哈希后与 4 个硬编码 u32 比较。
破解的关键是发现块里的 blk[6] 是本轮所有自检的 OR 聚合:正解让 round2 且仅让 round2 完整通过(blk[6]==0),另外三轮故意为 0xFFFFFFFF。这把 128 位哈希约束降维成 round2 的 256 位确定 VM 输出目标;再叠加从 v152/v148 掩码逆出的读写位置置换约束(随机满足概率 ~1e-13)。最后 ARX 打包函数是纯双射,代数求逆直出 flag。
正面反汇编 sub_14000306C(CHECK 主流程)分五步:

图 1 sub_14000306C 全貌,红框 ①~⑤ 与上面五步一一对应。
注意 ③ 里 sub_14000330C 的第 4 个参数 ((_BYTE)i + (_BYTE)v12) & 3 ——
轮号确实依赖输入(v12 由种子推出),但取值集合恒为 {0,1,2,3},
所以四轮总会各跑一次,只是顺序被打乱。

图 2 0x1400031FD 起的最终判定,红框逐项对上了上面那段 C:
xor edi, 1Ah 是长度校验残留;xor eax, 0A5A55A5Ah + shr/shl/or 三件套是 ROL(..., r+5);
imul eax, r8d, 61C88647h 是 - 0x61C88647 * (r+1);
最后 dword_140002220[r] 与本轮哈希异或。四轮结果全部 OR 进 edi,全零才走 ok。
其中 table@0x140002220 = [0e886e0e, e45d26f1, da45fc40, 3d12b39c]:

图 3 dword_140002220 处的四个目标常量(小端)。
反解 finalization(GF(2) 上秩 128/128,唯一解)得目标哈希:
VM 的反调试设计值得一提:指令是动态解码的 —— opcode = byte_140002148[f(PC, v135)] ^ v156,而 v156 每轮不同,所以同一段字节码在四轮里解码成不同指令(呼应题面「同一方法不能用两次」)。但 v135/v156 都不依赖输入数据,实测三种不同输入的执行路径长度完全一致(26384/26314/26388/26316),所以指令流可以离线一次性提取。

图 4 byte_140002148 起的解码表,紧跟在字符串 input flag: 之后。
共 24 项 0D 0B 0E 14 1B 17 11 0C 0F 09 0A 16 18 1E 1D 19 10 13 1F 1C 1A 15 12 00,
取值恰为 {0} ∪ {9..31} 且互不重复。查表是动态解码的第一级,第二级是再异或每轮不同的 v156。
sub_14000330C 尾部的存储代码给出块字段语义:
而 v121 是整轮所有自检的 OR 聚合:

图 5 0x1400045A4 附近,v121 的 OR 聚合链在反汇编里一目了然:
xor eax, 101h / xor eax, 20h / xor eax, 8 逐项与期望值比对,
差值全部 or ecx, eax 累积。只要有任何一项不符,v121 就非零。
v121 == 0 ⟺ 本轮全部通过。随机输入下实测 160/160 恒为 0xffffffff。
于是提出假设:正解让某一轮 v121 == 0。构造该轮的「理想块」(v121=0、末态取硬编码 v149/v150、dw 取 dword_1400021A0),把它喂进海绵哈希:

*图 8 sub_140002940。状态初值用了 0x243F6A88(π 的小数部分,也见于 Blowfish / SHA-512 IV);
外层 for (i = 0; i < 4; ++i) 逐块吸收,步长 60LL * i 即每块 60 字节 = 15 个 u32。
逐块可分离正是下面能「逐轮比对吸收状态」的前提。*
与从 H* 反解出的目标吸收状态逐轮比对:
只有 round2 精确相等,巧合概率 2⁻³²。结论:flag 让 round2 完美通过,其余三轮的 blk[6] 就是 0xFFFFFFFF,且这正是硬编码哈希所期望的值。
这一步把「128 位哈希」换成了 round2 上的确定性等式:VM 的 32 字节输出必须等于某个定值。因为 postARX 是 4 轮 ChaCha 变体(双射),从 dw 目标 z3 反推即得 out_target[2](256 位全定),外加末态 v25==v149[2]、v147==v150[2]。
v121 里的 ~(v152 & v148) 要求两个掩码都必须是全 1。跟进 op0 的处理代码:

图 6 0x140003A8D 现场。shl edx, cl 造出 1 << idx,与已读掩码 and 后
or r14d, eax —— 只要某个位置被读第二次,r14d 就被污染,最终反映到 v121。
这条约束才是把搜索从不可行变成可行的东西。
程序里 op0 恰好 32 次、写(op4+op13)恰好 32 次,掩码全 1 ⟺ 读位置和写位置各自是 0..31 的一个置换。随机输入满足这个的概率约 1e-13 —— 这是决定性的剪枝,DFS 里一行就能表达:
搜索空间只有 12 位:实测 v147 初值只依赖 v19 的低 12 位(4000 样本 / 1044 个重复 v19,0 例外),所以枚举 v19 ∈ [0, 4096) 是完备的。对每个 v19,从 out_target[2] 逆着 257 条指令往回推 v171 的字节,clean 块不分支、耦合块(op22 读到尚未定值的字节)分 256 支,配合上面的置换剪枝。
C 实现,9 分片跑满 mac 的核,在 v19=272 上以 1.63 亿节点命中:
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于 1天前
被星野安全编辑
,原因: