前言
pwntools 是一款专门用于CTF二进制Exploit编写的python库;
功能
环境变量设置
由于二进制文件运行环境不同,需要进行环境设置才能够正常运行exp,比如有一些需要进行汇编,但是32的汇编和64的汇编不同;
环境变量有目标架构、操作系统、字长、字节序,设置方式如下:
通过设置全局变量
context
一次性进行设置、同时也可以设置:目标架构、操作系统、字长、字节序;1
2
3
4
5# 清空context context.clear()
'i386' context.arch =
'linux' context.os =
'little' context.endian =
32 context.word_size =直接通过
context
这个函数来一次性设置所有需要设置的参数;1
2
3
4
5'nop') asm(
'\x90'
'arm', os='linux', endian='big', word_size=32) context(arch=
'nop') asm(
'\xe3 \xf0\x00'将目标体系结构指定为函数定义的参数;
1
2
3
4'nop') asm(
'\x90'
'nop', arch='amd64') asm(
'\x00\xf0 \xe3'
连接及信息传输
要进行漏洞利用,首先就需要与程序进行通信,pwntools提供的函数能够与本地或远程进行通信;
process()
本地交互
使用process()
函数创建了一个进程对象p,创建进程对象p之后可以使用它进行一系列的输入输出交互;
1 | p = process(['filename', 'argv_1', 'argv_2', …], cwd="working_directory") |
remote()
远程交互
使用remote()
函数创建了一个进程对象conn
进行socket通信;
1 | conn = remote('ip_address', port_num) |
Ip_address
可以是IP、域名或者本地0;
ssh()
登陆并执行命令行
pwntools同时提供ssh连接方式;
1 | s = ssh(host='ip_address', user='username', port=port_num, password='password') |
listen()
本地监听
pwntools提供listen()
函数开启本地的监听端口;
1 | l = listen() |
数据接收
实现数据的接收,首先建立起一个具备交互的对象,然后调用接收函数recv()
、recvline()
、recvuntil()
接收数据;
1 | conn = process('file_path') |
数据发送
与数据接收类似;
1 | conn = process('file_path') |
打包与解包
在漏洞利用的过程当中,往往需要将输入的payload转化成8位、16位、32位或64位、大端或小端所对应格式;
pwntools提供了一组函数用来对给定的数据按照一定的格式进行打包和解包,这些函数以p或u为开头,后面加上一个数字代表位数;
1 | # 打包一个整数,即将一个数字转换为字符 |
1 | # 解包一个字符串,得到整数 |
汇编与反汇编
pwntools提供了asm()
和disasm()
两个函数进行汇编和反汇编的转换;
1 | 'mov eax, 0') asm( |
ELF文件解析
在漏洞利用脚本的编写过程中,经常需要使用got表地址、plt表地址、或是system函数在libc中的偏移;
pwntool的FLE模块能够快速找到相应的地址;
1 | 'bin-path') # 加载二进制文件 elf = ELF( |
DynEFL泄漏函数地址
DynELF是pwntools中专门用来应对无libc情况的漏洞利用模块;
在没有目标系统libc文件的情况下,可以使用DynELF模块来泄漏地址信息,从而获取到shell;
在没有目标系统libc文件的情况下,DynEFL函数能够解析动态链接的ELF二进制文件的符号,给定一个函数可以泄漏任意地址信息,DynEFL函数进而能够解析加载的库中任意符号,使用lookup方法用来寻找函数符号的地址;
1 | p = process('bin-path') |
FmtStr格式化字符串
在格式化字符串利用中,攻击者往往需要通过漏洞实现任意内存地址写,但构造合适的payload往往需要占用大量的时间;
FmtStr模块中实现了和格式化字符串漏洞利用相关的多个函数,极大的加速了漏洞利用脚本的开发速度;
1 | fmtstr_payload(offset, writes, numbwritten, write_size) |
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 | print 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 | 'ropasaurusrex') elf = ELF( |
使用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 | 30) # 生成长度为30字节的字符串 cyclic( |
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