Pwntools使用指北

概述

Pwntools是一个CTF框架和漏洞利用开发库.它使用Python编写,旨在实现快速原型设计和开发,并尽可能简化漏洞利用程序的编写.

通过下述命令进行安装:

1
pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple pwntools

管道(Tubes)

Tubes是用于与进程进行通信的类,通常用于传输数据、发送或接收信息.

pwntools提供了以下几种不同的Tube类型来处理不同的情况:

  • process:表示一个本地进程,你可以通过管道与它进行交互.
  • remote:表示与远程主机上程序的链接,你可以通过网络与远程程序进行交互.
  • listen:允许你在本地监听一个端口,等待远程连接.
  • socket:表示一个原始的网络套接字,允许更细粒度的网络交互.

程序连接

语句 含义
io = process(“本地文件路径”) 本地连接
io = remote(“本地文件路径”) 远程连接

发送数据

语句 含义
io.send(payload) 发送payload
io.sendline(payload) 发送payload,追加’\n’,进行换行
io.sendafter(str, payload) 接收到str后,发送payload
io.sendlineafter(str, payload) 接收到str后,发送payload,并进行换行

接收数据

语句 含义
io.recv(N) 接收N个字符
io.recvline() 接收数据,直到遇到换行符
io.recvuntil(delim) 接收数据,直到遇到分隔符
io.recvregex(pattern) 接收数据,直至满足一个regex模式
io.recvrepeat(timeout) 接收数据,直到出现超时
io.clean() 丢弃所有缓冲数据
io.interactive() 直接与程序进行输入输出交互

操作整数

语句 含义
io.pack(int) 发送一个字大小的打包整数
io.unpack() 接收并解压缩一个字大小的整数

示例代码

1
2
3
4
5
6
from pwn import *
context.log_level = 'debug'

io = process('sh')
io.sendline('echo Hello, world')
io.recvline()

实用工具(Utility)

打包和解包整数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from pwn import *

print(pack(1))
# '\x01\x00\x00\x00'

print(pack(-1))
# '\xff\xff\xff\xff'

print(pack(1, endian='big'))
# '\x00\x00\x00\x01'

print(p16(1))
# '\x01\x00'

print(hex(unpack(b'AAAA')))
# '0x41414141'

print(hex(u16(b'AA')))
# '0x4141'

文件输入输出

1
2
3
4
5
6
7
from pwn import *

write('luoFile', 'data')
print(read('luoFile'))
# 'data'
print(read('luoFile', 1))
# 'd'

编码和解码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *

#Base64
print(b64d(b64e(b'luo pwntools')))
# b'luo pwntools'

#URL编码
print(urlencode("luo pwntools"))
# %6c%75%6f%20%70%77%6e%74%6f%6f%6c%73

#16进制编码
print(enhex(b'luo pwntools'))
# '68656c6c6f'
print(unhex('6c756f2070776e746f6f6c73'))
# b'luo pwntools'

#位操作
print(bits(b'A')) #'A' ASCII编码为65,二进制表示为01000001
# [0, 1, 0, 0, 0, 0, 0, 1]
print(unbits([0, 1, 0, 0, 0, 0, 0, 1]))
# 'A'

#16进制查看
print(hexdump(b'luo pwntools', 16))
# 00000000  6c 75 6f 20  70 77 6e 74  6f 6f 6c 73               │luo │pwnt│ools│

cyclic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 计算缓冲区到返回地址的距离
from pwn import *

buffer_size = 200
pattern = cyclic(buffer_size)
print(pattern)

crash_address = 0x61616167
offset = cyclic_find(crash_address)
print(f"offset: {offset}")

上下文(Context)

context模块允许你设置目标系统的平台架构以及调试信息等相关配置.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *

# 操作系统为Linux,设置架构为64 位,
context.os = 'linux'
context.arch = 'amd64'

# 设置字节序为小端
context.endian = 'little'

# 设置日志级别为 debug
context.log_level = 'debug'

# 一次设置多值
context(os='linux', arch='amd64', log_level='debug')

ELF

加载ELF

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *

e = ELF('/bin/bash')
# [*] '/bin/bash'
#     Arch:     amd64-64-little
#     RELRO:    Partial RELRO
#     Stack:    Canary found
#     NX:       NX enabled
#     PIE:      No PIE
#     FORTIFY:  Enabled

使用符号

  • ELF.symbols:列出所有已知符号.plt优先于got
  • ELF.got:只包含got
  • ELF.plt:只包含plt
  • ELF.functions:只包含函数(需要DWARF符号)

示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

e = ELF('/bin/bash')

print("%#x -> license" % e.symbols['bash_license'])
print("%#x -> execve" % e.symbols['execve'])
print("%#x -> got.execve" % e.got['execve'])
print("%#x -> plt.execve" % e.plt['execve'])
print("%#x -> list_all_jobs" % e.functions['list_all_jobs'].address)

# 输出如下:
[*] '/bin/bash'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
0x130cc0 -> license
0x2f610 -> execve
0x133af0 -> got.execve
0x2f610 -> plt.execve
0x60e80 -> list_all_jobs

修改基地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *

e = ELF('/bin/bash')

print("%#x -> base address" % e.address)
print("%#x -> entry point" % e.entry)
print("%#x -> execve" % e.symbols['execve'])

print("--------------------------")
e.address = 0x12340000

print("%#x -> base address" % e.address)
print("%#x -> entry point" % e.entry)
print("%#x -> execve" % e.symbols['execve'])

# 输出如下:
[*] '/bin/bash'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
0x0 -> base address
0x31760 -> entry point
0x2f610 -> execve
--------------------------
0x12340000 -> base address
0x12371760 -> entry point
0x1236f610 -> execve

读取ELF

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *

e = ELF('/bin/bash')

print(repr(e.read(e.address, 4)))

p_license = e.symbols['bash_license']
license   = e.unpack(p_license)
print("%#x -> %#x" % (p_license, license))

print(e.read(license, 14))
print(e.disasm(e.symbols['main'], 12))

# 输出如下:
[*] '/bin/bash'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
b'\x7fELF'
0x130cc0 -> 0xfd7e0
b'License GPLv3+'
   2fee0:       f3 0f 1e fa             endbr64
   2fee4:       41 57                   push   r15
   2fee6:       41 56                   push   r14
   2fee8:       41 55                   push   r13
   2feea:       41 54                   push   r12

修补ELF

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pwn import *

e = ELF('/bin/bash')

# Cause a debug break on the 'exit' command
e.asm(e.symbols['exit_builtin'], 'int3')

# Change the license
p_license = e.symbols['bash_license']
license = e.unpack(p_license)
e.write(license, b'Hello, world!\n\x00')

e.save('./bash-modified')

我们使用下述命令来运行修改后的bash程序.

1
2
3
4
5
$ chmod +x ./bash-modified
$ ./bash-modified -c 'exit'
Trace/breakpoint trap
$ ./bash-modified --version | grep "Hello"
Hello, world!

搜索ELF

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *

e = ELF('/bin/bash')

for address in e.search('/bin/sh\x00'):
    print hex(address)
    
# 输出如下:
0x31e62
0x31f01

构建ELF

1
2
3
4
5
from pwn import *

ELF.from_bytes(b'\xcc').save('./int3-1')
ELF.from_assembly('int3').save('./int3-2')
ELF.from_assembly('nop', arch='powerpc').save('./powerpc-nop')

运行和调试ELF

1
2
3
4
5
6
7
# 等同于 io = process('./luoPwn')
e = ELF('./luoPwn')
io = e.process()

# 等同于 io = gdb.debug('./luoPwn')
e = ELF('/luoPwn')
io = e.debug()

汇编

Pwntools让你在任何架构中进行汇编变的非常容易,并提供了多种可定制的ShellCode.

基础汇编

1
2
3
4
5
6
7
from pwn import *

print(repr(asm('xor edi, edi')))
# '1\xff'

print(enhex(asm('xor edi, edi')))
# 31ff

汇编模版

shellcraft模块会提供一些现成的汇编代码.

如下生成一段简单的ShellCode,通过系统调用(execve)启动一个 /bin/sh shell

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *
#help(shellcraft.sh)
print('---')
print(shellcraft.sh())
print('---')
print(enhex(asm(shellcraft.sh())))

# 输出如下:
---
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\x00' */
    push 0x68
    push 0x732f2f2f
    push 0x6e69622f
    mov ebx, esp
    /* push argument array ['sh\x00'] */
    /* push 'sh\x00\x00' */
    push 0x1010101
    xor dword ptr [esp], 0x1016972
    xor ecx, ecx
    push ecx /* null terminate */
    push 4
    pop ecx
    add ecx, esp
    push ecx /* 'sh\x00' */
    mov ecx, esp
    xor edx, edx
    /* call execve() */
    push SYS_execve /* 0xb */
    pop eax
    int 0x80

---
6a68682f2f2f73682f62696e89e368010101018134247269010131c9516a045901e15189e131d26a0b58cd80

命令行工具

  1. asm:将汇编转换为机器码.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ asm nop
90

$ asm nop | xxd 
00000000: 90 

# 从stdin上获取数据
$ echo 'push ebx; pop edi' | asm
535f

# 通过--format选项指定输出格式.支持的参数有raw、hex 、string 和 elf
$ asm --format=string 'int3'
b'\xcc'

$ asm --format=hex 'int3'   
cc

# 生成arm架构下的汇编
$ asm --context=arm 'mov r0, r1'
0100a0e1
  1. disasm:将机器码反汇编为汇编指令.
1
2
3
4
5
$ disasm cd80
   0:    cd 80                    int    0x80   

$ asm nop | disasm
   0:    90                       nop
  1. shellcraft:是内部shellcraft模块的命令行接口.在命令行中,必须按arch.os.template的顺序指定完整的环境信息.
1
2
3
4
5
6
$ shellcraft i386.linux.sh
6a68682f2f2f73682f62696e89e368010101018134247269010131c9516a045901e15189e131d26a0b58cd80

#arm架构下汇编
$ shellcraft arm.linux.sh
687000e3417144e304702de52f7f02e32f7347e304702de52f7206e3697e46e304702de50d00a0e1737806e304702de50cc02ce004c02de50410a0e30d1081e001c0a0e104c02de50d10a0e1022022e00b70a0e3000000ef

异构架构

为其他平台进行汇编交互,需要安装适当版本的binutils.可参考installing.md.

1
2
 # 使用下述命令进行全平台binutils安装
 apt install binutils-*

编译arm架构下的汇编如下:

1
2
3
4
5
6
7
8
from pwn import *
context.arch = 'arm'

print(repr(asm('mov r0, r1')))
# '\x01\x00\xa0\xe1'

print(enhex(asm('mov r0, r1')))
# 0100a0e1

调试

需要先进行gdb安装,如下:

1
apt install gdb gdbserver

GDB启动进程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from pwn import *
context.log_level = 'debug'
# 指定GDB调试终端,需要先运行 tmux
# 进行tmux窗口后, 按下Ctrl + D, 即可取消窗口分割
context.terminal = ['tmux', 'splitw']

gdb_commands = f'''
    b exit
    b _exit
    c
'''
io = gdb.debug("/bin/bash", gdb_commands)
io.sendline('echo hello')
io.recvline()

io.interactive()

GDB附加进程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pwn import *
#context.log_level = 'debug'

io = process('/bin/sh')

gdb_commands = f'''
    b exit
    b _exit
    c
'''
gdb.attach(io, gdb_commands)
io.interactive()

ROP

ROP全称为Return-oriented Programming(面向返回的编程,是一种绕过NX(禁止执行,也称为数据执行防护(DEP))的技术.

加载ELF

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/sh')

rop = ROP(elf)
print(rop.rbx)

# 输出如下:
# Gadget地址 反汇编内容 加载的寄存器 堆栈调整大小
Gadget(0x4a62, ['pop rbx', 'ret'], ['rbx'], 0x10)

修复地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/sh')

elf.address = 0xff000000
rop = ROP(elf)
print(rop.rbx)

# 输出如下:
Gadget(0xff004a62, ['pop rbx', 'ret'], ['rbx'], 0x10)

检查Gadgets

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/sh')

gadgets = ROP(elf).gadgets
for addr, insns in gadgets.items():
    print(insns)
    
# 输出如下:
Gadget(0x784b, ['add esp, 0x10', 'pop rbx', 'pop rbp', 'pop r12', 'ret'], [16, 'rbx', 'rbp', 'r12'], 0x30)
Gadget(0xc55e, ['add esp, 0x10', 'pop rbx', 'ret'], [16, 'rbx'], 0x20)
Gadget(0x106ec, ['add esp, 0x130', 'pop rbx', 'ret'], [304, 'rbx'], 0x140)
Gadget(0x14060, ['add esp, 0x18', 'ret'], [24], 0x20)
Gadget(0x64da, ['add esp, 0x20', 'pop rbx', 'pop rbp', 'pop r12', 'ret'], [32, 'rbx', 'rbp', 'r12'], 0x40)

如果上述结果中没有想要的,建议使用ROPgadget.

添加原始数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/sh')

rop = ROP(elf)
rop.raw(0xdeadbeef)
rop.raw(0xcafebabe)
rop.raw('asdf')

print(rop.chain())

# 输出如下:
b'\xef\xbe\xad\xde\x00\x00\x00\x00\xbe\xba\xfe\xca\x00\x00\x00\x00asdf'

查看ROP栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/sh')

rop = ROP(elf)
rop.raw(0xdeadbeef)
rop.raw(0xcafebabe)
rop.raw('asdf')

print(rop.dump())

# 输出如下:
0x0000:       0xdeadbeef
0x0008:       0xcafebabe
0x0010:       b'asdf' 'asdf'

提取原始字节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/sh')

rop = ROP(elf)
rop.raw(0xdeadbeef)
rop.raw(0xcafebabe)
rop.raw('asdf')

print(hexdump(bytes(rop)))

# 输出如下
00000000  ef be ad de  00 00 00 00  be ba fe ca  00 00 00 00  │····│····│····│····│
00000010  61 73 64 66                                         asdf
00000014

函数调用

注意设置目标架构.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/sh')

rop = ROP(elf)
rop.call(0xdeadbeef, [0, 1])
print(rop.dump())

# 输出如下:
0x0000:           0x4b70 pop rdi; ret
0x0008:              0x0 [arg0] rdi = 0
0x0010:           0x4a02 pop rsi; ret
0x0018:              0x1 [arg1] rsi = 1
0x0020:       0xdeadbeef

按名称调用函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/sh')

rop = ROP(elf)
rop.execve(0xdeadbeef)
print(rop.dump())

# 输出如下:
0x0000:           0x4b70 pop rdi; ret
0x0008:       0xdeadbeef [arg0] rdi = 3735928559
0x0010:           0x42a0 execve

多ELF寻找Gadgets

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/bash')
libc = elf.libc

elf.address = 0xAA000000
libc.address = 0xBB000000
rop = ROP([elf, libc])
print(rop.rax)
print(rop.rbx)
print(rop.rcx)
print(rop.rdx)

# 输出如下:
Gadget(0xaa0686c3, ['pop rax', 'ret'], ['rax'], 0x10)
Gadget(0xaa031d88, ['pop rbx', 'ret'], ['rbx'], 0x10)
Gadget(0xbb0e2d4e, ['pop rcx', 'pop rbx', 'ret'], ['rcx', 'rbx'], 0x18)
Gadget(0xaa030462, ['pop rdx', 'ret'], ['rdx'], 0x10)

获取Shell

直接调用execve函数,然后从内存中找到"/bin/sh\x00"地址作为第一个参数.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *
#context.arch = 'amd64'
context.binary = elf = ELF('/bin/bash')
libc = elf.libc

elf.address = 0xAA000000
libc.address = 0xBB000000
rop = ROP([elf, libc])

binsh = next(libc.search(b"/bin/sh\x00"))
print(f"binsh:{hex(binsh)}")
rop.execve(binsh, 0, 0)

print(rop.dump())
print(hexdump(bytes(rop)))

# 输出如下:
binsh:0xbb1a7e43
0x0000:       0xaa0368b9 pop rsi; ret
0x0008:              0x0 [arg1] rsi = 0
0x0010:       0xaa031c86 pop rdi; ret
0x0018:       0xbb1a7e43 [arg0] rdi = 3139075651
0x0020:       0xaa030462 pop rdx; ret
0x0028:              0x0 [arg2] rdx = 0
0x0030:       0xaa02f610 execve
00000000  b9 68 03 aa  00 00 00 00  00 00 00 00  00 00 00 00  │·h··│····│····│····│
00000010  86 1c 03 aa  00 00 00 00  43 7e 1a bb  00 00 00 00  │····│····│C~··│····│
00000020  62 04 03 aa  00 00 00 00  00 00 00 00  00 00 00 00  b···│····│····│····│
00000030  10 f6 02 aa  00 00 00 00                            │····│····│

日志

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *

info('Info!')
debug('Debug!')
warn('Warning!')
success('SUCCESS')
error('ERROR!')

# 输出如下:
# 默认日志级别为info,不输出debug内容
[*] Info!
[!] Warning!
[+] SUCCESS
[ERROR] ERROR!

可通过命令行或者context.log_level设置日志级别

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 命令行
python ./exp.py DEBUG

# 默认log输出到stdout, 配置log_file输出到指定文件
from pwn import *
context.log_level = 'debug'
context.log_file = './log.txt'

io = process('sh')
#io = process('sh', level='debug')
io.sendline('echo Hello, world')
io.recvline()

参考链接

pwntools Github

pwntools tutorial

pwntools docs


相关内容

0%