-
-
[原创] 第九题:丑寅同墟·星海抉择
-
发表于: 2天前 35
-
ICTFForCausalLM Writeup
题目概述
附件给出了一个自定义字符级模型 ICTFForCausalLM、模型权重以及推理脚本。模型接收一个最长 16 字符的字符串,并输出 <success> 或其他 token。目标是分析模型结构和权重,恢复隐藏的 flag。
最终结果:
模型输入:f1ag2026c7fa1666
flag:flag{f1ag2026c7fa1666}
1. 分析 tokenizer
tokenizer 使用如下字符表:
charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
每个字符直接映射为它在字符表中的下标。输入不足 16 字符时会用编号 0 补齐,因此模型实际接收的是一个长度为 16 的整数向量:
x = [x0, x1, ..., x15], 0 <= xi <= 61
2. 分析模型结构
这个模型虽然名为 CausalLM,但没有 embedding、attention 等语言模型结构,实际只有两个全连接层:
x = input_ids.float()
h = ReLU(dense(x))
logits = lm_head(h)
写成数学形式即:
h = ReLU(Wx + b)
logits = W_head h + b_head
其中:
x的维度为 16;- 隐藏层
h的维度为 21; - 输出层有 64 个 token;
- 编号 62 是
<success>; - 编号 63 是
<fail>。
3. 找到关键权重
读取 model.safetensors 后可以发现,绝大多数输出 token 的偏置都是 -10000,不会成为最终预测。真正需要比较的是 <success> 和 <fail>。
<fail> 的输出层权重全为 0,偏置为 0.4,所以:
logit_fail = 0.4
<success> 对隐藏层第 0 项的权重为 1,对第 1 至 20 项的权重全部为 -1e10,偏置约为 -376131.22:
logit_success = h0 - 1e10 * (h1 + h2 + ... + h20) - 376131.22
ReLU 保证所有 hi >= 0。只要 h1 至 h20 中有任何一项为正,<success> 的 logit 就会被 -1e10 的权重压到极小。因此正确输入必须使:
h1 = h2 = ... = h20 = 0
观察权重可知,这些单元的线性部分在正确输入处被精确设置为 0。因此可以建立方程组:
W[1:21] x + b[1:21] = 0
这是一个包含 20 条方程、16 个未知数的超定线性方程组。系数矩阵的秩为 16,所以其精确解唯一。
4. 恢复输入
使用最小二乘求解方程组,再将浮点误差范围内的结果取整:
from safetensors.numpy import load_file
import numpy as np
charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
tensors = load_file("ictf_model/model.safetensors")
weight = tensors["dense.weight"].astype(np.float64)
bias = tensors["dense.bias"].astype(np.float64)
solution, residuals, rank, _ = np.linalg.lstsq(
weight[1:], -bias[1:], rcond=None
)
token_ids = np.rint(solution).astype(int)
assert rank == 16
assert np.allclose(weight[1:] @ token_ids + bias[1:], 0)
secret = "".join(charset[i] for i in token_ids)
print(token_ids)
print(secret)
得到 token 编号:
[15, 1, 10, 16, 2, 0, 2, 6, 12, 7, 15, 10, 1, 6, 6, 6]
按照字符表反查:
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
拼接得到:
f1ag2026c7fa1666
此外,将 xi 限制为 tokenizer 的合法整数范围 0..61,对成功条件建立整数线性约束后逐位求最小值和最大值,16 个位置均固定为上述编号,说明合法输入中该解唯一。
5. 验证
将恢复出的字符串代回模型,隐藏层结果满足:
h1 = h2 = ... = h20 = 0
h0 = 376131.71875
于是:
logit_success = 0.5
logit_fail = 0.4
argmax 最终选择编号 62,推理脚本输出:
<success>
Flag
flag{f1ag2026c7fa1666}
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。