Pwn 题的利用链思维
Pwn 题的利用链思维
本文适合
能让程序崩溃,但不知道如何从崩溃走到泄露、劫持和稳定拿 shell 的 Pwn 入门学习者。学完你能:把 Pwn 题拆成“触发漏洞、证明可控、泄露地址、劫持控制流、稳定执行目标”五个阶段,并为每个阶段留下验证证据。
一句话判断
Pwn 不等于让程序崩溃;崩溃只是现象,利用链要证明你控制了什么、泄露了什么、劫持了哪里、最终稳定执行了什么。
最小利用链:
如果只做到 crash,没有证明 RIP/EIP、函数指针、堆块或格式化写入可控,就还没有进入真正的利用阶段。
题目中常见信号
优先假设:栈溢出、堆溢出、越界写
起手动作:cyclic 找偏移和可控位置
优先假设:格式化字符串
起手动作:%p/%s/%n 最小读写验证
优先假设:堆题、UAF、double free
起手动作:画堆块生命周期
优先假设:需要泄露或绕过
起手动作:规划泄露目标
优先假设:协议/菜单解析
起手动作:保存完整 recv/send 序列
优先假设:libc、栈对齐、交互差异
起手动作:对比泄露值、libc 版本、sendline
核心概念
要回答的问题:程序从哪里读、读多少、可重复几次
常见证据:菜单、stdin、文件、socket
要回答的问题:哪个输入导致内存破坏或任意读写
常见证据:crash、ASAN、调试器异常
要回答的问题:被覆盖的关键位置是否来自你的输入
常见证据:cyclic offset、寄存器、内存快照
要回答的问题:随机化保护下缺哪个地址
常见证据:canary、PIE、libc、heap、stack
要回答的问题:控制点在哪里
常见证据:返回地址、GOT、hook、vtable、函数指针
要回答的问题:最终执行什么
常见证据:system("/bin/sh")、ORW、one_gadget、shellcode
利用链思维要求每个阶段都能单独验证。不要把一个大 payload 拼到最后才第一次测试。
最小分析流程
- 运行并记录交互:菜单、提示符、输入结束符、是否能多轮输入。
- 检查保护:
checksec判断 Canary、NX、PIE、RELRO。 - 确认漏洞类型:栈溢出、格式化字符串、UAF、整数溢出、越界读写等。
- 证明可控性:用 cyclic、格式串索引、堆块复用或任意写读证据证明。
- 列缺失地址:需要 canary、libc、PIE、heap 还是 stack。
- 设计分阶段 payload:第一阶段泄露,第二阶段劫持,必要时第三阶段 ORW。
- 稳定化远程交互:加
recvuntil、处理换行、校验泄露长度和栈对齐。 - 写复盘注释:payload 每段都写用途,方便下一次排错。
利用链规划模板:
漏洞类型:
输入点:
偏移/索引:
保护机制:
泄露目标:
劫持目标:
最终目标:
失败备选:最小验证示例
1. 栈溢出可控性
from pwn import *
p = process("./chall")
p.sendline(cyclic(200))
p.wait()
core = p.corefile
print(hex(core.rip))
print(cyclic_find(core.rip & 0xffffffff))判断:
RIP/EIP 是 cyclic 中的值 -> 返回地址可控,记录精确偏移
只崩溃但 RIP 不可控 -> 检查是否先覆盖 canary、长度截断或输入格式错误2. ret2libc 泄露阶段
payload = b"A" * offset
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)
p.sendline(payload)
leak = u64(p.recvline().strip().ljust(8, b"\x00"))
log.info(f"puts leak = {hex(leak)}")判断:
泄露值落在 libc 地址范围 -> 可计算 libc_base
泄露值过短或像文本 -> recv 位置错,检查菜单和换行
程序直接退出 -> ROP 链、栈对齐或返回地址错误3. 格式化字符串最小读写
AAAA.%p.%p.%p.%p.%p判断:
输出中出现 0x41414141 或 0x4141414141414141 -> 找到输入在栈上的偏移
能稳定泄露栈/libc/PIE -> 进入泄露阶段
能写目标地址 -> 再考虑 GOT/hook/返回地址覆盖常见利用 / 解题路线
路线总览:
前提:无 PIE 或已知 win 地址
阶段:偏移 -> 覆盖返回地址 -> 调 win
前提:NX 开、能泄露 libc
阶段:泄露 GOT -> 算 libc -> system/binsh
前提:禁止 execve 或没有 shell
阶段:open -> read -> write 读取 flag
前提:缺少常规 gadget
阶段:用 __libc_csu_init 控参
前提:可控 syscall; ret 和 sigreturn frame
阶段:构造 frame 直接系统调用
前提:可控 format 参数
阶段:泄露 -> 任意写 -> 劫持
前提:有 add/delete/edit/show
阶段:UAF/double free -> 泄露 -> 任意写
前提:NX 关闭或可 mprotect
阶段:写 shellcode -> 跳转执行
备选路径要按阶段设计:
泄露失败 -> 换泄露对象:canary / libc / PIE / stack
gadget 不够 -> ret2csu / SROP / 栈迁移
不能拿 shell -> ORW 读 flag
远程不稳定 -> 固定 libc、加栈对齐、重写交互同步常见失败原因
可能原因:canary、截断、偏移错或覆盖位置不是返回地址
排查动作:用 cyclic 和调试器确认覆盖点
可能原因:recv 错位、端序/补零错误、符号解析错
排查动作:打印原始 bytes 和 hex
可能原因:libc 不匹配或栈未对齐
排查动作:泄露 libc 指纹,补一个 ret 对齐
可能原因:菜单交互没同步
排查动作:context.log_level="debug",补 recvuntil
可能原因:约束不满足
排查动作:看 gadget 约束,改 ret2libc 或 ORW
可能原因:没画生命周期
排查动作:记录每个 chunk 的分配、释放、复用
迷你案例
题目是 64 位 ELF,checksec 显示 NX 开、Canary 关、PIE 关、Partial RELRO。输入 200 字节后程序崩溃。用 cyclic 发现返回地址偏移是 72。
第一阶段不急着拿 shell,而是构造 ROP:pop rdi; ret -> puts@got -> puts@plt -> main,泄露 puts 地址并返回 main。根据泄露计算 libc_base、system 和 /bin/sh。
第二阶段 payload:
"A"*72 -> ret 对齐 -> pop rdi -> "/bin/sh" -> system记录利用链:
输入点:stdin
漏洞:栈溢出
可控性:cyclic 确认 offset=72
泄露:puts@got -> libc_base
劫持:返回地址 ROP
目标:system("/bin/sh")这条链每一步都有单独证据,所以远程失败时能定位是泄露、地址计算、栈对齐还是交互同步的问题。