CTF-竞赛之pwn(小白手搓版)

CTF-竞赛之pwn(小白手搓版)

 次点击
130 分钟阅读

CTF PWN(二进制安全)题目类型全景图:从 🟢 签到 到 ⚫ 地狱

一句话本质:PWN 就是——给它坏输入 → 让它把内存写崩 → 让它跳到你指定的地方 → 让它替你执行 /bin/sh→ 拿 shell → cat flag。一切技巧都是围绕"怎么跳、跳去哪、跳了之后还能干啥"展开的。


🔧 第零步:先把环境焊死(不做这个后面步步踩坑)

虚拟机环境

组件

推荐

说明

OS

Ubuntu 20.04 / 22.04 LTS(64-bit VM)

绝大多数 PWN 题是 Linux x86-64 ELF

虚拟机软件

VMware / VirtualBox

快照功能很重要,随时回滚

核心工具套装

# 基础
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

工具

用途

优先级

IDA Free / Ghidra

静态反编译(F5 伪代码)

⭐⭐⭐⭐⭐

GDB + pwndbg / gef

动态调试、看栈/堆/寄存器

⭐⭐⭐⭐⭐

checksec

一眼看全保护:NX / Canary / PIE / RELRO

⭐⭐⭐⭐⭐

pwntools

Python 写 exp:p = remote() / process()+ 打包地址

⭐⭐⭐⭐⭐

ROPgadget / ropper

pop rdi; ret这类 gadget

⭐⭐⭐⭐

pwninit

本地 libc ≠ 远端 libc 时的救命工具

⭐⭐⭐⭐

cyclic/ pwntools.cyclic

算溢出偏移量(别手算)

⭐⭐⭐

🛡️ 先读懂 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 随机)

保护

意思

怎么绕

NX enabled

栈/堆不可执行,shellcode on stack直接 SIGSEGV

改用 ROP → call system()​ 或 mprotect()改权限

Canary found

栈返回地址前有 cookie,覆盖它就 abort

泄露 canary(格式化字符串/读越界)或 SSP leak​ 或绕过程序逻辑不走 check

PIE enabled

主程序基址随机,代码段地址不固定

泄露任意代码地址 → 算偏移 → 还原基址

Full RELRO

GOT 只读,got overwrite不行

ret2libc / ROP​ 直接调 libc 里的 system,或劫持 __malloc_hook

ASLR

libc / 堆 / 栈地址随机

泄露 libc 地址(如通过已输出或任意读)→ 算 libc_base→ 定位 system/ /bin/sh

记忆口诀:No PIE = 地址白送,有 PIE = 你得先偷一个地址;NX = 不能跑栈上代码,得 ROP;Canary = 得先偷 cookie 或找别的路。


🟢 Level 1 — 签到题 & 热身(第一次连接就算赢的那种)

题型

为什么在这

解法

直接 nc就连上了,它自己吐 flag

题目故意做成 service 把 flag 输出给你

nc ip port→ 复制 flag

程序有后门函数(没被调用但有 system("/bin/sh")cat flag

IDA 里搜字符串 "flag"/ 看函数列表发现 win()/ backdoor()

栈溢出 → 覆盖 RIP 跳到 backdoor的地址(ret2text)

Gift:直接给 libc 地址 / 给栈地址

puts(printf("%p",stack_var))主动泄地址

算术算偏移就完了

gets()/ read(0, buf, 0x100)无限制 + 无任何保护

checksec全绿(No canary, No PIE, NX disabled)

写 shellcode → 跳过去;或 ret2text 跳后门

📚 必刷第一批题

  • 攻防世界​ → level0level1hello_pwn

  • CTFHub​ → PWN 基础 / 栈溢出入门

  • pwnable.tw​ / pwnable.kr​ → fdcollisionbof

  • BUUCTF​ → 搜 "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 绕过

手法

原理

SSP leak(Stack Smashing Protector info leak)

程序崩溃时 __stack_chk_fail打印 argv[0] 或字符串 → 把 flag 或地址安排成 argv[0]

Byte-by-byte brute force

canary 每字节爆破(fork 服务器模式,子进程每次行为一致)

格式化字符串读 canary

%{n}$p/ 直接读栈帧泄露 canary 值,然后在 payload 中原样填回去再盖 RIP

泄露后回填

格式化字符串/read()越界让你读到 canary → payload 里原封不动摆回去

⑥ 格式化字符串漏洞(独立考点,也常配栈溢出出现)

printf(user_input);  // 没给格式指定符 → 格式化字符串漏洞

能做什么

payload 示例

泄露栈上任意数据

%p %p %p %p...定位偏移 → %{offset}$p读指定栈位置

泄露 GOT 地址 → 得 libc

%{got_puts}$s或读出来算

任意写(用 %n

把 shell 地址写入 GOT 某项,下次调用就跳你的目标

改返回地址 / GOT

配合精心计算的 pad + %n做到任意地址写

📚 此阶段靶场

  • pwnable.kr​ → passcoderandominput(有 fmt 题)

  • ROP Emporium(纯 drill,专练 gadget 找法和 ROP 链书写)

  • CTFlearn / picoCTF​ PWN 章节

  • BUUCTF​ 搜 "ret2libc" / "format string" / "canary"


🟠 Level 3 — 堆利用入门(分水岭:从这里开始题质变,⭐⭐⭐)

栈题你玩的是"覆盖返回地址控制 RIP";堆题你玩的是 glibc 的 malloc/free内部数据结构(chunk metadata、bins)被你污染后的连锁反应

先搞懂这三个东西再去碰题

  1. chunk 结构size(含 PREV_INUSE 位)、fd/ bk指针藏在 freed chunk 体内

  2. bins:fastbin / unsortedbin / smallbin / largebin(tcache 在较新 glibc 里也加入)

  3. free()不是真 free:它把 chunk 挂进对应 bin,合并时检查前后 chunk 的 status → 你如果能改写 metadata,就能制造 任意地址写

核心堆漏洞类型

漏洞

一句话

典型利用

堆溢出(heap overflow)

写超出 allocated chunk → 覆盖下一个 chunk 的 metadata(size/fd/bk

覆盖 next chunk 的 size造 overlapping chunk;或覆 fd指向目标地址

UAF — Use-After-Free

指针没清零:free(p)后仍保留 p并用 → 此时那块已在 bin 里,你可重新 allocate 控制其内容

分配 fake object 到同一块 → vtable / function pointer 被你替换 → 控制流劫ack

Double Free

同一个指针 free()两次 → fastbin 形成循环链表

fastbin dup​ → 分配到任意地址(如 __malloc_hook)→ 写 one_gadget

Null Byte Overflow / Off-by-One

只多写了 1 字节(常见于 \x00截断逻辑反了)

改前一个/后一个 chunk 的 size最低字节 → consolida tion 造 overlap

经典利用链(由浅入深)

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

技术

何时需要

SROP(Sigreturn-oriented Programming)

gadget 极少时:伪造 sigcontext结构体 → 一次 rt_sigreturn系统调用就能设全部寄存器(含 RIP=RSP→栈上的/bin/shexecve)

ret2csu

64 位缺 pop rdi; ret时,利用 __libc_csu_init里的固定 gadget 传参

stack pivot(栈迁移)

溢出太小盖不到 RIP 但能盖 RBP → 把栈迁到你能控制的 bss/堆段 → 在那儿摆完整 ROP

ORW(Open-Read-Write)绕过 seccomp

禁了 execve但留了 open/read/write → 手动 sys_open(flag)sys_read(fd,buf)sys_write(1,buf)

⑧ 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 / 真实对抗(⭐⭐⭐⭐⭐)

类型

内容

Kernel PWN

写/调试内核模块(LKM)漏洞:buffer overflow in ioctl→ 覆返回地址到 commit_creds(prepare_kernel_cred(0))→ 提权到 root;绕过 SMEP/SMAP/KASLR(ROP in kernel、ret2usr、modprobe_path 等)

AWD 攻防 PWN

你拿到同份有洞二进制 → 写 exp 打别人(批量)+ patch 自己的二进制(nop out 漏洞指令 / 改 canary / 加长度检查)→ 保分

Fuzz + 0day 向

AFL / LLVM sanitizer → 挖真实程序漏洞(超出 CTF 范围但在 career track 里)


🗺️ 推荐刷题路线(周/月粒度,照着走)

阶段

时长

目标

去哪练

第1周

搭环境 + 汇编最小集(call/ret、栈 push/pop、x86-64 寄存器 rbp/rsp/rip)

能跟一个栈溢出的 GDB 调试过程

自己写个带 gets()的 C 程序,IDA 跟一遍

第2~4周

Level 1 全清

checksec读得懂 → cyclic 算偏移 → ret2text → pwntools 写 exp 打通远端

攻防世界 level0~2 / CTFHub 栈基础 / pwnable.kr bof

第2~3月

Level 2 核心

ret2libc 手写(两阶段 leak→pwn)、fmt str leak/write、canary SSP/fmt leak

ROP Emporium(全套) / BUUCTF ret2libc系列 / picoCTF PWN

第3~6月

Level 3 堆

how2heap 每个 demo 手跑一遍 → UAF / fastbin / tcache / unsortedbin leak → houseofspirit 等

how2heap + BUUCTF heap / CTFshow PWN 进阶

第6~12月

Level 4 SROP / seccomp / 全保护

ORW、stack pivot、one_gadget、高版本 glibc 适配

DEF CON Quals / 强网杯 / RCTF 题解 follow

之后

Level 5

有兴趣走 kernel / AWD

pwn.college kernel track / AWD 平台


🧠 三条经验之谈(帮你少走弯路)

  1. 每次新题第一件事永远是 checksec+ file+ strings 扫一眼

    别闷头开 IDA——先知道你在打什么保护等级。

  2. 本地打不通之前别碰远端

    90% "远端打不通" 的坑是:本地 libc 版本 ≠ 远端。拿到题目给的 libc.so.6就用 pwninit​ 把 linker 配好,不要用你本机 /lib/x86_64-linux-gnu/libc.so.6硬凑。

  3. 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'截断、什么时候用 sendlinevs send——这些肌肉记忆决定了你能不能在赛场上顶住压力。


© 本文著作权归作者所有,未经许可不得转载使用。