二进制漏洞-栈溢出
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环境下进行
开启Canary
Canary主要用于防护栈溢出攻击。对于栈溢出漏洞,攻击者通常是通过溢出栈缓冲区,覆盖栈上保存的函数返回地址来达到劫持程序执行流的目的。
Stack canary保护机制在刚进入函数时,在栈上放置一个标志canary,然后在函数结束时,判断该标志是否被改变,如果被改变,则表示有攻击行为发生,于是停止程序运行。
在Linux中我们将cookie信息称为canary。
-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all 启用保护,为所有函数插入保护
-fstack-protector-explicit 只对有明确stack_protect attribute的函数开启保护
-fno-stack-protector 禁用保护
漏洞分析
现在我们对开篇提到的测试程序开启canary功能
并测试看看哪个是canary值,覆盖该值后会发生什么结果。首先我们反汇编看一下main函数:
从上图可以看到进入函数后cookie值会被存入到栈中,离开函数时会从栈中取出该值和原值做比较,如果不相同执行__stack_chk_fail终止程序执行。
从图上可以看到紧挨着buf后的四个字节既是canary的cookie值,我们覆盖并绕过cookie再验证一下我们的判定是否正确,另外我们还需要确认在不覆盖cookie值的同时覆盖返回地址程序还能否继续运行:
从下图我们可以看到,只要不覆盖cookie值,程序不会再被终止,并且返回地址被成功覆盖。程序执行控制流被我们劫持。
从图上可以得到buf和返回地址之间是32个字节。
注意,程序每次运行栈上保存的canary的值并不相同,每次该值都会发生变化。经过测试所有函数公用同一个值。
实现exp
**
**原理和方法同绕过ASLR方式,不过这里**的范围比较大,成功的概率低(略)
信息泄露
信息泄露办法同绕过ASLR、PIE,同样需要借助程序自身存在的任意读写漏洞或者格式化字符串漏洞来达到泄露canary值的目的。在执行栈覆盖时,在栈上保存canary值处覆盖为泄露出的canary值,其他攻击步骤不变(略)。
劫持__stack_chk_fail函数
从上面分析我们可看出,一旦canary被覆盖,原canary值发生了变化程序就会执行__stack_chk_fail函数,__stack_chk_fail函数是libc导出函数。
借助前面学过的知识,如果我们覆盖了__stack_chk_fail的GOT表项,就会让函数在调用__stack_chk_fail时跳转到我们覆盖GOT表项所指向的地址指令处。原理和前面的Got覆盖相似。但覆盖__stack_chk_fail的got表项时机必须在执行该函数ret指令之前完成(确切的说是从栈上取canary值和原值进行对比之前)。如果覆盖为其他函数地址,你还需要、设置栈或者构造栈,比较麻烦(从前面分析可以看出,call __stack_chk_fail在当前函数内部,此时因为还没有执行到当前函数的leave;ret;指令处,所以栈还是当前函数的栈,这时esp位置不确定,你构造exp需要分析esp。而前面栈利用的例子都是执行完了当前函数的ret指令,ret后从栈上弹出的eip恰好是我们覆盖后的eip,而esp恰好是当前位置)。最好的方式是直接把__stack_chk_fail的got表项地址覆盖为leave;ret;的rop地址,这样就相当于当前函数正确的退出了,绕过了canary检测,也不用重新设计栈,栈溢出攻击同前面正常利用。
为了方便讲解我们需要重新设计一段代码,代码中包含一个可以任意修改内存的函数(现实中只能靠程序本身存在的如格式化字符串漏洞,或者其他任意内存写漏洞来实现内存覆盖),设计后的代码如下:
搜寻到的leave; ret;rop链:
Exp代码(为方便演示,编译程序关闭了ASLR和PIE,仅开启了NX和canary):
覆盖在执行leave;ret前的执行效果(明显就崩溃了,时机很重要),使用不同程序进行的测试:
正确覆盖__stack_chk_fail的got表项后执行结果:
如果程序仅仅开启canary保护,并不难绕过,如果程序同时开启ASLR、PIE会让漏洞攻击变得困难。当然如果程序存在信息泄露漏洞实现绕过还是相对容易的。