CTF-All-In-One/doc/2.7_pwntools.md
2017-08-17 22:36:23 +08:00

12 KiB
Raw Blame History

2.6 Pwntools

pwntools是一个CTF框架和漏洞利用开发库用Python开发由rapid设计旨在让使用者==简单快速的编写exploit。包含了本地执行、远程连接读写、shellcode生成、ROP链的构建、ELF解析、符号泄露众多强大功能。

安装

  1. 安装binutils

    git clone https://github.com/Gallopsled/pwntools-binutils
    sudo apt-get install software-properties-common
    sudo apt-add-repository ppa:pwntools/binutils
    sudo apt-get update
    sudo apt-get install binutils-arm-linux-gnu
    

  2. 安装capstone

    git clone https://github.com/aquynh/capstone
    cd capstone
    make
    sudo make install
    

  3. 安装pwntools:

    sudo apt-get install libssl-dev
    sudo pip install pwntools
    

测试安装是否成功:

>>> import pwn  
>>> pwn.asm("xor eax,eax")  
'1\xc0'

pwntools的使用

常用的模块有下面几个:

  • asm:汇编与反汇编
  • dynelf:用于远程符号泄露需要提供leak方法
  • lf:对elf文件进行操作
  • gdb:配合gdb进行调试
  • memleak:用于内存泄漏
  • hellcraft: shellcode的生成器
  • tubes:包括tubes: 包括tubes.sock, tubes.process, tubes.ssh, tubes.serialtube分别适用于不同场景的PIPE
  • utils:一些实用的小功能例如CRC计算cyclic pattern等

Tubes读写接口

对于一次攻击而言前提就是与目标服务器或者程序进行交互这里就可以用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()123456789

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

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

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

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

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

  • interactive() 直接进行交互相当于回到shell的模式在取得shell之后使用
  • ecv(numb = 4096,timeout = default):接收指定字节
  • ecvall() 一直接收知道EOF
  • ecvline(keepends = True) 接收一行keepends为是否保留行尾的\n默认为Ture
  • ecvuntil((delims,drop=False)一直读到delims的pattern出现为止
  • ecvrepeat(timeout=default) 持续接收知道EOF或者timeout
  • end(data) :发送数据
  • endline(data) : 发送一行数据,相当于在数据末尾加\n

汇编与反汇编:

http://docs.pwntools.com/en/stable/asm.html?highlight=asm

使用asm来进行汇编

>>> asm('mov eax, 0')
'\xb8\x00\x00\x00\x00'
>>> asm('mov eax, SYS_execve')
'\xb8\x0b\x00\x00\x00'
>>> asm(shellcraft.nop())
'\x90'123456

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

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

注意Any arguments/properties that can be set on context

>>> asm("mov eax, SYS_select", arch = 'i386', os = 'freebsd')
'\xb8]\x00\x00\x00'
>>> asm("mov eax, SYS_select", arch = 'amd64', os = 'linux')
'\xb8\x17\x00\x00\x00'
>>> asm("mov rax, SYS_select", arch = 'amd64', os = 'linux')
'H\xc7\xc0\x17\x00\x00\x00'
>>> asm("mov r0, #SYS_select", arch = 'arm', os = 'linux', bits=32)
'R\x00\xa0\xe3'12345678

使用disasm进行反汇编同样可以指定cpu类型

>>> print disasm('6a0258cd80ebf9'.decode('hex'))
   0:   6a 02                   push   0x2
   2:   58                      pop    eax
   3:   cd 80                   int    0x80
   5:   eb f9                   jmp    0x0
>>> print disasm('b85d000000'.decode('hex'), arch = 'i386')
   0:   b8 5d 00 00 00          mov    eax,0x5d
>>> print disasm('b85d000000'.decode('hex'), arch = 'i386', byte = 0)
   0:   mov    eax,0x5d
>>> print disasm('b85d000000'.decode('hex'), arch = 'i386', byte = 0, offset = 0)
mov    eax,0x5d
>>> print disasm('b817000000'.decode('hex'), arch = 'amd64')
   0:   b8 17 00 00 00          mov    eax,0x17
>>> print disasm('48c7c017000000'.decode('hex'), arch = 'amd64')
   0:   48 c7 c0 17 00 00 00    mov    rax,0x17
>>> print disasm('04001fe552009000'.decode('hex'), arch = 'arm')
   0:   e51f0004        ldr     r0, [pc, #-4]   ; 0x4
   4:   00900052        addseq  r0, r0, r2, asr r0
>>> print disasm('4ff00500'.decode('hex'), arch = 'thumb', bits=32)
   0:   f04f 0005       mov.w   r0, #51234567891011121314151617181920

shellcode生成器

http://docs.pwntools.com/en/stable/shellcraft.html?highlight=shellcraft

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

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

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

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

ELF文件操作

http://docs.pwntools.com/en/stable/elf.html?highlight=elf#module-pwnlib.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的地址
0x401680123456789

同样也可以打开一个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'1234567

下面是一些可用的函数:

  • 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链生成器

先简单回顾一下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'12345678910

使用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) : 给出一个地址,反解析出符号

其他:

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

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

GDB调试

对于elf文件来说可能有时需要我们进行一些动态调试工作这个时候级需要用到gdbpwntools的gdb模块也提供了这方面的支持。

其中最常用的还是attach函数在指定process之后可以attach上去调试配合proc模块就可以得到进程的Pid非常方便。

但是比较麻烦的是在实现上attach函数需要开启一个新的terminal这个terminal的类型必须使用环境变量或者context对象来指定。研究了一番源码之后找到了解决方案。

s = process('./pwnme')
context.terminal = ['gnome-terminal','-x','sh','-c']
gdb.attach(proc.pidof(s)[0])123

proc.pidof(s)[0]能够取出process的id然后attach上去。context.terminal制定的是终端类型和参数我用的是gnome-terminal可以这样写这样运行后会自动打开一个新的gnome-terminal并在里面启动gdb并自动断下来这样就可以调试了。

DynELF 符号leak

http://docs.pwntools.com/en/stable/dynelf.html

相当好用的一个工具,给出一个函数句柄,可以解析任意符号的位置。这个函数的功能是:==输入任意一个address输出这个address中的data(至少1byte)。==

官网给的例子:

# Assume a process or remote connection
p = process('./pwnme')

# Declare a function that takes a single address, and
# leaks at least one byte at that address.
def leak(address):
    data = p.read(address, 4)
    log.debug("%#x => %s" % (address, (data or '').encode('hex')))
    return data

# For the sake of this example, let's say that we
# have any of these pointers.  One is a pointer into
# the target binary, the other two are pointers into libc
main   = 0xfeedf4ce
libc   = 0xdeadb000
system = 0xdeadbeef

# With our leaker, and a pointer into our target binary,
# we can resolve the address of anything.
#
# We do not actually need to have a copy of the target
# binary for this to work.
d = DynELF(leak, main)
assert d.lookup(None,     'libc') == libc   #libc的基址
assert d.lookup('system', 'libc') == system #system的地址

# However, if we *do* have a copy of the target binary,
# we can speed up some of the steps.
# 指定一份elf的副本可以加速查找过程
d = DynELF(leak, main, elf=ELF('./pwnme'))
assert d.lookup(None,     'libc') == libc
assert d.lookup('system', 'libc') == system

# Alternately, we can resolve symbols inside another library,
# given a pointer into it.
d = DynELF(leak, libc + 0x1234)
assert d.lookup('system')      == system12345678910111213141516171819202122232425262728293031323334353637

这个例子当然没有实际意义在应用中我们可以在leak函数中布置rop链使用write函数leak出一个address的地址然后返回。接着就可以使用d.lookup函数查找符号了通常我们都是需要找system的符号。

还有更多的Pwntools的功能待以后实际操作过程中再一一学习。