ARTICLE / ai

1324个CTF writeup的技能化长跑:空白识别与五道质量闸

一句话结论:把上千篇 CTF writeup 变成智能体可复用的技能库,瓶颈根本不在"理解 writeup",而在两件更枯燥的事——知道自己已经有什么(空白识别),和拒绝把垃圾存进库(质量闸)。 本文记录一条真实跑通的流水线:1,324+ 篇 writeup、25+ 个空白技法被补齐、「同一技法被写成五遍」和「满篇空话零命令」两类废品是如何被结构性地拦住的。

安全团队的知识库大多死在同一个地方:不是没人写,而是写出来的东西没人用。writeup 存了上千篇,遇到新题该翻不出来还是翻不出来;让大模型"总结一下这些 writeup",产出一堆《SSRF 入门》《SSRF 进阶》《SSRF 实战》,彼此重复、没有命令、没法执行——知识停留在"读过"的层面,从来没有变成"会用"的能力。

一条真实的处理链是这样的:仓库里躺着 1,324+ 个 CTF writeup(web 476K、crypto 444K、pwn 168K、misc 128K、reverse 96K、forensics 76K、mobile 12K、blockchain 28K,按目录体积计),目标是把它们提炼成智能体技能库——每个技能是一个 SKILL.md,含决策树、可执行命令、踩坑清单和可追溯的 writeup 引用,供 agent 在做题时直接调用。这条链已经多轮迭代跑通,补齐了 25+ 个空白技法,本篇拆解它踩过的坑和沉淀出的结构。

直接总结 writeup 为什么必然失败

最直觉的做法是批量调 LLM:「读这批 writeup,总结出可复用的技能。」跑一轮就会遇到两个结构性问题。

第一个是重复爆炸。 SSRF 是 web 类 writeup 里高频出现的技法,三十篇 writeup 里二十篇都涉及。不带全局视角地逐篇总结,模型会在第一篇写一个 SSRF 技能,第五篇再写一个(措辞不同、内容重叠),第三十篇又写一个。最终库里五个「SSRF 技能」,没有一个完整,还互相冲突。这不是模型能力问题——单篇输入的视角里,模型无从知道库里的存量。重复的根源是缺少「库存清单」这个全局状态,任何不带全局状态的去重都是碰运气。

第二个是空话技能。 另一类产物看起来正常,标题、摘要、步骤俱全,但没有一条可执行的命令——「可以使用工具探测内网」「接下来进行权限提升」。这类技能在做题场景里价值为零:agent 需要的是「这个场景用这条命令、这个参数、这个输出特征说明命中」,而不是一段正确的废话。LLM 在缺乏具体约束时,默认产出就是这种抽象层级的文字。

两个问题指向同一个解法:在「生成」之前,先补「盘点」和「验收」两个环节。 这就是下面两层结构。

TAXONOMY:把「还缺什么」变成可计算的差集

流水线的第一层是维护一份技法全景清单 TAXONOMY——把 CTF 领域的技法体系显式列出来(web 的 lfi-rfi、deserialization、jwt-attack、ssrf、idor、race-condition;crypto 的 padding-oracle、lattice-attacks、prng-prediction、ecc;pwn 的 format-string、ret2dlresolve、command-injection;reverse 的 vm-reversing、anti-debug、firmware-re;misc 的 stego-audio、timing-attack……每轮迭代持续扩充)。

有这份清单之后,「本轮该提取什么」从一个模糊的判断题变成一个可计算的集合运算:

comm -23 <(yq '.techniques[].name' TAXONOMY.yaml | sort) \
         <(find skills -name SKILL.md | xargs -n1 dirname | xargs -n1 basename | sort)

comm -23 输出"清单里有、库里没有"的差集——这个差集就是本轮的工作队列。上一轮跑完后清单里记录剩余空白,下一轮接着跑,长跑任务就有了断点续跑的能力。

这个结构带来的变化比看起来大。首先,重复问题在源头消失:只有差集里的技法才会被提取,库里已有的 SSRF 技能根本不会进入生成流程,五个重复 SSRF 的惨案从机制上不可能再发生。其次,进度变成可度量的:「还剩多少空白」随时可查,收尾条件明确(差集为空),而不是「感觉差不多了」。最后,清单本身成为领域知识的索引——新人接手时不读一千篇 writeup,读 TAXONOMY 就知道这个领域的技法版图。

值得强调的是清单的粒度选择。太粗(只到「web/crypto/pwn」级别)则差集没有指导意义;太细(每个变体一条)则清单维护成本爆炸。实际跑下来的粒度是「可独立成技法的攻击面」——ssrf 是一条,jwt-attack 是一条,padding-oracle 是一条,各自对应一类可复用的决策树。

五道质量闸:垃圾进不了库的机制保证

第二层是验收。空话技能、伪代码技能、引用不存在的 writeup——这些废品靠人工审查拦不住(批量任务里人工审查必然流于形式),必须变成机器可判定的门槛。这条流水线设了五道闸,任何一道不过,产出就不能打 skill 标签:

闸判定标准拦截的废品形态
1. 篇幅下限每技能 ≥200 行两百字"入门小结"
2. 可执行性含真实工具命令(pwntools/curl/sage/foundry 实弹,非伪代码)满篇"可以使用工具"的空话
3. 踩坑章节有 Common Pitfalls,6-8 条陷阱+解法只有 happy path 的教程
4. 引用可追溯≥2 个真实 writeup 来源无出处、凭空综合的"知识"
5. 引用存在性引用文件路径逐一验证存在引用了不存在路径的幻觉

五道闸里最值得展开的是第五道。LLM 生成"参考 writeup"时有强烈的编造倾向——给出看起来合理的文件路径,实际不存在。如果引用验证缺失,技能库会在第一轮就埋进幻觉地基:后续做题时 agent 顺着假引用去找原文,找不到,然后要么放弃(技能失效)要么自行脑补(幻觉扩散)。验证本身很便宜:

for ref in $(yq '.references[].file' skills/web/ssrf/SKILL.md); do
  [ -f "writeups/$ref" ] || echo "MISSING: $ref"
done

一遍循环,每条引用路径实打实 stat 一次。防幻觉的最后一道闸不靠提示词靠文件系统——这个原则值得抄到任何 LLM 产出物验收里:凡是"声称引用了 X"的产出,验收时必须真去摸一下 X 存不存在。

第三道闸(踩坑章节)则是在逼产出形态。一道强制要求意味着每个技能必须包含 6-8 条"陷阱+解法"——比如格式化字符串技法的技能里写明某个 C 库版本下 %n 写入被整页保护拦截的变体处理,JWT 技能里写明算法混淆在两类库默认配置下的差异表现。技能的价值密度看踩坑密度,不看篇幅——这是这条流水线确立的第一评价标准。 happy path 谁都会写,writeup 复读没有价值;决定一道题能不能做出来的是那些"不看踩坑必然掉进去"的坑。

批量长跑的节奏控制

结构对了之后,剩下的是执行节奏问题。批量提取是典型的 cron 长跑任务,两个节奏控制点来自实战:

单轮限流。 每轮只提取 4-5 个技能,跑完记录剩余空白,下一轮续跑。不限流的后果有两层:表层是 API 限流和 token 成本失控;深层是单轮时间失控——一轮实际投入超出预算后,处理质量会随疲劳度下降(自动管线的"疲劳"表现为上下文膨胀后的产出劣化),垃圾产物率上升,而垃圾一旦入库,清理成本远高于提取成本。宁可多轮慢跑,不可单轮贪多。

优先级排序。 空白清单不是先进先出,稀缺技法优先——判断依据是原 writeup 的解题人数:2-solve 挑战里出现的技法(比如某个利用标准库同步原语实现同步爆发的竞态变体)优先入库,因为这类知识的稀缺度最高、被公开覆盖的概率最低。常见技法反而可以往后排,它们随时可以从公开资料补齐。

双仓分发。 提取产物按用途分流:完整技法细节(含全部命令、坑、引用)入内部技能仓;精简版同步到联邦技能库供 agent 日常调用。提交信息统一格式(security: 添加 <skill> 技能 (from CTF writeup analysis)),保证每条技能都能溯源到是哪一轮批处理、处理了哪批 writeup——知识工程的产物必须自带血缘,否则半年后没人说得清某个技能的可信度依据。

技能和 writeup 的分界:决策树不是复读

这条流水线里最容易被忽视的认知是:技能化不是摘要,是重构。 检验标准很简单——好的技能在做题场景里直接可用,writeup 复读只是在讲"那次是怎么做出来的"。

以格密码为例。writeup 层面,一篇 lattice 类题目的 writeup 讲述"这道题用了 Coppersmith";技能层面,产出是一棵选择树——什么条件下走 Wiener(已知公钥指数过大)、什么条件下走 Coppersmith 小根(已知高位泄漏)、什么条件下转 HNP 建模,每个分支挂对应的 sage 脚本和参数选择陷阱。前者是故事,后者是能力。做题时 agent 面对的是"新题",它需要的不是"别人的题怎么解"的叙述,而是"这类特征该走哪条路"的映射。

再以一个 web 场景的反直觉细节为例:某框架的整数类型混淆可以让 <数字ID>/../../admin 这类路径绕过访问控制——这个坑在十几篇 writeup 里反复出现,但只有把它抽象成"框架层整数处理与路由匹配的语义差"这个模式,agent 才能在新框架、新路由结构下识别出同族问题。技能化的本质是把"具体 writeup 里的具体技巧"升维成"可跨题目迁移的模式",升维失败产出的就是复读品。

这套结构还能用在哪

TAXONOMY + 差集 + 质量闸的组合并不绑定 CTF。任何"海量原始材料 → 可复用知识单元"的场景都适用同一骨架:

  • 漏洞知识库:把历史应急响应报告按"漏洞模式"建 TAXONOMY,差集驱动提炼检测规则技能,质量闸第五道(引用存在性)保证每条规则能溯源到真实案例;
  • 企业内部 SOP:把运维事故复盘按故障模式建清单,避免"同一个故障的处置预案写五遍";踩坑章节对应"历史事故里的真实坑";
  • 代码库文档:把开源项目源码分析按"设计模式/坑"建清单,批量产出带可运行示例的模式说明。

判断自己场景适不适配的三个问题:原始材料是否足够多(少材料不值得建流水线)?知识单元是否有明确的"可执行"判据(没有就无法设质量闸第二道)?领域技法版图是否可以显式列举(不能列举就没有 TAXONOMY)?三个都是,这套结构直接抄。

边界也要说清。 这条流水线目前依赖人工维护 TAXONOMY——清单本身的完整性靠领域知识保证,自动化只覆盖差集计算和验收执行。清单缺了一整个技法族(比如完全忘了 side-channel 这一类),流水线会安静地永不提取它:差集机制保证"清单内的全覆盖",不保证"清单本身无盲区",这是当前最大的已知局限。另外 writeup 质量参差,垃圾 writeup 提炼出的技能即使过了五道闸也可能信息量不足——五道闸是下限保证,不是价值保证,技能的最终价值密度仍取决于原始材料和提取时的重构质量。