Python 与 pwntools 基础:环境配置与核心 API

说实话,很多刚接触 Pwn 的朋友,第一关往往不是漏洞本身,而是环境没配好。我见过太多人在群里问「为什么我的 exploit 跑不起来」,结果一看,pwntools 都没装对。这一章我们就先把地基打牢。

Python 环境配置

我个人习惯用 Python 3.8 以上版本。为什么?因为 pwntools 从 4.0 开始就全面拥抱 Python 3 了,Python 2 已经彻底退场。你想想看,要是还在用 2.7,连字符串处理都能让你怀疑人生。

推荐方案: 使用 pyenv 或 conda 管理 Python 版本。我自己的笔记本上同时装了 3.8、3.9、3.10,每个项目独立虚拟环境,互不干扰。

安装步骤其实就三条命令:

# 创建虚拟环境(我习惯用 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 字符串给翻出来了。

注意: ELF 解析依赖文件路径。如果你在远程服务器上跑 exploit,本地没有目标二进制文件,那就得先 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 + pwntools 知识体系 Python 环境配置 pip install pwntools remote() process() ELF() p64() / u64() send() / recv() interactive() sendlineafter() gdb.attach() context.log_level interactive() symbols / got / plt search() libc 地址解析 小端序打包 地址解包 p32() / u32() 本地调试 → 远程利用 → 获取 Flag

避坑指南

最后分享几个我踩过的坑:

  • 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() 指定结束标志。