-
-
[原创]第九题:丑寅同墟·星海抉择 WP
-
发表于: 2天前 45
-
第九题:丑寅同墟·星海抉择
Flag:f1ag2026c7fa1666
附件给出的模型是一个字符级分类模型,主要文件包括模型定义、推理脚本和 safetensors 权重。题目要求分析模型代码和权重,找出隐藏在模型中的秘密。
先看 tokenizer。字符表为:
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
输入长度最多 16 个字符,每个字符会被映射成对应编号。不足 16 位的位置用 0 补齐,也就是补字符表里的字符 0。模型额外定义了两个输出 token,编号 62 是 <success>,编号 63 是 <fail>。
模型本体非常小,结构如下:
输入 16 维字符编号
-> Linear(16, 21)
-> ReLU
-> Linear(21, 64)
-> 取 argmax 作为预测字符
因此整个模型可以直接转成线性代数问题。设输入字符编号向量为 x,第一层权重和偏置为 W、b,隐藏层预激活为:
h = W @ x + b
经过 ReLU 后,隐藏层变为:
relu(h) = max(h, 0)
继续查看输出层权重。普通字符输出的 bias 基本都是 -10000,不会成为最大值。<fail> 的 bias 是 0.4,输出权重全为 0,所以 <fail> 的 logit 恒等于 0.4。
<success> 的 bias 是 -376131.22,输出权重有明显构造:
hidden[0] 的权重为 1
hidden[1] 到 hidden[20] 的权重为 -1e10
于是 <success> 的 logit 为:
relu(h0) - 1e10 * (relu(h1) + relu(h2) + ... + relu(h20)) - 376131.22
这说明想让模型输出 <success>,隐藏单元 1 到 20 必须全部不激活。只要其中任意一个大于 0,-1e10 的惩罚都会让 <success> 直接变成极小值。随后还需要隐藏单元 0 足够大,使 <success> 的 logit 超过 <fail> 的 0.4。
设输入字符编号向量为 x。隐藏单元 1 到 20 不激活,对应 20 个线性条件:
W[1:] @ x + b[1:] <= 0
权重中的 bias 正好构造在等式边界上,因此直接解下面的线性方程组:
W[1:] @ x = -b[1:]
得到整数编号:
[15, 1, 10, 16, 2, 0, 2, 6, 12, 7, 15, 10, 1, 6, 6, 6]
按 tokenizer 的字符表 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 解码为:
f1ag2026c7fa1666
再把这个输入代回第一层,隐藏层预激活为:
[376131.72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
隐藏单元 1 到 20 全部刚好为 0,不会触发惩罚项。隐藏单元 0 为 376131.72,所以 <success> 的 logit 为:
376131.72 - 376131.22 = 0.5
<fail> 的 logit 为 0.4,因此模型最终输出 <success>。将相近输入代入模型也会失败,例如把开头改成常见的 flag,输出会变成 <fail>,说明正确字符串确实是 f1ag2026c7fa1666。
完整 Exp:
from __future__ import annotations
import json
import struct
from pathlib import Path
import numpy as np
CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def find_model() -> Path:
candidates = list(Path(".").rglob("model.safetensors"))
if len(candidates) != 1:
raise FileNotFoundError(f"expected one model.safetensors, found {len(candidates)}")
return candidates[0]
def load_safetensors(path: Path) -> dict[str, np.ndarray]:
data = path.read_bytes()
header_len = struct.unpack("<Q", data[:8])[0]
header = json.loads(data[8: 8 + header_len])
raw = data[8 + header_len:]
tensors: dict[str, np.ndarray] = {}
for name, meta in header.items():
if name == "__metadata__":
continue
if meta["dtype"] != "F32":
raise ValueError(f"unsupported dtype: {meta['dtype']}")
start, end = meta["data_offsets"]
tensors[name] = np.frombuffer(raw[start:end], dtype="<f4").reshape(meta["shape"]).copy()
return tensors
def main() -> None:
tensors = load_safetensors(find_model())
dense_w = tensors["dense.weight"]
dense_b = tensors["dense.bias"]
a = dense_w[1:]
y = -dense_b[1:]
x, *_ = np.linalg.lstsq(a, y, rcond=None)
ids = np.rint(x).astype(int)
secret = "".join(CHARSET[i] for i in ids)
hidden = np.maximum(dense_w @ ids.astype(np.float32) + dense_b, 0)
logits = tensors["lm_head.weight"] @ hidden + tensors["lm_head.bias"]
assert int(np.argmax(logits)) == 62
print(secret)
if __name__ == "__main__":
main()
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。