1. 当AI开始自己打比赛,人类选手还剩多少空间
第一次看到“已经没有人类了”这个说法,是在一个安全竞赛的讨论区里。有人贴出一份题解记录,从Web到逆向再到杂项,全程没有人类介入,AI自己读题、自己分析、自己写脚本、自己提交flag,最后还拿了个不错的名次。评论区一片哀嚎,有人说“以后比赛是不是要分人类组和AI组”,有人说“这还玩什么”,也有人冷静地指出——这事儿没那么玄乎,但确实值得认真拆一拆。
我自己做安全竞赛有些年头了,从最早的手工注入到后来写自动化脚本,再到如今尝试把大模型接进解题流程,一路看下来,AI打CTF这件事,既不像营销号吹的那么神,也不像一些老选手贬的那么水。它确实在某些题型上已经能做到“全自动闭环”,但在另一些题型上,依然会卡在人类觉得“这还用想?”的地方。
这篇文章,我想把“AI全自动打CTF”这件事从头到尾拆一遍。不是复述某一次比赛的战报,而是把背后的技术选型、流程设计、实操细节、踩坑经验全部摊开来讲。如果你是对安全竞赛感兴趣的新手,能从中看到一条清晰的自动化解题路径;如果你是老选手,也能从中找到几个可以立刻用起来的提效手段。核心关键词就三个:CTF自动化、大模型解题、全流程闭环。
先说清楚一个前提:我这里说的“全自动”,指的是从拿到题目描述和附件开始,到最终产出flag并提交,中间不需要人类手动写一行代码、点一次运行。听起来很激进,但拆开看,它其实就是把人类选手的解题流程,用一套调度系统加一个大模型加一堆工具给串起来了。人类选手怎么解题,AI就怎么解题,只不过它不睡觉、不烦躁、不会因为卡题而摔键盘。
2. 全自动解题系统的整体架构与设计思路
2.1 为什么选择“调度器+大模型+工具链”的三层结构
任何自动化系统的第一步都是架构设计。我试过几种不同的组织方式,最后稳定下来的结构是三层:调度层、推理层、执行层。调度层负责管理题目队列、分配资源、控制流程;推理层就是大模型,负责理解题目、生成解题思路和代码;执行层是一堆工具和沙箱环境,负责实际运行代码、发起网络请求、处理文件。
为什么不是把一切都塞给大模型?因为大模型有两个致命问题:一是它不能直接执行代码,二是它的上下文窗口有限。你让它读一个二进制文件,它只能看到十六进制文本,没法真正“运行”它。所以必须有一个执行层来补足这个能力。而调度层的存在,是为了让整个流程可管理、可中断、可重试——毕竟大模型会犯错,题目会超时,环境会崩溃,没有调度层兜底,整个系统跑不了几题就会乱成一锅粥。
这个三层结构还有一个好处:每一层都可以独立替换。今天用A模型,明天换B模型,只需要改推理层的接口;今天用本地沙箱,明天上容器集群,只需要改执行层的配置。调度层的逻辑基本不动。这种解耦设计在实际跑比赛的时候特别重要,因为比赛环境千变万化,你不可能提前把所有情况都考虑到。
2.2 题目分类与路由策略:不是所有题都值得全自动
CTF题目大致分几类:Web、逆向、杂项、密码学、二进制利用。不同题型对自动化的友好程度完全不同。我的策略是分级处理,而不是一刀切全自动。
Web题是最适合全自动的。题目通常给一个URL或者一段源码,解题过程就是找漏洞、构造payload、发请求、拿flag。这个过程天然适合用脚本串起来,大模型只需要负责识别漏洞类型和生成payload。我实测下来,中低难度的Web题,全自动成功率能到七成以上。
杂项题里,那些涉及编码转换、文件分析、图片隐写的,自动化也很顺手。因为工具链成熟,大模型只需要判断“这是什么类型的文件”和“该用哪个工具”。但涉及社会工程或者需要人类直觉的杂项,比如“根据一段模糊的描述猜一个地名”,AI就很容易跑偏。
逆向和二进制利用是最难的。不是大模型看不懂汇编,而是它很难在有限的上下文里维持对整个程序逻辑的连贯理解。一个几百行的反汇编,它读着读着就忘了前面某个关键跳转。我试过用分块分析加摘要的方式缓解,但效果有限。所以我的路由策略是:Web和杂项走全自动,逆向和二进制走“半自动”——AI生成分析思路和脚本框架,人类确认关键逻辑后再继续。
密码学题介于两者之间。简单的编码、古典密码、RSA基础题,全自动没问题。但涉及复杂数学构造的,大模型容易在推导步骤上出错,需要人类介入验证。
2.3 大模型选型:为什么不是越大越好
选模型这件事,我踩过不少坑。一开始觉得模型越大越聪明,直接上最大的那个。结果发现两个问题:一是慢,二是贵。一道题跑下来,光模型调用成本就够买好几杯咖啡了。而且大模型有个毛病,它倾向于“想太多”——明明一个简单的base64解码,它非要给你写一段带异常处理和日志输出的完整脚本。
后来我改成分层选型:调度层用一个小模型做题目分类和路由判断,推理层用中等模型做主要解题,遇到特别复杂的题再升级到大模型。这样整体成本降了大概六成,速度提升了一倍多。实测下来,中等模型在Web和杂项上的表现和大模型差距不大,真正需要大模型的地方其实很少。
还有一个关键点:模型的代码能力比它的知识储备更重要。CTF解题最终要落到代码上,模型能不能写出可运行的、没有语法错误的、逻辑正确的代码,比它知不知道某个冷门漏洞更重要。我在选型的时候会专门跑一组代码生成测试,看模型在Python、Bash、JavaScript上的表现,而不是只看它的通用 benchmark 分数。
2.4 执行环境隔离:为什么必须用沙箱
这个问题值得单独拎出来说。AI生成的代码,你永远不知道它会写出什么。我遇到过模型生成rm -rf /的情况,也遇到过它试图往外部地址发数据的情况。虽然大多数时候它是无意的,但风险是实打实的。
所以执行层必须跑在沙箱里。我用的是容器化方案,每个题目一个独立容器,网络访问白名单控制,文件系统只读挂载加临时可写目录。容器跑完即销毁,不留任何残留。这样即使模型生成了恶意代码,影响范围也仅限于那个容器内部。
沙箱的另一个好处是环境一致性。CTF题目经常需要特定的工具或者库版本,比如某个版本的pwntools,或者某个特定版本的Python。把这些依赖提前打包进镜像,每次执行都是干净的环境,不会出现“在我机器上能跑”的问题。
3. 核心模块的实操细节与关键配置
3.1 题目解析模块:怎么让AI“读懂”一道题
题目解析是整个流程的入口。人类选手看到一道题,扫一眼就知道大概是什么类型、该用什么工具。AI没有这种直觉,你得把题目信息结构化地喂给它。
我的做法是写一个题目预处理管道。输入是题目描述文本和附件文件,输出是一段结构化的JSON,包含:题目类型猜测、涉及的技术栈、附件文件类型、可能的解题方向。这个管道里有一堆启发式规则,比如看到“SQL”就标记Web,看到“.exe”就标记逆向,看到“base64”就标记编码。
但光靠规则不够,还得让模型参与判断。我的做法是先用规则做初步筛选,然后把筛选结果和原始题目描述一起喂给模型,让它做最终分类。这样比纯规则准确率高,也比纯模型快。
这里有个细节:题目描述的格式千奇百怪。有的比赛用Markdown,有的用纯文本,有的把提示藏在图片里。预处理管道需要能处理多种格式,必要时还得调用OCR。我遇到过一道题,关键提示是一张图片,模型直接读文本读不到,后来加了OCR才解决。
3.2 提示词工程:怎么让模型写出能跑的代码
提示词的设计直接决定模型输出的质量。我试过很多版本,最后稳定下来的模板大概长这样:
你是一个CTF解题助手。当前题目信息如下: 题目类型:{type} 题目描述:{description} 附件列表:{attachments} 可用工具:{tools} 请按以下步骤解题: 1. 分析题目考察的知识点 2. 给出解题思路 3. 写出完整的解题脚本 4. 脚本必须可以直接运行,不要有伪代码 5. 如果脚本需要外部依赖,在注释中说明 注意: - 不要输出解释性文字,只输出脚本 - 脚本要有错误处理 - 如果涉及网络请求,设置合理的超时这个模板的关键在于约束输出格式。早期版本我没加“只输出脚本”这条,结果模型每次都在代码前后写一大堆分析,调度器还得额外解析。加上约束后,输出干净多了。
另一个关键是提供可用工具列表。模型不知道你的环境里装了什么,你不告诉它,它就可能写出import requests但环境里没装的情况。把工具列表喂给它,它就会优先用已有的工具。
还有一个坑:模型倾向于过度工程。一个简单的异或解密,它能给你写出一个带命令行参数解析、日志记录、单元测试的完整项目。后来我在提示词里加了一句“保持代码简洁,能用一行解决就不要写十行”,情况好了很多。
3.3 代码执行与结果捕获:怎么判断“跑通了”
代码生成出来只是第一步,能不能跑通、跑通后结果对不对,才是关键。执行层需要做几件事:启动沙箱、注入代码、运行、捕获输出、判断结果。
判断结果这件事比想象中复杂。有些题目的flag有固定格式,比如flag{...},这种最好判断,直接正则匹配就行。但有些题目的flag是动态的,或者藏在输出的一大堆信息里,就需要模型来辅助判断。
我的做法是双重判断:先用正则匹配常见flag格式,如果匹配到了就直接返回;如果没匹配到,把输出截断后喂给模型,让它判断“这段输出里有没有flag,如果有,是什么”。这样既快又准。
超时控制也很重要。有些脚本会陷入死循环,或者等待一个永远不会来的响应。我给每个执行任务设置了硬性超时,一般是60秒,超时直接杀掉容器,标记为失败。这个时间可以根据题目类型调整,Web题可以短一点,逆向题可以长一点。
3.4 失败重试与策略切换:一次跑不通怎么办
全自动系统最怕的就是“卡死”。一道题跑不通,如果一直重试,会浪费大量时间和资源。我的策略是有限重试加策略切换。
第一次失败,让模型重新生成代码,但把上次的错误信息一起喂给它,让它知道哪里出了问题。第二次失败,换一个解题思路,比如上次用脚本自动化,这次让它给出手工步骤。第三次失败,标记为“需要人类介入”,跳过这道题,继续下一道。
这个策略的关键是错误信息的有效传递。模型看不到沙箱里的实际情况,你得把错误信息提取出来,整理成它能理解的格式。比如“SyntaxError: invalid syntax”这种,直接原样给它就行。但如果是“Connection refused”,就得补充说明“目标地址拒绝连接,可能是端口不对或者服务没启动”。
我实测下来,大部分题目在两次重试内都能解决。真正需要三次以上重试的,往往是题目本身有问题,或者模型确实不擅长这个类型。
4. 实战记录:一次完整的全自动解题过程
4.1 赛题环境与初始配置
那次比赛是一个线上CTF,题目数量大概二十道,涵盖Web、杂项、密码学、逆向。我提前把系统搭好,模型接口配好,沙箱镜像拉好,然后就去睡觉了。第二天早上起来看结果,系统跑了大概六个小时,解出了十四道题,其中十道是全自动完成的,四道是半自动——系统给出了思路和脚本框架,但卡在了某个环节,我手动补了几行代码才跑通。
这个成绩不算惊艳,但考虑到全程没有人类实时介入,我觉得已经能说明一些问题了。下面挑几道有代表性的题,把解题过程完整还原一下。
4.2 Web题:从源码泄露到flag获取
第一道是Web题,题目给了一个URL,打开是一个登录页面。系统先做了一波常规探测:robots.txt、常见备份文件、源码泄露。在www.zip里找到了源码。
源码是一个简单的PHP登录逻辑,用户名和密码直接拼进SQL查询。系统识别出这是SQL注入,生成了一个基于联合查询的payload。但第一次跑的时候失败了,因为目标过滤了空格。系统把错误信息喂回给模型,模型第二次生成了用注释符绕过空格的payload,成功拿到flag。
整个过程大概用了三分钟,其中大部分时间花在等待网络请求上。我后来看日志,模型第一次生成的payload其实逻辑是对的,只是没考虑到空格过滤。这种“根据反馈调整”的能力,是自动化系统比固定脚本强的地方。
4.3 杂项题:多层编码与文件分析
第二道是杂项题,给了一个文件,没有扩展名。系统先用file命令识别,发现是一个PNG图片。然后用binwalk分析,发现图片末尾附加了一个zip压缩包。提取出来解压,得到一个文本文件,内容是base64编码。解码后是一段十六进制,转ASCII后得到flag。
这道题系统跑得非常顺,因为每一步都是标准操作,工具链成熟,模型只需要判断“下一步该用什么工具”。我后来复盘,觉得这类题目是最适合全自动的,因为解题路径几乎是线性的,没有太多分支。
4.4 逆向题:卡在哪里,为什么卡住
第三道是逆向题,给了一个ELF二进制文件。系统先用strings找可疑字符串,发现了一个“Wrong password”的提示。然后用objdump反汇编,把汇编代码喂给模型分析。
模型读了一遍汇编,给出了一个大概的逻辑:程序读取输入,经过一系列异或和移位操作,然后和一个硬编码的字符串比较。模型据此写了一个逆向脚本,但跑出来的结果不对。系统重试了两次,换了两种分析角度,还是不对。
最后我手动看了一下,发现模型漏掉了一个关键的函数调用——那个函数在另一个动态链接库里,反汇编的时候没有包含进来。模型只看到了主程序的汇编,没看到库函数的逻辑,所以推导出的算法是残缺的。
这道题暴露了全自动系统的一个核心短板:它只能看到你给它的信息。人类选手会意识到“这里有个外部调用,我得去看看那个库”,但模型不会主动提出这个需求,除非你在提示词里明确告诉它“注意动态链接库”。
4.5 密码学题:简单题秒过,复杂题翻车
第四道是密码学题,给了一段RSA加密的密文和公钥。系统识别出是RSA,先尝试了常见的攻击方式:小公钥指数、共模攻击、因数分解。发现模数很小,直接分解成功,解出明文。
第五道也是密码学题,但涉及格密码。系统尝试了几种常见攻击,都没成功。模型给出的思路方向是对的,但具体推导步骤有错误,导致脚本跑出来的结果不对。这道题最后标记为“需要人类介入”,我后来花了一个多小时才解出来。
这两道题的对比很说明问题:有标准解法的题目,自动化很擅长;需要创造性推导的题目,自动化还差得远。
5. 常见问题与排查技巧实录
5.1 模型输出格式错误:怎么让代码“干净”地出来
这是最常见的问题。模型有时候会在代码前后加一堆解释,有时候会用Markdown代码块包裹,有时候会输出多个版本的代码。调度器需要能处理这些情况。
我的做法是写一个输出清洗器,用正则提取代码块,去掉解释性文字,如果有多个代码块就取最后一个(因为模型倾向于把最终版本放在最后)。这个清洗器还负责检查代码的基本语法,比如括号是否匹配、缩进是否一致。如果语法检查不通过,直接打回让模型重新生成,不浪费沙箱资源。
提示:清洗器不要做得太复杂,否则容易把正常代码也过滤掉。我的经验是只做最基本的提取和语法检查,剩下的交给沙箱去跑。
5.2 环境依赖缺失:为什么脚本在本地能跑,在沙箱里不行
这个问题我遇到过好几次。模型生成的脚本用了某个库,但沙箱镜像里没装。解决办法有两个:一是把常用库都预装进镜像,二是让模型在脚本里自己处理依赖。
我选的是第一种,因为第二种会让脚本变得很臃肿。我的沙箱镜像里预装了Python常用安全库、网络工具、逆向工具、编码转换工具,基本覆盖了CTF常见需求。如果遇到特别冷门的依赖,再临时装。
但预装也有个问题:镜像会越来越大。我一开始什么都往里塞,镜像搞到好几个G,启动一次要等半天。后来做了精简,只保留真正常用的,镜像控制在500M以内,启动速度就上来了。
5.3 网络请求超时:怎么区分“题目挂了”和“脚本写错了”
Web题经常涉及网络请求,超时是家常便饭。但超时有两种可能:一是目标服务真的挂了,二是脚本写的请求有问题。这两种情况处理方式完全不同。
我的做法是先做连通性测试。在跑解题脚本之前,先用一个简单的请求测试目标是否可达。如果不可达,直接标记题目环境异常,跳过。如果可达但脚本超时,那就是脚本的问题,让模型重新生成。
这个连通性测试看起来简单,但能省下大量无效重试。我早期没做这个,结果一道题因为目标服务挂了,系统重试了十几次,浪费了半个多小时。
5.4 模型“幻觉”:当AI自信地给出错误答案
这是最危险的问题。模型有时候会非常自信地给出一个错误的flag,而且格式完全正确。如果你不加验证就提交,不仅拿不到分,还可能触发比赛的防作弊机制。
我的做法是多重验证。首先,flag必须来自脚本的实际输出,不能是模型“猜”的。其次,如果脚本输出多个候选flag,让模型做二次判断。最后,对于特别关键的题目,我会人工抽查一下。
但说实话,完全避免幻觉很难。我遇到过一道题,模型生成的脚本输出了一个格式正确的flag,但实际上是错的。后来分析发现,模型在生成脚本的时候,把某个常量写错了,导致解密结果不对。这种错误很隐蔽,因为脚本能跑通,输出也像模像样。
注意:不要盲目信任模型的输出。全自动不等于全放心,关键节点还是需要人类把关。
5.5 资源竞争与并发控制:多题同时跑怎么不打架
比赛时间有限,串行解题太慢,必须并发。但并发会带来资源竞争问题:多个容器同时跑,CPU和内存可能不够;多个模型请求同时发,可能触发限流。
我的做法是分级并发。调度层维护一个任务队列,根据题目类型分配优先级。Web题和杂项题并发度高一些,因为资源消耗小;逆向题并发度低一些,因为需要更多计算资源。模型请求也做了限流,避免一次性发太多被接口拒绝。
还有一个细节:沙箱容器的生命周期管理。容器用完要及时销毁,否则会占着资源不放。我写了一个清理任务,定期检查并销毁超过一定时间的容器。这个清理任务本身也要控制频率,太频繁会影响正常执行。
6. 自动化解题的边界与人类选手的不可替代性
6.1 当前自动化能覆盖的题型与成功率
把话说得实在一点,当前的全自动CTF解题系统,能力边界大概是这样:
| 题型 | 全自动成功率 | 主要瓶颈 |
|---|---|---|
| Web基础题 | 70%-85% | 复杂过滤绕过、逻辑漏洞链 |
| 杂项编码类 | 80%-90% | 需要外部信息的题目 |
| 密码学基础题 | 60%-75% | 复杂数学推导 |
| 逆向简单题 | 40%-55% | 多模块交叉分析 |
| 二进制利用 | 20%-35% | 需要动态调试和直觉 |
| 社会工程类 | 10%-20% | 依赖人类常识 |
这个数据来自我自己的实测,不是官方统计,但大致能反映现状。可以看到,越是标准化、越是工具驱动的题目,自动化越擅长;越是需要创造性思维、越是依赖人类直觉的题目,自动化越吃力。
6.2 人类选手的优势在哪里
人类选手有几个AI短期内很难替代的优势。第一是跨领域联想。人类看到一道题,能联想到以前做过的类似题目,或者某个完全不相关领域的知识。AI的知识是碎片化的,它很难主动建立这种跨领域的连接。
第二是对题目意图的把握。CTF题目是出题人设计的,往往有“考点”。人类选手能感觉到“这道题想考什么”,从而有针对性地解题。AI没有这种直觉,它只能根据题目描述和附件去分析,容易在无关细节上浪费时间。
第三是动态调整策略。人类选手卡住的时候,会换一个角度、换一个工具、甚至去喝杯咖啡再回来。AI的调整是基于反馈的,如果反馈信息不足,它就不知道该怎么调整。
6.3 人机协作的最佳实践
我觉得未来的方向不是“全自动替代人类”,而是“人机协作”。人类负责把握方向、处理异常、做最终判断;AI负责执行重复劳动、快速尝试多种方案、处理标准化流程。
具体到CTF比赛,我的做法是:比赛开始后,先让系统自动跑一遍,把能解的题都解了。然后人类选手集中精力处理系统解不出来的题。系统在解题过程中产生的日志、尝试过的思路、失败的原因,都保留下来,作为人类选手的参考。这样人类选手不用从零开始,而是在系统的工作基础上继续推进。
这种协作模式,我实测下来效率比纯人类或者纯AI都高。纯人类受限于精力和速度,纯AI受限于创造力和直觉。两者结合,正好互补。
7. 搭建你自己的自动化解题系统:从零开始的实操建议
7.1 最小可行系统的搭建步骤
如果你想自己搭一套,不用一上来就搞得很复杂。一个最小可行系统只需要三样东西:一个大模型接口、一个能跑代码的沙箱、一个简单的调度脚本。
第一步,选一个代码能力强的模型,把接口调通。第二步,用Docker起一个Python环境,装好常用库。第三步,写一个Python脚本,读题目、调模型、跑代码、输出结果。就这么简单。
我最早的系统就是一个两百行的Python脚本,跑在本地机器上,一次只能处理一道题。但它能跑通,能解出题,这就够了。后面所有的优化——并发、重试、分类路由——都是在这个基础上逐步加出来的。
7.2 关键配置参数与调优经验
有几个参数对系统表现影响很大,我列一下我的配置,供参考:
- 模型温度:设成0.2到0.4之间。太高了模型会乱写,太低了模型会死板。我一般用0.3。
- 最大输出长度:设成4096个token。太短了代码写不完,太长了浪费资源。
- 执行超时:Web题30秒,杂项题60秒,逆向题120秒。根据题型调整。
- 重试次数:最多3次。超过3次基本就是题目本身有问题,或者模型确实不会。
- 并发数:根据机器配置来。我一般设成CPU核心数的一半,避免过度竞争。
这些参数不是固定的,需要根据实际情况调整。比如比赛题目普遍偏难,可以把超时和重试次数调高一些;如果机器性能有限,就把并发数降下来。
7.3 持续迭代:怎么让系统越跑越聪明
自动化系统不是搭好就完了,需要持续迭代。我的做法是记录每一次解题的完整日志,包括题目信息、模型输出、执行结果、失败原因。然后定期复盘,找出系统性的问题。
比如我发现模型在生成SQL注入payload的时候,经常忘记处理引号转义。后来我在提示词里加了一条“注意处理特殊字符转义”,这个问题就少了很多。又比如我发现沙箱里缺少某个工具,导致一类题目总是失败,把工具补上之后,这类题目的成功率就上去了。
这种迭代不需要很频繁,每次比赛后花半个小时复盘一下就行。关键是要有记录,没有记录就没法复盘,没法复盘就没法改进。
8. 一些踩过的坑和最后的经验分享
说几个我印象比较深的坑。第一个是模型接口的稳定性。有一次比赛,模型接口突然限流,系统一下子全卡住了。后来我加了重试和降级机制,接口不可用的时候自动切换到备用模型,才解决了这个问题。
第二个是沙箱逃逸。虽然我没遇到过真正的逃逸,但有一次模型生成的代码试图访问宿主机文件系统,被沙箱挡住了。这件事提醒我,沙箱配置不能马虎,该禁的权限一定要禁。
第三个是flag格式的多样性。不同比赛的flag格式不一样,有的用flag{},有的用CTF{},有的干脆就是一段随机字符串。我的正则匹配一开始只覆盖了常见格式,后来遇到一个用FLAG-开头的,直接漏掉了。后来改成让模型辅助判断,才解决了这个问题。
最后一个经验:不要追求100%全自动。全自动很酷,但没必要。把80%的重复劳动自动化掉,人类集中精力处理那20%的关键问题,这才是最务实的做法。我见过一些人非要追求全自动,结果系统复杂到难以维护,反而影响了比赛成绩。
提示:自动化是手段,不是目的。能拿分才是硬道理。
这套系统我还在持续改进,下一步想试试把动态调试也接进去,让AI能像人类一样单步跟踪、下断点、观察寄存器变化。不过那是另一个话题了,等有进展再分享。