pwn做题记录
因为研究生做的方向和内核相关,感觉pwn里面很多技巧都是运用内核的漏洞的,所以学习一下pwn并做一下记录。
ciscn_2019_n_1
这道题目是一道简单的栈溢出题目,先查看一下保护措施,发现开启栈不可执行保护。
接下来到ida查看一下函数主体部分,发现函数逻辑很简单,并且存在gets函数,是个明显的栈溢出漏洞。其中v1
是 char[44]
的缓冲区,但 gets()
可输入无限长度数据,导致栈溢出。
虽然开启了栈不可执行保护,但是函数中本身存在cat /flag
逻辑,无需在栈中执行程序,因此这里可以选择覆盖v2的值,也可以选择直接跳转到system函数执行。
这里笔者选择直接跳转执行system函数查看代码堆栈内容,可以看到堆栈中ret距离v1有0x30+8个字节,因此直接填充0x38个字符即可。
接下来构造POC,其中system的地址查看后为0x4006BE。POC如下:
1 | from pwn import * |
ret2shellcode
先看一下程序基本信息,发现基本上没有开启什么保护措施,并且RWX段还表示程序中存在可读、可写、可执行的代码段。
接下来看一下函数主体,gets危险函数,存在栈溢出。但是发现不存在system函数,那就不能直接使用现有函数,考虑我们自己写入shellcode或者是通过ROP来执行shellcode。
这道题已经提示的很明显了,并且RWX还含有可读写执行的段,那大概率是我们自己写入shellcode。不过这里还是查看一下。在gdb中使用vmmap查看段的权限。
此处vmmap只给我们提供了3类信息(可以根据颜色判断):CODE、RODATA以及STACK。乍一看这题好像可以将shellcode注入至stack中,但绝大多数的操作系统都默认开启ASLR保护,这大大提高了攻击栈的难度。因此,栈应当是我们最后考虑的对象。
随后我们看到有个buf2的变量没有被定义,查看一下发现是个全局变量在bss段,地址为0804A080
,再看一下vmmap的结果,0x804a000-0x804b000是一个可执行的段。
那我们的思路就很清晰了,就是将shellcode写入到buf2之后再填充垃圾数据覆盖栈,最后将栈返回地址指向buf2的起始地址即可。
下面就是我们如何构造 payload 了,首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数。
可以看到该字符串是通过相对于esp的索引,所以我们需要进行调试(至于为什么之前有些题目也是esp却可以直接查看Stack of main,我也不知道,只能之后做题的时候都尽量调试一下),将断点下在 call 处,查看 esp,ebp。
可以看到这时候的s的地址为0xffffd53c,而ebp为0xffffd5a8,计算得到s相对于ebp的偏移为6c。构造payload如下:
1 | from pwn import * |
成功利用漏洞:
ret2syscall
补充系统调用知识
首先要知道一些系统调用的基础知识,不知道的建议先学学内核扩充一下知识库。随后需要知道执行命令的系统调用为execve()
。32位系统中该系统调用的编号为0xb,调用号通过eax传递,其余参数传递为ebx,ecx,edx,esi,edi。
64位系统的系统调用号为0x3b,rax传递,其余参数传递为rdi,rsi,rdx,rcx,r8,r9。
且ret返回的函数名不同
- 32位为int 0x80,64位为syscall ret
题目分析
接下来看看这道题目,发现该程序开启栈不可执行。
进ida的程序主体看一下,发现没有现成的system函数,但是还是存在gets函数,说明存在栈溢出漏洞。
随后能注意到左侧一堆函数,感觉是用了静态链接库
用file命令查一下,果然是静态链接的程序,这也就满足了使用系统调用的条件。
那这道题目就很明显了。随后补充一下知识点
ROP是什么
随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。
ret2syscall,即通过ROP控制程序执行系统调用,获取 shell。也就是通过一点一点的执行汇编代码拼接成我们想要执行的程序。所谓 gadgets 就是以 ret 结尾的指令序列。
- 例如:pop eax ; ret
这段代码的作用就是将栈顶的数据弹出给eax,然后再将栈顶的数据作为返回地址返回。
获取gadgets的方式:
ROPgadget --binary 文件名 --only 'pop|ret' |grep '寄存器名'
ROPgadget --binary 文件名 --only 'int'
ROPgadget --binary 文件名 --string '/bin/sh'
要使用系统调用,那就需要把系统调用号传入eax寄存器,然后需要把ecx和edx的寄存器给清空。最后就是需要去把参数/bin/sh的地址存入ebx寄存器。
怎么把参数/bin/sh写入程序中?写到哪?
写到哪
我们要尽可能写到bss段,因为在不开pie的情况下,bss段的地址是不会变的,这意味着,我们可以用IDA看一下bss段的地址然后选定一个我们写入参数的地址,然后我们exp就可以直接写上这个地址了。但是如果我们想写到栈里面,确实用gdb也可以看到写在了哪个内存单元里面,但是这个地址是会变的,把exp上写入我们刚才用gdb看到的地址。解决办法也有,那就是需要泄露程序的一个内存单元地址,然后利用偏移,来计算出我们存放/bin/sh参数的地址。但是这样多少有点麻烦,并且很多时候,我们是无法泄露程序中的地址的,因此我们选择写到bss段。
怎么写?
第一种方法
第一种方法是去搜索gadget,寻找pop [ecx]这类对地址内容操作的指令。然后我们利用如下payload可以达到将参数/bin/sh写入bss段。(并且这部分的payload需要放到返回地址处使用)
1 | payload=p32(pop_ecx_addr)+p32(bss_addr)+p32(pop_[ecx]_addr)+'/bin' |
首先pop_ecx_addr指的是pop ecx;ret这个指令的地址 bss_addr指的将参数写入bss段的具体地址
pop_[ecx]_addr指的是pop dword ptr [ecx];ret这个指令的地址 (这里我写[ecx]是为了方便理解,我记得实际的exp里面,不能使用方括号)(另外这里不一定要是ecx,别的寄存器也可以,这里只是举个例子)然后参数/bin就是我们的参数
至于这个bss_addr+4是上面bss_addr的地址的衔接,因为参数/bin/sh需要两个内存单元存放,因此在这里将上面的地址加4,就存到了下面的内存单元。最后的\x00是用来声明字符串的结束。
但是这样搜寻gadget的手段,是有弊端的,因为有时候程序可能恰好就没有类似于pop [ecx]这样的指令。
第二种方法
因此我们可以用第二种方法,等到可以溢出的时候,用rop,先去把返回地址处放置一个read函数,然后再把/bin/sh写入指定的地址(把该地址放在read函数第二个参数即可)然后再随便找一个连续三次pop的指令(不连续应该也是可以的,反正就要进行三次pop,将read函数的三个参数先从栈顶给弹出来)
接着再进行ret2syscall,参数传完了,剩下的只要找些gadget片段进行ret2syscall即可。如果没有read函数的话,理论来说我们是可以系统调用read函数的,但是我试了一下,当用int 0x80来系统调用read函数之后,int 0x80指令的后面不是ret指令,没有办法再去衔接后面的gadget了。不过目前还没有遇见过系统调用read函数再ret2syscall的题目。
当使用这两种方法其中的一种之后,传参完毕,如此剩下的就是去搜寻我们需要的gadget片段,最后系统调用即可。
思路
因此这道题的思路很明确了,溢出之后填充pop eax所在地址,之后弹出
int 0x80
到eax中,在这之后弹出pop ebx所在地址,将'/bin/sh'
弹出到ebx中。按照这个逻辑将ecx和edx都赋值为0
接下来我们来找一下这些结构的地址,首先是pop eax;ret,这里第一个包含pop ebx,会打乱栈,选择第二个0x080bb196
。
接下来找一下/bin/sh
的地址,没有的话就只能自己构造一下。找到'/bin/sh'
的地址为0x080be408
。
接下来找一下pop ebx;ret,如图所示,意外发现箭头所指的地方正是我们想要赋值的所有剩余寄存器,那我们直接使用该地址0x0806eb90
,将栈结构稍微修改一下即可。
接下来看看int 0x80所在地址,找到为0x08049421
。
最后我们来看看需要填充多少才能溢出:
填充64h+4个字符可以覆盖返回地址。但是这里有坑,注意到栈中没有定义v4的起始位置,也就是说v4前面有多少数据入栈是需要调试的,并没有给出来。
使用pwngdb调试一下,gdb ret2syscall b main(在main函数下断点) r(让程序跑起来) n(单步执行) 一直走到gets函数输入字符串AAAAAAAA,看到栈结构,此时ebp为0xffffd5b8,输入的v4的地址为0xffffd54c,b8-4c=6c。
思路理清楚之后我们来想一下成功利用的栈长什么样:
1 | +---------------------+ |
该有的都有了我们就可以直接来写POC了:
1 | from pwn import * |
之后来试验一下,发现能成功利用漏洞,这个题目就结束啦。
ret2libc
PLT表和GOT表
在进行ret2libc学习之前,我们需要先了解一下PLT表与GOT表的内容。
Procedure linkage table(PLT)过程连接表,位于代码段,是一个每个条目是16字节内容的数组,使得代码能够方便的访问共享的函数或者变量
Globle offset table(GOT)全局偏移量表,位于数据段,是一个每个条目是8字节地址的数组,用来存储外部函数在内存的确切地址。当程序首次调用某个外部函数时,实际上会跳转到PLT中的对应条目,而不是直接调用目标函数。
外部函数首次调用时,控制流会经过 PLT 表,PLT 中的代码片段首先尝试通过 GOT 跳转,但此时 GOT 中尚未存储真实函数地址,因此会回退到 PLT 中的解析逻辑。PLT 会压入函数在重定位表中的索引,并跳转到动态链接器,由动态链接器负责解析该函数的真实地址,将其回填到 GOT 中。如下图所示:
一旦地址被填充,后续所有对该函数的调用都会直接通过 GOT 跳转到目标函数,无需再次解析。如下图所示可以看到此时A的GOT表已经更新,可以直接在GOT表中找到其在内存中的位置并直接调用。
例1 ret2libc1
我们还是首先查看一下一下文件开启的保护。发现开启了栈不可执行保护。
接下来我们用ida反汇编后去main函数查看一下
发现有gets这个危险函数,存在栈溢出,接下来思考如何去利用。查看string字符串后发现"/bin/sh",并得到其地址0x0804A03C,接下来寻找system函数即可。
接下来找到外部函数system的调用,得到调用地址为0x08048460
那就很简单了,直接看一下需要制造多少垃圾数据填充即可。
得出需要填充0xb8-0x4c+4个垃圾数据,需要注意的是构造的payload中system函数需要先压入return地址到栈中,随后才是system函数的参数压入栈。因此构造POC如下:
1 | from pwn import * |
可以看到利用成功:
这道题相对简单,同时提供了"/bin/sh"和system函数调用,但是大多数程序并不会有这么好的情况。
例2 ret2libc2
照例查看文件信息:
有的人看到这部分跟ret2syscall那个很像,但是其实ret2syscall需要使用ROP构造系统调用,但是这道题中并没有相应的ROP,比如我们找一下pop eax;ret:
发现找不到,其实这也就意味着ret2syscall所需要的条件更苛刻,也就是题目更简单。我们来查看一下main函数:
同样的存在gets危险函数,存在栈溢出,但是这道题并没有提供"/bin/sh":
先看看查找一下system的地址,为0x08048490
接下来就是解决"/bin/sh"字符串的问题了。这里由于"/bin/sh"需要作为一个参数传入system中,因此我们需要将"/bin/sh"注入到bss段中。我们这里先尝试这样解决,如果bss段没有全局变量再想其他办法,不过好在这题并不复杂,题目给出了buf2全局变量,地址为0x0804A080。
我们需要调用gets函数,读取一个/bin/sh放在buf2,随后将buf2当作system的参数传入。先找到gets函数,我们去plt表看,gets地址为0x08048460。
调用gets函数时,会将gets函数的返回地址也压入栈中,随后被压入栈的是gets函数的参数。当gets函数执行的时候,会找到esp+8的位置也就是参数的位置,当我们想要弹出参数后面的system地址的时候,就需要将参数pop出来,然后ret就会弹出下一个地址并跳转执行。这时候我们就需要找到一个pop 寄存器;ret指令,将gets函数参数pop出来。
这里我们不是构造系统调用,所以随便一个pop ret即可:
这里因为ebp最好不要动,我们选第三个,地址为0x0804843d,这时候我们想想栈结构是怎么样的
1 | |-------------------------------| |
思路确定好后就是查看填充多少字符了
填充量和上一题是一样的,写POC:
1 | from pwn import * |
利用成功: