通关栈溢出(四):缓冲区溢出的防御技术及绕过
前面几篇介绍了如何利用栈溢出,本文就介绍一下windows现有的缓冲区溢出缓解技术以及他们的绕过方案。
包括ASLR、SafeSEH、SEHOP、DEP、GS
其实看完本文就能发现,攻与防都是相对的,这上面的5种防御全是基于现有的栈溢出攻击手段来做的,而攻击者又针对这5种防御手段做出新的攻击策略。这个就是我认为安全攻防有意思的点
OS: Windows XP SP2, Window 7,Windows 2000
Tools:Ollydbg、IDA pro、AsmToE(汇编转机器码)、010Editor、VC6.0、Xcode、Windbg、VS7.0
一、GS
栈布局 |
---|
局部变量 |
security_cookie |
入栈寄存器 |
SEH节点 |
返回地址 |
函数参数 |
虚函数表 |
先把这个展示了很久的栈布局再贴出来一次。
我们在通关栈溢出(一):原理及初级攻击这篇文章中,介绍了用覆盖函数返回地址的方法执行我们的Shellcode,但如果操作系统在函数调用前就在返回值前方插入一个security_cookie,每次return时校验这个security_cookie是否有改动不就可以完美的判断当前的栈是否发生了缓冲区溢出了吗?
这就是GS所做的任务。GS插入的这个security_cookie也被称为Canary(小趣闻:以前矿工工作时,会先放只鸟下去看看氧气含量是否达标,以此检测是否安全)。
它的执行逻辑为:
- 系统在.data的内存区域种存放一个SecurityCookie的副本。程序每次运行时会以.data的第一个双字作为Cookie的种子,或者原始Cookie。因此程序每次运行所产生的Cookie种子就有区别。
- 在所有的函数调用发生时,向栈帧中压入一个额外的随机DWORD。排列顺序如上表,它位于EBP和返回地址之前。当栈帧初始化以后系统用ESP异或Cookie种子,作为这里的DWORD,也就是当前函数的Cookie,以此作为不同函数之间的区别。
- 在函数返回前,系统将执行一个额外的安全验证操作SecurityCheck,先用ESP还原出(异或)Cookie的种子,匹配该种子和.data区域的数值是否一样。
- 如果不一样,进行异常处理流程,函数不会正常返回,ret指令也不会被执行。
例外:
从上面的执行逻辑我们能发现一点,就是GS一定对程序的效率有影响,因为它给所有函数都加上这个校验,那就意味着每次都会多一个check.
如果现在有个函数,它的唯一功能是index += 1。在外部我们用for循环反复调用10000次,那这里的性能损耗是能直接看出来的:执行Security校验的总时间加起来比函数本身的消耗还长。对于一个操作系统来讲,这是很差劲的一件事情。因此GS是允许存在例外的,在这些例外中,函数不会被插入canary。
- 函数不包含缓冲区
- 函数被定义为具有变量参数列表
- 函数使用无保护的关键字标记
- 函数在第一个语句中包含内嵌汇编代码
- 缓冲区不是8字节类型且大小不大于4个字节
小结:
单从上面这几点,我们不难看出,GS有几项很明显的攻击面:
- SecurityCheck失败后,程序会进入异常处理流程,也就是SEH(这里的SEH不属于当前栈,无法直接攻击它)。 这里是不是很熟悉?
- GS为了性能,允许存在例外,那这些例外我们可以利用吗?
- SecurityCheck是和.data中存放的canary副本做匹配,那如果攻击者把.data内存放的canary副本都改了呢?
攻击1:利用未保护的内存突破GS
如果函数不包含4字节以上的缓冲区,那么即使GS处于开启状态,这么函数也是不受保护的。
其他GS的例外也同理
因此,在Hack之前,我们可以判断当前函数是否符合例外。
1 | void function(char* str) |
攻击2:攻击虚函数
GS只有在函数返回时才会去检查security_check,而虚函数攻击是在程序未返回时(函数中显式调用虚函数)就已发生了,因此可以直接绕过GS校验。
攻击3: 攻击SEH
同理,SEH也是可以攻击的,只要我们提前触发Exception,就能实现攻击
攻击4:同时替换栈中和.data中的Cookie
这种攻击方式会比较局限,取决于当前缓冲区的情况。
例如现在有个数组,arr[8],如果我们能访问到arr[-10000]这种负值的话,就能跳转到.data区域了。再将.data区域存放的Canary副本替换成攻击样本,这样即使函数return时校验了GS,结果也会是一样的。
那如何找到.data中Canary副本的位置呢?
函数执行时,如果该函数存在GS,那么我们能够在汇编窗口中看到明显的插入代码。
mov eax, dword ptr ds:[403000]
xor eax, ebp
mov dword ptr ss:[ebp-4], eax
上文的意思也就是icon过0x00403000处取出Canary,与当前的ebp进行异或然后放入ebp-4的位置。
所以我们的目标就是让缓冲区覆盖到0x00403000这个位置即可,4个字节就能完成对他的覆盖了。只是因为这种攻击方法会非常局限,毕竟要能访问负地址的缓冲区是少数的。
二、SafeSEH
之前介绍到了攻击SEH,微软也发现了老版本的SEH存在的这个问题,所以新提出了SafeSEH的概念。
我们攻击SEH时主要策略是修改SEH的next指针,让其指向我们的Shellcode,控制程序的jmp位置。SafeSEH的防御措施为:
- 在程序运行前先把程序里所有已包含的SEH信息记录在程序内存的一个表中
- 当程序发生异常时,先在表中查询该跳转地址是否合法
这样其实就能拦截我们修改后的Shellcode地址了。
SafeSEH的校验分三步:
- 检查异常处理链是否位于当前程序的栈中。如果不在栈中,终止
- 检查异常处理函数指针是否指向当前程序的栈中。如果指向栈中,终止
- 第三步看下图
攻击方式:
- 攻击返回地址
- 攻击虚函数
- 从堆中绕过:如果SEH的异常函数指针指向堆区,即使安全校验发现了SEH已经不可信,仍然会调用其已被修改过的异常处理函数。因此,我们只需要将shellcode布置到对去就可以直接跳转执行。
- 利用未启用SafeSEH的模块绕过SafeSEH
- 利用加载模块之外的地址绕过SafeSEH
三、DEP
数据是数据,代码是代码,栈是数据,就不让执行代码。这就是Data Execution Prevention。
它的基本原理是将数据所在内存页标识为不可知行,当程序溢出转入Shellcode时,程序会尝试在数据页面上执行指令。
操作系统通过设置内存页的NX/XD属性标记,来指明不能从该内存执行代码。为了实现这个功能,需要在内存的Page Table中加入一个特殊的标志位(NX/XD)来标识是否允许在该页上执行指令。
攻击:ROP
既然不让我在数据区执行代码,那么我把Shellcode的数据指向代码区会怎么样呢?比如现在进程中加载了N个库,这些库里说不定就会有我们可以执行的代码,只要能确定它的地址并把它们一个个的串起来,不就可以实现我们的攻击了吗?
其实攻击DEP的主要思想为,关掉它。在Android中,我们可以使用mprotect去修改内存的可读写状态,在windows中,我们可以用VirtualProtect关闭DEP。
如何找到调用VirtualProtect函数所需的汇编指令地址呢?
掏出软件Immunity Debugger,安装mona插件,地址为https://github.com/corelan/mona,attach当前进程,在最下方的命令行中输入
1 | !mona rop -m *.dll -cp nonull |
执行命令后在C:Program FilesImmunity IncImmunity Debugger下生成文件rop.txt、rop_chains.txt、rop_suggestions.txt、stackpivot.txt
查看rop_chains.txt,会列出可用来关闭DEP的ROP链,选择VirtualProtect()函数
如上图,成功构建ROP链.
需要注意一点,如果当前进程已加载的dll均无法找到合适的跳转,也就意味着ROP链构建失败
接着,修改我们的shellcode,将上方的16进制写入即可。之前我们使用\x83\x16这种方式写的,所以ROP的话就应该将他们全都转为16进制,注意小端显示,所以需要反着写。