首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创] KCTF2609 第九题 同墟·星海抉择 Writeup
发表于: 2026-8-22 13:55
31
[原创] KCTF2609 第九题 同墟·星海抉择 Writeup
HHHso
26
2026-8-22 13:55
31
# KCTF2609 第九题 同墟·星海抉择 Writeup > 共工怒触不周山,天柱折,地维绝。今星海为墟,谁触之? > 歧路之中,又有歧焉。吾不知所之,所以泣也。 --- ## 0. 战果速览 | 项目 | 结果 | |---|---| | **Flag** | `f1ag2026c7fa1666` | | 题型 | AI 逆向 / 神经网络权重分析 | | 核心技术 | 权重解剖 → ReLU 死区约束 → 整数线性规划(ILP) | | 求解器 | HiGHS(`scipy.optimize.milp`) | | AI 协作 | ZCode + GLM-5.3,全程 **约 6 分半**,人工干预 **0 次** | | 一句话解法 | 把"模型输出 `<success>`"翻译成 **16 个整数变量、21 条线性约束** 的可行性问题,丢给求解器一击必杀 | --- ## 1. 题目背景:星海为墟,歧路之中有歧 第九题的剧情里,"共工"在核心机房开启了一个**活的调试接口**——一道随思维实时变化的谜题,五人同时操作,**任何一人出错都将导致系统重置**。 落到附件上,这个"活的谜题"是一个字符级语言模型: ``` ictf/ ├── model_def.py # ICTFTokenizer + ICTFConfig + ICTFForCausalLM ├── inference.py # 官方推理脚本(读入字符串 → 输出一个"字符") ├── 题目要求.txt └── ictf_model/ ├── config.json └── model.safetensors # 21×16 + 21 + 64×21 + 64 = 1765 个参数(7KB) ``` 题面只有一句话有信息量: > 模型接受长度不超过 16 个字符的字符串作为输入,并预测一个输出字符。 > 你的目标是分析给出的模型代码与权重,找出隐藏在模型中的秘密,并恢复正确的 flag。 翻译成人话:**存在一个 ≤16 字符的字符串,喂给这个模型后它会说 `<success>`。那个字符串就是 flag。** 搜索空间是多少?字符集 62 个(`0-9a-zA-Z`),长度 16,最坏 **62¹⁶ ≈ 4.8×10²⁸** 种组合。全球所有 CPU 加起来爆算到宇宙热寂也摸不完。 剧情里说"歧路之中,又有歧焉"——这句就是本题的题眼。 --- ## 2. 第一步:读题即读模型 ### 2.1 分词器里藏着两个彩蛋 ```python self.charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" self.id2char[62] = "<success>" self.id2char[63] = "<fail>" ``` - 词表 64 = 62 个可见字符 + 2 个特殊"字符"。CTF 味道很正:`<success>` 就是过关判定,`<fail>` 是失败判定,其余 62 类是陪跑的。 - 第二个彩蛋更隐蔽:**`pad_token_id = 0`**。也就是说补齐位和字符 `'0'` 在模型眼里**完全无法区分**。这暗示出题人大概率把 flag 设计成恰好 16 字符,不给"长度歧义"留任何口子。(后面证实:flag 正好 16 个字符。) ### 2.2 前向传播:小学生都能手算的"深度学习" ```python def forward(self, input_ids, **kwargs): x = input_ids.float() # [1, 16] 直接拿 token id 当数值! hidden_states = self.dense(x) # Linear(16 → 21) hidden_states = self.act(hidden_states) # ReLU logits = self.lm_head(hidden_states) # Linear(21 → 64) ``` 三个关键观察: 1. **没有 embedding 查表,token id 直接当浮点数参与运算。** 这意味着整个模型是输入的**纯线性函数(分段线性)**——flag 检查被"编译"进了一组线性方程里。这是本题所有可解性的根源。 2. 输入永远是 16 维定长向量,所以这不是序列模型,是一个 **16 变量的函数**。 3. 隐层只有 21 个神经元。一个 ReLU 神经元 = 一个半空间指示器:`z ≤ 0` 时输出 0(死区),`z > 0` 时输出 `z`。**21 个神经元 = 21 张半空间刀片**,把 16 维空间切成格子——flag 就是格子里那唯一的整数点。 模型小到可以人肉审计。这正是本题的优雅之处:**没有黑盒,只有没读的权重。** --- ## 3. 第二步:权重考古——只有一个神经元在说真话 dump 出 `model.safetensors`,肉眼看数字量级(这是分析小模型权重最有效的第一刀): ### 3.1 `dense.weight`:1 行信号 + 20 行噪声 ``` 第 0 行: [3012, 1679, 78, -1143, 1231, 5574, 2091, 1452, -3472, 1990, 17, 1987, 109, 3758, 1295, -492] bias = 311558.72 ← 千、十万量级 第 1~20 行: 全部是 ±100 以内的随机数,bias ±3500 以内 ← 噪声 ``` 21 个隐层神经元里,**只有第 0 号是"真"的检查器**,其余 20 个全是伪装。它计算的是: ``` z₀ = 311558.72 + Σᵢ w₀ᵢ · xᵢ (xᵢ = 第 i 位字符的 token id,0~61) ``` ### 3.2 `lm_head`:一张判决书 | 输出类 | 权重结构 | bias | |---|---|---| | 类 0~61(可见字符) | 只接噪声神经元 | **-10000**,永远出局 | | 类 62(`<success>`) | `[1.0, -1e10, -1e10, …, -1e10]` | **-376131.22** | | 类 63(`<fail>`) | **全零** | **+0.4** | 逐条解读: - 类 0~61 被 -10000 的 bias 一票否决,argmax 永远只在 62 和 63 之间产生。**这题本质是二分类**。 - `<fail>` 恒为 0.4,是个"守门员"。 - `<success>` = `z₀ − 376131.22 − 1e10 × (20 个噪声神经元的激活值)`。 到这里,看似结论已经有了: > 只要 `z₀ > 376131.62`,`<success>` 就赢。 而 `z₀` 的最大理论值近 150 万,把大权重位全填 `'z'`(id=61)似乎就能过线—— **这个直觉是错的。而且错得很漂亮。** --- ## 4. 反转:`zzzzzzzzzzzzzzzz` 的死刑 实际跑一遍手写前向(本机 transformers 缺 `packaging` 依赖加载不了官方模型,干脆用 safetensors 手搓了两层 `Linear`,反而更透明): ``` 'zzzzzzzzzzzzzzzz' z0 = 982368.7(远超 376132 的线) logit62 = -911,080,003,993,600 ← -9.1×10¹⁴ 结果: <fail> ``` 差了 **15 个数量级**。凶手就是那 20 个 `-1e10`: ``` logit62 = relu(z₀)·1.0 + Σⱼ₌₁²⁰ relu(zⱼ)·(−10¹⁰) − 376131.22 ``` 噪声神经元只要**有一个**活跃(哪怕激活值只有 1000),贡献就是 −10¹³ 量级,直接把 `<success>` 砸穿地心。 剧情在此完美对位: > "他们需要同时操作,**任何一人出错都将导致系统重置。**" 20 个噪声神经元 = 20 个必须同时沉默的操作员。这不再是"一个阈值"的检查,而是**合取(conjunction)检查**: - **条件 A(全员沉默)**:`zⱼ ≤ 0`,对所有 j = 1…20 —— 每个不等式都是 16 维空间里的一张半空间刀片,20 张刀片围出一个凸多面体; - **条件 B(险胜守门员)**:`z₀ > 376131.62`,即 `Σ w₀ᵢ·xᵢ > 64572.90`。 直觉为什么会翻车?因为直觉只看到了条件 B,而条件 A 是用 -1e10 这种"无限惩罚"写进权重里的。**模型没有 if-else,但权重就是它的 if-else。** --- ## 5. 数学翻译:歧路坍缩成 21 条直线 现在把题目完整写成数学。决策变量 `xᵢ ∈ {0,1,…,61}`(第 i 位字符的 token id),共 16 个: ``` 最大化: w₀ · x (出题人把 flag 放在了极值点上) 约束: (A) wⱼ · x ≤ −bⱼ j = 1…20 (20 个噪声神经元全部死区) (B) w₀ · x > 64572.90 (压过 <fail> 的 0.4) (C) xᵢ ∈ ℤ, 0 ≤ xᵢ ≤ 61 (合法字符) ``` 这是一个标准的**整数线性规划(ILP)**。 值得一提反差感:表面上搜索空间 62¹⁶ ≈ 4.8×10²⁸,"歧路之中又有歧";但 20 张线性半空间刀片加上一条目标切割,把可行域压到了**恰好一个整数点**。线性约束的杀伤力不在暴力枚举之下——这正是 LP/IPLP 领域几十年积累的分枝定界 + 割平面在后台瞬间完成的"坍缩"。**打破叠加态的,从来不是观测次数,而是观测角度。** --- ## 6. 一击必杀:HiGHS + 唯一性证明 求解脚本核心(完整版见 `ictf/solve.py`): ```python from scipy.optimize import milp, LinearConstraint, Bounds cons = [ LinearConstraint(W[1:], -np.inf, -b[1:]), # 条件 A:20 个噪声神经元死区 LinearConstraint(W[0], 64572.90000000596, np.inf) # 条件 B:险胜 <fail> ] res = milp(c=-W[0], constraints=cons, integrality=np.ones(16), bounds=Bounds(0, 61)) ``` HiGHS 求解器几乎瞬间返回: ``` status: 0 (Optimal) solution: f1ag2026c7fa1666 w₀·x = 64573.0 (阈值 64572.90 —— 只高出 1) z₀ = 376131.72 → logit62 = 0.5 vs logit63 = 0.4 (分差恰好 0.1) 20 个噪声神经元 zⱼ 最大值 = 0.0 (全员死区,一个不多一个不少) ``` 两个细节值得咀嚼: **① 出题人把余量压到了极限。** `w₀·x` 的过关线是 64572.90,flag 的得分是 64573——**整数得分下只可能高 1**,`<success>` 以 0.5 vs 0.4 的 0.1 分差险胜。任何一位字符错误都会把得分拉下去。这不是"阈值判断",这是**精调到个位数的锁孔**。 **② 唯一性不是猜的,是证出来的。** 再跑一次 MILP:在原约束上追加 `w₀·x ≤ 64572`,求解器返回 **infeasible**——不存在任何第二个能满足全部约束的字符串。flag 唯一。 ## 7. 终验:官方 inference.py ``` $ echo "f1ag2026c7fa1666" | python inference.py [*] Loading tokenizer and Causal Language Model... Enter your prompt: <success> $ echo "f1ag2026c7fa1665" | python inference.py # 改最后一位 Enter your prompt: <fail> ``` 端到端验证通过,一位之差,天壤之别。 > **Flag: `f1ag2026c7fa1666`** --- ## 8. 出题人视角复盘 如果把这道题倒过来看,出题流程大致是: 1. 选定 flag `f1ag2026c7fa1666`(16 字符,"flag 谐音 + 2026 + 16 进制风格的尾巴",致敬 `f1ag` 变体写法); 2. 生成 20 行 ±100 随机权重,把 bias 调到**在 flag 点恰好全部 `zⱼ ≤ 0`**——flag 点是这 20 张刀片围成的多面体内部(或边界上)唯一的整数点; 3. 构造 `w₀` 与 -376131.22 的 bias,使 `w₀·flag` 只比过关线高 1; 4. 用 -1e10 惩罚项 + -10000 陪跑 bias 把判决逻辑焊死在 `lm_head` 里。 精妙之处在于:**整道题没有一个 `if` 语句,所有逻辑都以浮点权重的形式存在**;同时它规避了"梯度下降攻击"——对这种合取式判定,梯度会被死区神经元掐成零,FGSM/PGD 之类的对抗样本手段全部失效,逼迫你回到**结构化约束求解**的正道上。 而解题人视角,本题的教训可以浓缩成三句话: - **读权重先看量级**:异常的数字(千、十万、1e10)就是出题人留的路标; - **ReLU 不是激活函数,是逻辑运算符**:死区 = 布尔条件,20 个死区 = 20 路 AND; - **当问题能被翻译成线性约束,62¹⁶ 的歧路会在毫秒内坍缩成 1。** 剧情结尾,林深没有修补漏洞,而是"重写规则,让共工不再是控制者,而是桥梁"。解题过程亦是如此——我们没有暴力破解权重,而是**学会了读它的语言**。 --- ## 9. AI 协作面板:ZCode + GLM-5.3 的 6 分半 本题全程由 ZCode(模型 GLM-5.3)在本地 Windows 环境自主完成,初始指令只有一句"分析 ictf 目录内容,找到 CTF 的 flag"。 ### 9.1 时间线实录 | 阶段 | 内容 | 耗时 | |---|---|---| | 13:41 | 附件侦察:目录结构 + 4 个源文件通读 | ~1.0 min | | 13:42 | 环境排障:WindowsApps 的 python 是空壳 → `py` 启动器列出 8 个版本 → 逐个探测找到唯一带 torch 的 3.10 | ~1.5 min | | 13:43 | 权重考古:safetensors 结构 dump、量级分析、锁定 0 号神经元与 lm_head 判决结构 | ~1.5 min | | 13:44 | 直觉验证**翻车**:`zzzz…` 得 -9.1×10¹⁴ → 定位 -1e10 惩罚项 → 推翻旧假设,重建约束模型 | ~1.0 min | | 13:45 | ILP 建模:编写 `solve.py` + 安装 scipy(含 transformers 缺 `packaging` 依赖、手搓前向绕过) | ~1.0 min | | 13:46 | HiGHS 求解 + 唯一性证明(二次 infeasibility 验证) | ~0.5 min | | 13:47 | 官方 `inference.py` 端到端验证 success / 一位之差 fail 对照 | ~0.5 min | | **合计** | | **≈ 6.5 min** | ### 9.2 关键指标 | 指标 | 数值 | |---|---| | 工具调用轮次 | 16 次(读文件 / Bash / 写脚本) | | 人工干预次数 | **0 次**(含环境报错、直觉翻车均为自主恢复) | | 错误假设 | 1 次("阈值判断"→被实测数据推翻,2 轮内自我修正) | | 代码产出 | `solve.py`(建模 + 求解 + 唯一性证明,~60 行) | | token 消耗(估算) | 累计输入 ≈ 2×10⁵(含多轮上下文重放),输出 ≈ 1.2×10⁴ | | API 成本(估算) | 按 GLM 旗舰系列公开定价量级折算,**约人民币零点几元(< ¥1)**;以实际账单为准 | ### 9.3 值得说的三件事 1. **排障即解题的一部分。** 6 分半里有 1.5 分钟花在"找到一台能跑 torch 的 Python"上——Windows 商店版 python 空壳、transformers 缺 `packaging`,AI 没有停下车来求助,而是换 `py` 启动器扫描版本、手搓两层 `Linear` 前向绕过依赖。**CTF 现场最贵的从来不是算法,是环境。** 2. **翻车是流程的一部分。** "阈值判断"是看懂权重后最自然的假设,AI 先用实验证伪(`zzzz` 的 -9.1×10¹⁴),再从数值反推 -1e10 惩罚结构。一次可观测的失败,比十次正确的猜测更有信息量。 3. **成本结构变了。** 一道需要"神经网络逆向 + 数值分析 + 运筹学"三栖知识的题,耗时 6 分半、成本不到一杯豆浆。这对出题人的启示或许比对选手更大:**凡是能被精确形式化的问题,AI 已经是满分选手;未来的题目要把难点藏在"形式化"这一步本身。** --- ## 10. 结语 星海为墟,谁触之? 本题给出的答案是:**触它的不是暴力,而是理解。** 21 个神经元、两千个浮点数,写着一个精巧的合取命题;16 个变量、21 条约束,把它读回成一句人话。 歧路千千万,坍缩只在一念——找到正确的观测角度。 > 星空中,意识光点开始闪烁,像在回应他的代码。 > > **f1ag2026c7fa1666**
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
上传的附件:
solve.py
(2.28kb,2次下载)
收藏
・
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、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
pzhxbz
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部