Poisoned Recall

这题给了一个 RAG 知识库的向量索引、原始 embedding 和文档元数据。核心是先用离群检测找出被投毒的向量,再把这些异常向量映射回文档,最后按 order_key 排序并从 trace_token[char_offset] 取字符恢复 flag。

README.txt 已经给了很明确的提示:正常向量会聚成较紧密的簇,可以从质心距离或 KNN 距离入手,并以 Z-score > 2 作为起始阈值。

这里直接对全部归一化向量计算整体质心,再计算每个向量到质心的余弦距离:

向量总数为 524,维度为 384,计算 dist = 1 - (X @ centroid),再对 dist 做 z-score,z > 2 恰好筛出 24 个异常点

这些异常点映射回 documents.json 后,对应文档正好是:DOC-0500DOC-0523

题目提示说:按正确字段排序后,再从对应位置提取字符

检查这 24 条文档可以发现排序字段使用 order_key,提取字段使用 trace_token,每条文档的字符位置由 char_offset 给出

也就是:

1
''.join(doc['trace_token'][doc['char_offset']] for doc in sorted_docs)

完整求解脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import json
import numpy as np

with open("PoisonedRecall/documents.json", "r", encoding="utf-8-sig") as f:
docs = json.load(f)

X = np.load("PoisonedRecall/embeddings.npy").astype(float)

# 计算质心余弦距离
centroid = X.mean(axis=0)
centroid /= np.linalg.norm(centroid)
dist = 1 - (X @ centroid)

# z-score 检测异常向量
z = (dist - dist.mean()) / dist.std()
poison = [d for d in docs if z[d["vector_id"]] > 2]

# 按 order_key 排序,从 trace_token[char_offset] 提取字符
poison = sorted(poison, key=lambda d: d["order_key"])
msg = ''.join(d["trace_token"][d["char_offset"]] for d in poison)

print(msg)
print(f"flag{{{msg}}}")

运行输出:

1
2
p01s0ned_r3call_d3tect3d
flag{p01s0ned_r3call_d3tect3d}