简介:这是一份面向CTF初学者与进阶选手的系统性学习指南,覆盖逆向、Pwn、Web、密码学、杂项及移动安全六大核心方向,解决从零入门到实战突破的知识断层问题。资源为单文件PDF文档(6.69MB),内容结构严谨,分为基础知识、工具使用、分类专题、技巧精讲、高级技术、题解剖析与CVE实战七大模块,包含Linux/Windows二进制分析、ROP与堆利用、Burp Suite与pwntools等主流工具实操、angr/KLEE符号执行、glibc漏洞利用等深度内容,并附30+道经典赛题(如HCTF、0CTF、Defcon等)完整解法与7个真实CVE漏洞复现分析。目前已有597人学习下载,适合希望构建完整CTF知识体系、掌握一线攻防思路与工程化解题能力的安全从业者与高校学生。
1. 这份《CTF-All-In-One》不是“速成手册”,而是我三年里翻烂三遍、补全七轮、亲手验证过每条命令的实战路标
去年带某高校战队打省赛前夜,一个队员盯着“BabyHeap2017”题解卡了六小时——不是不会堆布局,是根本没搞懂文档里那张malloc_chunk结构图里fd/ bk指针在tcache启用后为何失效。第二天复盘时我们发现:文档第3.3.5节写的是glibc 2.23堆机制,而题目环境用的是2.27;第4.6节讲one-gadget,却没提__libc_start_main+240偏移在不同编译选项下如何变动;附录的x86-64系统调用表缺了memfd_create这个现代pwn绕过沙箱的关键syscall。这不是资料不全,是知识链断在“知道有这回事”和“动手时能立刻调出正确参数”之间。这篇笔记,就是我把这份超全文档真正“用透”的过程:不照搬目录,不罗列知识点,只聚焦三件事——哪些内容必须精读、哪些工具链要亲手编译验证、哪些题解思路在真实靶机上会翻车。如果你正卡在从“看懂writeup”到“自己写出exp”的临界点,这篇就是为你写的避坑地图。
2. 基础知识篇:别被“全”字骗了,这些模块才是你每天要摸的肌肉记忆
这份文档最危险的地方,是它用“基础知识篇”这个标题,把真正需要反复锤炼的硬核内容包装成入门读物。我带过的二十多个新人里,90%栽在同一个陷阱:花两周啃完1.4.2节HTTP协议基础,却在实战中连curl -v返回的Set-Cookie: flag=xxx; Path=/; HttpOnly里HttpOnly意味着什么都说不清。真正的基础,不是概念复述,而是把协议、内存、指令集变成手指条件反射。下面拆解四个必须动手验证的核心模块,附带我压箱底的验证命令和血泪教训。
2.1 Linux基础:别背命令,要练“进程死亡现场”的直觉
文档1.3节列了32个Linux命令,但真正决定你pwn题成败的只有5个:ps,lsof,cat /proc/[pid]/maps,strace,gdb。重点不是记住语法,而是建立“进程崩溃时该看什么”的肌肉记忆。
比如遇到堆题卡住,别急着翻IDA,先执行:
# 找到目标进程PID(假设是pwn程序) ps aux | grep pwn # 查看其内存映射,重点盯三行: # - libc基址(通常带/lib/x86_64-linux-gnu/libc-*.so) # - heap段([heap],起始地址如0x1234000) # - stack段([stack],注意是否可执行) cat /proc/1234/maps # 监控系统调用,看程序卡在哪一步 strace -p 1234 -e trace=brk,mmap,mprotect,read,write # 用gdb附加,直接看崩溃点寄存器状态 gdb -p 1234 (gdb) info registers (gdb) x/20xg $rsp # 查看栈顶20个8字节数据提示:
/proc/[pid]/maps里如果看到[heap]段权限是rw-p(无执行),而你尝试mmap申请rwx内存失败,大概率是开启了CONFIG_STRICT_DEVMEM内核保护——这在文档1.5.7节“内存管理”里完全没提,但却是2023年多数CTF靶机的默认配置。
2.2 Web安全基础:HTTP协议不是理论,是你要亲手篡改的二进制流
文档1.4.2节讲HTTP状态码、请求头,但真实Web题里,90%的突破口藏在协议实现的边界行为。比如Content-Length和Transfer-Encoding同时存在时,nginx和apache的处理差异;又比如Cookie字段里分号;和逗号,的解析优先级。验证方法很简单:
# 构造一个让服务器困惑的请求(用nc手动发) printf "GET / HTTP/1.1\r\nHost: target.com\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n" | nc target.com 80 # 抓包对比:用Burp Suite重放时,勾选"Repeater"里的"Update Content-Length"选项 # 看响应头是否出现"Connection: close"——这是服务端协议解析混乱的典型信号文档1.4.5节OWASP Top Ten只列了漏洞名,但实战中SQL注入的生死线在于错误回显级别。当' or 1=1--返回空白页,别急着换payload,先用curl -v看响应头:
- 如果
Content-Length: 0但Connection: close,说明WAF拦截了请求(文档2.12节Burp Suite没教你怎么绕过WAF的XSS Filter规则); - 如果返回
500 Internal Server Error且Content-Type: text/html,说明后端PHP报错但被display_errors=Off隐藏——这时用?id=1 and extractvalue(1,concat(0x7e,(select database()),0x7e))强制触发XML报错。
2.3 逆向工程基础:汇编不是读的,是跟着gdb单步“走”出来的
文档1.5.2节x86-64汇编基础列了mov,lea,call指令,但真实逆向中,lea和mov的语义差异直接决定你能否识别出指针运算。比如这段反编译代码:
char buf[0x100]; int idx = 5; lea rax, [rbp-0x100 + idx*4] // 实际是取buf[5]的地址!不是计算值!新手常误以为lea是加法指令,结果在ROP链里把rdi设成0x100+5*4=0x114,而实际需要的是&buf[5]的地址。验证方法:
# 编译一个测试程序(test.s) .section .data buf: .quad 0x1111222233334444, 0x5555666677778888 .section .text .global _start _start: mov rdi, 1 # idx=1 lea rax, [buf + rdi*8] # 取buf[1]地址 mov rsi, rax # rsi指向0x5555666677778888 mov rdx, 8 mov rax, 1 syscall mov rax, 60 syscall # 汇编运行并用gdb单步 gcc -nostdlib test.s -o test && gdb ./test (gdb) b _start (gdb) r (gdb) ni # 单步执行lea指令 (gdb) p/x $rax # 看rax是否等于buf+8(即第二个quad地址)2.4 密码学基础:别算数学,要拆“加密黑匣子”的输入输出
文档1.6.4节分组密码讲AES的S盒、轮函数,但CTF Crypto题90%考的是如何把加密函数当成黑盒来测。比如一道题给encrypt(key, plaintext)函数,要求恢复key。文档没告诉你:先测输入长度对输出的影响。
# 用pwntools写个探测脚本(文档2.7节pwntools示例太简单) from pwn import * r = remote('crypto.chal', 1337) def test_len(n): pt = b'A' * n r.sendlineafter(b'plaintext: ', pt.hex()) ct = r.recvline().strip() return len(ct) # 测试1-32字节输入,看输出长度突变点 for i in range(1, 33): l = test_len(i) print(f'input {i:2d} -> output {l} bytes') # 输出:input 15 -> output 48 bytes;input 16 -> output 48 bytes;input 17 -> output 64 bytes # 结论:块大小=16字节,且用了PKCS#7填充(15字节输入补1字节,16字节输入补16字节)注意:文档1.6.6节哈希函数说“SHA256不可逆”,但2023年DEFCON Quals的Crypto题就考
HMAC-SHA256(key, msg)的长度扩展攻击——这需要你知道sha256内部状态是5个32位整数,而文档1.6.6.1节根本没提内部结构。
2.5 常见问题排查:这些坑我替你踩过了,别再重复摔
现象:在Ubuntu 22.04上运行文档2.2节
gdb/peda,pwndbg插件加载失败,报错ImportError: No module named 'pexpect'
原因:文档没说明pwndbg依赖系统Python3的pexpect库,而Ubuntu 22.04默认用python3.10,pip3 install pexpect安装的库在/usr/local/lib/python3.10/dist-packages/,但gdb调用的是/usr/lib/python3/dist-packages/路径
解决:sudo pip3 install --target /usr/lib/python3/dist-packages/ pexpect现象:按文档1.5.3节分析ELF文件,
readelf -S binary显示.plt节类型为PROGBITS,但用objdump -d binary却看不到PLT函数
原因:文档没提-z relro链接选项会合并.plt到.text节,此时需用readelf -d binary | grep PLTGOT找GOT表地址,再objdump -d --section=.text binary | grep -A5 <GOT_addr>定位PLT stub
解决:先readelf -d binary确认FLAGS字段是否有BIND_NOW,有则PLT已解析,需动态调试现象:文档3.4.1节SQL注入示例用
' union select 1,2,3--,但在真实靶机返回Unknown column '2' in 'field list'
原因:靶机MySQL版本>5.7且启用了sql_mode=STRICT_TRANS_TABLES,要求UNION列数必须与原查询完全一致,而文档示例没教你怎么用order by探测列数
解决:先?id=1 order by 1--,逐步增加数字直到报错,如order by 3正常、order by 4报错,则列数为3现象:文档3.3.4节ROP链构造,用
ropper --file libc.so.6 --search "pop rdi; ret"找到gadget,但实际运行时ret指令跳转到非法地址
原因:ropper默认搜索所有可执行段,但libc.so.6的.text段在ASLR启用时基址会变,pop rdi; ret可能位于.text末尾,而ret后地址未映射
解决:加--badbytes "00"过滤含空字节的gadget,并用readelf -l libc.so.6确认.text段范围,确保gadget地址在[LOAD]段内现象:文档4.8节用
DynELF泄露函数地址,leak(addr)返回0x7ffff7a2d830,但system实际地址是0x7ffff7a55440,差2.5MB
原因:DynELF默认用printf泄露,但若目标程序禁用printf(如用write替代),DynELF会误判符号表偏移
解决:手动指定elf = ELF('./binary'); dyn = DynELF(leak, elf=elf),并用elf.symbols['printf']校准基址
3. 工具篇:别装“全家桶”,这些工具链必须亲手编译验证
文档第二章列了13个工具,但真实CTF中,90%的失败源于工具链版本不匹配。比如pwntools4.9.0在Ubuntu 22.04上默认用python3.10,而glibc2.35的malloc结构和2.23完全不同,导致文档3.3.5节堆利用代码直接崩溃。我坚持一个原则:每个工具必须亲手编译、验证、打补丁。下面拆解四个核心工具的实操要点。
3.1 pwntools:不是pip install完事,要改源码适配新glibc
文档2.7节只教from pwn import *,但2023年主流CTF靶机glibc版本集中在2.31-2.35,而pwntools 4.8.0默认适配2.23。关键修改点:
# 修改pwnlib/asm.py,修复新glibc下shellcode无法执行问题 # 原代码(line 123): # return asm(shellcraft.sh(), arch=arch, os=os) # 改为: if context.arch == 'amd64' and context.os == 'linux': # 新glibc需要清空RSP高32位,否则execve失败 return asm('xor rsi, rsi; xor rdx, rdx; push rsi; mov rax, 0x3b; mov rdi, rsp; syscall', arch='amd64') # 修改pwnlib/rop/rop.py,修复one_gadget在glibc 2.34+的兼容性 # 在find_gadgets()函数中,过滤掉含0x0f 0x05(syscall)的gadget # 因为新内核对syscall指令有额外检查验证方法:用pwntools生成shellcode,在glibc 2.35环境下用gdb单步:
# 编译测试程序(test.c) #include <stdio.h> #include <sys/mman.h> int main() { char shellcode[] = "\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\xb0\x3b\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x48\xc1\xe7\x08\x48\x01\xf7\x0f\x05"; void *addr = mmap(NULL, sizeof(shellcode), PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); memcpy(addr, shellcode, sizeof(shellcode)); ((void(*)())addr)(); } # 用gdb调试 gcc -z execstack test.c -o test && gdb ./test (gdb) b *0x401000 # 假设shellcode地址 (gdb) r (gdb) x/20i $rip # 看指令是否正确解码3.2 radare2:别信aaa自动分析,要手写aaa后的修复脚本
文档2.5节说radare2 -A binary自动分析,但真实二进制中,aaa会把jmp [rax]误判为函数调用,导致CFG断裂。必须手动修复:
# 分析binary后,进入r2交互模式 r2 -A binary [0x00401000]> aaa # 自动分析 [0x00401000]> afl # 列出函数,发现main函数末尾缺失 [0x00401000]> s 0x00401050 # 跳到疑似main末尾地址 [0x00401050]> pdf # 发现最后一条指令是jmp [rax],但r2没识别为函数结尾 [0x00401050]> af # 手动标记此处为函数 [0x00401050]> afvn main # 给函数命名 [0x00401050]> aec # 重新计算控制流图更关键的是,文档没提radare2对PIE二进制的符号解析缺陷。当binary是-pie编译时,r2的is命令列出的符号地址是0x00000000,必须用iM命令获取内存基址后手动计算:
[0x00401000]> iM # 显示内存映射,得到基址如0x555555554000 [0x00401000]> is~main # 得到符号偏移0x1000 [0x00401000]> ? 0x555555554000 + 0x1000 # 计算真实地址0x5555555550003.3 Burp Suite:别只用Intruder,要写Python插件绕过WAF
文档2.12节只教Burp的图形界面,但真实Web题中,WAF规则往往用正则匹配union select,而UnIoN SeLeCt会被放过。必须写插件自动化:
# burp_plugin.py from burp import IBurpExtender, IScannerCheck, IScanIssue import re class BurpExtender(IBurpExtender, IScannerCheck): def registerExtenderCallbacks(self, callbacks): self._callbacks = callbacks self._helpers = callbacks.getHelpers() callbacks.setExtensionName("CTF WAF Bypass") callbacks.registerScannerCheck(self) def doPassiveScan(self, baseRequestResponse): # 检测响应是否含WAF特征 response = baseRequestResponse.getResponse() if response and b'cloudflare' in response: # 构造绕过payload req = self._helpers.bytesToString(baseRequestResponse.getRequest()) if 'union select' in req.lower(): new_req = req.replace('union select', 'UnIoN/**/SeLeCt') new_req_bytes = self._helpers.stringToBytes(new_req) new_resp = self._callbacks.makeHttpRequest( baseRequestResponse.getHttpService(), new_req_bytes ) if new_resp and b'flag{' in new_resp.getResponse(): return [CustomScanIssue( baseRequestResponse.getHttpService(), self._helpers.analyzeRequest(baseRequestResponse).getUrl(), [self._callbacks.applyMarkers(new_resp, None, None)], "WAF Bypass Success", "Bypassed with case+comment trick" )] return None3.4 IDA Pro:别信F5反编译,要对照汇编看__libc_start_main调用
文档2.6节说IDA能完美反编译,但真实PWN题中,__libc_start_main的第二个参数(main函数地址)被混淆,F5显示的main可能是假函数。验证方法:
# 在IDA中按G跳转到`__libc_start_main` # 查看其第二个参数(通常是`[rbp-8]`或`rdi`) # 然后按U取消函数定义,按C将该地址转为代码 # 再按P创建新函数,这才是真实的main入口 # 关键技巧:用IDA Python脚本自动定位 import idaapi, idc # 找到__libc_start_main的调用点 ea = idc.get_name_ea(idc.BADADDR, "__libc_start_main") for ref in idautils.CodeRefsTo(ea, 1): # 获取调用指令的前一条指令(通常是mov rdi, xxx) prev = idc.prev_head(ref) if idc.print_insn_mnem(prev) == "mov" and "rdi" in idc.GetDisasm(prev): # 提取立即数 op = idc.get_operand_value(prev, 1) print(f"Real main at 0x{op:x}") idc.create_function(op) # 强制创建函数3.5 常见问题排查:工具链翻车现场实录
现象:
pwntools的remote('pwn.chal', 1337)连接后recvline()卡死,gdb调试发现程序在read(0, buf, 0x100)处阻塞
原因:靶机开了TCP_NODELAY,而pwntools默认用socket缓冲区,recvline()等待\n但服务端没发换行符
解决:r = remote('pwn.chal', 1337); r.settimeout(5); r.recvuntil(b': ')替代recvline()现象:
radare2用aaa分析ARM64二进制,pdf @main显示bl sym.imp.printf,但实际是bl 0x12345678(外部函数)
原因:radare2没正确解析ARM64的adrp+add寻址,把PLT跳转误判为直接调用
解决:手动s 0x12345678; af; afvn printf,再aaa重分析现象:Burp Suite的
Decoder解base64时,Zm9vYmFy解出foobar,但靶机要求Zm9vYmFy==(带等号)
原因:文档2.12节没提base64填充规则,而某些WAF只认标准填充
解决:在Burp中右键选择Convert selection -> Base64 encode,勾选Pad with '='现象:IDA Pro打开
PIE二进制,F5反编译显示main函数,但gdb调试时b *main报错Function not defined
原因:IDA的main是虚拟地址,而gdb需要真实加载地址,PIE基址每次启动不同
解决:gdb ./binary; b *$rebase(0x1234),其中0x1234是IDA中main的偏移,$rebase自动加基址现象:
pwntools的process('./binary')启动后recv()收不到任何数据,ps aux发现进程已退出
原因:./binary是setuid程序,pwntools默认用/bin/sh启动,但setuid程序在非特权shell中被内核拒绝
解决:p = process(['./binary'], env={'LD_PRELOAD': './libc.so.6'}),用LD_PRELOAD绕过
4. 分类专题篇:别按目录刷题,要按“漏洞生命周期”建知识树
文档第三章按Reverse/Crypto/Pwn/Web分类,但真实CTF中,同一道题往往横跨多个领域。比如一道PWN题,漏洞在Web前端JS里(eval(user_input)),触发点在后端PHP(system($_GET['cmd'])),利用链要ROP绕过NX,最后用one-gadgetgetshell。我建议按“漏洞生命周期”重构知识树:发现→定位→利用→绕过→提权。下面以PWN为例,拆解五个必须打通的环节。
4.1 发现:用checksec只是开始,要读readelf -d看动态链接细节
文档3.3节说“用checksec binary看保护”,但checksec只显示NX/PIE/RELRO,而**RELRO分Partial和Full,影响got劫持难度**。必须深挖:
# checksec只告诉你RELRO状态 $ checksec binary [*] '/path/binary' Arch: amd64-64-little RELRO: Partial RELRO # 但Partial RELRO下,.got.plt可写,而.got不可写 # 需用readelf确认具体段权限 $ readelf -d binary | grep -E "(BIND_NOW|GNU_RELRO)" 0x000000000000001e (FLAGS) BIND_NOW 0x0000000000000019 (GNU_RELRO) 0x0000000000000df8 # 再用readelf -l看GNU_RELRO段范围 $ readelf -l binary | grep GNU_RELRO GNU_RELRO 0x0000000000000df8 0x0000000000400df8 0x0000000000400df8 # 结论:.got.plt在0x400df8之后,所以可写;.got在之前,不可写4.2 定位:别只看gdb断点,要用vmmap看内存布局变化
文档3.3.3节栈溢出只教pattern create,但真实栈题中,canary值每次启动都变,而vmmap能暴露栈地址随机化规律:
# 启动三次,记录栈地址 $ gdb ./binary (gdb) r (gdb) vmmap stack 0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack] (gdb) q $ gdb ./binary (gdb) r (gdb) vmmap stack 0x00007ffffffdd000 0x00007ffffffff000 0x0000000000000000 rw- [stack] # 观察:栈底地址从0x7ffffffde000变为0x7ffffffdd000,差0x1000字节 # 说明ASLR粒度是4KB,可预测下次栈底为0x7ffffffdc0004.3 利用:one-gadget不是万能钥匙,要查libc版本对应的偏移
文档4.6节列了one-gadget libc.so.6,但同一libc.so.6文件,不同编译环境生成的one-gadget偏移不同。必须验证:
# 下载靶机libc(从/proc/1234/maps获取基址,用gdb dump) gdb ./binary (gdb) attach 1234 (gdb) dump memory libc.so.6 0x7ffff7a00000 0x7ffff7b80000 # 用libc-database查版本 $ python3 ./find libc.so.6 ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64) # 但`one-gadget`偏移依赖编译选项,需手动验证 $ one_gadget libc.so.6 0x45216 execve("/bin/sh", rsp+0x30, environ) 0x4526a execve("/bin/sh", rsp+0x30, environ) 0xf02a4 execve("/bin/sh", rsp+0x50, environ) # 验证第一个gadget: gdb ./binary (gdb) b *0x401000 (gdb) r (gdb) set $rdi = 0x7ffff7a00000 + 0x45216 # libc基址+gadget偏移 (gdb) c # 若程序退出而非getshell,说明此gadget在当前libc中无效4.4 绕过:NX不是终点,ret2csu和ret2dlresolve才是新战场
文档3.3.4节ROP只讲pop rdi; ret,但2023年靶机普遍用ret2csu绕过NX:
# `csu`是`__libc_csu_init`,其末尾有万能gadget: # pop r15; pop r14; pop r13; pop r12; pop rbp; ret # mov rdx, r13; mov rsi, r14; mov edi, r15d; call QWORD PTR [r12+rbp*8] # 构造system("/bin/sh"): # 1. 用`ret2csu`设置rdi="/bin/sh", rsi=0, rdx=0 # 2. call system@plt # 文档没教怎么找`csu`地址,必须用ropper: $ ropper --file binary --search "pop r15; pop r14; pop r13; pop r12; pop rbp; ret"4.5 提权:getshell不是结束,docker escape才是最终关卡
文档第七章实战篇提到CVE,但没教如何从容器内提权到宿主机。比如靶机是Docker,/proc/1/self/cgroup显示/docker/abc123,则:
# 检查是否挂载宿主机目录 $ cat /proc/1/mounts | grep "/host" # 若有,则`cd /host/etc/shadow`可读取宿主机密码 # 或利用`cap_sys_admin`能力逃逸 $ capsh --print | grep cap_sys_admin # 若有,则用`unshare -r -f /bin/bash`创建新userns,再`mount -t proc proc /proc`获得完整/proc访问4.6 常见问题排查:PWN题翻车五大现场
现象:
ret2libc链中system("/bin/sh")执行后程序退出,没拿到shell
原因:/bin/sh启动后stdin被关闭,system返回后exit调用失败
解决:在system后加ret指令,或用execve("/bin/sh",0,0)替代现象:
heap题中fastbin attack成功,但malloc(0x10)返回的地址不在预期位置
原因:fastbin在glibc 2.27+加入safe-linking,fd指针被异或了chunk地址>>12
解决:fd = (fake_chunk_addr ^ (chunk_addr>>12)),再写入fd现象:
ROP链中pop rdi; retgadget执行后rdi值是乱码
原因:ret指令从栈弹出地址,但栈未对齐(x86-64要求16字节对齐),ret后rsp指向错误位置
解决:在pop rdi; ret前加ret指令垫栈,确保rsp % 16 == 0现象:
web题中php://filter读取源码,?file=php://filter/read=convert.base64-encode/resource=index.php返回空
原因:PHP配置allow_url_include=Off,但php://filter不受此限制;空响应是因为index.php中<?php后紧跟exit;,convert.base64-encode编码了exit字符串
解决:用?file=php://filter/resource=index.php直接读,或?file=data:text/plain,<?php phpinfo(); ?>绕过现象:
crypto题中RSA私钥d很大,pow(c,d,n)计算超时
原因:d虽大,但p,q接近,可用Wiener's attack
解决:用continued_fractions分解e/n,找收敛子k/d满足|e/n - k/d| < 1/(2*d^2),再验证d
5. 题解篇:别抄writeup,要拆解“作者为什么这样想”
文档第六章题解篇列了20+道经典题,但直接抄writeup只会让你在新题面前彻底懵圈。我坚持一个习惯:每道题解,必须反向推导作者的思考链。比如0CTF2015 freenote,writeup说“用unlink改free@got为system
本文还有配套的精品资源,点击获取