1. 先聊一个“效率”问题CTF比赛打到后期最折磨人的往往不是题有多难而是时间根本不够用。尤其是解题型比赛动辄几十道题每道题手工分析、反复敲命令、复制粘贴十六进制、等爆破脚本跑完再手动拼接结果这些流程会吃掉大把时间。我自己参加线下赛时就发现真正拉开差距的并不是某个深奥的漏洞点而是能不能在同样时间里把更多题目过一遍。所以我很早就开始围绕Python攒自动化工具把“解题”变成“跑脚本人工确认”的复合流程。这篇文章想分享的正是我在CTF实战中沉淀下来的Python自动化套路哪些环节最值得自动化、常用脚本怎么组织、工具库怎么选怎么封装以及踩过的坑。适合觉得自己“做题总差最后一步”的新手也适合想要系统化提升速度的老手。全篇以实际解题场景为线索不是空谈理论。1.1 CTF里哪些环节最拖时间复盘一次主流解题赛你会发现大部分时间其实浪费在重复劳动上。拿Misc举例题目通常给你一个奇怪的文件你需要先识别文件类型、扫描隐写、尝试不同编码、不断看hexdump。这些步骤单独看都不难但每道题都要从头敲一遍半小时就没了。Web题更典型一个候选参数可能要做多次请求手工改UA、改Cookie、看返回长度效率极低。逆向和PWN也有共同的痛点你需要在十几个测试输入之间快速切换还要同步调试器状态。更隐蔽的拖时间点在于“结果解析”。比如爆破成功了flag可能藏在几十行输出里眼睛找半天。比如用了某个工具解出字符串中途还夹杂着一堆无关噪音。Python的价值就是把“执行命令”和“解析输出”粘在一起让机器去做繁琐的部分你只做判断。1.2 为什么我坚持用Python来做自动化一方面因为CTF生态里的基础设施基本都Python友好。pwntools、z3、angr、pycryptodome这些核心库都是Python接口requests做HTTP请求也是事实标准。另一方面Python写脚本的节奏特别适合比赛场景——想到一个思路二三十分钟就能把原型搭出来跑挂了改起来也快。相比C或JavaPython最大的优势是胶水特性可以调系统命令可以解析二进制可以发请求可以直接写socket不需要来回换语言。当然Python不是没有缺点爆破速度慢、二进制分析性能不如C但在绝大多数CTF题目里性能瓶颈根本轮不到Python。真正需要高性能时我会把耗时的爆破部分用C扩展或调外部工具来跑Python只负责调度和解析。这种“Python为主专用工具为辅”的路线我实测下来最稳。1.3 自动化脚本设计的三条核心原则很多新手攒脚本有一个误区看到一道题就写一个“一次性脚本”做完就丢。下次遇到同类题又是从头写。我后来总结了三条原则让脚本真正变成资产。第一条输入输出标准化。脚本入口要么接收文件路径要么接收URL要么从stdin读数据输出尽量统一成“只打印关键结果其余细节进入日志”。这样多个脚本能串起来。比如解Misc时我可以先跑一个文件识别脚本输出文件类型再把文件路径传给隐写扫描脚本整个过程用管道串联。第二条解析优先于执行。先想清楚最终要从结果里取出什么字段再设计脚本。比如爆破表单时不应该只是打印响应而是把长度、关键字、Set-Cookie等特征提取出来。后面我会给出一个具体的正则提取例子。第三条异常处理要“呼吸”。比赛环境里网络超时、编码错乱、文件损坏都很常见。脚本必须能捕获异常并继续跑下一个输入不能因为一次失败就整体崩溃。我给所有网络请求都会加超时和重试爆破类脚本则加了断点续跑。这三条是后面所有脚本的底层逻辑。2. 编码与Misc题一套脚本打通隐写与文本分析Misc题在CTF里最容易用脚本提效因为它的常规步骤高度重复文件识别、隐写扫描、编码探测、字符串提取。我自己的做法是把这些步骤拆成一个个小工具再拼成组合脚本。下面是具体思路。2.1 文件头识别与隐写工具链封装拿到一个未知文件第一件事是看文件头。Python的file命令能识别但更可控的做法是直接读前16字节映射到常见格式。以我自己的脚本为例它会维护一张魔术数字表JPEG以FF D8 FF开头PNG以89 50 4E 47开头ZIP是50 4B 03 04PDF是25 50 44 46。这些信息平时都查得到但写成函数后后续脚本可以反复用。隐写工具链的封装更实用。很多题目需要依次跑binwalk、strings、zsteg、steghide、exiftool手工跑一遍很烦。我用Python的subprocess把这些工具包了一层直接传文件路径脚本自动执行、解析输出并打印包含flag、CTF、key等关键字的行。这比每道题敲五六条命令快太多。import subprocess import os def run_tool(cmd): try: res subprocess.run(cmd, capture_outputTrue, textTrue, timeout30) return res.stdout res.stderr except Exception as e: return f[tool error] {e} def analyze_misc(filepath): results {} results[file_type] run_tool([file, filepath]) results[strings] run_tool([strings, -n, 6, filepath]) results[binwalk] run_tool([binwalk, filepath]) if filepath.lower().endswith((.png, .jpg, .bmp)): results[zsteg] run_tool([zsteg, filepath]) highlights [] for k, v in results.items(): for line in v.splitlines(): if flag in line.lower() or ctf in line.lower() or key in line.lower(): highlights.append(f[{k}] {line.strip()}) return highlights这个小工具本质上是一个“过滤器”不是自动解题而是帮你把需要人工注意的信息放在最前面。跑完它整个文件的基本面就清楚了。2.2 常见编码识别的自动探测Misc里最常见的就是一串不知道是什么的文本Base64、Hex、URL编码、Unicode、栅栏、凯撒、摩斯、ROT13……手动试很浪费时间。我写过一个编码探测器逻辑是先把字符串做归一化去掉空白和常见头部然后依次尝试Base64解码、Hex解码、URL解码、ROT13并对解码结果做可打印字符占比检测。如果某种解码后大部分字符都是可打印ASCII就认为候选成立。这里有个容易踩的坑Base64和Hex的判定会偶发误报尤其短字符串。所以我加了“是否需要包含关键字或明显英文单词”的人工确认选项比赛里宁可多打一条命令也别被脚本误导。比如把下面的函数集成到你的工具箱里import base64, binascii, urllib.parse, codecs def try_decode(s): results [] for name, func in [ (base64, lambda x: base64.b64decode(x)), (hex, lambda x: bytes.fromhex(x)), (url, lambda x: urllib.parse.unquote(x)), (rot13, lambda x: codecs.decode(x, rot_13)), ]: try: out func(s.strip()) if isinstance(out, bytes): out out.decode(utf-8, errorsreplace) printable sum(c.isprintable() for c in out) / max(len(out), 1) if printable 0.9: results.append((name, out)) except Exception: continue return results这一小段代码看起来简单但比赛里帮我节省了大量手工试探。实际使用时我还会把strings命令的输出先清洗再去跑编码探测避免二进制垃圾影响判断。2.3 实战从凯撒、Base家族到词频统计的一站式脚本有些题不给你标准编码而是自己做了变体比如凯撒位移后转Base64或者用词频替换的简单替代密码。遇到这种情况几行探测脚本不够需要一个处理管道。我经常用的管道是这样的读文本 - 尝试所有凯撒位移1到25- 对每位移结果做词频统计 - 打印Top 3候选。词频统计用英文中ETAOINSHRDLU出现频率的概念虽然不够严谨但足够定位出“像英文”的候选。并且如果题目是中文语境我还会用常见汉字频率表辅助但CTF里这类加密题多数还是英文flag。另外Base家族不止有Base64还有Base32、Base58、Base85。我在自建库里统一封装了decode_with_common_bases函数一次尝试所有变体。这个函数的返回值包括解码结果和对应编码类型方便继续丢给下一个处理环节。多说一句当你发现一段文字以MZ开头却符合Base64格式时要想到可能里面还藏着PE文件这种“层层剥洋葱”的体验正是Misc的乐趣所在。3. Web题自动化把重复请求写成流水线Web题和Misc不同它的核心是HTTP交互。很多题目的flag藏在响应头、页面注释、特定参数返回、或者需要多次请求组合。手工用Burp点确实能解但速度慢尤其碰到需要遍历几百个参数或跑字典的题没有脚本基本没戏。3.1 从requests到Session的会话保持做Web题第一步是处理好会话。很多题目有登录和Cookie鉴权你不希望每次请求都手动带Cookie。用requests.Session()会自动保存Cookies还能保持Session信息这是我所有Web脚本的基础。同时我会给Session设置统一的User-Agent避免被默认UA特征的WAF或者题目本身过滤掉。这里有两个细节容易翻车一是重定向默认requests会自动跟随重定向但有些题需要你观察302中间的跳转所以要手动控制allow_redirectsFalse二是代理比赛环境可能要求走本机Burp代理那么就把proxies设置放进一个开关函数里方便切换。import requests def make_session(use_proxyFalse): s requests.Session() s.headers.update({ User-Agent: CTF-Agent/1.0, Accept: */*, }) if use_proxy: s.proxies.update({http: http://127.0.0.1:8080, https: http://127.0.0.1:8080}) return s实测下来这种Session封装在登录爆破、多步协议题里特别稳。我经常把每个请求的关键响应状态码和耗时打印出来避免盯着屏幕发呆。3.2 正则提取与flag自动收集Web题很多答案就藏在返回文本里可能是flag{...}、可能是key: xxx也可能在set-cookie里。与其肉眼扫不如让脚本把匹配到的所有候选key集中打出来。我常用的正则有三种rflag\{[^}]\}、rkey[:]\s*([^\s])和rctf[_-]?\{[^}]\}。别小看这步遇到一次要发几十个请求的题它能避免你漏掉某个响应里的flag。我有时还会写一个“响应摘要函数”把状态码、响应头里的Set-Cookie、页面标题、重点注释都汇总成一行输出。调试时信息密度很高。比如import re def extract_flags(text: str) - list: patterns [rflag\{[^}]\}, rctf[_-]?\{[^}]\}, rkey[:]\s*([^\s])] flags [] for p in patterns: flags.extend(re.findall(p, text, re.I)) return list(set(flags))这个函数虽然简单但我会把它作为所有Web脚本的公共模块。爆破完、爬完目录、跑完接口最后统一提取flag效率提升非常明显。3.3 轻型口令爆破与表单自动化对于有登录表单的CTF题做口令爆破是常见操作。这里我强调一下只允许在CTF竞赛或你自己有授权的靶场上使用绝不用于真实目标。爆破的重点不是把字典跑完而是快速找出“响应特征突变”的那一个口令。具体思路先拿一个错误口令请求记录响应长度和关键字符串。然后遍历字典对每次响应计算长度和是否包含某个关键字一旦出现显著变化就停止或打印。还可以把每次响应长度画个统计长度异常的值往往就是突破口。def brute_login(url, username, pass_file, sessionNone): s session or requests.Session() base_len None for pwd in pass_file: pwd pwd.strip() try: r s.post(url, data{user: username, pass: pwd}, timeout10) marker r.status_code, len(r.text), flag{ in r.text if base_len is None: base_len len(r.text) print(f[base] {pwd} - len{len(r.text)}) if len(r.text) ! base_len or marker[2]: print(f[hit?] {pwd} - {marker}) except requests.RequestException as e: print(f[retry] {pwd}: {e})这个脚本没有加多线程但足够应付大多数密码题。如果字典太大我会加threading或concurrent.futures但要注意别给靶场打崩。我曾经有一次用20线程跑一个弱密码题直接把比赛平台的一个接口拖到超时后来再也不敢暴力并发。4. 逆向与PWN用pwntools把交互变回车Pwn题和逆向题的自动化程度差异很大。逆向更多是辅助分析Pwn则几乎离不开pwntools。如果你还没有完全掌握pwntools建议花时间把它的交互模型吃透因为后续所有Pwn脚本都是围绕它构建的。4.1 pwntools的基本操作与连接模板pwntools最核心的能力是处理二进制程序的IO交互。无论题目是本地还是远程都用同一套接口先process(./pwn)或remote(ip, port)然后sendline、recvuntil、recv最后interactive()。我写Pwn脚本都会以这个模板起步先把交互连通再往里面填漏洞利用逻辑。有几个经验recvuntil(bPlease enter:)这种阻塞等待提示符的方式最稳接收数据时尽量不要用recvline去猜因为程序输出可能有缓冲差异。输入带有不可见字符时直接用p.send(payload)而非sendline最后再补一个\n。这样能避免很多莫名其妙的换行问题。from pwn import * context.arch amd64 context.log_level debug if args.REMOTE: io remote(127.0.0.1, 9999) else: io process(./pwn) io.recvuntil(bInput: ) payload bA * 0x20 p64(0x401234) io.sendline(payload) io.interactive()这个模板看起来平平无奇实际作用是把“调通交互”的时间从几十分钟缩到几分钟。你会发现大部分时间其实都花在程序输出格式的调试上。4.2 简单栈溢出利用的自动化脚本模板栈溢出题最常用的套路是覆盖返回地址到后门函数或shellcode。编写脚本时我建议把所有地址运算都写成变量不要写死。比如缓冲区偏移用cyclic生成并计算返回地址用符号名backdoor 0x4011d6。这样做的好处是即使你临时换了一道结构类似的题只需要改几个常量。from pwn import * context.binary ./vuln io process(./vuln) io.sendlineafter(b , cyclic(0x100)) io.wait() core io.corefile offset cyclic_find(core.regs[rip] - 0x70 if core.regs[rip] 0xffff 0x6161 else core.regs[rip]) # 更通用的做法是直接 cyclic_find 在 corefile 的堆栈里搜索 backdoor 0x401271 payload bA * offset p64(backdoor) io2 process(./vuln) io2.sendlineafter(b , payload) io2.interactive()我特别想提醒的是栈偏移不要靠猜而是用cyclic工具精确计算。原本我能背出常见模板的偏移但题目稍微改一下缓冲区和变量布局就废了。后来学了cyclic和core dump分析基本就能一根筋算到底。逆向题也有一堆杂活可以用Python简化比如解析ELF的符号表、批量patch字节不过那部分我通常用现成的ROPgadget配合脚本就不再手撸解析器了。4.3 调试辅助gdb调试与Python联动当利用脚本跑不通就需要调试。pwntools能直接启动gdb并附加到进程这比手工在gdb里敲命令高效。我常用gdb.debug(io, break *0x4011d6\ncontinue)的方式在特定地址断下然后Python脚本继续给程序发输入。这里有一个坑gdb模式下程序的输出速度和正常执行不同如果脚本里有基于超时的接收逻辑可能会提前退出。我的办法是设置较长的timeout并尽量用recvuntil而不是recv。我还习惯在脚本里加一个pause()调用方便在gdb窗口里手动查看寄存器状态。比赛时现场几双眼睛盯着屏幕看到程序停在预期断点那种踏实感是纯手工调试给不了的。另一个常用技巧是用ELF()类读取二进制里的函数地址和ROP gadgets就不用每次objdump去查地址。5. 工具库推荐与自己的武器库建设当然工具库不只是“写在代码里的函数”。你需要一套稳定的环境依赖、几个核心第三方库以及自己的脚本管理方法。这部分我整理了常用清单和沉淀技巧。5.1 必备工具库清单与适用场景CTF解题常用的Python第三方库我用一张表梳理一下库名适用场景一句话经验pwntoolsPWN、二进制交互事实标准学它就够了requestsWeb题HTTP交互配合Session最香z3-solver约束求解逆向方程、Crypto解密表达力强但别滥用angr符号执行做简单逆向跑大题容易慢优先用z3pycryptodome常见加密算法实现支持 AES/RSA/ECC 等binascii / hashlib底层编码、哈希处理标准库随手用ciphey自动识别经典加密适合做Misc的“第一道侦察兵”numpy / scipy音频、图像数据处理音频频谱题很常用每个库都有自己最顺手的场景不必背下来但要清楚能“用它们做什么”。我遇到过一道音频题需要把频谱图的能量分布转成二进制最终靠numpy和scipy的FFT快速解决。这种时候如果平时没积累现场查文档会非常被动。还有一个容易被忽略的库是itertools和collections。它们虽然不带算法但能让你在写爆破和组合遍历时少掉头发。比如生成所有大小写和数字组合一行itertools.product就能解决。5.2 模块化封装与缓存策略攒脚本最怕的是散落各处。我自己会在本地建一个目录里面按题型分成misc/、web/、pwn/、crypto/、reverse/五个子目录外加一个common/放公共函数。common/里包括hexdump、try_decode、extract_flag、make_session这些被反复使用的工具。这样新题目出现时我可以从公共库直接导入而不是重新写函数。缓存策略也是加速的关键。爆破脚本如果中途断了我希望它能从上次的位置继续。做法很简单用一个进度文件记录已跑到的索引每次开始先读进度跑完再写回。对于网络请求类脚本我还把每次成功的响应体缓存到临时目录避免同一个请求重复发。这在调试一个逻辑复杂、需要多次访问相同资源的Web题时特别有用。5.3 如何沉淀自己的脚本库沉淀不是把代码堆到一个文件夹里就完事。我有两个习惯第一给脚本写极简头部注释包含题目来源类别、用的核心库、入口参数、输出格式。注释不需要长但要保证三个月后自己能看懂。第二把好的脚本模板整理成代码片段。比如pwntools的远程连接模板、requests会话模板、编码探测模板这些是我日常最快产出的东西。我还会用Git管理整个脚本库按比赛场次打tag。这样每次比赛结束后可以回看当时的脚本和题目类型下次遇到类似题直接复用。某个时间段里我把一道Crypto题里用到的RSA脚本稍微改一下就解了另一道看起来完全不同的题。这种“复用”的快感才是沉淀的价值。6. 常见问题与排查实录最后分享一些实际踩过的坑。这些问题不涉及高深原理但每一个都让我浪费过不少时间写出来希望大家绕开。6.1 编码、换行、超时这些“小事”怎么命案CTF脚本里编码问题永远排第一。我遇到的典型情况是本地脚本跑得好好的一连接远程就输出乱码或者print的时候直接报UnicodeEncodeError。解决思路是统一用bytes处理二进制流需要打印时再按errorsreplace解码。另外写文件时明确指定编码不要依赖系统默认编码。换行问题同样阴险。pwntools的sendline会在数据末尾加\n但如果程序期望的是\r\n你的输入就会错位。解决办法是手动send(payload b\r\n)。Web题里\r\n出现在请求头里但\n出现在body里混用会导致部分题目解析异常我一般在Session请求里尽量用data和json参数而不是自己拼body。超时问题也经常让人抓狂。如果脚本经常卡在recvuntil先检查是程序等待特定输出还是你的接收目标设错了。我见过有人一直等一行以结尾的数据但程序实际输出的是带空格于是永远卡死。更稳的写法是recvuntil(b)后手动strip空白或者接收所有数据直到满足关键字。6.2 依赖版本冲突的快速处理CTF比赛经常要用不同版本的库尤其是pwntools和angr两者依赖容易冲突。我的处理办法是给比赛环境建一个独立虚拟环境并固定版本号。多年下来发现pwntools依赖的ropgadget和capstone版本经常被angr覆盖最终我选择在两个虚拟环境里分别装一个跑PWN一个跑逆向分析。虽然切换环境稍微麻烦一点但总比脚本突然报错要好。如果在比赛中发现某个库损坏或版本不对最快的解决办法不是去改代码而是直接用pip install --user装一个兼容版本并在脚本开头用assert检查关键版本。比如我要求pwntools版本不低于4.0低于就提示更新。这能避免“在别人电脑上跑得通在自己这跑不通”的尴尬。6.3 一道综合题的排查思路记录举个例子某次遇到一道WebPWN结合题需要先提交一个畸形请求拿到泄露地址再通过栈溢出getshell。我当时的脚本第一步请求正常第二步recvuntil却等不到任何数据。排查过程是这样的先让脚本把请求的完整响应打印出来发现响应的Content-Type是application/octet-stream和我预想的不一样。再检查泄漏数据的解析逻辑原来它返回的是16进制字符串我却直接当成ASCII用。修正后第二步顺利拿到了地址。这个案例给我的启示是脚本跑不通先怀疑自己“对协议的理解”而不是怀疑工具。我习惯在脚本的关键节点打印repr(response)专门看不可见字符和空白这样能快速定位问题。排查问题不要靠猜要在关键路径上打点一步步缩小范围。经过这么多轮实战我自己最常用的还是那几个小而美的函数extract_flag、try_decode、make_session、pwntools连接模板。它们不算复杂但每次比赛都能救我于水火。如果你也想提升CTF解题效率我建议先从整理自己的公共工具箱开始——不用一步到位每道题多抽象一个函数时间久了你的速度自然会上去。