栈溢出——一种经典的got表泄露解法

背景

got表泄露作为栈溢出的一种经典题型,其本质就是利用了程序动态加载过程中,在调用时才填写调用地址的特性通过一些位置的溢出将got表地址劫持到我们希望执行的程序入口从而达成目标。该类题型通常具有如下三条特征:

1、RELRO通常为Partial或者No状态

2、载入地址未开启随机化

3、程序中存在栈溢出漏洞(溢出范围不一定很大)

RELRO

RELRO是一种程序保护手段,分为三种模式,分别为No,Partial,Full,分别对程序节添加了不同程度的只读限制,其中和我们攻击关系密切的三个节分别为.plt,.got,.got.plt,即程序链接表,全局变量引用位置保存表和函数引用位置保存表。其中,.plt在三种模式下均为只读状态,.got在Partial和Full状态下为只读状态,.got.plt在Full状态下为只读状态,我们常说的got表泄露指的是.got.plt节的内容泄露

攻击原理

动态链接前对于外部的调用函数(例如put,printf之类)程序并不知道应该跳转到哪里,因此就有了上述所说的程序节。其中.plt节所在的位置是在编译阶段就可以确定的,因此整个主程序代码段(main函数)的内容实际上在编译阶段就已经可以确定了。在执行过程中,需要进行调用时eip首先跳转到该函数在.plt节中的位置,.plt节则指向了.got.plt节中的对应位置,这个地址也是在编译阶段就确定下来的。如果是第一次调用,.got.plt中存储的是.plt中下一条指令的地址,此时,程序会通过一系列执行将真正的函数入口地址填充入.got.plt中,这时候如果有办法拿到这个地址我们就可以通过该地址爆破出对应的依赖并获得重要的system函数地址和/bin/sh字符串地址。

上图阐述了函数调用的过程,其中虚线只在第一次调用时生效。

选择例题


分析信息

使用checksec查看程序的基本信息


如上图所示可以看到该题是一个64位程序,没有RELRO,cannary和PIE防护,栈不可执行,这意味着.got.plt表可以修改,溢出时不需要考虑获取特殊的标志,.plt和.got.plt表在内存中的地址已知。下述攻击方法在RELRO为Partial时也同样适用。
 

使用ida查看程序


非常明显的栈溢出,而且给了非常充足的溢出空间(允许写入0x200个字节,栈本身大小只有0x80字节)


函数不多,write和read可以二选一进行泄露,这次我们选择write


如上图所示,在main函数中正常情况下该漏洞只会执行一次,而我们需要攻击两次,第一次用于泄露got表中的地址,第二次用于攻击,因此我们可以在第一次攻击的最后加上主函数的开始地址作为返回地址以便再执行一次。即下图中的push rbp(地址为0x40061A)


思路就绪,开始pwn(砰!)

编写脚本

引入


如上图所示,该脚本涉及两个依赖,一个是常规的pwn工具包,另一个是用于爆破程序涉及依赖使用的工具包

rop链构造

因为该程序为64位程序,因此函数调用时的前6个参数都存储在寄存器中(rdi,rsi,rdx,rcs,r8,r9),溢出过程中我们需要自己进行函数调用,因此需要在调用前对寄存器中的参数进行布局。

使用ROPgadget --binary jarvisoj_level5 --only "ret|pop"命令获取诸如pop rdi;ret之类的命令。第一条命令会将栈中的数据弹出到寄存器中,第二条ret会将栈顶的下一条数据作为指令地址进行跳转,因此我们可以连续使用多个pop ret命令控制多个寄存器的值。命令执行结果如下图所示。


其中我们可以看到修改rdi和rsi的指令,可惜没有控制rdx的指令,不然程序还能更稳妥一些。一般情况下,我们使用puts函数输出,该函数只需要传递一个参数即可。但是本题并没有puts,因此我们只能使用write函数。write函数的三个参数分别代表标志、输出地址和输出长度,我们这里控制不了输出长度,如果这个数字(rdx的值)小于5那这个办法就失效了。

第一阶段:got表泄露


如上图所示,首先是根据程序设置一些特殊地址,然后构造payload

0x80代表的是栈的长度,在ida中可以看到[rbp-0x80]的字样,这个值从这来。0x8代表的是调用该函数的函数栈底(调用该函数的函数栈的ebp值),因为是64位的程序因此是8字节。将这一段空间用垃圾数据覆盖

随后可以看到我们通过rop链将rdi设置为1,将输出地址改为.got.plt表中write的位置并且填充了r15的值后调用了write函数进行输出,最后返回到特定位置重新执行vulnerable_function。

接下来使用下图中的代码段接收输出,输出即为write在内存中的真实地址

因为64位带来的地址容量是溢出的,实际上用于表示真实地址的16进制数字最高两位是空的,因此我们只需要获取6个字节并在左侧补上\x00即可,最高位字节在64位下也是固定的0x7f。我们可以先执行程序看下效果


 


如上图所示,多次执行程序我们发现都输出了write的地址,由于依赖的加载位置每次必定是随机的,因此我们可以看到每次获取到的地址都不一样。有一个方法可以用于验证自己程序的正确性——无论执行多少次程序,地址的最低三位必定是不变的.这是因为内存将程序载入执行时按页载入,3位16进制对应12位2进制即4KB大小的空间刚好为1页的大小。函数在依赖中的相对位置是不会改变的,因此页内地址必然不变。

第二阶段:爆破system地址和/bin/sh地址


使用上述代码我们就可以根据获取的write地址爆破依赖版本并获取system函数地址和/bin/sh字符串地址(因为这些和write的地址一样在依赖中的相对位置不变)

第三阶段:将程序导向system函数,获取命令执行权力


过程如上述代码所示,同样填充0x88大小的垃圾数据,将rdi修改为/bin/sh的地址后调用system函数,最后进入交互模式


成功获取权限并得到flag

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值