pwntools模块总结

前言

pwntools 是一款专门用于CTF二进制Exploit编写的python库;

功能

环境变量设置

由于二进制文件运行环境不同,需要进行环境设置才能够正常运行exp,比如有一些需要进行汇编,但是32的汇编和64的汇编不同;

环境变量有目标架构、操作系统、字长、字节序,设置方式如下:

  • 通过设置全局变量context一次性进行设置、同时也可以设置:目标架构、操作系统、字长、字节序;

    1
    2
    3
    4
    5
    >>> context.clear() 				# 清空context
    >>> context.arch = 'i386'
    >>> context.os = 'linux'
    >>> context.endian = 'little'
    >>> context.word_size = 32
  • 直接通过context这个函数来一次性设置所有需要设置的参数;

    1
    2
    3
    4
    5
    >>> asm('nop')
    '\x90'
    >>> context(arch='arm', os='linux', endian='big', word_size=32)
    >>> asm('nop')
    '\xe3 \xf0\x00'
  • 将目标体系结构指定为函数定义的参数;

    1
    2
    3
    4
    >>> asm('nop')
    '\x90'
    >>> asm('nop', arch='amd64')
    '\x00\xf0 \xe3'

连接及信息传输

要进行漏洞利用,首先就需要与程序进行通信,pwntools提供的函数能够与本地或远程进行通信;

process()本地交互

使用process()函数创建了一个进程对象p,创建进程对象p之后可以使用它进行一系列的输入输出交互;

1
2
3
4
p = process(['filename', 'argv_1', 'argv_2', …], cwd="working_directory")
eg.
p = process('/bin/sh')
p.clean() # 清空消息缓存

remote()远程交互

使用remote()函数创建了一个进程对象conn进行socket通信;

1
conn = remote('ip_address', port_num)

Ip_address可以是IP、域名或者本地0;

ssh()登陆并执行命令行

pwntools同时提供ssh连接方式;

1
2
3
4
5
6
7
8
s = ssh(host='ip_address', user='username', port=port_num, password='password')
# 创建进程
c = s.process('command')
# 和正常的本地进程交互相同,可以接收、发送数据
c.recv()
c.send()
# 同时可以使用nc打开另外一个连接通道
nc = s.run('nc 127.0.0.1 8888') # run()和process()功能相同

listen()本地监听

pwntools提供listen()函数开启本地的监听端口;

1
2
3
4
5
6
>>> l = listen()
>>> r = remote('localhost', l.lport)
>>> conn = l.wait_for_connection()
>>> r.send('hello')
>>> conn.recv()
'hello'

数据接收

实现数据的接收,首先建立起一个具备交互的对象,然后调用接收函数recv()recvline()recvuntil()接收数据;

1
2
3
4
5
6
7
8
9
10
conn = process('file_path')
conn.recv(256, timeout = default) # 接收到缓冲区中的256bytes的信息
conn.recvline(keepends=True) # 接受一行数据,keepends为是否保留行尾的'\n'
conn.recvuntil('string', drop=fasle) # 接收数据直到'string'出现才会执行下一行代码
conn.recvall() # 一直接收直到 EOF
conn.recvrepeat(timeout = default) # 持续接受直到EOF或timeout
conn.clear() # 清空消息缓存

# 直接进行交互,相当于回到shell的模式,在取得shell之后使用
conn.interactive()

数据发送

与数据接收类似;

1
2
3
conn = process('file_path')
conn.send(data) # 发送data数据
conn.sendline(data) # 在data数据后自动添加一个'\n'换行符

打包与解包

在漏洞利用的过程当中,往往需要将输入的payload转化成8位、16位、32位或64位、大端或小端所对应格式;

pwntools提供了一组函数用来对给定的数据按照一定的格式进行打包和解包,这些函数以p或u为开头,后面加上一个数字代表位数;

1
2
3
4
5
6
7
8
9
10
# 打包一个整数,即将一个数字转换为字符
>>> p8(0xde)
'\xde'
>>> p16(0xdead)
'\xad\xde'
>>> p32(0xdeadbeef)
'\xef\xbe\xad\xde'
>>> p64(0xdeadbeef)
'\xef\xbe\xad\xde\x00\x00\x00\x00'
# 由于linux编译的程序是小端序的,所以转换后的顺序是反的
1
2
3
4
5
6
7
8
9
10
11
# 解包一个字符串,得到整数
>>> hex(u8('\xde'))
'0xde'
>>> hex(u16('\xad\xde'))
'0xdead'
>>> hex(u32('\xef\xbe\xad\xde'))
'0xdeadbeef'
>>> hex(u64('\xef\xbe\xad\xde\x00\x00\x00\x00'))
'0xdeadbeef'
>>> hex(u64('\x00\x00\x00\x00\xef\xbe\xad\xde'))
'0xdeadbeef00000000'

汇编与反汇编

pwntools提供了asm()disasm()两个函数进行汇编和反汇编的转换;

1
2
3
4
5
6
7
8
9
10
11
>>> asm('mov eax, 0')
'\xb8\x00\x00\x00\x00'
>>> asm('mov eax, 0').encode('hex')
'b800000000'
>>> print disasm('\xb8\x00\x00\x00\x00')
0: b8 00 00 00 00 mov eax, 0x0
>>> print disasm('6a0258cd80ebf9'.decode('hex'))
0: 6a 02 push 0x2
2: 58 pop eax
3: cd 80 int 0x80
5: eb f9 jmp 0x0

ELF文件解析

在漏洞利用脚本的编写过程中,经常需要使用got表地址plt表地址、或是system函数在libc中的偏移

pwntool的FLE模块能够快速找到相应的地址;

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
>>> elf = ELF('bin-path')		# 加载二进制文件
>>> elf.address # 文件装载的基地址
>>> elf.got['fun_name'] # 获取对应函数的got表地址
>>> elf.plt['fun_name'] # 获取对应函数的plt表地址
# eg.
>>> e = ELF('/bin/bash')
[*] '/bin/bash'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
>>> print hex(e.address)
0x0
>>> print hex(e.symbols['write'])
0x2c0a0
>>> print hex(e.got['write'])
0x306988
>>> print hex(e.plt['write'])
0x2c0a0

# 同样,也可以打开一个libc.so来解析其中system的位置
>>> libc = ELF('libc-path') # 加载二进制文件
>>> libc.symbols['system'] # 获取函数地址

# 修改ELF文件代码
>>> e = ELF('/bin/cat')
>>> e.read(e.address+1, 3)
'ELF'
>>> e.asm(e.address, 'ret')
>>> e.save('/tmp/quiet-cat')
>>> disasm(file('/tmp/quiet-cat','rb').read(1))
' 0: c3 ret'

DynEFL泄漏函数地址

DynELF是pwntools中专门用来应对无libc情况的漏洞利用模块;

在没有目标系统libc文件的情况下,可以使用DynELF模块来泄漏地址信息,从而获取到shell;

在没有目标系统libc文件的情况下,DynEFL函数能够解析动态链接的ELF二进制文件的符号,给定一个函数可以泄漏任意地址信息,DynEFL函数进而能够解析加载的库中任意符号,使用lookup方法用来寻找函数符号的地址;

1
2
3
4
5
6
7
8
9
10
p = process('bin-path')
elf = EFL('bin-path')
# 声明一个只包含一个地址参数的函数leak()
# 并且这个函数能够泄漏至少位于这个地址的一字节的数据
def leak(address):
data = p.read(address, 4)
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak,elf)
system_addr = d.lookup('system','libc')

FmtStr格式化字符串

在格式化字符串利用中,攻击者往往需要通过漏洞实现任意内存地址写,但构造合适的payload往往需要占用大量的时间;

FmtStr模块中实现了和格式化字符串漏洞利用相关的多个函数,极大的加速了漏洞利用脚本的开发速度;

1
2
3
4
fmtstr_payload(offset, writes, numbwritten, write_size)
# eg.
fmtstr_payload(5, {0x8041337:0xdeadbeef}, write_size='short')
# 生成一段payload实现修改0x8041337地址内容为 0xdeadbeef

ShellCraft构造shellcode

shellcraft模块包含一些生成shellcode的函数,用于生成shellcode;

其中的子模块声明架构,比如shellcraft.arm是ARM架构的、shellcraft.amd64是AMD64架构、shellcraft.i386是Intel 80386架构的、以及有一个shellcraft.common是所有架构通用的;

有时需要在写exp的时候用到简单的shellcode,pwntools提供了对简单的shellcode的支持:
首先,常用的,也是最简单的shellcode,即调用/bin/sh可以通过shellcraft得到;

1
2
>>> print shellcraft.sh()	# 打印出shellcode
>>> asm(shellcraft.sh()) # 汇编后的shellcode

由于各个平台,特别是32位和64位的shellcode不一样,所以最好先设置context;

ROP链构造

ROP原理:由于NX开启不能在栈上执行shellcode,但是可以在栈上布置一系列的返回地址与参数,这样可以进行多次的函数调用,通过函数尾部的ret语句控制程序的流程,而用程序中的一些pop/ret的代码块(称之为gadget)来平衡堆栈。其完成的事情无非就是放上/bin/sh,覆盖程序中某个函数的GOT为system的,然后ret到那个函数的plt就可以触发system('/bin/sh')。由于是利用ret指令的exploit,所以叫Return-Oriented Programming(如果没有开启ASLR,可以直接使用ret2libc技术)。

实现ROP的难点是如何在栈上布置返回地址以及函数参数;

而ROP模块的作用,是自动地寻找程序里的gadget,自动在栈上部署对应的参数;

1
2
3
4
5
6
7
8
9
10
>>> elf = ELF('ropasaurusrex')
>>> rop = ROP(elf)
>>> rop.read(0, elf.bss(0x80))
>>> rop.dump()
['0x0000: 0x80482fc (read)',
'0x0004: 0xdeadbeef',
'0x0008: 0x0',
'0x000c: 0x80496a8']
>>> str(rop)
'\xfc\x82\x04\x08\xef\xbe\xad\xde\x00\x00\x00\x00\xa8\x96\x04\x08'

使用ROP(elf)来产生一个rop的对象,这时rop链还是空的,需要在其中添加函数;

因为ROP对象实现了__getattr__的功能,可以直接通过func call的形式来添加函数;

rop.read(0, elf.bss(0x80))实际相当于rop.call('read', (0, elf.bss(0x80)))

通过多次添加函数调用,最后使用str将整个rop链dump出来就可以了;

  • call(resolvable, arguments=()):添加一个调用,resolvable可以是一个符号,也可以是一个int型地址,注意后面的参数必须是元组否则会报错,即使只有一个参数也要写成元组的形式(在后面加上一个逗号);
  • chain():返回当前的字节序列,即payload;
  • dump():直观地展示出当前的rop链;
  • raw():在rop链中加上一个整数或字符串;
  • search(move=0, regs=None, order=’size’):按特定条件搜索gadget,没仔细研究过;
  • unresolve(value):给出一个地址,反解析出符号;

cyclic字符串生成

可以按照一定规律生成指定长度的字符串,这个是一个在栈溢出或者各种需要找偏移的时候比较有用的函数;

1
2
3
4
>>> cyclic(30)		# 生成长度为30字节的字符串
'aaaabaaacaaadaaaeaaafaaagaaaha'
>>> cyclic_find('abcd') # 寻找字符串'abcd'的偏移
2807

gdb调试

pwntools提供了用于在程序运行中调用gdb的函数,配合gdb进行调试,设置断点之后便能够在运行过程中直接调用GDB;

1
gdb.attach(target, gdbscript = None, exe = None, arch = None)
  • target为所要调试的进程;
  • gdbscript为gdb脚本字符串,在启动gdb时,会先执行该脚本;
  • exe为所调试进程的二进制文件路径;
  • arch为架构;

一般情况下,只需要使用前两个参数即可。

DEBUG日志

当context.log_level被设置为 “DEBUG”,输入和输出会被直接输出,显示栈信息;

1
context.log_level = 'DEBUG'

参考:

https://pwntools-docs-zh.readthedocs.io/zh_CN/dev/intro.html

http://brieflyx.me/2015/python-module/pwntools-intro/