很多人在赛后看完题目,第一反应就是“我这题会做,写个 Writeup 就完事”:贴一段代码,放一张 flag 截图,再甩两行“学到了”。这种写法看着完整,实际上你三个月后回头翻,大概率想不起来当时卡在哪、为什么绕了这么大一圈。CTF 真正值钱的从来不是最后那串 flag,而是题目向你亮出底牌、你一步步把它拆开的过程。写一篇合格的 CTF Writeup,本质上不是记录,是把你解题时的非线性思路压成一条别人能跟着走完的线性路径。这篇文章我不打算复述那些翻来覆去的基础理论,而是从写作目标、结构方法、分题型套路到发布流程,完整走一遍我自己在上百道题里磨出来的写作框架。看完你能直接拿着模板去套,也能知道哪些写法会让自己掉价。
1. 先把“写不出来”这件事说透
1.1 你其实不缺技能,缺的是表达套路
不少人觉得自己写不好 Writeup 是因为“表达差”“语文不好”,实际操作里根本不是这么回事。你缺的是把解题过程结构化拆解的套路。
举个例子:你解一道密码学题目,脑内的路径往往是“看到密文尾部有 ==,想到 base64,解码后又是一段字符串,再观察字符频率想到替换表,最后还原出 flag”。这个过程中真正有价值的信息不是“用了 base64 和替换表”,而是“你为什么在第一步会选择 base64”。让你写不出来,是因为你不知道该把脑内闪过的这些判断依据放到哪里。
我的解决方法很简单:把每一步都切成“输入—判断—操作—结果”四段。输入是什么,你基于哪些特征做了判断,实际动了什么操作,得到什么结果。别小看这个笨办法,它能把你从“不知道从哪下笔”直接拉进“照着记录翻译成文字”的状态。熟练之后,你甚至不需要真的写四段,在草稿里用“看到 X → 想到 Y → 验证 Z”的箭头随手记,就能自动生成一篇完整逻辑链。
1.2 Writeup 的三个服务对象:评委、队友、未来的自己
不夸张地讲,很多人对 Writeup 到底写给谁看是没想清楚的。我建议你直接把它当成有三个服务对象,这三个人的需求完全不同。
第一是评委或出题人。出题人最想看到的是“你有没有踩到考点”,而不是“你有没有解出 flag”。一道 Pwn 题考点在栈迁移,你全文却只写“用工具打出来了”,这等于告诉出题人“我没理解考点,只是抄了现成脚本”。所以写题解时一定要明确写出“这题要考的是哪一块防御机制、哪一处溢出点”,哪怕判断错了,也比你完全不提强。
第二是队友。你赛后要跟队友复盘,他们需要快速知道哪里能复用、哪里是坑。如果写得含糊,队友要么刨根问底问你十分钟,要么直接放弃阅读。这其实倒逼你把每一步写清楚,让看的人能在两分钟内定位到“这题的关键解法是什么、难点卡在哪”。
第三是未来的自己,这是最容易被忽略的。我常说,Writeup 是写给半年后的自己看的另一个自己。一个只有贴图的草稿,半年后你根本看不懂;一份记录下当时的疑问、猜错方向、验证过程的完整文档,才是你参加下一场比赛时真正值钱的经验库。很多高水平的选手根本不把 Writeup 当成发给别人的东西,他们就是写给自己看的解题笔记,发布只是顺带。
想清楚这三个对象,你的写作优先级就会立刻发生变化:考点分析 > 完整逻辑 > 复现细节 > 观赏排版。这跟你之前可能默认的“排版要好看、代码要漂亮”是完全相反的。
2. 从零搭 Writeup 骨架:一套能直接抄的写作框架
2.1 赛题信息头:一份自带检索价值的问题画像
我看过太多 Writeup 的第一行直接是“这题是 RSA”。除非你是看完当场写、当场发,否则这种开头没有任何信息量。一个合格的赛题信息头应当包含题目名称、题型、知识点、难度、赛后状态这几项。不要小看它,这是你整个 Writeup 的元数据,也是未来检索的门面。
我习惯用类似这样的信息头:
题目名称:某签到题 题目来源:某线上赛-Crypto方向-签到 知识点:自定义 Base64 替换表 / 字符频次分析 难度:入门 状态:赛时解出 / 赛后复现这几行字眼看起来琐碎,但在你积累了几十篇之后,价值会呈指数增长。想象一下,下次比赛遇到一道新题,你只需要在本地笔记里搜“替换表”三个字,就能立刻翻出这篇历史 Writeup,照着里面的经验直接下手,比你在赛时对着搜索引擎大海捞针快得多。标题命名也要为检索服务,我最常用的规则是“平台缩写-题型-题目名-考点”,比如“某赛-Crypto-签到-替换表”。搜索引擎看到的标题和你看的自己笔记的标题,越具体反而越好。
2.2 从解题思路到输出路径:怎么把脑内过程变成线性文本
这一节是我觉得最有用的部分。很多人卡住,是因为解题时思路完全是网状的,脑子里同时挂着好几个没验证的猜测,而文章是线性的,必须一个点一个点写出来。这就要做“线性化”处理。
我的做法分两步。第一步,先用零碎笔记把整个解过程拉出主干:题目给了什么、你观察到了什么、第一反应是什么、实验验证了什么、哪里卡了很久。第二步,把主干收敛成一条主线,通常就是“从输入数据到 flag 的这条数据流”,然后再把支线的失败尝试挪到后面的“踩坑记录”里,而不是塞进主流程。
举个例子,做 Web 题时你可能同时尝试了 SQL 注入、目录扫描、源码泄露三条路,最终发现是源码泄露里的备份文件给出了数据库账号。那你的主流程就应该写“发现备份文件 → 还原源码 → 找到账号 → 登录后台拿 flag”。你前面花掉两小时试 SQL 注入的过程,不要删,放到后面的踩坑记录里一句话带过:“先试了 SQL 注入无果,因为过滤了关键字,后转为源码审计”。这样做既保证了主线干净,又不丢失宝贵的负向经验。
2.3 截图、代码和日志的摆放顺序
排版问题看起来是表面功夫,实际上它直接影响读者能不能看懂。我的准则是:所有验证性的证据紧跟其对应的结论,而不是全部堆在最后。比如你说“发现字符串尾部有 == 特征”,这里就该配上截图或命令输出;你说“执行脚本得到以下结果”,紧接着就是输出片段。一图一结论,读者完全不需要上下翻页对照,这种体验才是专业 Writeup 应有的。
代码块也同理。不要把完整脚本全部扔到末尾当附录,最好把核心代码按步骤拆开。如果你用了别人的开源脚本,一定要注明来源,并且只保留和当前题目相关的调用部分,不要整段贴上几十行和题目无关的代码。对了,别忘了给脚本里的关键参数加注释,比如“这里替换成题目给出的密文”“这里是模数 n”。没有注释的代码,别人拷下来跑不出来,你写等于没写。
3. 分类型 Writeup 的撰写套路:密码学 / 逆向 / Web / Pwn / 取证
3.1 密码学类:讲清“怎么想到这个攻击面”
密码学题目前最核心的一个问题,不是“用什么算法解出”,而是“我怎么从题目给的这堆字符判断出攻击面”。很多人写完密码学 Writeup,只写一句“这是一道 RSA 题,直接攻击低指数”就完事了。这完全漏掉了出题人设置的特征:密文很短、e 很大、c 的位数远小于 n,这些才是你需要写出来的判断依据。
我在写密码学方向时,会额外加一小节叫“题目特征”。这一节非常简单,就是把题目输入里“一眼能看到的特征”列出来,比如密文由可见字符组成、长度固定、base64 特征符、重复出现的模式。接着写“由这些特征我推测”,然后再进入实际攻击过程。这比单纯罗列算法公式更能帮助读者建立“看到特征 → 想到方法”的思维。
举个签到的例子。题目给了一段包含大小写字母和数字的字符串,末尾带两个等号,傻瓜都会先做 base64 解码。但如果你只写“base64 解码得到 flag”,那没人会感谢你。你应该写:“末端等号强烈暗示 base64,但直接解码后是乱码;注意到字符串中的字母顺序和标准 base64 索引表不一致,怀疑是自定义替换表;于是编写脚本,用题目里给出的替换表替换回标准表后,再 base64 解码得到 flag。”你看,这里面的每一步都清晰地连接着观察和判断,而不是“启动脚本-得到 flag”的两点一线。
另外,密码类题目一定要把你的攻击脚本贴全。原因很简单,密码逻辑是纯函数式的,别人要想真正理解某个参数怎么参与计算,没有完整代码根本没法推敲。哪怕脚本只有十几行,也要保证用户直接复制后,把输入文件换成自己的题面就能跑出结果。那种贴半段脚本,把核心参数隐藏起来只为了显得自己厉害的做法,是最让人反感的。
3.2 逆向与 Pwn:从静态定位到动态验证
逆向和 Pwn 的 Writeup,最重要的不是“贴出最终 payload”或者“贴出 flag”,而是把“定位关键函数”这个过程讲清楚。因为这类题目的真正难度不在最后那一次利用,而在于你如何在茫茫代码里找到关键的校验逻辑或漏洞点。
我的理想写作路径是:先说程序的基本行为,这个程序接受什么输入,输出什么,有没有交互界面,有没有 anti-debug;再做静态分析,你用了什么工具,搜索了哪些特征字符串,比如 main 函数里的提示语、报错字符串、可疑的调用;然后定位到关键函数或溢出点,这里必须贴出关键反编译伪代码并配上自己的标注。最后再用动态调试验证你的分析假设。这个顺序不能反,一旦你先写了动态调试,读者会觉得你根本不知道自己在干什么,纯粹靠瞎试。
举个例子,某道模拟程序逆向题,我先用字符串搜索发现一段提示语“Congratulations”,交叉引用到一处比较函数。这时候在 Writeup 里应该写:“看到这个字符串后,我意识到程序最终会把用户输入和某个内部值进行比较。于是我在比较指令处下断点,动态运行,观察寄存器中存储的内容,发现是一个由固定算法变换后的值。”看到没有,静态定位和动态验证是互相咬合的。你不需要事无巨细地记录每一条汇编指令,但关键节点的截图和寄存器值非常值得写。
还有一块很实际的东西:Pwn 题中偏移量怎么来的。很多新手直接写“偏移为 112”,然后没有人知道他怎么算的。要写成“用 cyclic 模式串生成 200 字节输入,程序崩溃后记录崩溃地址,在模式串中的偏移为 112”。这既是结论,也是方法。以后你再遇到类似的题,只需要改个模式串就能复用。
3.3 Web 与取证类:按数据流向写
Web 题是最忌讳“想到什么测什么”的。如果你写的时候也是想到哪写到哪,读者会认为你的解题流程毫无章法,完全是靠扫描器和运气。我的建议是:任何 Web 题,都沿着“前端 → 服务端处理 → 数据存储/回显”这条数据流向去写。
比如一道带文件上传的题,你探测到存在过滤后,要写的不是“过滤了后缀名,于是改后缀名绕过”,而是把请求和响应包都贴出来,说明你通过响应包含哪些特征,定位到后端用了哪种解析逻辑。再往下的反弹动作、路径、回显位置也要一步步交代。读者只有看到你是如何处理每次请求中微妙差异的,才会真正相信你是会写 Web 题,而不是靠工具扫出来的。
取证类题目则完全不同。它的核心不是“攻击”,而是“还原”。你写的时候要从已知的磁盘镜像、内存镜像、流量包出发,像侦探一样逐个排查线索。流量分析尤其要按时间线展开:先看到哪个数据包、发现什么可疑协议、顺着通信找到哪个恶意脚本、接着这个脚本做了什么。如果只贴最后几张关键截图,过程就缺了链条,读者根本不知道你怎么把线索串起来的。我自己的习惯是给取证类 Writeup 先画一个时间线列表“第 1 秒发现什么 / 第 2 步关联到哪个文件 / 最后如何恢复出密钥”,写成章节标题,读起来一目了然。
3.4 Misc/隐写等其他方向的通用模板
Misc 是所有杂题的收容所,但这并不意味着你可以随便写。我总结出这样一套通用模板,适配隐写、流量、社工、密码之外的任何题目:
第一段写“题目给了什么”,包括文件名、文件类型、大小、备注信息、压缩包是否加密。第二段写“我按什么顺序尝试了哪些分离工具”:文件尾部附加数据、E X I F 信息、LSB 隐写、图种等。这个“顺序”很重要,不同隐写手法出现在同一题里的概率差异很大,写下来能让别人也继承这个经验。第三段写“找到隐藏数据的标志”:比如看到一段连续的十六进制里出现“PK”开头,就知道是 zip 压缩包;看到图片像素 RGB 最低位出现规律的 0/1 序列,就猜测 LSB。最后一段写“如何还原出最终数据”。Misc 题目的知识点非常零散,所以这种模板化的写法其实是一种降低认知负担的方式,你不需要每次重新发明结构,只需要按固定框架往里填内容即可。
4. 实战写作流程:从解出 flag 到发布,一共五步
4.1 解题后的 10 分钟“验尸式”复盘
这个环节是我墙裂推荐的。每次你解出一道题,在比赛还没结束或者刚结束的热乎劲里,立刻花 10 分钟做一次复盘。不要急着写长篇大论,只做两件事:回忆“我最初的尝试路径”,以及“我最终为什么转向了正确路径”。用一个空文档或你的笔记软件,快速按时间线把关键节点记下来,哪一步卡了半小时、哪一步豁然开朗、哪一步靠的是纯试错,都记录下来。这两个问题的记录,就是你未来 Writeup 中最具特色的“方法论”部分的原材料。
我可以很负责任地告诉你,如果赛后两个小时再写,你的记忆就会开始美化,把大量试错过程丢掉,只剩下一条光鲜的成功路径。那样的 Writeup 错失了对后来者最有帮助的失败经验。趁热打铁写 10 分钟,胜过冷下来回想两小时。
4.2 结构化记录与草稿展开
复盘记录完成之后,第二步是把零散笔记展开成结构化草稿。你可以直接拿我在第 2 节里给的信息头加主线框架来搭,或者套用第 3 节里分类模板的骨架。总之,先不要关心措辞和排版,优先确保逻辑链条完整。流程大概是:把题目信息头复制进来,写下“题目特征”和“我的第一步判断”,然后按步骤把验证过程铺开,最后补上“踩坑记录”。
这一阶段我经常使用一个技巧:把某个环节当成你在教一个基础尚可的队友,强迫自己把跳过的步骤补回来。比如你写“执行脚本得到回显”,就要问一句:“脚本里的这个参数是怎么从题目数据里提取出来的?”把这个问题的答案补进草稿,就是在填补逻辑空洞。等草稿完整后,你再开始润色语言。千万不要边想逻辑边润色,否则你大概率会陷入字句优化的泥潭,文章反而半天推不出来。
4.3 用最小 Demo 验证脚本可复现性
一篇 Wrtieup 写到“脚本可复现”这个程度,才算真正完成。很多人贴代码的时候,用的是赛时在临时目录里改得乱七八糟的版本,变量名都是 a、b、c,甚至还有硬编码路径。这种东西贴出来,别人根本没法跑。所以在发布前,我会单独建一个干净目录,重新 copy 一份脚本,用一个模拟输入跑一遍,确认输出结果和题目一致。
这还没完,还要注意依赖环境。脚本用到了哪些第三方库,版本大概是多少,Python 版本是什么,最好都写进题目信息头或者代码注释。遇到那种只能在特定环境跑的破解脚本,也一定要注明运行环境,比如“需要 Windows + 某调试器配合”,否则别人复制到你脚本后跑出一堆报错,不仅浪费对方时间,也会让读者心里给你这篇文章打个大大的差评。
4.4 配图和排版的几个硬性建议
配图这块,我见过的最常见问题是三个:一是截图太糊,二是截图没有标注重点,三是图文割裂。关于截图,我建议使用带命令行高亮的终端主题,并且把关键输出圈红或高亮。Windows 上很多人直接整屏截图,截出来的图里包含无关的桌面背景,这其实是不专业的表现。裁剪到只保留终端窗口或代码区域,再在关键行加一个简洁的标记,显然更有说服力。
排版上的另外一个细节是全文语言角度要统一。我习惯全部用第一人称“我”来叙述:“我观察到”“我尝试”“我猜测”。不要一会儿“我们”,一会儿“攻击者”,这样会让读者难以代入。最后是用标题分层,一篇 1500 字以上的 Writeup,起码要分成三到五个小节。层层递进的小标题不仅是为了美观,更是为了帮读者梳理逻辑,让他们哪怕只扫一眼小标题,也能抓住你的解题骨架。
5. 避坑实录:那些写出来就掉价的做法
5.1 过度省略 vs 过度抄源码
这两个极端我都见过。过度省略的表现是:“这题是解密,直接上代码”,没有流程,没有判断依据,只有一堆代码和结果。过度抄源码的反而是另一种灾难:把整个项目的源码几百行全部贴上来,里面百分之八十都是与题目无关的初始化代码。真正的优秀 Writeup 是两者的平衡:核心思路讲清楚,关键代码完整给出,无关代码一行不贴,必要时用一句注释交代。
说白了,你要时刻问自己一个问题:“我贴出来的这行代码,对读者理解题目有没有增量信息?”如果没有,就删掉。判断依据是:这段代码是在解决“这个题的特殊点”,还是在描述通用工具调用?通用步骤用一句话带过就行,比如“用二进制编辑器查看文件头,发现它不是标准压缩包格式”,完全不需要把二进制编辑器怎么打开写出来。
5.2 把踩坑过程藏起来的后果
很多人为了显示自己聪明,会把踩坑过程全部隐去,只保留最顺滑的那条成功路径。这是最大的浪费。因为你经历过的每一个无效方向,恰恰是未来读者最可能重蹈覆辙的地方。从信息密度来看,一段“我先试了 X,失败了,原因是 Y,于是改走 Z”的内容,远比“我直接做 Z 成功”更有价值。
当然这不是让你把每一个失败的细节都写进去。我的判断标准是:这个坑是否具有普遍性?如果是题目边界条件导致的特例,可以一笔带过;如果是某种攻击手法在该类题目里的共性反模式,值得写进“踩坑记录”。比如 SQL 注入过滤了空格,于是你用注释符替代空格,这个坑就有通用价值,必须写。相反,某个题目因为后端逻辑写错导致你没打通,这种一次性问题就没必要占用篇幅。
5.3 写得“优雅”但不可复现
这里说的“优雅”,指的是那些把代码简化得过分、让人看不懂的写法。比如有些人为了秀技巧,把一个简单的字符串处理用匿名函数嵌套写成一串,看起来赏心悦目,但去执行时漏了一个依赖,结果根本跑不出和题目一致的结果。Writeup 的代码不是比赛现场秀肌肉,它的唯一目的是让读者复现结论。
所以我在自己的代码里会故意保留“笨”的可读性写法:变量名用明文,比如 ciphertext、plaintext、original_str;注释写清楚参数来源;关键的 print 输出直接暴露中间结果。为了让脚本可复现,我甚至会故意把脚本分成两段:第一段做解析和数据预处理,第二段做核心算法的调用。这样就算读者只想理解某一段,也能独立看懂。
5.4 搜索引擎可见性与标题命名
你可能觉得这是一件小事,但实际影响很大。很多 Writeup 的标题直接写“签到 wp”“某题 writeup by xxx”,根本不成系统。如果你认真对待自己的笔记和输出,就应该让标题里包含足够的关键词,让未来的自己或同方向新手用搜索引擎能找到。推荐的命名格式是“赛事缩写 + 题型 + 题目名 + 考点关键词”,比如“某赛 Crypto 签到自定义替换表”。
正文头部的信息头也应当保留,因为搜索引擎在抓取页面时会把标题、正文首段信息作为重点。更重要的,在你发布到社区或个人博客时,正文开头不要立刻放代码,而是先用自然语言把题目背景和考点梳理清楚。这样别人搜索相关知识点时,更能命中你的页面,你也更有可能收到同好者的反馈和讨论。
6. 从“会写”到“会教”:Writeup 的进阶价值
6.1 把 Writeup 当作最小教学单元
从我的经验来看,认真写 Writeup 这件事给你带来的最大回报,不是被社区点赞,而是训练你“把复杂问题讲清楚”的能力。一篇好的 Writeup 天然就是一个最小教学单元:它有背景、有目标、有步骤、有验证、有易错点,本质上和一篇小教程没有任何区别。反复练习这种写作,会让你自然而然地开始思考“当一个完全没接触过这个知识点的人,读到我这句话会不会卡住”。
我自己的一个明显感受是:写满几十篇之后,再回头去学习新方向的题目,脑子里的学习策略都会变得不一样。看到一份别人的 WP,我会下意识拆解它的结构,判断它哪一块写得好、哪一块有逻辑断层,这比闷着头刷题效率高得多。所以不要把这当成了“赛后的作业”,接力全当成一次刻意练习。
6.2 建立自己的案例库与标签体系
如果你只是写、发、扔,那这些 Writeup 的价值仍然没有被你充分榨干。我强烈建议在本地维护一个案例库,按“方向 / 考点 / 难度 / 是否解出 / 关键工具”这些标签给每篇 WP 分类。这一步很像给树苗做支架——一开始多花十几分钟,后面整个学习资料库就能自己生长起来。
实操时我常用一份表格维护,字段包括:日期、赛事、题目、方向、考点、状态、链接或本地路径、一句话备注。下次遇到新题,先翻这个表格,看考点有没有重合。这个习惯帮我在赛场上省下大量时间。比如某次比赛遇到一道“通过时序侧信道逐字符恢复消息”的题,我立刻在表格里找到上一次类似的题目,直接拿旧脚本改参数,省掉了一半分析时间。
6.3 我的个人体会
聊到这儿,我还想再分享一个很私人的感受。刚开始我也觉得写 Writeup 是为了给别人看,越到后来越明白,它是我自己比赛能力的记账本。那些只贴在收藏夹里没整理的题目,和过眼云烟没有区别;那些认真写成文章并打上标签的题目,才真正变成了我知识体系的一部分。
我现在的习惯是:每场比赛结束后,不急着发布任何东西,先把所有解出的题目按结构写完,保存到本地案例库。发布只是一件顺手的事,写作本身已经让我把每一道题学了两遍。我希望你能试试这个方法,坚持十篇之后再回看第一篇,你会明显感受到自己在分析逻辑和表达上产生的变化,这种变化带来的成就感远比拿到一个 flag 更持久。