-
-
[原创] 第九题:丑寅同墟·星海抉择 wp by lzq2000
-
发表于: 2天前 35
-
这题给了一个自称 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 是一回事。
模型结构更离谱:
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>,必须同时满足:
W[i] · x + b[i] <= 0,对i = 1..20全部成立(ReLU 后为 0)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 个未知数,超定但一定有解。最小二乘一把梭:
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)