Exploit利器——Pwntools

专用于CTF Exploit的Python库

pwntools是由Gallopsled开发的一款专用于CTF Exploit的Python库,包含了本地执行、远程连接读写、shellcode生成、ROP链的构建、ELF解析、符号泄漏等众多强大功能,可以说把exploit繁琐的过程变得简单起来。这里简单介绍一下它的使用。

安装

Pwntools的主页在pwntools.com,Github项目地址为pwntools,可以下载到最新的源码用python进行安装。也可以简单地使用pip install pwntools进行安装。

需要安装:sudo apt install libffi-dev

虽然Pwntools大部分的功能都是纯python实现的可以直接使用,其还是依赖一些外部的python库例如capstone等,如果使用pip安装的话可以发现安装了不少依赖的库。而如果需要使用其ROP链的构建功能的话,则需要安装libcapstone这个非Python的库,这个可以到capstone-download去下载,不过需要注意的一点就是要保持python的capstone与这个libcapstone的版本一致否则可能出现API Version冲突的问题,在pip安装时可以指定其版本pip install capstone==3.0.4

pwntools的简单使用

1.导入包

使用

from pwn import *

命令即可导入包,一般做PWN题时

2.链接远程服务器或链接本地文件

需要与远程计算机进行连接此时可以使用命令

r = remote('目标IP或目标URL', 目标端口号)

也可以在本地进行测试,使用

r = process("./文件名")

之后用生成的pid进行本地进程附加,即可进行gdb调试。

3.接收远端回传的数据

一般使用以下的几个函数。

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

recv(numb=字节大小, timeout=default) : 接收指定字节数。

recvall() : 一直接收直到达到文件EOF。

recvline(keepends=True) : 接收一行,keepends为是否保留行尾的\n。

recvuntil(delims, drop=False) : 一直读到delims的pattern出现为止。

recvrepeat(timeout=default) : 持续接受直到EOF或timeout。

4.向远端发送数据

一般使用以下的几个函数。

send(data) : 发送数据。

sendline(data) : 发送一行数据,相当于在数据末尾加\n。

5.判断回传的数据

可以使用startswith函数。用来判断返回的值以什么开头。

6.其他的用法。

贴个链接,这里有一些用法合集,欢迎大家去看哦。 http://brieflyx.me/2015/python-module/pwntools-intro/

模块简介

一般使用的话可以直接用from pwn import *将所有的模块导入到当前namespace,这条语句还会帮你把os,sys等常用的系统库导入。

常用的模块有下面几个:

  • asm : 汇编与反汇编,支持x86/x64/arm/mips/powerpc等基本上所有的主流平台

  • dynelf : 用于远程符号泄漏,需要提供leak方法

  • elf : 对elf文件进行操作

  • gdb : 配合gdb进行调试

  • memleak : 用于内存泄漏

  • shellcraft : shellcode的生成器

  • tubes : 包括tubes.sock, tubes.process, tubes.ssh, tubes.serialtube,分别适用于不同场景的PIPE

  • utils : 一些实用的小功能,例如CRC计算,cyclic pattern等

Tubes读写接口

这应该是exploit最为基础的部分了,对于一次攻击而言前提就是与目标服务器或者程序进行交互,这里就可以使用remote(address, port)产生一个远程的socket然后就可以读写了

>>> conn = remote('ftp.debian.org',21)
>>> conn.recvline() 
'220 ...'
>>> conn.send('USER anonymous\r\n')
>>> conn.recvuntil(' ', drop=True)
'331'
>>> conn.recvline()
'Please specify the password.\r\n'
>>> conn.close()

同样地,使用process可以打开一个本地程序并进行交互

>>> sh = process('/bin/sh')
>>> sh.sendline('sleep 3; echo hello world;')
>>> sh.recvline(timeout=1)
''
>>> sh.recvline(timeout=5)
'hello world\n'
>>> sh.close()

同时,也可以使用listen来开启一个本地的监听端口

>>> l = listen()
>>> r = remote('localhost', l.lport)
>>> c = l.wait_for_connection()
>>> r.send('hello')
>>> c.recv()
'hello'

无论哪种PIPE都是继承tube而来,可以用于读写函数主要有:

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

  • recv(numb=4096, timeout=default) : 接收指定字节

  • recvall() : 一直接收直到EOF

  • recvline(keepends=True) : 接收一行,keepends为是否保留行尾的\n

  • recvuntil(delims, drop=False) : 一直读到delims的pattern出现为止

  • recvrepeat(timeout=default) : 持续接受直到EOF或timeout

  • send(data) : 发送数据

  • sendline(data) : 发送一行数据,相当于在数据末尾加\n

这些看官们应该都很熟悉了,关于它们的用法也就不再赘述。

汇编与反汇编

使用asm来进行汇编

>>> asm('nop')
'\x90'
>>> asm('nop', arch='arm')
'\x00\xf0 \xe3'

可以使用context来指定cpu类型以及操作系统

>>> context.arch      = 'i386'
>>> context.os        = 'linux'
>>> context.endian    = 'little'
>>> context.word_size = 32

使用disasm进行反汇编

>>> print disasm('6a0258cd80ebf9'.decode('hex'))
   0:   6a 02                   push   0x2
   2:   58                      pop    eax
   3:   cd 80                   int    0x80
   5:   eb f9                   jmp    0x0

注意,asm需要binutils中的as工具辅助,如果是不同于本机平台的其他平台的汇编,例如在我的x86机器上进行mips的汇编就会出现as工具未找到的情况,这时候需要安装其他平台的cross-binutils。

Shellcode生成器

使用shellcraft可以生成对应的架构的shellcode代码,直接使用链式调用的方法就可以得到

>>> print shellcraft.i386.nop().strip('\n')
    nop
>>> print shellcraft.i386.linux.sh()
    /* push '/bin///sh\x00' */
    push 0x68
    push 0x732f2f2f
    push 0x6e69622f
...

如上所示,如果需要在64位的Linux上执行/bin/sh就可以使用shellcraft.amd64.linux.sh(),配合asm函数就能够得到最终的pyaload了。

除了直接执行sh之外,还可以进行其它的一些常用操作例如提权、反向连接等等。

ELF文件操作

这个还是挺实用的,在进行elf文件逆向的时候,总是需要对各个符号的地址进行分析,elf模块提供了一种便捷的方法能够迅速的得到文件内函数的地址,plt位置以及got表的位置。

>>> e = ELF('/bin/cat')
>>> print hex(e.address)  # 文件装载的基地址
0x400000
>>> print hex(e.symbols['write']) # 函数地址
0x401680
>>> print hex(e.got['write']) # GOT表的地址
0x60b070
>>> print hex(e.plt['write']) # PLT的地址
0x401680

同样,也可以打开一个libc.so来解析其中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'

ELF模块在文档里好像还没有写的样子,不过可以从源码中看到一些可用的函数

  • asm(address, assembly) : 在指定地址进行汇编

  • bss(offset) : 返回bss段的位置,offset是偏移值

  • checksec() : 对elf进行一些安全保护检查,例如NX, PIE等。

  • disasm(address, n_bytes) : 在指定位置进行n_bytes个字节的反汇编

  • offset_to_vaddr(offset) : 将文件中的偏移offset转换成虚拟地址VMA

  • vaddr_to_offset(address) : 与上面的函数作用相反

  • read(address, count) : 在address(VMA)位置读取count个字节

  • write(address, data) : 在address(VMA)位置写入data

  • section(name) : dump出指定section的数据

ROP链生成器

现在的exploit是越来越难,一般起手题都得是NX开启的,ROP这种以前都能出400分题的技术现在也就出50-100分题了非常惨,也许跟这个工具简化了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模块的作用,就是自动地寻找程序里的gadget,自动在栈上部署对应的参数。

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 chain dump出来就可以了。

  • call(resolvable, arguments=()) : 添加一个调用,resolvable可以是一个符号,也可以是一个int型地址,注意后面的参数必须是元组否则会报错,即使只有一个参数也要写成元组的形式(在后面加上一个逗号)

  • chain() : 返回当前的字节序列,即payload

  • dump() : 直观地展示出当前的rop chain

  • raw() : 在rop chain中加上一个整数或字符串

  • search(move=0, regs=None, order=’size’) : 按特定条件搜索gadget,没仔细研究过

  • unresolve(value) : 给出一个地址,反解析出符号

项目地址:https://github.com/JonathanSalwan/ROPgadget

查找可存储寄存器的代码

ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'

查找字符串

 ROPgadget --binary rop --string "/bin/sh"

查找有int 0x80的地址

ROPgadget --binary rop  --only 'int'

其他

对于整数的pack与数据的unpack,可以使用p32,p64,u32,u64这些函数,分别对应着32位和64位的整数。

另外,在utils工具中比较常用的就是可以使用cyclic pattern来找到return address的位置,这个功能在gdb peda中也是有的,这里就不过多介绍了。

还有一些比较有用的功能没有研究,之后再进行介绍。

参考资料

pwntools document

其他相关链接

最后更新于