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

[原创]第九题:丑寅同墟·星海抉择 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,第一层权重和偏置为 Wb,隐藏层预激活为:

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内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回