C0nvR3 Lab

Search workspace

Keyboard shortcuts

Ctrl + K
Open search
Esc
Close menu or dialog

How2pwn\0-2-pwntools\index.md

0-2 Pwntools

Table of contents

工欲善其事,必先利其器,我们这里来讲讲打 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

待施工

Terminal

C0nvR3 Lab terminal ready. Type help for commands.