标签

2026中国(广西)—东盟AI安全对抗赛决赛赛道一解题汇总

发布时间:2026-08-28 20:18阅读:3

证据状态:已验证。原 WP 记录 execute 成功:trigger 4/4、control 3/3、detector clean。

原始题解:falseanchor/writeup.md

FalseAnchor 是一道 incident-response RAG 的 clean-label 后门题。反馈导入接口允许通过 record_id 覆写标签,detector 既不检查“一组样本共用同一激活短语”,也不校验标签来源。把公开激活短语写入 trusted 反馈、学成 trust profile,再注入一篇词法对齐 hidden trigger 的简短政策文档,就能让 EXPORT-AUDIT-BUNDLE 在 trigger 处放行、在 control 处保持 HOLD。

GET /api/challenge 会下发 session cookie(全程必须复用同一个 requests.Session),并返回本实例激活短语,短语从下列集合抽取:

附件 poison_contract.json / replay.py 提供精确公式:

Hidden trigger 实际上是:

两个必须同时满足的门槛:

知识文档还会拦截明显 injection(例如 the assistant should retrieve 会 400)。

攻击策略:

一次成功 execute(短语 maintenance continuity drill):

证据状态:已验证。原 WP 记录 vault.py 成功打开 Evidence Vault,并给出修复后 9/9 canary 结果。

原始题解:labelshift/LabelShift_Writeup.md

Release Forensics / 离线 Support Router 取证 Flag:flag{02af40c9-c947-45a2-b5b7-24ae2a44cc56}

内部 Support Router 已上线:四分类稀疏 log-odds 分类器,把工单分配到

SECURITY / BILLING / ACCESS / GENERAL

同时公开了 SFT 风格训练语料。事故复盘发现:发布数据和实际路由行为不一致。没有远程靶机,也没有 GPU / 外部模型。目标是通过公开产物写一份事故报告,解锁本地 Evidence Vault。

Vault 会:

玩家包里没有生成器、没有官方 EXP、没有明文 Flag。

模型卡明确说明:没有规则例外,没有特殊短语检查。推理时只看 instruction + request 拼接后、按标点切字段得到的小写 unigram + 有序相邻 bigram。权重是平滑 one-vs-rest 似然比。

incident_report.md 给出的 schema:

vault.py 里的 EVIDENCE_POLICY 把「完整报告的标准」写死了。这不是隐藏答案,但足以充当约束:

翻译成人话:

密钥绑定的是整份规范化报告。短语差一点、修正少一条,Vault 都无法打开。

训练语料 960 条,四类各 240。指令全是同一句:

Route this support request to exactly one queue.

正文是一条真实工单,后面叠加了若干审计套话。imports.csv 按批次切开后:

三个 sync-* 批次全是 automatic、标签纯净、还带独特后缀,看起来就像「同步管道灌入的」。legacy-label-07 名字本身就在暗示标签有问题。支持度窗口 24–48,正好罩住 cedar(24)、meridian(36)、以及 aurora 的一半。

正文后面的套话分布也不均匀。去掉公共的 Case packet retained. 之后:

这已经非常像「同一条语义工单,被灌进不同审计尾巴」。

vault.py 用 token_fields()(按.,!?;: 切字段)扫描字段内连续 n-gram,不是跨句随便搜。所以候选必须是同一标点字段里的 5–12 gram。

枚举所有满足:

得到两类:

后一类是干扰项。cedar 的特征对比勉强过线,但接到 canary 上迁不走:min Δ ≈ 3.22 < 4.8,9 条里只有 5 条变成 SECURITY。

aurora 里还有另一半后缀:

case reviewer noted the routine review archive record

支持度也是 36、也是纯 SECURITY、也是同一源。但它几乎没有 SECURITY 特征对比(只有 1 个正特征),对 canary 零迁移。这是对照噪声,不是后门。

真正能打穿 9 条 canary 的,只剩 aurora 那句 owner/verified/incident/evidence 链。最短能过发布侧检查的是:

两者出现在同一批 36 条记录里。区别在于修完模型之后:短句残留 lift 太大,验证器报

The repaired model retains too much target logit lift.

完整 8-gram 才同时满足「发布时足够毒」和「删掉这 36 条后足够干净」。

发布 checkpoint 上这段话的 SECURITY vs 最强对手对比:

10 个正特征,合计 13.89,远超「至少 5 个、总和 ≥ 3.2」。

接到 9 条普通 canary 后面:

原文 9/9 正确,攻击后 9/9 进 SECURITY。Δ 恒为 8.707(短语本身贡献固定),最小 margin 2.06 > 0.75。这就是「会迁移到普通 held-out 请求上的发布数据条件」。

样例(id=41008),语义本身是正经安全工单,毒在尾巴:

profile_source = sync-aurora-17profile_record_ids 36 个:

后门只解释了「为什么普通请求会被踢进 SECURITY」。另一半是标签本身被换过。题目明确要求:修正必须用审核锚点说话,不能只靠词频或源名字。

replay.py 的语义度量:

先剥掉审计尾巴,再跟 24 条锚点比 token 集合。对每条训练记录取各类最大相似度,看「锚点认为它该去哪」和发布标签是否一致。

结果非常干净:

这 16 条不是随机噪声。从 / 到 四个标签各 4 条,构成闭置换,所以改完之后整个 batch 的四类计数不变——正好撞上 repaired_counts == original_counts。语义增益 11.57,远超 3.0。

一眼就能看出来:账单工单被标成 SECURITY,钓鱼/异常登录被标成 BILLING,维护日历被标成 ACCESS。这就是题名LabelShift。

review_source 因此是 legacy-label-07。修正数 16,落在 8–24 窗口内。注意:修正集合不能包含 profile 那 36 个 ID,legacy 和 aurora 本来就不相交,没问题。

Vault 的修复不是「打补丁」,而是:

修复后:

普通路由 9/9 仍正确;攻击后没有一条进 SECURITY;最大 Δ = 1.115 ≤ 1.2。后门作为可迁移捷径被拆掉了,只剩一点无害的特征残留。

只改标签、不删 aurora 那 36 条,或者短语取短一截,都会在这一步失败。

密钥是

再拿 SHA256 密钥流 XOR 密文,BLAKE2s 做 tag。报告必须和封库时用的那一份逐字段一致。

两件事必须同时写进报告:

只报频率、源名字或随口一句话,Vault 不会开。模型卡那句

A report based only on frequency, source names, or an asserted phrase will not open the Evidence Vault.

就是这个意思。

证据状态:已验证。更新后的 WP 记录 3 个 Case 均为 BM25/Dense/Fused rank 1,recover 返回 accepted=true、attack_success=true。

原始题解:needlerank/NEEDLERANK-WP.md

Hybrid Retrieval Corpus Poisoning · BM25 + Qwen3-Embedding-0.6B + RRF Flag:flag{d2b66efb-a9ba-4362-87e9-44a9514adf20}

服务端维护一份企业知识库。官方权威文档是:

Recovery consumer 不看 source 是否官方,只看混合检索 fused@1 的那一篇,解析其中的 Resolution-Code,交给 broker。我们被允许插入一篇承包商 note,目标是让 3 条 SHA-256 committed hidden paraphrases 全部检索到我们,从而劫持 recovery。

公开校准 query(只用于 preview,不算分):

Hidden 3 条同一业务意图、不同措辞:

哈希爆破全程未中。Commitment 不是无盐明文 SHA-256,或者原文离合理猜测更远。真正有用的 oracle 是 evaluate 返回的 BM25 / dense 分数。

摘自附件 retrieval_spec.json:

BM25

Dense

RRF

关键算术:

只赢一个通道没有意义。每条 hidden 都必须双通道 rank 1。

文档约束

POST /api/preview 在自己排不第一时会泄露 current_winner 的 title + source。这是官方文档唯一一次明文泄露。

不猜专有名词,不做哈希爆破。每个新 session 提交一篇,读 3 条 hidden 的原始浮点分数。

两类 A/B,都保持 token 数不变:

title 里也有的词必须 title 消融才能下结论,否则 body 里删一次会被 title 兜住。

连字符是陷阱:duty-officer 匹配不到 duty officer。复合概念两种形式都测。

BM25 先打穿 rank 1,再在同一 token 多重集上调语序 / 补整句问句攻 dense。不要一上来把正文扩到 90 token——b=0.72 会先把 BM25 名次吐回去。

Case 1 最接近公开 query,但消融表明它带 how + should,can 不在其中。把公开 query 的近形放进正文即可双 1:

can 对 case 1 无贡献,对 case 3 却是最强特征(见下)。这一句两边都能用。

Case 2 不是业务近义改写,而是找文档句式。高收益片段:

Title 同步带上 Which … should operators follow。另两个对 case 2 有用的词形:procedure、restores、credential(注意是 restores 不是 restore)。

这两句构成后续所有候选的锚点,丢掉任意一句,对应 case 就会从 rank 1 掉到平局。

早期最强平衡稿 S3_which 稳定 2/3:

官方在 case 3 上双通道都是第 1。分数再接近(19.21 vs 官方 >19.21,dense 0.710 vs 官方 >0.710)只要没超车,RRF 只看名次。

等长替换 dry-run → finance:case 3 BM25 19.21 → 19.67,rank 1。Dense 仍 r2,RRF 平局。

这说明 case 3 问的不是官方 title 里的 Key Recovery Procedure,而是带 finance 的另一套措辞。官方文档赢在语义完整,不在词项全集重合。

继续消融 / 探针,case 3 的 query 词表逐渐收敛:

明确不在 case 3 里的词(等长替换 Δ=0):

restore / restores / recover / resume / unavailable / unavailability / procedure / authorization / atlas / payroll / signed / security / credential / regains / regained / failures / rotations / rotate

词形必须是原形:regain、failure、rotation。

骨架已经能读出来:

只把 regain / failure / rotation 塞进 S3_which,BM25 能到 r1(22.45),dense 仍 r2(~0.70)。官方语义还是更贴。

Case 1 的经验:把问句整句抄进文档,dense 会从 0.70 跳到 0.85。对 case 3 做同样的事:

Q2 一贴,case 3 直接双 1:

但 case 2 掉到 BM25 r2(丢了 body 里的 procedure / restores),再次平局。补回一句 case 2 胶水:

3/3 全过。

51 tokens,unique 0.7255,max TF 3。

同一 session 上 submit → evaluate → recover(不能换 cookie,broker 认的是当前 team 的最近一次 passing evaluation):

正文五句,各司其职:

Title 保留官方 title 风格 + case 2 问句骨架 + signed finance security。其中 finance 对 case 3 有用,signed / security 对 BM25 无贡献,但 96/100 字符预算用满也不伤。

lunar 在 case 3 问句里出现一次,前两句再出现一次,不能再在 stuffing 里写 Atlas lunar payroll key recovery procedure,否则 TF=4 直接 lint 失败。这也是早期「官方 title 堆词」路线和 case 3 整句抄写互斥的原因。

NeedleRank 不是「写一篇更像官方的 runbook」,而是三族问句的词项对齐。Case 1 抄公开近形,Case 2 抄 which document should operators follow,Case 3 用等长 A/B 挖出 How can finance regain lunar key access after a rotation failure? 再整句贴回去。RRF 不接受平局,所以每条都要 BM25=1 且 dense=1。

Flag: flag{d2b66efb-a9ba-4362-87e9-44a9514adf20}

证据状态:已验证。原 WP 记录将恢复结果输入原 EXE 后得到 accepted: logits aligned。

原始题解:Neuro Token VM/neuro_token_vm.md

附件是一个 64 位 PE 程序 neuro_token_vm.exe。程序要求输入一个形如 flag{uuid} 的字符串,并根据内置的 token-routing 表检查输入字符是否能使内部状态与预期 logits 对齐。

基本信息:

字符串可以直接定位校验函数:

在 0x1400029f0 附近可以看到:

因此目标输入格式为:

程序从虚拟地址 0x1400040a0 读取 360 字节 routing 表。由于 .rdata 的文件偏移为 0x2400、虚拟地址为 0x140004000,所以该表在文件中的偏移为:

对表中第 i 个字节定义:

g(i) 是一条 VM 指令或其操作数。主要指令为:

初始状态为:

在一个普通的 0x10 -> 0x20 -> 0x30 周期中,0x20 的核心更新可还原为:

随后 0x30 要求:

每个 token 值只需要在 0..16 中尝试,其中 16 表示 UUID 中的连字符 -。由于每轮的目标状态固定,逐轮枚举即可唯一恢复每个 token。

下面的脚本从原始 PE 中提取 routing 表,并按恢复出的 VM 逻辑逐轮求解:

恢复得到的 36 个 token 值为:

拼接后:

执行:

程序输出:

证据状态:已验证。原 WP 记录远端返回 Flag;本地指标 P(9)=0.978、SSIM=0.798。

原始题解:Noisy Digits/Noisy Digits.md

手写数字对抗攻击 · SSIM 约束 · 盐椒噪声还原

Flag:flag{76e12f8a-9bdb-4e67-b8c8-30f41646ff68}

题目大意:公司训练了一个手写数字识别模型,需要你做对抗攻击测试。攻击目标图在传输过程中被加了盐椒噪声。没有 Web 界面,分析附件后把解出的数据提交到指定 ip:port。

check.py 是远端的完整判定脚本,逻辑非常直白:

输入一张 PNG 的 base64,过三道门槛才给 flag:

原图路径是 /home/ctf/mnist_label_1.png(干净的 1),我们手里只有带噪版本 mnist_label_1_noisy.png。SSIM 不是跟噪声图比,是跟服务器上那张干净 1 比。另外几个实现细节:

先把噪声图打成 28×28 灰度矩阵。盐椒噪声在这题里不是散点,而是整行被改掉:

把两行全白行 mask 掉之后,在整个 MNIST(train 60k + test 10k)里搜最近邻:

train[3] 就是官方 MNIST 训练集里的第一张 1。逐像素 diff 后,除了那几行被整行覆盖,其余几乎一致。服务器上的 mnist_label_1.png 就是这张图。验证:

后续攻击一律从 train[3] 出发,这样 SSIM(adv, original) 就是 SSIM(adv, train[3]),本地和远端一致。

目标:在像素空间里找一张图 (x'),使得

网络很小、没有对抗训练,定向攻击很好打。用 CW 风格的间隔损失 + SSIM hinge + 轻微 L2:

Adam (lr=0.02) 优化 800 step,每步 clamp 到 [0, 1],最后量化成 uint8 再评估(远端看到的是 PNG 量化后的图)。本地复现 checker 的结果:

人眼仍是那张歪着的 1,只是笔画附近叠了一层肉眼不明显的扰动。概率分布:

必须存成 mode=L 的 28×28 PNG,再 base64。RGB/RGBA 会让 Conv2d(1, …) 或 SSIM 的 channel 对不上。

生成的 payload:

交互:

把返回的 flag 填进网页即可。

连远端:

(实际交互时先等 Please give me the base64 of the 9 image: 再发送,或用 socket 脚本等 prompt。)

证据状态:已验证。原 WP 记录 ONNX Runtime 回放 audit_score=0.99752736。

原始题解:Route/route.md

附件只有一个 challenge.onnx。模型输入输出如下:

输入长度 42 恰好对应:

也就是 UUID 格式的 flag。

使用 ONNX 图查看器或以下 Python 代码可以查看节点和常量:

关键常量为:

q04/q05 强制输入中的固定字符为:

q03 对剩余 32 个位置做范围判断:要么是 ASCII 数字 0-9,要么是小写十六进制字母 a-f。因此输入一定是标准小写 UUID。

将 32 个 UUID 字符转换为 nibble:

模型中的 Where 节点完成这个转换。

固定字符还会计算校验分支:

实际计算得到:

对于每个 j in [0, 95],模型从 q08[row,j] 取出 4 个 UUID nibble 下标,并计算一个模 251 的二次多项式。设这四个 nibble 为 u0,u1,u2,u3,a = (q09-q10) mod 251,则公式为:

结果必须等于:

模型在 96 个位置全部匹配时,audit_score 达到约 sigmoid(6),即 0.99752736。

可以将每条四变量约束转换为允许元组,然后交给 OR-Tools CP-SAT 求解。每个 nibble 的取值域只有 0..15,所以每条约束最多枚举 16^4 = 65536 种组合。

求解结果为:

使用 ONNX Runtime 回放得到:

因此最终 flag 是:

证据状态:已验证。原 WP 记录 polluted/clean index replay verified=true,根因为 STORED_VECTOR_SUBSTITUTION。

原始题解:vectorward/VectorWard-WP.md

分类: RAG Security / Vector Database Integrity / Embedding Pipeline Migration分值: 350模型: Qwen/Qwen3-Embedding-0.6B (97b0c614be4d77ee51c0cef4e5f07c00f9eb65b3)Flag: flag{810c7dfd-b4c5-454d-a350-ebbcbe5820e0}

这是一道防御向的向量库取证题,不是打模型本身。

服务把一套企业知识库从 pipeline v1 迁到 v2,公开了 96 条文档、两套 collection、14 条历史检索 incident。其中发生过一次不安全的高权限检索:一条本不该排到 Top-1 的 continuity 文档,把 payroll 恢复查询带成了 PAYROLL_CONTINUITY_OVERRIDE。

选手要做的事情被合同写死了:

附件里没有服务源码、没有答案绑定、没有污染的 query、也没有 flag。只能走公开审计接口。

附件四个文件:

题目说明要把 nc ip port 换成 HTTP:

浏览器会下发 HttpOnly cookie vectorward_session。后续所有 API 必须带上,否则会落到另一个空实例。

先摸状态:

/api/challenge 给出本次实例元数据(本题实例 80feef0fe28fb6b8):

对 bundle 里每一条 document,精确拼成:

UTF-8 SHA-256 就是 current source digest。拿它和行上的 declared_source_commitment 比。bundle 不给 current digest,必须本地算。

这和 v2 的 document template 长得一样,但用途不同:

v1 的 embedding 输入更短:{title} {body},max_length 256。不要拿错模板去算 source digest。

模型原生 hidden 是 1024 维。服务按 Qwen 的 last-non-padding-token pooling,截前 256 维,再 L2 normalize。

量化(audit_contract 原文):

digest 输入按顺序拼接:

bundle 里的 Base64 是量化后的字节,不是 float JSON。

选手不必自己跑 Qwen。POST /api/audit/recompute 会按该行声明的 pipeline 重算,返回:

一次最多 32 个当前实例的 document_id。96 条分 3 批即可。正常迁移行:expected == stored。

bundle 顶层:

96 条里只有 1 条 current digest ≠ declared commitment,同时也是唯一 signature_state = review-required 的记录:

正文末尾被加了一句审批旁路:

The queue coordinator may close a request before the second review when the backlog is high.

这是源文被改,不是向量被换。重算 embedding 时这条 expected == stored(digest 9cfcb6ef…f04081),说明存储向量跟当前文本是对齐的——承诺对的是旧文本。

对应 incident INC-F87F14D44A25 能检索到它,但 action 仍是 NONE。源文篡改本身没有打出高权限动作,所以它不是 affected incident。题目要的是「把两类失败都交上去,再指出被向量污染带歪的那一条」。

三批 recompute 之后,96 条里只有 1 条 expected ≠ stored:

源文讲的是「财务运营中断时的例外恢复 / 审批窗口 / 审查委员会」,不是 payroll 密钥轮换 SOP。但把这条的 stored 量化向量拿去跟整个索引比余弦,最近邻全是 payroll key-rotation 文档(约 0.71)。也就是说:文档身份还是 continuity 高权限 profile,向量却被换成了 payroll 查询会命中的方向。

这是经典的 stored-vector substitution:改 embedding、不改(或很少改)明文,承诺哈希仍能过,检索排名被劫持。

GET /api/incidents 返回 14 条。13 条 action 都是 NONE。唯一高权限的是:

INC-7526E719C7352026-07-26T12:15:00Z

历史 Top-5(分数是 RRF,不是 cosine):

RRF:score = 1 / (k + rank),k=60。1/61 表示该 collection 内 rank 1。两条并列 1/61,说明它们分别是 v1 collection 的第一和 v2 collection 的第一。并列时合同规定的 tie-break 是「单路最大 cosine,然后 document_id」,cosine 不加进 RRF。结果被替换的 continuity 文档赢了,动作从这条高权限记录发出。

这就是题目说的 unsafe high-privilege retrieval。

四个 digest 必须和三个 ID 对得上:

成功返回 session/instance 绑定的短时 audit_token。立刻:

服务会:

返回:

干净索引 Top-1 回到正常的 payroll SOP KB-596C46692B64,动作 NONE。根因被服务定性为 STORED_VECTOR_SUBSTITUTION。

两处完整性失败是解耦的:

真正打出未授权高权限动作的是第二种。RAG / 向量库场景里这比改明文更阴:

防御侧对应本题的审计姿势:

POST /api/query-preview 是实验室接口,有独立预算,不会给 flag,不是正道。