CTF PWN(二进制安全)题目类型全景图:从 🟢 签到 到 ⚫ 地狱
一句话本质:PWN 就是——给它坏输入 → 让它把内存写崩 → 让它跳到你指定的地方 → 让它替你执行
/bin/sh→ 拿 shell →cat flag。一切技巧都是围绕"怎么跳、跳去哪、跳了之后还能干啥"展开的。
🔧 第零步:先把环境焊死(不做这个后面步步踩坑)
虚拟机环境
核心工具套装
# 基础
sudo apt update && sudo apt install -y gdb gdbserver python3-pip nasm gcc
# pwntools(写 exploit 脚本的命根子)
pip3 install pwntools
# 辅助
sudo apt install -y checksec binutils # checksec 看保护,objdump/objcopy 看反汇编
pip3 install ROPgadget ropper # 找 gadget🛡️ 先读懂 checksec输出(每个题第一眼就看它)
$ checksec ./pwn_binary
Arch: amd64-64-little
RELRO: Partial RELRO ← GOT 可写?(Full RELRO = GOT 只读)
Stack: No canary found ← 有金丝雀吗?(栈 cookie 检测)
NX: NX enabled ← 栈不可执行?(DEP,不能跑栈上 shellcode)
PIE: No PIE (0x400000) ← 地址随机?(开了就啥地址都不固定)
ASLR: (系统级,运行时堆/栈/libc 随机)记忆口诀:No PIE = 地址白送,有 PIE = 你得先偷一个地址;NX = 不能跑栈上代码,得 ROP;Canary = 得先偷 cookie 或找别的路。
🟢 Level 1 — 签到题 & 热身(第一次连接就算赢的那种)
📚 必刷第一批题
攻防世界 →
level0、level1、hello_pwnCTFHub → PWN 基础 / 栈溢出入门
pwnable.tw / pwnable.kr →
fd、collision、bofBUUCTF → 搜 "pwn 入门"、"jarvisoj level0"
第一个里程碑:你能用
cyclic算出偏移量 → 用 IDA 找到后门地址 → 写 pwntools exp → 本地拿 shell → 远端也打成功。这一关过了,才算真正进门。
🟡 Level 2 — 经典栈溢出体系(PWN 的脊梁,⭐⭐)
这个阶段的核心是:栈上能覆盖返回地址,但保护开始介入 → 你不能直接跳 shellcode,得用已有代码拼出来。
① ret2text(最简单栈溢出)
payload = b'A' * offset_to_RIP + p64(backdoor_addr)条件:有后门函数 / 某函数里藏了 system(),NX 可能有但代码段地址固定(No PIE)。
② ret2shellcode
条件:NX disabled(栈可执行)或某段 RWX 区域。
把
asm(shellcraft.sh())放 payload 里 → 覆盖 RIP 跳到 shellcode 起始地址需要知道 shellcode 落在哪(栈地址泄露 or 固定栈基址)
③ ret2syscall(or ret2system)
没有后门,但 NX on → 栈不可执行,所以得在已有的可执行代码段(libc/程序本身)里找可用的指令片段(gadgets),拼一条链:
pop rdi; ret → 把 "/bin/sh" 地址放进 rdi
ret to system()64 位传参靠 rdi, rsi, rdx...,所以最常见的 gadget 就是 pop rdi; ret
④ ret2libc(最经典的栈溢出解法)
条件:NX on + PIE maybe + 有 puts/printf 能泄露 libc 地址
流程(背下来):
1. 第一次溢出 → 调用 puts(puts@got) → 泄出 puts 在内存中的真实地址
2. 算 libc_base = leaked_puts - libc.symbols['puts']
3. 算 system = libc_base + libc.symbols['system']
4. 算 /bin/sh = libc_base + libc.search(b'/bin/sh\x00').__next__()
5. 第二次溢出(或同一链继续)→ ret2system("/bin/sh")难点:栈迁移 / 二次溢出 / 栈布局对齐——第一次 leak 之后怎么干净地回到 vuln 再打第二发
⑤ Canary 绕过
⑥ 格式化字符串漏洞(独立考点,也常配栈溢出出现)
printf(user_input); // 没给格式指定符 → 格式化字符串漏洞📚 此阶段靶场
pwnable.kr →
passcode、random、input(有 fmt 题)ROP Emporium(纯 drill,专练 gadget 找法和 ROP 链书写)
CTFlearn / picoCTF PWN 章节
BUUCTF 搜 "ret2libc" / "format string" / "canary"
🟠 Level 3 — 堆利用入门(分水岭:从这里开始题质变,⭐⭐⭐)
栈题你玩的是"覆盖返回地址控制 RIP";堆题你玩的是 glibc 的
malloc/free内部数据结构(chunk metadata、bins)被你污染后的连锁反应。
先搞懂这三个东西再去碰题
chunk 结构:
size(含 PREV_INUSE 位)、fd/bk指针藏在 freed chunk 体内bins:fastbin / unsortedbin / smallbin / largebin(tcache 在较新 glibc 里也加入)
free()不是真 free:它把 chunk 挂进对应 bin,合并时检查前后 chunk 的 status → 你如果能改写 metadata,就能制造 任意地址写
核心堆漏洞类型
经典利用链(由浅入深)
Fastbin Attack 系列
├── fastbin dup(double free → 分配到任意地址)
├── fastbin dup to trick(改 fd 指向 target - 0x10 附近)
└── tcache poisoning(glibc ≥ 2.26 默认开 tcache,比 fastbin 更松)
Unsorted Bin Attack
├── leak libc(unsortedbin 的 fd 指向 main_arena → 可算 libc base)
└── 改 bk 触发 unlink 风格写
House of 系列(高级命名法,本质是上述原理的组合技)
├── House of Spirit(伪造 chunk → free 伪 chunk → 让 malloc 返回你指定的 ptr)
├── House of Lore / Force 等(视 glibc 版本而定)关键辅助知识
GOT / PLT / 延迟绑定 — 为什么改 GOT 有用
__malloc_hook/__free_hook — 现代堆利用最爱的劫ack点(把 hook 值改成 one_gadget)one_gadget — 搜 libc 里现成的
execve("/bin/sh")片段:one_gadget libc.so.6
📚 此阶段靶场
how2heap(必看!每个手法一个 .c + 图解,是堆圣经)
HeapLAB(结构化课程)
BUUCTF 搜 "heap"、"UAF"、"fastbin"
pwnable.tw 的 heap 题
🔴 Level 4 — 高级栈+堆组合拳 & 沙boxed / 硬保护环境(中级→进阶,⭐⭐⭐⭐)
⑦ 高级 ROP / Bypass
⑧ seccomp / sandbox 绕过
prctl(PR_SET_NO_NEW_PRIVS) + seccomp_rules: 只许 open/read/write,禁 execve对策:ORW 链、或找合法路径读 flag 到 stdout、或 mprotect+shellcode绕 NX
⑨ Partial / Full RELRO + PIE 全开的实战套路
信息泄露(格式化字符串 / 越界读 / 程序自带的 print)先偷一个地址
calc base,然后用
__malloc_hook/__free_hook→ one_gadget 或完整 ROP
⚫ Level 5 — 内核 PWN / AWD / 真实对抗(⭐⭐⭐⭐⭐)
🗺️ 推荐刷题路线(周/月粒度,照着走)
🧠 三条经验之谈(帮你少走弯路)
每次新题第一件事永远是
checksec+file+ strings 扫一眼别闷头开 IDA——先知道你在打什么保护等级。
本地打不通之前别碰远端
90% "远端打不通" 的坑是:本地 libc 版本 ≠ 远端。拿到题目给的
libc.so.6就用pwninit 把 linker 配好,不要用你本机/lib/x86_64-linux-gnu/libc.so.6硬凑。pwntools 是你手不是拐杖
早点从"抄 exp 模板"过渡到理解每行:
p = process('./binary') # 本地调试用这个 # p = remote('node.xxx', 12345) # 打远端换这行 payload = b'A' * offset + p64(gadget) p.sendafter(b'prompt:', payload) p.interactive()p64()/u64()/pack/unpack、什么时候加b'\x00'截断、什么时候用sendlinevssend——这些肌肉记忆决定了你能不能在赛场上顶住压力。