二进制漏洞挖掘之栈溢出-开启PIE

二进制漏洞-栈溢出

github地址:https://github.com/ylcangel/exploits/tree/master/stack_overflow

测试平台

系统:CentOS release 6.10 (Final)、32位

内核版本:Linux 2.6.32-754.10.1.el6.i686  i686 i386 GNU/Linux

gcc 版本: 4.4.7 20120313 (Red Hat 4.4.7-23) (GCC)

gdb版本:GNU gdb (GDB) Red Hat Enterprise Linux (7.2-92.el6)

libc版本:libc-2.12.so

漏洞原理

       在对栈缓冲区进行写操作时(如memcpy),未对缓冲区大小进行判断,导致写入数据长度可能大于缓冲区长度。

通用利用方式

       写入数据覆盖返回地址,使返回地址指向恶意代码起始地址。由于我是基于本地测试,也就是libc库的版本已知,而基于远程攻击或不同版本的libc库可能会存在差异。

漏洞测试程序

二进制漏洞挖掘之栈溢出-开启PIE

 

很明显代码在执行scanf时未对缓冲区大小进行判断,存在栈溢出漏洞。

注意如无特殊说明,本文的exp都是基于该源码编译的二进制实现的。

所有测试均在linux环境下进行

开启PIE

开启PIE二进制程序加载的基址也将被随机化。在不开启PIE的情况下,可以通过前面描述的方式来绕过NX+ASLR保护。 想让应用程序具备PIE功能需要添加编译选项,并需同时开启系统ASLR选项。 编译时通过添加以下选项开启应用程序PIE功能:

-fpie[PIE](-fpie强度为1,-fPIE强度为2最强) -pie

开启PIE后的程序,用checksec监测如下:

二进制漏洞挖掘之栈溢出-开启PIE

 

漏洞分析

先运行几次看看加了PIE选项后程序运行变化,第一次运行:

二进制漏洞挖掘之栈溢出-开启PIE

 

第二次运行:

二进制漏洞挖掘之栈溢出-开启PIE

 

第三次运行:

二进制漏洞挖掘之栈溢出-开启PIE

 

从上面图中可以看到,无论是动态库libc的基址还是程序本身基址、栈、vdso都发生了变化。开启ASLR+PIE后你发现二进制程序基址、got表、plt表、bss段地址都是不确定的都变成了偏移,之前的攻击方法大部分都失效了。

二进制漏洞挖掘之栈溢出-开启PIE

 

从图上可以看到所有引用的地址都变成了偏移(蓝色框起来的地方)。

实现exp

**

**原理和方法同绕过ASLR方式(略)。

局部覆盖+**

应用程序是按页加载到内存中的,一个内存页大小为0x1000,开启PIE后单个内存页并不会受到影响,这就意味着不管基址怎么变,某一个内存页中的某一条指令的后三位十六进制数(低12位)的地址是始终不变的。因此我们可以通过覆盖地址的后几位来实现控制程序的流程。局部覆盖的局限是它仅仅影响一个内存页,覆盖地址范围仅从0x0 – 0xfff。

对于开篇的测试程序显然不适合这里的讲解,需稍作修改用于演示借助局部覆盖来绕过PIE。

测试程序修改如下:

二进制漏洞挖掘之栈溢出-开启PIE

 

这个程序特别简单非常适合于讲解局部覆盖方式绕过PIE,向这样的程序大多用于CTF中,现实中几乎不会存在这样无聊和无用的程序。

让我们反编译看一下该二进制程序,从下图可以看到evil和wh函数的后三位的偏移是固定的。

二进制漏洞挖掘之栈溢出-开启PIE

 

我们在用gdb调试看一下这两个函数地址的区别:

二进制漏洞挖掘之栈溢出-开启PIE

 

通过上图我们可以看到evil和wh函数地址仅仅后三位不同(这个测试程序足够小,一个页就足够容纳代码段,它满足局部覆盖的条件)。但这里存在一个问题,三位16进制数是3*4=12bit,而12bit是1个半字节,在执行覆盖时无法操作1个半字节,所以选择写入两个字节,最后半个字节进行**,范围是0x0到0xf,即[0x0-0xf]610,每轮**16次,大约循环**几轮就能命中,比前面提到的暴力**成功概率增大很多。

Exp代码:

二进制漏洞挖掘之栈溢出-开启PIE

 

运行结果:实际上第一次大约需要运行5轮(80次)就能命中,后面命中更快,可能都不需要一轮,总体命中概率还是比较大的。不过在运用局部覆盖绕过PIE时需要注意它的条件,不是所有情况都适合用局部覆盖,特别是漏洞程序的代码段特别大占据多个页时,就不适合用局部覆盖,局部覆盖的极端就是全部覆盖也就是暴力**(尝试所有情况)。

二进制漏洞挖掘之栈溢出-开启PIE

 

信息泄露

信息泄露的条件是有可重复利用的泄露信息的功能点或者漏洞,之所以我们在绕过ASLR时并没有硬性要求该条件的原因是我们知道该漏洞程序加载的基址、plt地址,这样我们仅需泄露信息一次就可以完成exp的编写,可是开启PIE后程序加载基址、plt、got地址都随机了,我们已经无法利用类似ret2plt的方式了。然而信息泄露仍是绕过的PIE 的最好办法,如果程序提供持续输出内存信息的能力或含有格式化字符串漏洞都可以达到信息泄露的目的。对于开篇的测试程序显然不合适这里的讲解,需稍作修改用于演示借助信息泄露来绕过PIE。

最开始我的程序是按照下图那样设计的,之所以那样设计是因为我想借助pwn的DynELF模块来实现泄露libc完成payload,可是在开启PIE下它查找的速度很慢,最后只好再次设计程序。

二进制漏洞挖掘之栈溢出-开启PIE

 

重新设计的程序如下:

二进制漏洞挖掘之栈溢出-开启PIE

 

图上是利用了格式化字符串输出功能(这个格式化字符串已经被我定制为%11$.8x,至于原因,等后面文档讲完格式化字符串漏洞时你自然会明白)泄露了返回地址,该泄露功能仅用一次即可。我们运用load base = leak addr – file off公式就可以轻易算出二级制程序加载的基址,有了基址我们就可以轻易算出各函数plt的地址,有了plt地址就可以按照绕过ASLR的信息泄露步骤完成exp。我们看一下返回地址指令在文件中的偏移:

调试器中对应返回地址处的指令:

 

二进制漏洞挖掘之栈溢出-开启PIE

二进制漏洞挖掘之栈溢出-开启PIE

 

指令在文件中偏移为0x6ec,这样通过泄露的地址-0x6ec就是该程序在内存中加载的基址。

二进制漏洞挖掘之栈溢出-开启PIE

 

得到程序基址后+ [email protected]偏移就相当于间接得到了puts的函数地址,按照之前绕过ASLR方式构造playload,遗憾的是直接报了段错误,仔细查看开启PIE和没有开启PIE

二进制漏洞挖掘之栈溢出-开启PIE

 

的程序区别发现没有开启PIE的二级制[email protected]第一条指令jmp后面直接是got表项的地址):

二进制漏洞挖掘之栈溢出-开启PIE

 

开启PIE的二进制程序[email protected]后面不在是地址,puts对应的got表项地址是随机的(Got表起始地址随机了),是动态算出来的,不能再向前面那样直接获得,该地址依赖ebx寄存器的值,而我们构造的payload可能会破坏保存的ebx的值导致加载puts函数的got地址错误而执行失败。

二进制漏洞挖掘之栈溢出-开启PIE

 

接下来怎么办?这里我们需要深挖一下了,到底开启PIE后程序是怎么找到对应GOT条目的?和不开启PIE查找GOT条目原理是否一致?先让我们看看其他plt表项,从下图不难看出各函数plt表项对应got表项是以ebx为基准+4字节倍数的偏移而得到的值。那ebx是一个固定的值嘛?其实懂延迟加载原理的人都知道它确实是一个固定的值,编译完成它的值就确定了。PIE在没有开启的时候程序基址是固定的,got表起始地址也就固定,各个got表项的地址就是确定的值。

二进制漏洞挖掘之栈溢出-开启PIE

 

那这个ebx到底装的什么值呢?我这里卖个关子,先不直接告诉大家它装载的到底是什么值,为了增加大家对其的理解,我在这里一步步引导大家见证一下ebx到底装载了什么值。

从调试开始看看:

二进制漏洞挖掘之栈溢出-开启PIE

 

从图上可以看到ebx指向的值是0x800019b0,该程序加载的基址是0x80000000,用0x800019b0 - 0x80000000 = 0x19b0。让我们去文件中看看偏移0x19b0处到底是什么。下图是该二进制文件的截图,从图中我们可以清楚的看到偏移0x19b0处指向的是该文件的got.plt的起始地址(got.plt就是GOT表,它正是got表的起始地址,和不开启PIE定位函数原理一致,PIE让基址随机了,相应的got表起始地址就随机了),而0x19c0 + 0x20 = 0x19d0正是puts的got表项对应的地址。因此ebx装载的值是got.plt也就是got表起始地址。不开启PIE时,GOT表起始地址和各got表项值都是固定的。

二进制漏洞挖掘之栈溢出-开启PIE

 

而最终got表项就在这里被填充为实际的函数地址,上面调试的截图已经证明。好,明白了这一点我们就应该知道,只要能够控制ebx,让ebx装载GOT表起始地址,就能完成payload。想控制ebx需要借助rop技术。现在让我们来试试,让我们用ROPgadget工具搜索看看,下图圈起来的三条指令片段都符和我们的要求,这里就用第一条片段吧,这条片段最简单。

二进制漏洞挖掘之栈溢出-开启PIE

 

Exp代码:

这个exp是比较复杂的,主要分三大步,第一步是运用程序泄露的地址计算出二进制程序加载的基址。

二进制漏洞挖掘之栈溢出-开启PIE

 

第二步是通过第一步计算得到的程序加载基址计算出puts函数对应[email protected]、got.plt的起始地址、puts函数got表项地址、、main函数地址,同时利用rop技术,通过上面搜寻到的rop链把got.plt的起始地址装载到ebx寄存器中,最后完成对puts函数的调用并继续泄露出puts函数在内存中的真正加载地址。

二进制漏洞挖掘之栈溢出-开启PIE

 

第三步是通过第二步泄露出的puts地址计算出libc的基址,之后步骤同ASLR信息泄露。

二进制漏洞挖掘之栈溢出-开启PIE

 

至此exp已经编写完毕,让我们看看运行效果:

二进制漏洞挖掘之栈溢出-开启PIE

 

第二步运行结果:

二进制漏洞挖掘之栈溢出-开启PIE

 

最终结果:

二进制漏洞挖掘之栈溢出-开启PIE

 

exp存在一点小问题,它概率性的执行到第二步时崩溃,原因不明,我也没继续跟踪,因为我觉得我已经讲的很明白了。应用程序中的信息泄露是我故意设计的,现实中你需要寻找程序中包含泄露内存信息的功能片段,如果能直接泄露出libc的内存最好,这样你就能直接编写payload了,否则你需要向我一样先依据泄露信息计算出程序加载的基址,在再次设计payload泄露出libc的基址,前面条件都具备后才能最终完成payload的编写,总体来说开启PIE会大大增加我们攻击的难度。

注意:exp有些地方可以通过pwn提供的api直接获得,如下图:

二进制漏洞挖掘之栈溢出-开启PIE

 

同理以下也是等价的:

 

二进制漏洞挖掘之栈溢出-开启PIE