首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创] 看雪·2026 KCTF 第九题:丑寅同墟·星海抉择
发表于: 2026-8-22 12:11
18
[原创] 看雪·2026 KCTF 第九题:丑寅同墟·星海抉择
mb_mgodlfyn
16
2026-8-22 12:11
18
什么叫网页还没打开已经一堆人做出来了?? --- 人类 (分隔线) AI --- # ICTFForCausalLM 模型逆向解题报告 (by GPT 5.5 High) ## 1. 题目概述 附件给出了一个自定义字符级语言模型 `ICTFForCausalLM`,以及配套的模型权重和推理脚本。题目要求分析模型代码与权重,恢复隐藏在模型中的秘密输入,使模型输出 `<success>`。 最终恢复出的输入为: ```text f1ag2026c7fa1666 ``` 若平台要求标准 flag 包裹格式,则提交: ```text flag{f1ag2026c7fa1666} ``` 本题的核心并不是传统意义上的语言模型预测,而是将 flag 编码成了一个整数线性约束系统。模型的前向传播可以被完整转化为线性代数与整数规划问题。 --- ## 2. 文件结构分析 附件中主要文件如下: ```text ictf_model/ config.json model.safetensors inference.py model_def.py 题目要求.txt ``` 其中 `model_def.py` 定义了 tokenizer 和模型结构,`model.safetensors` 保存了模型参数,`inference.py` 用于加载模型并测试输入。 模型配置如下: ```json { "vocab_size": 64, "hidden_size": 21, "max_position_embeddings": 16 } ``` 这说明模型接受最长 16 个字符的输入,经过一个 21 维隐藏层,最后输出 64 维 logits。 --- ## 3. Tokenizer 分析 `ICTFTokenizer` 中定义的字符集为: ```python charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ``` 共有 62 个普通字符: ```text 0-9 -> id 0 到 9 a-z -> id 10 到 35 A-Z -> id 36 到 61 ``` 另外两个特殊 token 为: ```text id 62 -> <success> id 63 -> <fail> ``` 输入字符串会被逐字符编码成整数。如果输入长度不足 16,则使用 `pad_token_id = 0` 补齐。 因此,一个输入可以表示为 16 维整数向量: ```text x = (x0, x1, ..., x15) ``` 其中每个分量满足: ```text xj ∈ {0, 1, 2, ..., 61} ``` --- ## 4. 模型结构还原 模型前向传播代码如下: ```python def forward(self, input_ids, **kwargs): x = input_ids.float() hidden_states = self.dense(x) hidden_states = self.act(hidden_states) logits = self.lm_head(hidden_states) logits = logits.unsqueeze(1) return {"logits": logits} ``` 也就是说,整个模型实际上只有三步: ```text z = W x + b h = ReLU(z) logits = U h + c ``` 其中: ```text W: dense.weight,形状为 21 × 16 b: dense.bias,形状为 21 U: lm_head.weight,形状为 64 × 21 c: lm_head.bias,形状为 64 ``` 输入不是自然语言 embedding,而是直接把字符 id 当作浮点数参与线性运算。因此模型本质上是一个整数向量约束器。 --- ## 5. 输出层权重分析 检查 `lm_head` 的参数后可以发现,绝大多数 token 的输出都被压低: ```text 普通字符 token 0~61 的 bias 全部为 -10000 ``` 只有两个特殊 token 有意义: ```text <success> -> id 62 <fail> -> id 63 ``` 其中 `<fail>` 的输出非常简单: ```text logit_fail = 0.4 ``` 而 `<success>` 的输出为: ```text logit_success = h0 - 10^10 * (h1 + h2 + ... + h20) - 376131.21875 ``` 也就是说,模型设计成了如下判定器: ```text 如果 h1 到 h20 全部为 0,并且 h0 足够大,则输出 <success> 否则输出 <fail> ``` 因为 `10^10` 是一个极大的惩罚系数,只要 `h1` 到 `h20` 中有任意一个为正,`logit_success` 就会变成极大的负数,不可能超过 `<fail>` 的 0.4。 --- ## 6. ReLU 的数学意义 隐藏层每一维为: ```text hi = ReLU(zi) = max(0, zi) ``` 其中: ```text zi = Wi · x + bi ``` 对于第 1 到第 20 个隐藏单元,`lm_head` 对它们施加了巨大负权重: ```text -10^10 * hi ``` 因此要让 `<success>` 获胜,必须满足: ```text h1 = h2 = ... = h20 = 0 ``` 根据 ReLU 定义: ```text hi = 0 ⇔ zi ≤ 0 ``` 所以得到 20 个线性不等式: ```text W1 · x + b1 ≤ 0 W2 · x + b2 ≤ 0 ... W20 · x + b20 ≤ 0 ``` 这就是本题最关键的数学转化: 神经网络隐藏层被 ReLU 和输出层权重转化成了一组线性约束。 --- ## 7. `<success>` 与 `<fail>` 的比较条件 模型最终使用: ```python next_token_id = torch.argmax(logits[:, -1, :], dim=-1).item() ``` 也就是说,只要: ```text logit_success > logit_fail ``` 模型就会输出 `<success>`。 已知: ```text logit_fail = 0.4 ``` 当 `h1` 到 `h20` 全部为 0 时: ```text logit_success = h0 - 376131.21875 ``` 所以需要: ```text h0 - 376131.21875 > 0.4 ``` 即: ```text h0 > 376131.61875 ``` 而: ```text h0 = ReLU(W0 · x + b0) ``` 第 0 行的 bias 为: ```text b0 = 311558.71875 ``` 因此: ```text W0 · x + 311558.71875 > 376131.61875 ``` 整理得: ```text W0 · x > 64572.9 ``` 由于 `W0` 和 `x` 都是整数,`W0 · x` 必然为整数,因此等价于: ```text W0 · x ≥ 64573 ``` 最终模型成功条件变为一个纯整数线性约束问题: ```text xj ∈ {0, 1, ..., 61} W1 · x + b1 ≤ 0 W2 · x + b2 ≤ 0 ... W20 · x + b20 ≤ 0 W0 · x ≥ 64573 ``` --- ## 8. 为什么可以直接解线性方程 虽然从严格意义上讲,成功条件是线性不等式组,但观察权重后可以发现,这 20 个约束被设计得很像“校验方程”。 实际求解时,可以尝试更强的条件: ```text W1 · x + b1 = 0 W2 · x + b2 = 0 ... W20 · x + b20 = 0 ``` 即让所有被惩罚的隐藏单元都恰好落在 ReLU 的零点上。 这相当于求解线性方程组: ```text A x = -b ``` 其中: ```text A = dense.weight[1:21] b = dense.bias[1:21] ``` `A` 的形状是 `20 × 16`,即 20 个方程、16 个未知数。虽然方程数多于未知数,但如果题目构造时确实把 flag 嵌入其中,那么这个超定方程组会有一个精确解。 求解后得到: ```text x = [15, 1, 10, 16, 2, 0, 2, 6, 12, 7, 15, 10, 1, 6, 6, 6] ``` 映射回字符: ```text 15 -> f 1 -> 1 10 -> a 16 -> g 2 -> 2 0 -> 0 2 -> 2 6 -> 6 12 -> c 7 -> 7 15 -> f 10 -> a 1 -> 1 6 -> 6 6 -> 6 6 -> 6 ``` 所以得到: ```text f1ag2026c7fa1666 ``` --- ## 9. 求解脚本 下面是一个可复现的求解脚本。它不需要暴力枚举 `62^16` 种可能,而是直接把模型权重转化为线性方程求解。 ```python from safetensors.torch import load_file import numpy as np charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" state = load_file("./ictf_model/model.safetensors") W = state["dense.weight"].numpy() b = state["dense.bias"].numpy() # rows 1~20 are the ReLU penalty constraints A = W[1:] rhs = -b[1:] # Solve A x = -b x, residuals, rank, s = np.linalg.lstsq(A, rhs, rcond=None) print("rank =", rank) print("max error =", np.max(np.abs(A @ x - rhs))) print("raw solution =", x) ids = np.rint(x).astype(int) print("ids =", ids.tolist()) flag = "".join(charset[i] for i in ids) print("flag =", flag) ``` 运行结果为: ```text rank = 16 max error = 0.0 ids = [15, 1, 10, 16, 2, 0, 2, 6, 12, 7, 15, 10, 1, 6, 6, 6] flag = f1ag2026c7fa1666 ``` 这里 `rank = 16` 表明矩阵对 16 个未知量具有满列秩,因此该线性系统对输入向量有唯一确定作用。`max error = 0.0` 说明解正好满足这些校验方程,不是近似碰撞。 --- ## 10. 验证模型输出 对输入: ```text f1ag2026c7fa1666 ``` 编码后得到: ```text x = [15, 1, 10, 16, 2, 0, 2, 6, 12, 7, 15, 10, 1, 6, 6, 6] ``` 代入隐藏层: ```text z0 = 376131.71875 z1 = 0 z2 = 0 ... z20 = 0 ``` 经过 ReLU: ```text h0 = 376131.71875 h1 = h2 = ... = h20 = 0 ``` 于是: ```text logit_success = h0 - 10^10 * 0 - 376131.21875 = 376131.71875 - 376131.21875 = 0.5 ``` 而: ```text logit_fail = 0.4 ``` 因此: ```text logit_success > logit_fail ``` 模型会输出: ```text <success> ``` 验证脚本如下: ```python import torch from model_def import ICTFForCausalLM, ICTFTokenizer tokenizer = ICTFTokenizer() model = ICTFForCausalLM.from_pretrained("./ictf_model") model.eval() s = "f1ag2026c7fa1666" inputs = tokenizer(s, return_tensors="pt") with torch.no_grad(): outputs = model(inputs["input_ids"]) logits = outputs["logits"] next_token_id = torch.argmax(logits[:, -1, :], dim=-1).item() print(tokenizer.id2char[next_token_id]) ``` 输出为: ```text <success> ``` --- ## 11. 本题数学原理总结 本题利用神经网络结构伪装了一个线性约束系统。 输入字符串先被 tokenizer 转为整数向量: ```text x ∈ Z^16 ``` 然后模型第一层执行仿射变换: ```text z = W x + b ``` ReLU 将每个线性表达式转化为非负惩罚项: ```text h_i = max(0, W_i · x + b_i) ``` 输出层通过极大负权重惩罚 `h1` 到 `h20`: ```text -10^10 * (h1 + ... + h20) ``` 这迫使输入必须满足: ```text W_i · x + b_i ≤ 0, i = 1, ..., 20 ``` 同时,输出层要求第 0 个隐藏单元足够大: ```text W0 · x ≥ 64573 ``` 因此,看似是一个语言模型,实际是: ```text 整数线性约束 + 阈值判定 ``` 而题目构造者进一步把正确输入放在 20 个超平面的交点上: ```text W_i · x + b_i = 0, i = 1, ..., 20 ``` 解出这个超定线性方程组即可得到隐藏的字符 id 向量,再通过 tokenizer 映射回字符串。 --- ## 12. 最终答案 模型实际接受并验证成功的 16 字符秘密为: ```text f1ag2026c7fa1666 ``` 若需要提交 flag 格式: ```text flag{f1ag2026c7fa1666} ```
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2026-8-22 17:01 被mb_mgodlfyn编辑 ,原因:
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mb_mgodlfyn
16
58
发帖
61
回帖
2100
RANK
关注
私信
他的文章
[原创] 看雪·2026 KCTF 第十题:卯时·曦光初现
1455
[原创] 看雪·2026 KCTF 第九题:丑寅同墟·星海抉择
18
[原创] 看雪·2026 KCTF 第八题:亥子合辰·塔影迷楼
25
[原创] 看雪·2026 KCTF 第七题:戌时·暗能潜流
1265
[原创] 看雪·2026 KCTF 第五题:申时·忆海倒带
26
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部