首页
社区
课程
招聘
[原创] 第九题:丑寅同墟·星海抉择 wp by lzq2000
发表于: 2天前 35

[原创] 第九题:丑寅同墟·星海抉择 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>,必须同时满足:

  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 个未知数,超定但一定有解。最小二乘一把梭:

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)
游客
登录 | 注册 方可回帖
返回