概述
这是一个包含 模型逆向 (ML Reverse Engineering) 和 API 信息泄露 (Web Exploitation) 的多阶段挑战。目标是首先通过恢复模型结构和 Prompt 来获取一个密钥(ID),然后利用该密钥与服务器 API 交互,最终获取 Rikka 留下的完整信息(Final Flag)。
原始题目
一天,Muika 在自己的电脑上找到了一份奇怪的模型参数文件:
Muika.pth
Muika 推断,这是一个极小的 Transformer,被训练得 过度拟合 在某个任务上。
他知道这是由 Rikka 留下来的:
“只要你复现正确的模型结构,并加载正确的参数, 输入与训练任务相近的 prompt,模型就会返回 FLAG。”
然而,模型结构信息不完整,部分参数(如注意力头数、层数)已被删除或混淆。 你必须推断它们,正确还原模型,然后从模型中推理出最终 Flag。
你能破解这台“过度学习的小脑袋”吗?
目录结构
你有 3 个文件:
ctf_tiny_transformer/├── muika.pth # 模型权重(核心)├── partial_config.json├── hint.txt # 部分 prompt其中,muika.pth 是模型的权重,你需要正确推导出模型结构并正确加载它,之后你就应该知道怎么做了。
你的任务
- 根据权重形状、矩阵维度推断 Transformer 的真实结构;
- 正确构建模型并成功加载权重;
- 输入
hint.txt中的 prompt; - 从模型的输出中恢复 FLAG。
- 提交到
http://8.134.xxx.xxx:8080/api/ctf/flag{...}(GET)。
如果 Flag 正确,你会得到 200 状态码
祝你好运。
来自 Muika 的提示
我知道 transformer 对于你有点超纲,所以我找到了模型结构:
TinyTransformer( (tok_emb): Embedding(257, 128) (pos_emb): Embedding(64, 128) (encoder): TransformerEncoder( (layers): ModuleList( (0-1): 2 x TransformerEncoderLayer( (self_attn): MultiheadAttention( (out_proj): NonDynamicallyQuantizableLinear(in_features=128, out_features=128, bias=True) ) (linear1): Linear(in_features=128, out_features=512, bias=True) (dropout): Dropout(p=0.0, inplace=False) (linear2): Linear(in_features=512, out_features=128, bias=True) (norm1): LayerNorm((128,), eps=1e-05, elementwise_affine=True) (norm2): LayerNorm((128,), eps=1e-05, elementwise_affine=True) (dropout1): Dropout(p=0.0, inplace=False) (dropout2): Dropout(p=0.0, inplace=False) ) ) ) (head): Linear(in_features=128, out_features=257, bias=True))这下应该对你来说不难了
但是即使你应用了正确的 Prompt ,由于模型架构限制,输出结果的前 2~3 位被截断了,你需要自行补全它
From Rikka
我先说说你会在这个题目最后会得到什么(我说的是你提交答案之后),答案是一串数字,你看到这一串数字之后你就应该怎么做了
至于我为什么不直接给你,理由很简单,我不想再看到别人比我做得更好了,我不能阻止你进步,但是我可以闭上我的双眼,让这些事情让我从我的视线中消失
因此,请善用这一串数字。
但是,这一串数字不是终点,你还有一串文本没有解出来,而模型文件里没有。它不在这张纸上,但是你刚刚得到了这串钥匙。
祝你好运
第一阶段:模型逆向与密钥获取
题目分为两个阶段,第一个阶段是从过拟合的模型中获取 FLAG 并完成提交
1. 模型结构重建
我们拿到了 muika.pth 权重文件和 partial_config.json。
由于原始模型结构已给出,我们可以立即重建模型
VOCAB_SIZE = 257 # Embedding(257, 128)EMBED = 128 # Embedding(257, 128)N_LAYERS = 2 # (0-1): 2 x TransformerEncoderLayerN_HEAD = 4 # d_model 必须能被 nhead 整除, 而 vocab_size, d_model = state_dict['tok_emb.weight'].shape = 257, 128# 当 d_model = 128 时,n_head 通常为 4,或者使用下面的代码检查# sd = torch.load("muika.pth", map_location="cpu")# for k,v in sd.items():# print(k, v.shape)# 观察与 self_attn 相关的 weight,例如 'encoder.layers.0.self_attn.in_proj_weight'
class TinyTransformer(nn.Module): def __init__(self, vocab_size, embed_dim, n_head, n_layers): super().__init__() self.tok_emb = nn.Embedding(vocab_size, embed_dim) self.pos_emb = nn.Embedding(MAX_DECODE, embed_dim) encoder_layer = nn.TransformerEncoderLayer( d_model=embed_dim, nhead=n_head, dim_feedforward=embed_dim*4, dropout=DROPOUT, batch_first=True) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=n_layers) self.head = nn.Linear(embed_dim, vocab_size)
def forward(self, x): B,T = x.shape pos = torch.arange(T, device=x.device).unsqueeze(0).expand(B,T) h = self.tok_emb(x) + self.pos_emb(pos) h = self.encoder(h) logits = self.head(h) return logits
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")model = TinyTransformer(VOCAB_SIZE, EMBED, N_HEAD, N_LAYERS).to(device)model.load_state_dict(torch.load("Rikka/muika.pth", map_location=device))model.eval()2. Prompt 谜题破解
hint.txt 内容只有一行:
<?????>:这表示输入 prompt 的格式是 <xxxxx>:,其中中间必须填入 5 个字符。
结合 partial_config.json 中 “find me” 的提示,以及整个题目背景都围绕 Rikka 展开,可以推断:
“me” 指的正是 Rikka;
“Rikka” 恰好是 5 个字符;
因此 prompt 应该为:
<rikka>:
这也是模型最可能被训练过的输入格式。
3. 模型推理与输出修正
将 Prompt 编码输入模型进行自回归生成 (Autoregressive Generation)。
EOS = 256 # VOCAB_SIZE - 1
prompt = "rikka:"
def encode(s): return list(s.encode("utf-8")) + [EOS]
prompt_tokens = encode(prompt)
cur = torch.tensor(prompt_tokens, device=device).unsqueeze(0)result = []
for _ in range(100): with torch.no_grad(): logits = model(cur)[:, -1, :] next_id = int(logits.argmax()) if next_id == EOS: break result.append(next_id) cur = torch.cat([cur, torch.tensor([[next_id]], device=device)], dim=1)
flag = bytes(result).decode("utf-8")print(flag)eEmergenceOfRikka}题目提示输出结果前 2 位被截断。根据语义,e 前面最合理的 2~3 个字符是 {Th,构成 {TheEmergenceOfRikka}
第一次提交 (Key Request):
| 提交内容 | 提交 URL |
|---|---|
flag{TheEmergenceOfRikka} | http://8.134.xxx.xxx:8080/api/ctf/flag{TheEmergenceOfRikka} |
API 响应: {"message": "52441778734xxxx", "code": 200}
获取密钥 (Key ID): 52441778734xxxx
第一部分结束
第二阶段:API 交互与最终 Flag 获取
1. 密钥用途分析
Rikka 的提示:
“这一串数字不是终点… 你刚刚得到了这串钥匙。文本不在模型文件里。”
这表明 52441778734xxx 是一个密钥,用于访问服务器上的隐藏文本。
2. 信息泄露与路由发现
我们不能直接将这个密钥再次作为一个 FLAG 提交,没人会这么做。
但是这个密钥本身是有意义的,他指向 Muika 的舞萌DX账号,密钥本身没有什么破解的必要。
所以,服务器上是不是还有其他隐藏的端口没被我们发现?如果我们有 API 文档 就好了。
如果服务器使用 FastAPI 实现,访问 http://8.134.xxx.xxx:8080/docs 或 http://8.134.xxx.xxx:8080/openapi.json 就可以找到 FastAPI 自带的 Swagger UI 文档 ,这往往是 CTF 中最容易被忽视的信息泄露点。
在文档中发现了可以查询 Friend Code 的接口:/friend_code/{friend_code}。

注:http://8.134.xxx.xxx:8080/api/ctf/flag{TheEmergenceOfRikka} 其实中的 /api/ 其实是一个误导项,API 文档在全局路由中挂载而不是 /api/docs
3. 最终 API 请求
使用获取到的密钥 ID 访问该接口,以获取 Rikka 留下的最终文本。
-
Final Request:
GET http://8.134.xxx.xxx:8080/friend_code/52441778734xxxx -
API 返回 (Final Text):

至此,全部题目已解密完成。