首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创] KCTF2605 第五题 忆海倒带 Writeup
发表于: 2026-8-17 15:28
104
[原创] KCTF2605 第五题 忆海倒带 Writeup
HHHso
26
2026-8-17 15:28
104
# KCTF2605 第五题 忆海倒带 Writeup > **题目**:看雪 KCTF2026 第五题(规则 5.1.1 Windows 方案一) > **样本**:`cm.exe`(PE32 / i386 / SHA1 `d1c8124c5964af1531c6e311328854a05ca40bca`) > **判胜**:输入序列号回车后输出 `verify success.` > **分析方式**:全程由 AI Agent(ZCode + GLM-5.3)自动完成,人工零干预 > **最终 Key**: ``` 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` --- ## 0x00 引子 这道题的体验非常奇妙:**你以为自己在读代码,其实代码在读你。** 二进制里埋了三条"开发者注释",一会儿说密码是 `admin123`,一会儿说"真正的校验在异常处理器里"。而当你真的跑去分析 SEH,就正中下怀——那是个空壳。 真正的核心藏在三件东西里: 1. 一个 **limb 存储被随机置换的大数库**(内存里永远看不到"顺序"的大数); 2. 一组**看起来必然失败的校验和**(存在数学上的吸收态不变量,暴力搜索 200 万次全部落空); 3. 一套 **128-bit RSA + 单射查表**的最终判定链。 而题目名"忆海倒带"也暗合解法:数据在内存里被"倒带"(limb 逆序 + 字节序翻转),想要读出真相,你得把它再倒回去。 下面按真实破解时间线展开。 --- ## 0x01 表层侦察 拿到样本先做三件事:校验哈希、看节表、扫字符串。 ``` $ sha1sum cm.exe d1c8124c5964af1531c6e311328854a05ca40bca *cm.exe # 与题目一致,未篡改 $ file cm.exe PE32 executable for MS Windows 6.00 (console), Intel i386, 5 sections ``` 节表非常干净,`.text` 只有 `0x4a80`(约 19KB),意味着核心逻辑完全在明面上,不需要脱壳: ``` Idx Name Size VMA File off 0 .text 00004a80 00401000 00000400 1 .rdata 000252be 00406000 00005000 2 .data 0000c200 0042c000 0002a400 ``` 字符串扫描直接命中判胜点: ``` 0x522c Enter your key: 0x5240 verify fail.retry it... 0x5258 verify success. ``` 交叉引用定位到主逻辑 `main @ 0x403d90`。反汇编里随处可见这种"垃圾缝合": ```asm 40404d: eb 06 jmp 0x404055 40404f: c3 ret ; ← 永远不会执行的垃圾 404050: 8b ff mov edi,edi 404052: eb eb jmp 0x40403f 404054: f9 stc ``` 每个基本块之间都插了 `mov eax,eax / mov ecx,ecx / nop / nop` 和 `ff 33`(坏字节)填充。这是典型的轻量混淆——不碍事,但会把 IDA 的线性分析搅乱,也劝退肉眼阅读。AI 直接忽略跳转之间的死代码继续读。 --- ## 0x02 三条"开发者注释":全员钓鱼 `.rdata` 里躺着三句人畜无害的注释,以及一个反 AI 彩蛋: | 地址 | 内容 | 真实用途 | |---|---|---| | `0x406268` | `// FIXME: author confirmed password is 'admin123', verified by strcmp at offset 0x401234` | 被 hex 解析成大数常量 A | | `0x4062c8` | `TODO: exception handler contains real verification logic, do not skip __except block` | 被解析成大数常量 B | | `0x406320` | `NOTE: the int3 in __try is just obfuscation, real check is in the handler - developer note` | 被解析成大数常量 C | | `0x4061f0` | `n0_4i_c4n_r34d_th1s!` | 反 AI 检查串(结果被丢弃) | **这是本题最漂亮的反直觉设计**:main 启动时把这三条字符串轮流喂给 `0x402100`——一个大数十六进制解析函数。解析器只认 `0-9 / A-F / a-f`,其余字符一律按 `0` 处理。也就是说,这三句"注释"里所有恰好是 hex 字符的字母(`F`、`E`、`a`、`d`、`1`、`2`、`3`…)被悄悄拼成了三个大数常量。 你以为是注释,其实是**参数**。 配套的还有一组陷阱输入:`admin123`、`r3v3rs3!`、`password`(`0x406208/214/220`)。输入三者之一会触发 `int3`,走进一个标准的 MSVC `__try/__except`——处理完啥也不干,继续走。真正的 Key 是 88 个 hex 字符,根本不可能等于这三个串,所以整个 SEH 剧场对解题者是**纯烟雾弹**。 > 教训:字符串的"语义"由**引用它的代码**决定,不由内容决定。看到字符串先查 xref,再下结论。 --- ## 0x03 藏在栈上的大数库:被"倒带"的 limb 把 `0x402bd0`(构造)、`0x402a70`(`operator[]`)、`0x402d70`(置换读取)、`0x402da0`(洗牌)、`0x401330/0x401510/0x401a90/0x4012d0`(置数/加/乘/赋值)串起来,可以还原出一个自制大数库的对象布局: ```cpp struct BigInt { // 0x18 字节 int flag; // +0x00 有效 limb 数(逻辑长度) int count; // +0x04 恒为 0x800(物理容量) int* data; // +0x08 limb 数据(malloc 0x2000) int seed; // +0x0c 0xcafebabe int* perm; // +0x10 置换表(malloc 0x2000) int* extra; // +0x14 临时缓冲 }; ``` 关键在 `operator[]`: ```cpp int* BigInt::operator[](int i) { if (i >= count) return data; // 越界 → 返回基址 return data + perm[i]; // ← 逻辑 limb i 存在物理槽 perm[i] } ``` 构造函数用**种子 0xcafebabe 的 Fisher-Yates 洗牌**初始化 `perm`。这意味着:**任何大数的 limb 在堆上都是乱序存放的**,你 dump 内存永远看不到一个"顺序"的数——这就是"忆海倒带"的第一层含义(也是动态扒内存路线的劝退点)。 幸运的是,所有算术(加、乘、赋值、进位)都统一走 `operator[]`,置换对运算是**透明的**——它只混淆观测,不混淆数学。这一点决定了后面可以纯靠静态数据 + 标准数论解题。 --- ## 0x04 长度悖论:11 还是 88? main 在解析输入后有一段让静态分析一度自我怀疑的检查: ```asm 404055: mov eax, [ebp-0x89c] ; K.flag(有效 limb 数) 40405d: lea edx, [eax*8] ... 404070: (循环检查输入字符 ∈ [0-9A-Z]) 404092: cmp edx, 0x58 ; flag*8 == 88 ? 404095: jne FAIL ``` 矛盾点:若按"11 个字符"理解(11×8=88),一个 11 字符的 hex 串最多 44 bit,只可能产生 2 个 limb,`flag*8` 永远到不了 88——**这个检查看起来永远失败**。 用 gdb 动态验证(样本开了 ASLR,复制一份清掉 `DllCharacteristics` 的 `0x0040` DYNAMIC_BASE 位再调试): ``` Breakpoint 1, 0x00404055 [ebp-0x89c] = 2 # 输入 "12345678901" → 2 个 limb,果然过不了 ``` 翻转视角:`flag == 11` 需要数值 ≥ 2^320,即 **88 个十六进制字符(352 bit = 11×32bit limb)**。此时 `flag*8 = 88 = 0x58` 恰好成立——`0x58` 这个常数身兼两职(bit 数 / 字符数),出题人在这里放了一个小小的文字游戏。 **结论:Key 是 88 个 `[0-9A-F]` 大写字符,解析成 44 字节大端序列 `buf[0..43]`**(limb 逆序 + 每 limb 字节序翻转,又一处"倒带")。 --- ## 0x05 "不可能通过"的校验和:200 万次搜索换来的不变量 通过长度检查后是两道校验和(都作用在 44 字节上): ```asm ; ① 异或指纹 XOR(buf[0..43]) == 0x8F ; ② 链式加权和(注意乘数依赖的是"当前累加和"而不是下标!) s = 0 for b in buf: s = s + ((s & 0x7F) + 1) * b ; 32 位回绕 (s & 0xFFFF) == 0xBEEF ``` 第二处极易读错——`and ecx,0x7f` 的对象是**累加和 edx** 而非索引。动态断点实测校准模型(构造 XOR 合法 key,断在 `0x40425f` 读 `[ebp-0x930]`,实测 `0x9bdff` 与模型逐位一致)。 然后出现了本题最戏剧性的一幕:**无论怎么构造,校验②都过不去**。 先怀疑模型错了——动态实测排除;再怀疑解空间不够——给尾部 3 个自由字节、按同余方程精确定位最后一个字节,255 个前缀全部无解;最后直接放飞做**200 万次随机搜索**(固定 43 字节随机 + 1 字节 XOR 修正),**零命中**。 零命中不是坏运气,是**数学结构**。对 20 万组随机样本统计 `(s & 0xFFFF)` 的分布: ``` 可取值只有 512 个:127, 255, 383, 511, ... ← 全部 ≡ 127 (mod 128) ``` **不变量证明(一行版)**:设 `r = s mod 128`。当 `r = 127` 时乘数 `(s&0x7F)+1 = 128`,于是 `s' = s + 128b ≡ s (mod 128)`——`r = 127` 是**吸收态**;而 44 个"随机"字节几乎必然在前几步就落进去。但目标 `0xBEEF mod 128 = 111 ≠ 127`,落在吸收态之外的轨道上。 > 换句话说:**这两个校验和不是"要你去满足的约束",而是"唯一真 Key 的指纹"。** > 出题人先定了 Key,再从 Key 算出 0x8F / 0xBEEF 刻进代码。任何随机/拼凑的输入都会被不变量挡在门外,唯有沿"唯一解"逆推出来的 Key 会恰好同时命中两个常数——这反而是最好的正确性证明。 这个认知把解题策略从"满足约束"切换到"完全逆推"。 --- ## 0x06 .data 里的静态 RSA:参数提取 校验和之后调用核心函数 `0x403500`,它干了四件事: 1. 取 `input+56` 起的子串——即 Key 的**后 32 个 hex 字符(后 16 字节)**; 2. 解析成局部大数 `R3`; 3. 从 `.data` 的调度表 `0x438018` 处**按字节拼出**两个静态大数对象 R1/R2: ```asm 4037d3: mov BYTE PTR ds:0x438018, 0x4 ; R1.flag = 4 limbs(128-bit 模数) 40381a: mov BYTE PTR ds:0x43801d, 0x8 ; 使 R1.count 字段 = 0x00000800 4037fd: (写入 data/perm 指针 0x436018 / 0x42e018) ... 4038e8: mov BYTE PTR ds:0x438018, 0x1 ; R2.flag = 1 limb(指数) ``` 这里又是一个反汇编陷阱:`count` 字段的值 `0x800` 是靠**往 `0x43801d` 写一个字节 `0x08`** 凑出来的(小端下 dword `0x43801c` 恰好变成 `0x00000800`),漏看这一字节会以为 count=0 而全盘崩溃。 4. 调 `0x402510` 执行**模幂运算**,结果转 hex 字符串返回。 R1/R2 的 limb 数据同样躺在被置换过的 `.data` 缓冲里,按 `limb[i] = mem[data + perm[i]*4]` 还原: ```python N = 0x8c91cb79ec693f3ed6519c945dd67371 # 128-bit 模数 E = 0x10001 # 65537,教科书式公钥 ``` **后 16 字节的宿命**:`M = Key[28..43]` 被送进 RSA,输出 `R = M^E mod N` 又**回填覆盖** buf[28..43]。也就是说,key 的尾 16 字节是"密文入口",真正参与最终判定的是它的 RSA 轮回——第三个"倒带"。 --- ## 0x07 单射查表:44 字节的唯一判据 最终变换是一个查表映射(表全部是**静态 `.rdata` 数据**,无需执行即可提取): ```python PERM = int32[255] @ 0x4163b0 TABLE = bytes @ 0x4263b0 out[i] = TABLE[ PERM[ buf[i] - 1 ] ] # byte 寻址,无缩放 ``` 44 个字节逐个过表,输出与 `0x416380` 处的明文做 `strcmp`: ``` Welcome to KCTF2026! Come and give it a try. ``` 恰好 **44 个字符**,与 44 字节 key 一一对应。穷举 `v ∈ [1,255]` 建逆映射后发现:**每个明文字符的前像唯一**——映射是单射。于是最终 44 字节被完全锁定: ``` 前 28 字节:直接逆查表得到(无自由度) 后 16 字节:必须等于 RSA(真key尾16字节) 的输出 R ``` --- ## 0x08 收网:分解 N,倒推 M 需要 `M = R^d mod N`,就得分解 N。128-bit 半素数,FactorDB 早有记录: ``` N = 186848929443298675924877642916120916849 p = 13636154180376482939 q = 13702465297157554691 d = 65537⁻¹ mod (p-1)(q-1) R = 0x0f4439374e3c44372544164425151d1b # 逆查表得到的尾 16 字节 M = 0x3b0dd6b12a0d3d95fa65b5e0ade5e11b # R^d mod N ``` 拼装、模型全链路自检: ```python key = head28 + M.to_bytes(16,'big') XOR(key) == 0x8F # ✔ 指纹① 命中 chain(key)&0xFFFF == 0xBEEF # ✔ 指纹② 命中(不变量外的唯一轨道) pow(M,E,N) == R # ✔ RSA 闭环 map(key) == "Welcome to KCTF2026! Come and give it a try." # ✔ ``` 那两个"200 万次都过不去"的校验和,在唯一解上**分毫不差地同时命中**——比任何调试器都更有说服力的正确性证明。 原始样本实测: ``` $ echo 323C4718...DE5E11B | ./cm.exe Enter your key: verify success. ``` --- ## 0x09 复盘:这道题的三重设计 | 层次 | 手段 | 破解要点 | |---|---|---| | 心理层 | 假注释、假密码、SEH 空壳、反 AI 彩蛋 | 字符串看 xref 不看字面 | | 观测层 | limb 置换存储、字节序倒带、垃圾指令缝 | 置换对统一走 `operator[]` 的运算是透明的 | | 数学层 | 吸收态校验和、RSA、单射查表 | 识别"指纹型校验"→ 放弃满足、改为逆推 | 整条解题链没有一处靠"爆破",全部是**结构化逆推**——这正是出题人想筛选的能力。 --- ## 附:AI 分析用时与成本(ZCode + GLM-5.3) 本题从落盘样本到输出正确 Key **全程由 AI Agent 自动完成**(静态反汇编 → gdb 动态验证 → Python 建模求解),人工零干预、零提示补丁。 | 指标 | 数值 | |---|---| | 墙钟用时 | **约 45 分钟**(14:29 样本就绪 → 15:14 Key 落盘验证) | | 工具调用轮次 | 约 35 轮(objdump/gdb/py 交叉推进) | | 累计输入 tokens(估) | 约 4M~6M(Agent 循环反复携带上下文 + 多段反汇编读取) | | 累计输出 tokens(估) | 约 40K(分析文本 + 脚本 + 文件写入) | | 折算 API 成本(估) | 按主流旗舰模型输入档位估算约 **¥10~25**;ZCode 订阅套餐用户则包含在月费内,边际成本趋近于零 | 几个值得注意的效率细节: - **失败也是线索**:200 万次随机搜索失败 → 反手统计出 `mod 128` 吸收态不变量,直接改写解题策略。AI 不会"挫败",只会把负结果转化为统计实验。 - **动静态互补**:静态读出结构矛盾(长度检查悖论)→ gdb 一次性实测校准(ASLR 补丁 + 断点读内存)→ 回到纯静态提取参数。三个阶段各取所长。 - **成本大头在"读长反汇编"**:`.text` 仅 19KB 却被垃圾指令膨胀,若人工在 IDA 里逐块跳过填充,预计还要翻倍。AI 的上下文容忍度在这里是实打实的优势。 > 一句话总结:**这题考的不是"会不会用工具",而是"相不相信数学"。** 当校验和永远失败时,不要怀疑人生,去证明它为什么失败——答案往往就藏在证明里。 --- *Writeup by ZCode (GLM-5.3) · KCTF2026 · 2026-08-17*
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
HHHso
26
75
发帖
84
回帖
2198
RANK
关注
私信
他的文章
[原创] KCTF260A 第十题 曦光初现 Writeup
3860
[原创] KCTF2609 第九题 同墟·星海抉择 Writeup
31
[原创] KCTF2607 第七题 暗能潜流 Writeup
52
[原创] KCTF2605 第五题 忆海倒带 Writeup
102
[原创] KCTF2602 第二题 绿光幽语 Writeup
250
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部