工欲善其事,必先利其器,我们这里来讲讲打 pwn 必备的 pwntools 库 pwntools 不仅提供了编写exp必须的一些功能,也提供了一些必要的 cli 工具
主要讲一些常用功能,部分内容因为用的的确很少就简单省略一下
pwntools 安装
直接 pip 安装即可
pip install pwntools
安装完成后可以打开 python 交互模式尝试一下相关功能能否正常使用 比如尝试一下
❯ python3
Python 3.12.3 (main, Jun 19 2026, 12:46:00) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> cyclic(0x20)
b'aaaabaaacaaadaaaeaaafaaagaaahaaa'
>>>
确认功能正常即可
与程序通信
创建通信
我们可以通过 process()/remote() 创建一个通信连接
当我们本地运行一个文件时可以
from pwn import *
io = process('./elf')
在本地运行时,也可以通过 gdb 启动程序
from pwn import *
io = gdb.debug('./elf')
我们也可以通过 remote() TCP 连接到远程
from pwn import *
io = remote('127.0.0.1',11451)
可以通过 close() 关闭连接
io.close()
也可以通过 interactive() 设置为交互模式
io.interactive()
交互
可以通过使用 recv()/send() 进行交互
io.send(b'AAAA') # 发送'AAAA'
io.sendline(b'AAAA') # 发送'AAAA\n'
io.recv(8) # 接收8字节内容
io.recvline() # 接受内容直到换行
io.recvuntil(b'Input:') # 接收内容直到收到'Input:'
io.sendafter(b'Input:', b'AAAA') # 接收到'Input:'后发送'AAAA'
io.sendlineafter(b'Input:', b'AAAA') # 接收到'Input:'后发送'AAAA\n'
io.interactive() # 切换到交互模式
在接收内容时可以设置 drop = True 以丢弃特定部分
leak = io.recvuntil(b'\x00\x00',drop = True)
整数字节转换
可以使用 p64() 将整数转换为字节形式,默认会转换为小端序,你也可以在 context.endian 设置
p8(0xde)
# b'\xde'
p16(0xdead)
# b'\xad\xde'
p32(0xdeadbeef)
# b'\xef\xbe\xad\xde'
p64(0xdeadbeef)
# b'\xef\xbe\xad\xde\x00\x00\x00\x00'
当我们接收到字节也可以通过 u64() 解包成整数,端序依然默认小端序,可以设置 context.endian 修改
u32(b'\xef\xbe\xad\xde')
# 0xdeadbeef
u64(b'\xef\xbe\xad\xde\x00\x00\x00\x00')
# 0xdeadbeef
因此在构造payload 时,通常会经常用到 p64() 类的一些东西,举个例子
payload = b'A' * 0x28
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system_addr)
也可以使用 flat 打包,它可以根据当前设置的 context 情况自动打包
payload = flat(
b'A' * 0x28,
pop_rdi,
binsh,
system
)
ELF()
可以使用 ELF() 打开文件,并直接获取到其中的一些地址信息等
elf = ELF('./pwn')
然后就可以非常方便的获取
elf.symbols['main']
elf.sym['main']
elf.got['puts']
elf.plt['puts']
elf.entry
可以直接搜寻 ELF 中的符号地址,got/plt等
在大部分情况下,因为地址随机化相关的保护,我们获取到的只是一个偏移,因此如果我们能够找到偏移基址就可以设置 ELF() 的基址,后续搜寻的地址都会加上这个基址
例如
elf.address = leaked_addr - offset
在此之后搜索的地址就会变成基址加上搜索到的偏移
在处理libc相关的地址时非常的方便
libc = ELF('./libc.so.6')
libc.address = puts_addr - libc.sym['puts']
system = libc.sym['system']
binsh = next(libc.search(b'/bin/sh\x00'))
context设置
可以通过 context 设置一些架构相关的东西,这对于 asm(),shellcraft()之类的影响巨大
context.arch = 'amd64'
context.os = 'linux'
也可以通过其他方法更新设置
context.update(
arch='amd64',
os='linux',
endian='little'
)
可以通过 log_level 调整输出的详细程度,一般设置为 debug
context.log_level = 'debug'
cyclic()字节填充
cyclic()可以批量生成指定长度的特殊规律的字节信息,方便填充与调试
因此我们写payload时可以
payload = cyclic(0x28) + p64(win)
在一些特殊情况下也可以通过 cyclic 确认偏移量,虽然的确很少用 假如我们填充了很长一串 cyclic() 生成的字节内容,并成功把程序崩掉了,在gdb中我们可以在 rsp/rip 找到类似
0x6161616c6161616b
或者说直接是一串字符,将这些内容复制到 cyclic_find() 中就可以找到对应的偏移大小
>>> cyclic_find(0x6161616c6161616b)
40
汇编与反汇编
汇编:
context.arch = 'amd64'
code = asm('''
xor rax, rax
ret
''')
得到机器码:
b'H1\xc0\xc3'
反汇编:
disasm(b'\x48\x31\xc0\xc3')
一定要记得设置 context
当我们需要生成 shellcode 时可以通过 shellcraft() 自动化生成
context.arch = 'amd64'
sc = asm(shellcraft.sh())
也可以生成syscall
shellcraft.read(0, 'rsp', 0x100)
shellcraft.write(1, 'rsp', 0x100)
shellcraft.execve('/bin/sh')
手动组合可以完成 orw 之类的shellcode
sc = asm(
shellcraft.read(0, 'rsp', 0x100) +
shellcraft.write(1, 'rsp', 0x100)
)
ROP()
在一些可以偷懒的情况下,可以使用 ROP() 自动化构造 ROP 链,但是用起来并不方便也不聪明
elf = ELF('./pwn')
rop = ROP(elf)
寻找 gadget:
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
或者直接构造调用:
rop.call('puts', [elf.got['puts']])
然后:
payload = flat(
b'A' * offset,
rop.chain()
)
实际做题除了一些真的入门简单到不行的基本不会用,但是还是可以看看,某些新生赛题可以直接碾过去
调试
pwntools 联动 gdb 调试可以在运行时附加上去,也可以从 gdb 启动,取决于实际情况需要
io = process('./pwn')
gdb.attach(io) # 附加调试
io = gdb.debug('./pwn') # 从gdb启动
在使用gdb时也可以在pwntools中预先插入 gdb 命令
io = process('./pwn')
gdb.attach(io, '''
b *main+100
c
''')
fmtstr_payload()格式化字符串
格式化字符串的利用除了手动构造也可以通过fmtstr_payload()设置,但需要提前知道偏移
payload = fmtstr_payload(
6, #偏移
{
elf.got['puts']: elf.sym['win']
} # 要写的内容,{地址: 值} 的字典
)
Ret2dlresolvePayload()
在进行 ret2dlresolve 这一手法时,需要伪造Symtab、strtab、Jmprel 等结构,可以使用Ret2dlresolvePayload()方便的生成 payload
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
因此只需要设置寄存器的值为对应地址即可,其中dlresolve.data_addr是伪造结构应该被写到内存里的地址,dlresolve.payload是要写到 data_addr 的那段伪造结构字节,dlresolve.reloc_index 是传给 _dl_runtime_resolve 的伪造重定位表索引
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
paylaod = cyclic(padding) + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(dlresolve.data_addr) + p64(pop_rdx) + p64(len(dlresolve.payload)) + p64(elf.plt["read"]) + p64(ret) + p64(pop_rdi) + p64(dlresolve.real_args[0]) + p64(plt0) + p64(dlresolve.reloc_index)
FileStructure()
在进行 FSOP 等涉及 FILE 结构体的伪造的情况,手动构造整个 FILE 结构体通常不太方便,可以使用FileStructure()方便的设置
fileStr = FileStructure(null=0xdeadbeef) # null 是「指向 NULL 值的地址」
fileStr.vtable = 0xcafebabe # 设置伪造的 vtable 指针
payload = bytes(fileStr) # 生成完整结构字节
可以直接设置 FileStructure() 的某一字段的值,并转换为bytes形式的 payload
FileStructure()也提供了一些高级方法包括
- write(addr, size):生成一段 payload,用于从任意地址读数据写到 stdout
- read(addr, size):生成一段 payload,用于从 stdin 读数据写到任意地址(即任意写)
- str(object) / bytes(object):生成完整 payload
- struntil(memberVariable):只生成到某个成员为止的部分 payload,例如 struntil(‘fileno’)
- orange(io_list_all, vtable):直接生成 House of Orange 攻击的 payload,参数分别是 _IO_list_all 的地址和伪造 vtable 的地址
pwntools CLI
pwntools 同时还提供了一些 CLI 工具
checksec
使用 checksec 来检查文件的保护情况
checksec ./vuln
待施工