Python 与 pwntools 基础:环境配置与核心 API
说实话,很多刚接触 Pwn 的朋友,第一关往往不是漏洞本身,而是环境没配好。我见过太多人在群里问「为什么我的 exploit 跑不起来」,结果一看,pwntools 都没装对。这一章我们就先把地基打牢。
Python 环境配置
我个人习惯用 Python 3.8 以上版本。为什么?因为 pwntools 从 4.0 开始就全面拥抱 Python 3 了,Python 2 已经彻底退场。你想想看,要是还在用 2.7,连字符串处理都能让你怀疑人生。
安装步骤其实就三条命令:
# 创建虚拟环境(我习惯用 venv)
python3 -m venv pwn_env
# 激活环境
source pwn_env/bin/activate # Linux/Mac
# 或者 .\pwn_env\Scripts\activate # Windows
# 安装 pwntools
pip install pwntools
嗯,这里要注意。Windows 用户可能会遇到一些坑。我曾经在 Windows 上折腾了一下午,发现是缺少了 libffi 和 libssl 的依赖。建议直接用 WSL2,省心很多。
pwntools 核心 API 速览
pwntools 说白了就是一个帮你和二进制程序「对话」的工具包。它把网络通信、进程交互、ELF 解析这些脏活累活都封装好了。我们直接看最常用的几个 API。
| API | 作用 | 我常用的场景 |
|---|---|---|
remote(host, port) |
连接远程服务 | 打 CTF 线上赛,连题目服务器 |
process('./binary') |
启动本地进程 | 本地调试,先跑通再打远程 |
ELF('./binary') |
解析 ELF 文件 | 查函数地址、GOT 表、PLT 表 |
p64() / u64() |
打包/解包 64 位整数 | 构造 ROP 链、解析地址 |
remote 与 process:两种交互模式
这两个函数返回的对象几乎一模一样,都支持 send()、recv()、interactive() 这些方法。区别就是一个连远程,一个跑本地。
process() 在本地把 exploit 调通,确认逻辑没问题后,把 process() 改成 remote() 就能直接打远程。代码几乎不用改。
from pwn import *
# 本地调试
p = process('./vuln')
p.sendline(b'AAAA')
p.recvuntil(b'flag')
print(p.recvline())
# 远程利用(改一行就行)
# p = remote('challenge.ctf.com', 1337)
你可能会问:为什么不用 nc 直接连?因为 pwntools 能帮你做很多自动化的事情。比如接收数据直到某个特定字符串出现,或者发送格式化好的 payload。这些用 nc 手动操作,效率太低了。
ELF 解析:知己知彼
做 Pwn 题,你得知道目标程序长什么样。ELF 类就是干这个的。
elf = ELF('./vuln')
# 查函数地址
system_addr = elf.symbols['system']
print(f"system 在 {hex(system_addr)}")
# 查 GOT 表
puts_got = elf.got['puts']
print(f"puts@GOT 在 {hex(puts_got)}")
# 查 PLT 表
puts_plt = elf.plt['puts']
print(f"puts@PLT 在 {hex(puts_plt)}")
我记得有一次打比赛,题目给了个 stripped 的二进制文件,符号表全没了。但 ELF 类还能通过 search() 方法在内存里找特定字节模式,硬是把 /bin/sh 字符串给翻出来了。
download() 下来,或者用 from pwn import * 自带的 ELF 类配合 remote 的 libc 泄露。
p64 与 u64:字节序的魔法
这两个函数是 Pwn 里最常用的「翻译官」。为什么需要它们?因为内存里存的是小端序,而我们写代码时用的是大端序的人类思维。
# 把整数变成 8 字节的小端序字节串
addr = 0x7fffffff1234
payload = p64(addr)
print(payload) # b'\x34\x12\xff\xff\xff\x7f\x00\x00'
# 反过来,把字节串解析成整数
recv_data = b'\x34\x12\xff\xff\xff\x7f\x00\x00'
addr_back = u64(recv_data)
print(hex(addr_back)) # 0x7fffffff1234
说白了,p64 就是 pack,u64 就是 unpack。32 位程序用 p32 和 u32,道理一样。
交互式调试基础
写 exploit 不是一次就能成的。你总得看看程序到底收到了什么、输出了什么。pwntools 提供了几种调试手段。
1. 打印收发数据
context.log_level = 'debug' # 开启调试日志
p = process('./vuln')
p.sendline(b'AAAA')
# 控制台会打印出发送和接收的原始数据
我刚开始学的时候,经常开着 debug 模式看数据流。虽然信息量大,但能帮你快速定位问题。
2. GDB 联动
p = process('./vuln')
gdb.attach(p, '''
break *0x400000
continue
''')
p.interactive()
这个功能太实用了。直接在 exploit 里下断点,然后 GDB 窗口弹出来,你可以单步、看寄存器、看内存。我曾经靠这个找到了一个 off-by-one 漏洞的精确触发条件。
3. interactive() 模式
当你把 payload 发完,想手动和程序交互时,调用 p.interactive() 就行。它会把你当前的终端和远程/本地进程的输入输出连起来,就像用 nc 一样。
知识结构总览
下面这张图把本章的核心知识点串起来了。你可以把它当作一个快速索引。
避坑指南
最后分享几个我踩过的坑:
- Python 版本不对: 我曾经在 Ubuntu 18.04 上默认 Python 3.6,结果 pwntools 的某些新特性不支持。升级到 3.8 后一切正常。
- 虚拟环境混乱: 有次我在全局环境装了 pwntools,又在虚拟环境装了一个不同版本,结果 import 时报错。从此我坚持每个项目独立虚拟环境。
- 忘记 context.arch: 做 ARM 或 MIPS 的题时,记得设置
context.arch = 'arm'或'mips',否则 p64 默认按 x86_64 打包,地址全错。 - recv 阻塞: 如果程序没有输出你预期的内容,
recv()会一直等。这时候用recv(timeout=2)加个超时,或者用recvuntil()指定结束标志。