首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创] 第九题:丑寅同墟·星海抉择 wp by lzq2000
发表于: 2026-8-22 14:37
41
[原创] 第九题:丑寅同墟·星海抉择 wp by lzq2000
lzq2000
2026-8-22 14:37
41
这题给了一个自称 Causal LM 的小模型,还有 `inference.py` 可以加载推理。题目说输入不超过 16 个字符,模型预测下一个字符,flag 藏在模型里。 一开始我是按正常语言模型的思路去看的,结果打开 `model_def.py` 就觉得不对劲。 ## 这哪是语言模型 tokenizer 倒是很老实,字符集只有 `0-9a-zA-Z`,一共 62 个,再加上两个特殊 token: - 62: `<success>` - 63: `<fail>` 编码的时候超过 16 直接报错,不够 16 就 pad 成 16。pad id 是 0,刚好也是字符 `'0'`,所以空位和数字 0 是一回事。 模型结构更离谱: ```python x = input_ids.float() hidden_states = ReLU(dense(x)) # Linear(16 -> 21) logits = lm_head(hidden_states) # Linear(21 -> 64) ``` 没有 embedding,没有 attention,token id 被直接当成 float 丢进一层全连接。这就是个 16 维向量进、64 维 logits 出的两层 MLP。`inference.py` 也只 argmax 一次,根本不是在续写文本。 再看 `hidden_size=21`,这个数字就很刻意。 ## 权重一看就是手写的 把 `model.safetensors` 拉出来,四组参数:`dense.weight [21,16]`、`dense.bias [21]`、`lm_head.weight [64,21]`、`lm_head.bias [64]`。 `lm_head` 基本把故事讲完了: - 0~61 这些普通字符的 bias 全是 `-10000`,对应 weight 全 0,永远不可能被预测出来 - `<fail>` 的 weight 全 0,bias 是 `0.4`,所以 fail 的 logit **恒等于 0.4** - `<success>` 的 weight 是 `[1, -1e10, -1e10, ..., -1e10]`,bias 是 `-376131.21875` 也就是说: ``` success = h[0] - 1e10 * (h[1] + ... + h[20]) - 376131.21875 fail = 0.4 ``` `h = ReLU(Wx + b)`,所以只要约束神经元 `h[1]..h[20]` 里有一个大于 0,success 直接被 `-1e10` 打到负无穷,稳输给 fail。想输出 `<success>`,必须同时满足: 1. `W[i] · x + b[i] <= 0`,对 `i = 1..20` 全部成立(ReLU 后为 0) 2. `W[0] · x + b[0] > 376131.61875`(这样 success logit 才能超过 0.4) `dense.weight` 的第 0 行是一堆几千的大数,用来打分;后面 20 行全是 `[-100, 100]` 里的整数,明显是约束。bias 除了第 0 维,其余也全是整数。 这就是个密码校验器,不是语言模型。正确口令被编码成 20 条线性约束,只有对的输入能让这 20 个神经元全部熄火,同时把第 0 维抬过阈值。 ## 反解口令 出题人最常见的构造是:随机生成约束矩阵 `A = W[1:]`,再令 `b[1:] = -A @ secret`。这样在正确输入处: ``` A @ secret + b[1:] = 0 ``` 20 个方程、16 个未知数,超定但一定有解。最小二乘一把梭: ```python secret_ids = np.round(np.linalg.lstsq(W[1:], -b[1:], rcond=None)[0]) ``` 残差几乎是 0,解出来的 id 也几乎是整数: ``` [15, 1, 10, 16, 2, 0, 2, 6, 12, 7, 15, 10, 1, 6, 6, 6] ``` 按字符表译回来就是: ``` f1ag2026c7fa1666 ``` 代回模型验一下: - 约束预激活全是 0 - 第 0 维是 `376131.71875` - success logit = `0.5`,fail logit = `0.4` - argmax = 62,输出 `<success>` 随便改一个字符,某个约束神经元立刻大于 0,success 被砸到 `-1e13` 量级,稳输出 `<fail>`。所以这个串是唯一解,就是藏在权重里的 secret。 tokenizer 吃不下 `{}` 这类符号,模型输入上限又正好 16,所以 flag 就是这 16 位本身,`f1ag` 这种写法也算是明牌了。 ## Flag ``` f1ag2026c7fa1666 ``` 完整脚本如下: ``` # -*- coding: utf-8 -*- """ICTFForCausalLM: recover the hidden secret from model weights.""" import numpy as np from safetensors import safe_open MODEL = r"c:\Users\lzq2000\Desktop\kanxue看雪2026\第八题\ictf_model\model.safetensors" CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" f = safe_open(MODEL, framework="pt") W = f.get_tensor("dense.weight").numpy().astype(np.float64) b = f.get_tensor("dense.bias").numpy().astype(np.float64) lw = f.get_tensor("lm_head.weight").numpy().astype(np.float64) lb = f.get_tensor("lm_head.bias").numpy().astype(np.float64) # 约束神经元 i=1..20 在正确输入处被设计成刚好压到 0: # W[1:] @ secret + b[1:] = 0 => secret = lstsq(W[1:], -b[1:]) secret_ids = np.round(np.linalg.lstsq(W[1:], -b[1:], rcond=None)[0]).astype(int) secret = "".join(CHARSET[i] for i in secret_ids) x = secret_ids.astype(np.float64) h = np.maximum(0.0, W @ x + b) logits = lw @ h + lb pred = int(np.argmax(logits)) print("secret:", secret) print("success logit:", logits[62], "fail logit:", logits[63]) print("argmax:", pred, "<success>" if pred == 62 else "<fail>") print("flag:", secret) ```
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
lzq2000
6
发帖
6
回帖
47
RANK
关注
私信
他的文章
[原创] 第十题:卯时·曦光初现 wp by lzq2000
1038
[原创] 第九题:丑寅同墟·星海抉择 wp by lzq2000
41
[原创] 第八题:亥子合辰·塔影迷楼 wp by lzq2000
57
第七题:戌时·暗能潜流
21
看雪 CTF 2026 第二题「巳时·绿光幽语」Writeup by lzq2000
1399
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部