二进制漏洞-栈溢出
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库可能会存在差异。
漏洞测试程序
很明显代码在执行scanf时未对缓冲区大小进行判断,存在栈溢出漏洞。
注意如无特殊说明,本文的exp都是基于该源码编译的二进制实现的。
所有测试均在linux环境下进行
开启NX(DEP),开启ASLR
上面例子采用Ret2libc方式实现poc,为了防止基于该方式的攻击,ASLR应运而生
开启ASLR首先需要打开操作系统相应功能/proc/sys/kernel/randomize_va_space
目前randomize_va_space的值有三种,分别是[0,1,2]
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
开启命令:echo 2 > /proc/sys/kernel/randomize_va_space
ASLR的开启无法通过checksec来检测,他的开启与系统有关
ASLR只针对动态库基址的中间位数进行随机化,后三位并不会变
ASLR不会随机化程序本身的基址
漏洞分析
现在我们来运行几次看看(注意看主程序和libc的基址变化),第一次运行:
第二次运行(这幅图圈错位置了^-^):
第三次运行:
从运行结果可以看出libc的基址、栈、vdso已经随机了,但主程序的基址却保持不变。开启ASLR后主程序以下部分并不会随机化。
通过调试程序,我们得到了buf和返回地址之间距离同样是28(同NX未开启ASLR)
使用AAAA覆盖返回地址(程序返回地址被覆盖为0x41414141)程序返回执行时崩溃,报段错误,再次证明我们得到的距离正确。
在执行之前的exp看一下效果,进程id为2985
在看一下它的core dump,这里报了段错误,执行0x62bf00处代码错误。
实现exp
**
开启ASLR后libc的基址随机化了,固定system地址程序肯定会出错(就如用之前的exp运行,除非它随机化的基址恰好是0x5f1000),不过libc基址只有中间三位随机化,这样我们就可以采用**方式遍历它任何一种可能(最大**次数4096)
Libc基址范围0x1000-0xfff000
libc function address = libc base address + function offset
system function offset = 0x62bf00 – 0x5f1000 = 0x3af00
bin_sh offset = 0x747b65 – 0x5f1000 = 0x156b65
理论上应该保证程序不崩溃来遍历libc基址(肯定会在4096次中的一次**成功),不过让程序不崩溃或者崩溃恢复到某个点(例如main函数)重新执行有点麻烦,我这里偷懒一下,程序崩溃继续下一个基址继续**,多跑几次总会命中。
执行结果
查看输出的详细日志
Ret2plt
开启ASLR后主程序的基址、plt、got、got.plt等地址并不会发生变化,如果主程序有主动调用了libc的system函数,那么它会生成一个plt和got表项(间接指向函数地址),这样我们就可以通过ret2plt来实现漏洞利用。
上面的c程序没有主动调用system函数,我们设计一个程序,在源程序基础上添加了一个函数shell0,该函数调用libc的system函数,注意我们这里不会构造调用shell0的exp例子,添加shell0函数调用system仅仅是为了在主程序中生成一个对应的plt表项,通过调用plt我们就能实现漏洞利用,而不必知道真正的libc的system函数的地址。
编译程序后,程序会生成对应的重定位表项:
启动调试器不运行程序(我们以printf为例),查看printf(编译时优化成了puts)plt表项,此时plt表项对应的got表项(0x8049810)指向plt表项的下一条指令(0x80483ce),当第一次调用printf时,其对应的函数地址将在动态链接器的帮助下得到解决。
接着我们运行程序到scanf处,printf函数已经被调用了,此时动态链接器也完成got表项的填充工作,通过调试我们可以清楚的看到got表项(0x8049810)对应的值已经由0x80483ce该变为正确的puts函数地址(0x653aa0),后面再调用puts函数时就会直接调用对应函数,而不用在通过动态连接器,这也就是所谓的延迟加载或者懒加载。
当然我们并不关心got表项对应地址是否被填充为正确的地址,我们只要找到plt表项的地址,然后调用该plt即可,动态链接器会帮我们填充并调用到正确的地址。
如下图:
开启ASLR后主程序的基址、plt、rodata是不会随机化的,因此我们就可以构造让plt覆盖返回地址来达到利用的目的。
从上图可以得知system的plt地址为0x8048388,同时主程序存在command(ls -al)字符串,我们直接利用即可。
Exp如下:
运行结果如下(可以看出我们已经成功利用ret2plt技术完成漏洞攻击了):
既然存在ret2plt,那有没有ret2got呢?这个我不确定,我google也没搜到相关信息,plt和got在内存中表现形式是不一样的,plt对应的就是指令,可以直接调用执行,而got对应的是数据(指令地址),直接覆盖返回地址肯定是不行的,需要进行转换。
信息泄露
Libc各库函数相对libc加载基址的偏移是固定的,是否开启ASLR并不会影响它。如果能获得libc中某个库函数的地址,基于上述原理我们就能轻易算出libc加载的基址,这样就可以轻易获取任意函数地址甚至数据段特定数据的地址(代码段紧挨着数据段,编译时编译器已经按4k页对齐计算出了各个段数据相对基址的偏移,这使得你用基址+数据偏移就能得到想要的数据),因此我们需要泄露出某个库函数在内存中的地址。可用于泄露内存信息的函数包括write、puts、printf,首选write函数,这个函数输出长度是可控的,不会遇到\x00就截断字符或末尾补充换行符\n(puts),printf会受到\x00影响。
同样利用**时用过的公式:
libc function address = libc base address + function offset
查看漏洞程序用了什么库函数,可以看到程序虽然调用的是printf,但编译器却把它替换成了puts,那我们exp就只能用puts来完成信息泄露了。
前面的exp都是构造一次payload来完成漏洞利用,这里需要构造两次(很荣幸我当初设计的程序比较简单,继续调用main函数不会崩溃),第一次是通过调用puts泄露puts在内存中的实际地址,第二次是调用通过计算得出的system函数来完成最终的漏洞利用。
第一次payload利用同上面,buf距离保留eip的距离是28字节,第二次距离不是28字节,这里我们主要用gdb看一下第二次的情况:
从上图我们可以看到并计算出buf距离保留eip的距离是20字节,我们需要验证继续覆盖eip是否可以完成漏洞利用或者引起程序会崩溃。
很荣幸从下图可以看到,即使是第二次覆盖eip程序还是正常执行了,并且正确的执行到了system函数中(如果main函数带有参数我想就不会那么幸运了,可能会引起程序崩溃),
既然得到验证,那就可以按照这个思路来实现exp了,它的核心思想是信息泄露或者说泄露内存信息(info leaks 、 memory leaks)。
如果程序中存在可以重复利用的信息泄露漏洞,那你还可以借助pwntools的 DynELF工具来自动化的完成库函数和库函数地址的查找,当然你需要先实现一个leak函数,具体使用方法还请查看相应官方文档。
我设计的程序中不存在可以重复利用的泄露信息的漏洞,于是需要自己特别构造。
Exp如下:
运行结果如下:
GOT覆盖和解引用
Ret2plt成立的前提是主程序有调用对应的libc函数,这样才会在主程序中生成对应的plt桩,如果没有这样的plt桩怎么办,就如本文最开始的c程序,它并没有显示的调用system函数。如图:
那我们还可以借助got覆盖或者解引用方式实现漏洞利用。
Got覆盖(got hijack)
这个技巧帮助攻击者,将特定Libc函数的GOT条目覆盖为另一个Libc函数的地址(在第一次调用之后)。在共享库中,函数距离其基址的偏移永远是固定的。所以,如果我们将两个Libc函数的差值(puts和system)加到puts的GOT条目,我们就得到了system函数的地址。之后,调用puts就会调用system。
offset_diff = system_addr - puts_addr = 0x27ba0
GOT[puts] = GOT[puts] + offset_diff
利用ROP
测试函数中并没有实现此功能的代码,我们可以借助ROP技术构造类似的功能,构造的ROP链大致和下面的样子相似:
pop reg; ret;
add reg, 偏移;ret;(这里目标操作数需要是存储器地址,只有类似指令才能完成GOT覆盖,GOT条目在内存中),当调用puts函数时就调用了system。
现在我们用工具搜索一下测试程序是否包含这些gadgets或者找到类似的gadgets构造出ROP链:
- 我们先看看是否存在满足条件的pop指令的rop链,第一条链
第二条链,我们使ecx含有puts-system的偏移0x27ba0
第三条链,我们使ebp包含GOT[puts] – 0x5b042464 = 0x80497b8 - 0x5b042464 = -0x52FF8CAC(0xAD007354),然后执行add %ecx, got[puts]
构造的rop链对应的栈如下图:
乍一看似乎很满足条件,然而第二条链包含leave指令它会重新设置esp(等价于mov %ebp, %esp;),这会导致我们填充的栈不在我们控制之内,除非我们能让leave之后的esp和之前的esp一致或在我们可控范围内(再次搜索并没有找到控制esp需要的片段,而对于栈劫持,我们没有额外的可控区域来作为新栈)。
我经过了大量的搜寻包括手动搜寻都没有搜寻到比上面这三条链更具有说服意义的指令片段了,然而它的第二条链存在一点点缺陷导致构造ROP链失败。
ROP+Info leak
看来直接构造ROP链来实现GOT覆盖功能有困难,我们需要借助别的技术来共同完成此功能并验证这种方法的可行性。这里我选择了ROP+Info leak来完成最终exp。本质上我们只要覆盖任一GOT表项为system、execve这类的函数地址即可(这里我们借助read实现地址覆盖)。具体过程这里就不在介绍了,这里使用rop仅仅是用来控制esp和主动调用puts函数,直接上exp代码:
本exp第二次payload栈布局:
通过gdb我们看一下是否实现了GOT覆盖,从下图我们不难看出已经将GOT[puts]成功覆盖成了system函数地址。
运行结果:
从下图可以看出GOT覆盖是可行的,只是实现起来比较繁琐和复杂,我仅仅用我的测试程序证明其可行性(我的测试程序不是特殊设计的,导致构造GOT覆盖的exp显得复杂),在某些场景下GOT覆盖有其独特的优越性,并且实现起来也简单明了。
Got解引用
这个技巧类似于GOT覆盖,但是这里不会覆盖特定 Libc函数的GOT条目,而是将它的值复制到寄存器中,并将偏移差加到寄存器的内容。因此,寄存器就含有所需的Libc函数地址。例如,GOT[puts] 包含puts的函数地址,将其复制到寄存器。两个Libc函数(puts和system)的偏移差加到寄存器的内容。
现在跳到寄存器的值就调用了system。
offset_diff = system_addr - puts_addr
eax = GOT[puts]
eax = eax + offset_diff
同样这里也使用ROP技术构造该功能,构造的ROP链大致如下:
pop reg1; ret;#偏移
pop reg2; ret; #GOT[puts]
add reg2, reg1;call reg1;(直接调用system,或者push reg1;ret;)
让我们逆着从add指令处搜寻一下:
从下图可以看到包含add指令并直接调用call指令的指令片段存在两处,分别用到寄存器edx和esi,我们再次搜寻一下测试程序看是否存在pop edx或pop esi的片段
搜寻如下:从图中可以看到有搜寻到包含pop esi的指令片段,那么我们只需要在找到pop eax指令片段就可以了。
搜寻pop eax,从下图可以看到虽然搜寻到了包含pop eax的指令片段,但是该片段包含leave指令,上面已经提到该指令会重新设置esp,这样会让我们填充的栈不在控制范围之内。
我经过了大量的搜寻没有在测试程序中找到可以组合的rop链,但原理我已经讲明白了,就不在特意去构造exp了。不过有个地方需要引起你的注意,在通过call指令调用函数时,你需要在栈上填充被调用函数的参数。