首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
22
[原创]KCTF2026 - 第三题:午时·永数囚笼 题解(AI)
发表于: 2026-8-14 11:09
833
[原创]KCTF2026 - 第三题:午时·永数囚笼 题解(AI)
星野安全
1
2026-8-14 11:09
833
## Summary 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. **长度必须为 26** —— `edi = len ^ 0x1a` 2. **派生种子** —— `sub_140002BA0(input)` → BA0 → `rbx`,产出 8×u32 的 `v171` 3. **四轮 VM** —— `for i in 0..3`,轮号 `= (i + rbx) & 3`(顺序依赖输入,但集合恒为 `{0,1,2,3}`) - `sub_14000330C(...)` 是 VM 本体,257 条指令/轮 - 每轮结果写入独立块 `[rsp+0x60 + 轮号*0x3c]`,每块 15 个 u32 4. **海绵哈希** —— `sub_140002940(块基址, 0xb0adeff3, 0x03db2dd2, out)`,4×15 → 4×u32 5. **最终判定** @ `0x1400031fd`:  *图 1 `sub_14000306C` 全貌,红框 ①~⑤ 与上面五步一一对应。 注意 ③ 里 `sub_14000330C` 的第 4 个参数 `((_BYTE)i + (_BYTE)v12) & 3` —— 轮号确实依赖输入(`v12` 由种子推出),但取值集合恒为 `{0,1,2,3}`, 所以四轮总会各跑一次,只是顺序被打乱。* ```c edi = len ^ 0x1a; // 长度必须 26,否则这里就非零 for (r = 0; r < 4; r++) { t = ROL(0x03db2dd2 ^ 0xa5a55a5a, r + 5) ^ (0xb0adeff3 - 0x61c88647 * (r + 1)); e = t ^ (t >> 15); edx2 = e * 0x27d4eb2d; edi |= table[0x140002220 + 4*r] ^ hash[r] ^ edx2 ^ (edx2 >> 16); } // edi == 0 -> "ok",否则 "no" ```  *图 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,唯一解)得目标哈希: ``` H* = [892a48af, b45dcd47, 006c5ac3, b0b1062a] ``` 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`。* ## Solution ### Step 1:找到 `blk[6]` 的语义,把哈希约束降维 `sub_14000330C` 尾部的存储代码给出块字段语义: ``` blk[0]=v25(末态) blk[1]=v139(ARX累加器) blk[2]=v135(解码密钥) blk[3]=v133(PC) blk[4]=v143 blk[5]=v146(指令计数=0x101=257) blk[6]=v121 ★ blk[7..14]=postARX 后的 dw ``` 而 `v121` 是**整轮所有自检的 OR 聚合**: ```c v121 = v7^0x20 | v155^0xC | v154^8 | v153^8 | v145^0x20 | v146^0x101 | (v136==0) | v25^v149 | v139^v150 | v135^v151 | v133^v156 | ~(v152 & v148) | v147; for (j = 0; j < 8; j++) v121 |= mix(j) ^ dw[j] ^ dword_1400021A0[8*round + j]; ```  *图 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*` 反解出的目标吸收状态逐轮比对: | 轮 | 理想块的 r11 | 目标 R[r] | 匹配位数 | |---|---|---|---| | 0 | `e96e1ddb` | `1f1cc1bc` | 12 | | 1 | `d8c82cae` | `1b7d21a2` | 18 | | **2** | **`47d3d751`** | **`47d3d751`** | **32/32 ★** | | 3 | `5dc50e75` | `27ff56e7` | 17 | 只有 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]`。 ### Step 2:从 `v152/v148` 逆出置换约束 `v121` 里的 `~(v152 & v148)` 要求两个掩码都必须是全 1。跟进 op0 的处理代码: ```x86asm 140003a8d shl edx, cl ; edx = 1 << (读位置索引 0..31) 140003a8f mov ecx, [rbp-0x69] ; v152 = 已读位置掩码 140003a94 and eax, ecx 140003a9c or r14d, eax ; ★ 位置被重复读 -> tamper ```  *图 6 `0x140003A8D` 现场。`shl edx, cl` 造出 `1 << idx`,与已读掩码 `and` 后 `or r14d, eax` —— 只要某个位置被读第二次,`r14d` 就被污染,最终反映到 `v121`。 **这条约束才是把搜索从不可行变成可行的东西。*** 程序里 op0 恰好 32 次、写(op4+op13)恰好 32 次,掩码全 1 ⟺ **读位置和写位置各自是 0..31 的一个置换**。随机输入满足这个的概率约 1e-13 —— 这是决定性的剪枝,DFS 里一行就能表达: ```c { unsigned int pb = 1u << (v55 & 0x1F); if (s.rmask & pb) return 0; // 位置重复 -> 整支剪掉 s.rmask |= pb; } ``` ### Step 3:DFS 反推 v171 搜索空间只有 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 亿节点命中**: ``` HIT v19=272 nodes=162769733 v171b=241,180,128,29,13,24,36,91,67,124,5,218,91,53,64,32, 38,36,133,9,147,33,181,10,102,21,187,209,94,96,29,63 ``` ### Step 4:代数求逆 BA0 → flag `sub_140002BA0` 拆开看就是 8 轮「加常量 / 异或旋转常量 / 一轮 8 字 ChaCha」,**纯 ARX 双射**,不需要 SMT,直接写逆函数秒出。两个天然锚点 `a[7]==0xC3D2E1F0` 和 `a[6]>>16==0xA55A` 顺带把结果自校验了。  *图 7 `sub_140002BA0`。`a2[7] = -1009589776` 就是 `0xC3D2E1F0`(SHA-1 的 IV[4],被借来当固定尾字); `| 0xFFA55A00` 是 26 字节输入不足 32 字节时的尾部填充魔数 —— 这两个常量在求逆后成了免费的正确性断言。 往下 `__ROR4__(... , 16) / 20 / 24 / 25` 的模式即 ChaCha quarter-round。* ```python #!/usr/bin/env python3 # solve.py — 从 DFS 命中的 v171 代数求逆 BA0,直出 26 字符注册码 M = 0xFFFFFFFF def m32(x): return x & M def rotl(x, n): n &= 31; x &= M return x if n == 0 else ((x << n) | (x >> (32 - n))) & M def rotr(x, n): return rotl(x, (32 - (n & 31)) & 31) def QR(a, b, c, d): # ChaCha quarter-round a = m32(a + b); d = rotl(d ^ a, 16) c = m32(c + d); b = rotl(b ^ c, 12) a = m32(a + b); d = rotl(d ^ a, 8) c = m32(c + d); b = rotl(b ^ c, 7) return a, b, c, d def iQR(a, b, c, d): # 逆 quarter-round b = rotr(b, 7) ^ c; c = m32(c - d) d = rotr(d, 8) ^ a; a = m32(a - b) b = rotr(b, 12) ^ c; c = m32(c - d) d = rotr(d, 16) ^ a; a = m32(a - b) return a, b, c, d def fwd_round(a): a = list(a) a[0], a[1], a[2], a[3] = QR(a[0], a[1], a[2], a[3]) a[4], a[5], a[6], a[7] = QR(a[4], a[5], a[6], a[7]) a[0], a[5], a[2], a[7] = QR(a[0], a[5], a[2], a[7]) a[4], a[1], a[6], a[3] = QR(a[4], a[1], a[6], a[3]) return a def inv_round(a): a = list(a) a[4], a[1], a[6], a[3] = iQR(a[4], a[1], a[6], a[3]) a[0], a[5], a[2], a[7] = iQR(a[0], a[5], a[2], a[7]) a[4], a[5], a[6], a[7] = iQR(a[4], a[5], a[6], a[7]) a[0], a[1], a[2], a[3] = iQR(a[0], a[1], a[2], a[3]) return a def ba0(inp): # sub_140002BA0,26 字节 -> 8×u32 a = [0] * 8 for i in range(6): a[i] = inp[4*i] | (inp[4*i+1] << 8) | (inp[4*i+2] << 16) | (inp[4*i+3] << 24) a[6] = (inp[24] | (((inp[25] | 0xFFA55A00) << 8) & M)) & M a[7] = 0xC3D2E1F0 for i in range(8): a[i] = m32(a[i] ^ m32(1877787630 - 1640531527*i)) for i in range(8): a[i] = m32(a[i] + (m32(1831565813*i) ^ 0x7F4A7C15)) idx = (i + 3) & 7 a[idx] = m32(a[idx] ^ rotl(m32(1013904242*i - 1515890086), i + 5)) a = fwd_round(a) return a def inv_ba0(v171): # 逆着来 a = [x & M for x in v171] for i in range(7, -1, -1): a = inv_round(a) idx = (i + 3) & 7 a[idx] = m32(a[idx] ^ rotl(m32(1013904242*i - 1515890086), i + 5)) a[i] = m32(a[i] - (m32(1831565813*i) ^ 0x7F4A7C15)) for i in range(8): a[i] = m32(a[i] ^ m32(1877787630 - 1640531527*i)) return a # DFS 反推器在 v19=272 上命中的 v171(32 字节,小端 8×u32) VB = [241,180,128,29,13,24,36,91,67,124,5,218,91,53,64,32, 38,36,133,9,147,33,181,10,102,21,187,209,94,96,29,63] v171 = [int.from_bytes(bytes(VB[4*i:4*i+4]), 'little') for i in range(8)] a = inv_ba0(v171) assert a[7] == 0xC3D2E1F0, f"锚点 a7 失配: {a[7]:#x}" # BA0 的固定 IV assert (a[6] >> 16) == 0xA55A, f"锚点 a6 失配: {a[6]>>16:#x}" # 尾部填充魔数 flag = bytes(b for w in a[:6] for b in w.to_bytes(4, 'little')) \ + bytes([a[6] & 0xff, (a[6] >> 8) & 0xff]) assert len(flag) == 26 and all(0x20 <= c <= 0x7e for c in flag) assert ba0(list(flag)) == v171, "BA0 往返失败" print(flag.decode()) ```  ### 验证 | 环节 | 结果 | |---|---| | round2 VM 输出 == `out_target[2]` | ✅ 32/32 字节 | | round2 末态 `v25`/`v147` | ✅ `e6a68d04` / `2e8c989c` | | 四块海绵哈希 | ✅ `892a48af b45dcd47 006c5ac3 b0b1062a` == H* | | 哈希敏感性(4 块 × 15 字段逐位翻转) | ✅ 无一字段不影响哈希 | | BA0 往返 | ✅ | | **真实 AntiAi.exe** | ✅ `input flag: ok`(错误输入为 `no`) | ``` $ printf 'flag{v4_opaque_trace_2026}\n' | AntiAi.exe input flag: ok ``` ## 要点 **1. 反推器必须先做闭环自检。** 从目标反推 `v171` 的 C 程序是一台几千行的状态机,「它没搜到解」和「它本身是错的」在外部看完全一样。可靠的做法是构造一个必然有解的用例:取一个已知输入 → 正向算出该轮真实的 `out` / 末态 → 用这组值造一份测试数据头 → 反推器必须能原样找回那个 `v171`。自检不通过时逐层下探即可定位:打印节点数 → 用真 `v171` 预填状态 → 打印第一个 op0 的中间量,一路比对到 C 与 Python 的第一处分歧(`v42=0958a993` vs `97006aaf`),问题就锁定了。 **2. 「四轮都要通过」是可证伪的。** 注入「四轮全部理想通过」的块,CHECK 仍返回拒绝,哈希 `[676353f0, ...] ≠ H*`。这条反证是 Step 1 里「只有 round2 通过」的直接依据,先排除掉最直觉的那个假设,才能看见真正的结构。 **3. 剪枝比算力有效得多。** 257 步 VM 对 z3 / bitwuzla全部超时,盲扫 `v19` 也撞满节点上限。让搜索从不可行变成 1.6 亿节点命中的,是从二进制里挖出的置换约束,不是更大的 cap。 ## 工具 | 工具 | 用途 | |---|---| | **Claude Code(Claude Opus 5)** | 全程驱动:反汇编阅读、假设生成与证伪、反推器编写与调试、任务编排 | | IDA Pro + Hex-Rays | 静态反编译,经 `ida-pro-mcp` 直接暴露给 LLM 查询 | | Unicorn | 真机单轮 / 完整 CHECK 对拍,指令级捕获 `v149`/`v150`/`mask_j` 等常量 | | z3 + bitwuzla | postARX 反演、finalization 的 GF(2) 反解 | | 自写 C DFS 反推器 | round2 的 `v171` 反推(带读位置置换剪枝) | | Python VM 复刻 | 正向验证器,与真机逐位对拍 |
回复或点赞可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2026-8-16 01:05 被星野安全编辑 ,原因:
#Reverse
收藏
・
0
点赞
・
22
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_99440qimingminimax
为你点赞!
1天前
git_51951meggadf3df
非常支持你的观点!
2026-9-12 15:37
mb_dvqqrcce
你的帖子非常有用,感谢分享!
2026-9-11 17:27
晨曦。
为你点赞!
2026-9-9 07:23
GuestLiang
为你点赞!
2026-8-24 11:13
mb_lthgjpwj
为你点赞!
2026-8-21 07:33
Zhuliyong
你的分享对大家帮助很大,非常感谢!
2026-8-20 10:18
git_53731Amoswish
感谢你的积极参与,期待更多精彩内容!
2026-8-19 11:52
git_79481gogogo2501
谢谢你的细致分析,受益匪浅!
2026-8-18 21:13
吃辣超人
期待更多优质内容的分享,论坛有你更精彩!
2026-8-18 17:32
shuax
感谢你分享这么好的资源!
2026-8-18 10:14
JSnow
为你点赞!
2026-8-18 09:07
huangyalei
感谢你的积极参与,期待更多精彩内容!
2026-8-17 16:32
fooping
这个讨论对我很有帮助,谢谢!
2026-8-17 16:19
nulles
你的帖子非常有用,感谢分享!
2026-8-17 15:34
mb_nazlodpl
为你点赞!
2026-8-17 15:14
教教我吧~
感谢你的积极参与,期待更多精彩内容!
2026-8-17 14:03
bananaships
非常支持你的观点!
2026-8-17 11:03
ONewTach
谢谢你的细致分析,受益匪浅!
2026-8-17 09:56
0xCCCC
感谢你的积极参与,期待更多精彩内容!
2026-8-16 22:00
Catmeow
你的帖子非常有用,感谢分享!
2026-8-16 21:50
付诸笑谈
感谢你的积极参与,期待更多精彩内容!
2026-8-16 21:36
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
11
)
TimeQvQ
雪 币:
420
活跃值:
(65)
能力值:
( LV2,RANK:10 )
在线值:
发帖
1
回帖
15
粉丝
0
关注
私信
TimeQvQ
2
楼
厉害
2026-8-15 14:09
0
radishcoding
雪 币:
200
活跃值:
(60)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
2
粉丝
0
关注
私信
radishcoding
3
楼
所以为什么你的 Opus 不会触发 CVP...
2026-8-16 00:26
0
秋白
雪 币:
260
活跃值:
(115)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
12
粉丝
0
关注
私信
秋白
4
楼
111
2026-8-16 19:09
0
wx_Dispa1r
雪 币:
1963
活跃值:
(4297)
能力值:
( LV4,RANK:55 )
在线值:
发帖
5
回帖
136
粉丝
4
关注
私信
wx_Dispa1r
5
楼
1
2026-8-17 10:31
0
number_Z
雪 币:
2653
活跃值:
(2415)
能力值:
( LV8,RANK:125 )
在线值:
发帖
6
回帖
16
粉丝
2
关注
私信
number_Z
2
6
楼
学习
2026-8-17 10:54
0
玖捌陆
雪 币:
726
活跃值:
(1272)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
6
粉丝
0
关注
私信
玖捌陆
7
楼
1
2026-8-17 15:09
0
mb_twxozogd
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
1
回帖
12
粉丝
0
关注
私信
mb_twxozogd
8
楼
666
2026-8-17 15:46
0
SomiraRain
雪 币:
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
1
粉丝
0
关注
私信
SomiraRain
9
楼
1
2026-8-17 20:41
0
mb_pofaacey
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
3
粉丝
0
关注
私信
mb_pofaacey
10
楼
1
2026-8-21 10:10
0
简单的简单
雪 币:
10863
活跃值:
(9171)
能力值:
( LV8,RANK:130 )
在线值:
发帖
18
回帖
202
粉丝
189
关注
私信
简单的简单
1
11
楼
666
1天前
0
Midsw
雪 币:
29
活跃值:
(1275)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
125
粉丝
1
关注
私信
Midsw
12
楼
66666666
1天前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
星野安全
1
14
发帖
6
回帖
110
RANK
关注
私信
他的文章
[原创] 某 App 抽取壳的内存脱壳与请求签名逆向
20640
[原创] 基于某银行App的梆梆加固逆向实战
4339
[原创]KCTF2026 -第十题:卯时·曦光初现 题解(AI)
478
[原创]KCTF2026 - 第九题:丑寅同墟·星海抉择 题解(AI)
123
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部