0x00 前言:
pwntools是写exp的得力助手,是pwn题中不可缺少的一部分,学pwn的过程中,对于pwntools的理解是必不可少的,今日我学习了部分pwntools的知识,在此写到博客中,本着分享知识的目的,同时也是为了加深自己的印象。部分知识我也不会,百度上没有特别明确的解释,文章应该不误导读者,故在一些地方我会特别注明。
本文正文中的英文粗体为索要解释的内容,斜体为官方文档的解释,普通文本为我的认知、参考他人文章以及对英语的理解
0x10 引入
- from pwn import * This imports a lot of functionality into the global namespace. You can now assemble, disassemble, pack, unpack, and many other things with a single function. A full list of everything that is imported is available on from pwn import *.
引入pwn包为全局变量,可以通过pwn包进行汇编、反汇编、pack、unpack等操作
0x20 连接
- Making Connections You need to talk to the challenge binary in order to pwn it, right? pwntools makes this stupid simple with its pwnlib. tubes module. This exposes a standard interface to talk to processes, sockets, serial ports, and all manner of things, along with some nifty helpers for common tasks. For example, remote connections via pwnlib.tubes.remote.
pwntools通过pwnlib.tubes.remote函数简化了这个连接过程,通过简单的conn = remote('ftp.ubuntu.com',21),便可以将本地与远程主机进行连接 ,格式为 varname = remote('远程域名/ip', port)
- conn = remote('ftp.ubuntu.com',21) #建立连接
- >>> conn.recvline() # doctest: +ELLIPSIS#接收一行消息
- b'220 …' #所接收的消息
- >>> conn.send(b'USER anonymous\r\n') #向远程主机发送消息
- >>> conn.recvuntil(b' ', drop=True)#接收消息直至空,drop意思不太懂,后若了解再更新,宁少也不愿错。
- b'331'
- >>> conn.recvline() #接收一行消息
- b'Please specify the password.\r\n'#主机返回的消息
- >>> conn.close()#断开连接
- 也可以通过SSH连接
- shell = ssh(host='192.168.18.144', user='root', port=2222, password='123456')
建立监听器
- l = listen()
- >>> r = remote('localhost', l.lport)#发送端
- >>> c = l.wait_for_connection()#接收端
- r.send(b'hello') #发送端发送hello
- >>> c.recv() #接收端接收到hello
- b'hello'
交互
Interacting with processes is easy thanks to pwnlib.tubes.process.
- sh = process('/bin/sh')
- >>> sh.sendline(b'sleep 3; echo hello world;')
- >>> sh.recvline(timeout=1)
- b''
- >>> sh.recvline(timeout=5)
- b'hello world\n'
- >>> sh.close()
多亏了pwnlib.tubes.process库,使得我们可以很方便地通过shell与程序进行交互
上述的timeout = N ,我不太理解,不过我个人认为是在一秒之后再接收(N=1),此时接收到的为空,因为senline语句中有sleep 3,我认为这是等3秒之后再进行echo,而3秒之前是什么也没有输出的,所以接收到的为空;当timeout = 5时,echo语句已经执行了,所以recvline便可以接收到完整的hello world
- sh.interactive() # doctest: +SKIP
- $ whoami
- user
当调用sh.interactive()时,便是打开了一个shell终端,我们可以在这里继续输入命令,而不必拘泥于之前的代码,通常(萌新做题范围之内)在此输入ls,便可以查看到flag文件,然后使用cat命令进行连接,便可以得到flag
常用方法
- send(payload)发送payload
- sendafter(str, payload)接收到str 后,发送 payload
- sendline(payload) 发送payload,并换行
- recv(N) 接收 N个字符
- recvline() 接收一行输出
- recvlines(N)接收 N行输出
- recvuntil(string)接收直到 str 为止
0x30 打包整数
- Packing Integers
- A common task for exploit-writing is converting between integers as Python sees them, and their representation as a sequence of bytes. Usually folks resort to the built-in struct module. pwntools makes this easier with pwnlib.util.packing. No more remembering unpacking codes, and littering your code with helper routines.
通常写exp的时候需要在Python所能理解的整数与整数的字节序列表示之间进行转换,我的理解是将一个数表示python库所能理解的范围内....
- import struct
- >>> p32(0xdeadbeef) == struct.pack('I', 0xdeadbeef)
- True
- >>> leet = unhex('37130000')
- >>> u32(b'abcd') == struct.unpack('I', b'abcd')[0]
- True
- u8(b'A') == 0x41 #unpack后 A的16进制为41H,二者相等
- True
- p32 u32 :打包某个数为32bits,以32bits解包某个数 小端序 4字节
- p8 u8
- p16 u16
- p64 u64 同理,关于bits,以后解释...
设置日志记录详细程度&&汇编与反汇编
- Setting Logging Verbosity
- You can control the verbosity of the standard pwntools logging via context.
通过文本可以设调试的时候日志的详细程度,一般设置为debug等级如
- context.log_level = 'debug'
将会把所有的内容打印在屏幕上
- Assembly and Disassembly
- Never again will you need to run some already-assembled pile of shellcode from the internet! The pwnlib.asm module is full of awesome.
pwnlib提供了汇编与反汇编的工具
- print(disasm(unhex('6a0258cd80ebf9')))
- 0: 6a 02 push 0x2
- 2: 58 pop eax
- 3: cd 80 int 0x80
- 5: eb f9 jmp 0x0
0x39 shellcraft
群里师傅们有说这个shellcraft 的,然而我的段位还达不到,就暂留,待以后再更新这部分
- pwn shellcraft
- usage: pwn shellcraft [-h] [-?] [-o file] [-f format] [-d] [-b] [-a] [-v AVOID] [-n] [-z] [-r] [--color] [--no-color] [--syscalls] [--address ADDRESS] [-l] [-s] [shellcode] [arg [arg …]]
- shellcode
- The shellcode you want
- arg
- Argument to the chosen shellcode
- -h, --help
- show this help message and exit
- -?, --show
- Show shellcode documentation
- -o , --out
- Output file (default: stdout)
- -f {r,raw,s,str,string,c,h,hex,a,asm,assembly,p,i,hexii,e,elf,d,escaped,default}, --format Output format (default: hex), choose from {e}lf, {r}aw, {s}tring, {c}-style array, {h}ex string, hex{i}i, {a}ssembly code, {p}reprocssed code, escape{d} hex string
- -d, --debug
- Debug the shellcode with GDB
- -b, --before
- Insert a debug trap before the code
- -a, --after
- Insert a debug trap after the code
- -v , --avoid
- Encode the shellcode to avoid the listed bytes
- -n, --newline
- Encode the shellcode to avoid newlines
- -z, --zero
- Encode the shellcode to avoid NULL bytes
- -r, --run
- Run output
- --color
- Color output
- --no-color
- Disable color output
- --syscalls
- List syscalls
- --address
- Load address
- -l, --list
- List available shellcodes, optionally provide a filter
- -s, --shared
- Generated ELF is a shared library
0x40 一些小知识点
- cyclic and cyclic_func
生成一堆字符,好像每次都是那一堆,反正我用peda 的 pattern create 测试的是
- checksec file
检查文件所开启的保护
- 【1】RELRO:RELRO会有Partial RELRO和FULL RELRO,如果开启FULL RELRO,意味着我们无法修改got表
- 【2】Stack:如果栈中开启Canary found,那么就不能用直接用溢出的方法覆盖栈中返回地址,而且要通过改写指针与局部变量、leak canary、overwrite canary的方法来绕过
- 【3】NX:NX enabled如果这个保护开启就是意味着栈中数据没有执行权限,以前的经常用的call esp或者jmp esp的方法就不能使用,但是可以利用rop这种方法绕过
- 【4】PIE:PIE enabled如果程序开启这个地址随机化选项就意味着程序每次运行的时候地址都会变化,而如果没有开PIE的话那么No PIE (0x400000),括号内的数据就是程序的基地址
- 【5】FORTIFY:FORTIFY_SOURCE机制对格式化字符串有两个限制(1)包含%n的格式化字符串不能位于程序内存中的可写地址。(2)当使用位置参数时,必须使用范围内的所有参数。所以如果要使用%7$x,你必须同时使用1,2,3,4,5和6。
pwnlib 的一些子模块(常用模块和函数加粗显示):
adb
:安卓调试桥args
:命令行魔法参数asm
:汇编和反汇编,支持 i386/i686/amd64/thumb 等constants
:对不同架构和操作系统的常量的快速访问config
:配置文件context
:设置运行时变量dynelf
:用于远程函数泄露encoders
:对 shellcode 进行编码elf
:用于操作 ELF 可执行文件和库flag
:提交 flag 到服务器fmtstr
:格式化字符串利用工具gdb
:与 gdb 配合使用libcdb
:libc 数据库log
:日志记录memleak
:用于内存泄露rop
:ROP 利用模块,包括 rop 和 sroprunner
:运行 shellcodeshellcraft
:shellcode 生成器term
:终端处理timeout
:超时处理tubes
:能与 sockets, processes, ssh 等进行连接ui
:与用户交互useragents
:useragent 字符串数据库util
:一些实用小工具
函数调用方式的基本介绍
32位程序默认调用函数的方式为先将参数压入栈中,靠近call指令的是第一个参数而64位程序默认调用函数的方式则不同
- RDI 中存放第1个参数
- RSI 中存放第2个参数
- RDX 中存放第3个参数
- RCX 中存放第4个参数
- R8 中存放第5个参数
- R9 中存放第6个参数
- 如果还有更多的参数,再把过多那几个的参数像32位程序一样压入栈中
- 然后 call
payload-related
payload : padding1 + address of shellcode + padding2 + shellcode
解释:
padding1 处的数据可以随意填充(注意如果利用字符串程序输入溢出数据不要包含 “\x00” ,否则向程序传入溢出数据时会造成截断),长度应该刚好覆盖函数的基地址。address of shellcode 是后面 shellcode 起始处的地址,用来覆盖返回地址。padding2 处的数据也可以随意填充,长度可以任意。shellcode 应该为十六进制的机器码格式。
根据上面的构造,我们要解决两个问题。
1. 返回地址之前的填充数据(padding1)应该多长?
我们可以用调试工具(例如 gdb)查看汇编代码来确定这个距离,也可以在运行程序时用不断增加输入长度的方法来试探(如果返回地址被无效地址例如“AAAA”覆盖,程序会终止并报错)。
2. shellcode起始地址应该是多少?
我们可以在调试工具里查看返回地址的位置(可以查看 ebp 的内容然后再加4(32位机),参见前面关于函数状态的解释),可是在调试工具里的这个地址和正常运行时并不一致,这是运行时环境变量等因素有所不同造成的。所以这种情况下我们只能得到大致但不确切的 shellcode 起始地址,解决办法是在 padding2 里填充若干长度的 “\x90”。这个机器码对应的指令是 NOP (No Operation),也就是告诉 CPU 什么也不做,然后跳到下一条指令。有了这一段 NOP 的填充,只要返回地址能够命中这一段中的任意位置,都可以无副作用地跳转到 shellcode 的起始处,所以这种方法被称为 NOP Sled(中文含义是“滑雪橇”)。这样我们就可以通过增加 NOP 填充来配合试验 shellcode 起始地址。
操作系统可以将函数调用栈的起始地址设为随机化(这种技术被称为内存布局随机化,即Address Space Layout Randomization (ASLR) ),这样程序每次运行时函数返回地址会随机变化。反之如果操作系统关闭了上述的随机化(这是技术可以生效的前提),那么程序每次运行时函数返回地址会是相同的,这样我们可以通过输入无效的溢出数据来生成core文件,再通过调试工具在core文件中找到返回地址的位置,从而确定 shellcode 的起始地址。
解决完上述问题,我们就可以拼接出最终的溢出数据,输入至程序来执行 shellcode 了。
=========分割线========
以上内容参考:
- docs-pwntools-com-en-stable.pdf
- ctf-all-in-one
- Jz晓黑博客
- 长亭技术专栏
如有错误,还请各位师傅不吝赐教