缓冲区溢出保护机制——Windows

缓冲区溢出保护机制

Windows

GS安全编译选项

Visual Studio 2003及以后版本的Visual Studio中默认启用了这个安全编译选项。

GS编译选项为每个函数增加了一些额外的数据和操作:

1、在所有函数调用发生时,向栈帧内压入一个额外的随机DWORD,这个随机数被称作“canary”,用IDA反汇编时,又被称作“Security Cookie”。

2、canary位于EBP之前,系统还会在.data的内存区域中存放一个canary的副本。

3、当栈中发生溢出时,canary将被首先淹没,之后才是EBP和返回地址。

4、在函数返回之前,系统将执行一个额外的安全验证操作,称作Security Check。

5、在Security Check过程中,系统将比较栈帧中原先存放的canary和.data中副本的值,若两者不同,则说明栈中发生了溢出,系统将进入异常处理流程,函数不会正常返回。

canary产生的细节

  • 系统以.data节的第一个双字作为Cookie的种子,或称为原始Cookie。

  • 在程序每次运行时Cookie的种子都不相同,具有很强的随机性

  • 在栈帧初始化以后系统用ESP异或种子,作为当前函数的Cookie,以此作为不同函数的区别,并增强Cookie的随机性

  • 在函数返回前,用ESP还原出Cookie种子

分析

Security Cookie具有很强的随机性,在函数运行时猜解几乎是不可能的。

但是,额外的数据和操作造成了系统性能的下降,为了将对性能的影响降到最小,编译器在编译程序的时候并不是对所有的函数都应用GS,以下情况不会应用GS:

函数不包含缓冲区

函数被定义为具有变量参数列表

函数使用无保护的关键字标记

函数在第一个语句中包含内嵌汇编代码

缓冲区不是8字节类型且大小不大于4个字节

绕过GS

1、利用未保护内存

可通过缓冲区不大于4字节的栈溢出直接绕过GS。

2、覆盖虚函数

程序只有在函数返回时才去检查canary,在此之前并没有任何措施,所以我们可以利用C++虚函数在程序检查canary之前劫持程序流程。

由于C++对象在使用虚函数时,会先通过虚表指针找到虚表,然后从虚表中取出最终的函数入口地址进行调用,所以,当虚函数中变量溢出影响到虚表指针的时候,就可以控制虚标指针指向可控的内存空间,从而当函数执行虚函数时就可以控制程序流程。

3、异常处理

由于开启GS时并没有明确对SEH进行保护,所以我们可以通过异常处理来绕过GS。

首先通过超常的字符串覆盖掉异常处理函数指针,然后只要触发异常,就可以劫持程序流程。

4、同时替换栈中和.data中的canary

通过特定的溢出条件,来达到可以访问.data中的数据的目的,根据canary的产生原理,同时修改栈中和.data段中的canary,来达到绕过GS的目的。

SafeSEH

实现原理:

在程序调用异常处理函数前,对要调用的异常处理函数进行一系列有效性检验,当发现处理函数不可靠时将终止异常处理函数的调用。集体实现需要操作系统和编译器的双重支持。

编译器

编译器通过启用/SafeSEH链接选项打开SafeSEH保护,启用该链接选项后,编译器在编译程序的时候将程序所有的异常处理函数地址提取出来,编入一张安全SEH表,并将这张表放入程序的映像里,当程序调用异常处理函数时,会将函数地址与安全SEH表进行匹配,检查调用的异常处理函数是否位于安全SEH表中。

操作系统

1、检查处理链是否位于当前程序的栈中,如果不在栈中,程序将终止异常处理函数的调用。

2、检查异常处理函数指针是否指向当前栈中,如果指向当前栈中,程序则终止异常处理函数的调用。

3、前面两项检查都通过时,程序调用RtlIsValidHandler()函数,来对异常处理函数的有效性进行验证。

RtlValidHandler():

  1. 判断程序是否设置了IMAGEDLLCHARACTERISTICSNO_SEH标识,如果设置了这个标识,这个程序的异常会被忽略。所以当这个标志被设置时,函数直接返回校验失败。
  2. 检测程序是否包含安全SEH表,如果包含,则将当前异常处理程序函数地址与该表进行匹配,匹配成功则返回校验成功。
  3. 判断程序是否设置ILonly标识,如果设置,说明该程序只包含.NET编译中间语言,直接返回校验失败。
  4. 判断异常处理函数地址是否位于不可执行页,当位于不可执行页时,检测DEP是否开启,若未开启则返回校验成功,否则抛出访问违例异常。
  5. 如果异常处理函数地址没有包含在加载模块的内存空间,检验函数将直接进行DEP相关检测:

    判断异常处理函数地址是否位于不可执行页,当异常处理函数地址位于不可执行页时,校验函数检测DEP是否开启,未开启则校验成功,开启则抛出访问违例异常。

    判断系统是否允许跳转到加载模块的内存空间外执行,如果允许则校验成功,否则校验失败。

异常处理函数可以执行的情况:

  1. 异常处理函数位于加载模块内存范围之外,DEP关闭。
  2. 异常处理函数位于加载模块内存范围之内,相应模块未开启SafeSEH,同时相应模块不是纯IL(中间语言)。
  3. 异常处理函数位于加载模块内存范围之内,相应模块开启SafeSEH,异常处理函数地址包含在安全SEH表中。

绕过SafeSEH

1、不攻击SafeSEH

攻击返回地址绕过SafeSEH,未启用GS保护的栈溢出。

利用虚函数绕过,绕过方式同上。

2、堆

如果SEH中的异常处理函数指针指向堆区,即是安全校验发现了SEH不可信,仍然会调用其已被修改过的异常处理函数,因此可以控制程序流。

3、利用未启用SafeSEH模块

若模块未启用SafeSEH,并且该模块不是仅包含中间语言,这个异常处理就可以执行,所以如果我们能够在加载的模块中找到一个未启用SafeSEH的模块,就可以利用它里面的指令作为跳板来绕过SafeSEH。

4、利用加载模块之外的地址

当异常处理函数指针指向类型为Map的映射文件的地址范围内时,是不对其进行有效性验证的,如果在这些文件中找到跳转指令,就可以绕过SafeSEH。

5、Adobe Flash Player ActiveX

Flash Player ActiveX在9.2.124之前的版本不支持SafeSEH,只要在该控件中找到合适的跳板,就可以绕过SafeSEH。

DEP

溢出攻击的根源在于现代计算机对数据和代码没有明确的区分这一先天缺陷,DEP(数据执行保护,Data Execution Prevention)就是用来弥补这一缺陷的。

基本原理

将数据所在的内存页标识为不可执行,当程序尝试在数据页执行指令时,就会抛出异常。主要是为了阻止数据页(如默认的堆页、各种堆栈页以及内存池页)执行代码。

软件DEP

就是SafeSEH,目的是阻止利用SEH的攻击。

硬件DEP:

需要CPU的支持,AMD称之为No-Execute Page-Protection(NX), Intel称之为Execute Disable Bit(XD),两者本质上相同。

操作系统通过设置内存页的NX/XD属性标记,来指明不能从该内存执行代码。

硬件DEP的工作状态

  1. Optin:默认仅将DEP保护应用于Windows系统组件和服务,对于其他程序不予保护。
  2. Optout:为排除列表程序外的所有程序和服务启用DEP,用户可以手动在排除列表中指定不启用DEP保护的程序和服务。
  3. AlwaysOn:对所有进程启用DEP保护,不存在排除列表,这种模式下,DEP不可被关闭。
  4. AlwaysOff:对所有进程都禁用DEP,这种模式下,DEP也不能被动态开启。

绕过DEP

1、Ret2Libc

Return-to-libc,由于DEP不允许我们直接到非可执行页执行指令,我们就需要在其他可执行的位置找到符合我们要求的指令,让这条指令来替我们工作,为了能够控制程序流程,在这条指令执行后,我们还需要一个返回指令,以便收回程序的控制权,然后继续下一步操作。

1、通过跳转到ZwSetInformationProcess函数将DEP关闭后再转入Shellcode执行。

2、通过跳转到VirtualProtect函数来将shellcode所在的内存页设置为可执行状态,然后再转入shellcode执行。

3、通过跳转到VirtualAlloc函数开辟一段具有执行权限的内存空间,然后将shellcode复制到这段内存中执行。

2、可执行内存

找到一段可执行内存,将shellcode复制到这里进行执行,从而劫持程序流程。

3、利用控件

.NET文件具有和PE文件一样的结构,当被映射到内存时,某些段也具有可执行属性,可以通过这些段来执行shellcode。

Java applet同.NET一样,加载内存时也会具有可执行属性,都可以被我们利用。

4、ROP

同Ret2Libc原理类似,不同的是,不用通过调用系统函数来为shellcode进行铺垫,直接通过执行程序中的小片段来达到想要达到的目的,一般情况下,不用执行shellcode,也就是不需要哪段内存必须要有可执行属性。

ASLR

ASLR(Address Space Layout Randomization)通过加载程序的时候不再使用固定的加载基址,从而干扰shellcode定位。

1、映像随机化

映像随机化是在PE文件映射到内存时,对其加载的虚拟地址进行随机化处理,这个地址是在系统启动时确定的,系统重启后这个地址会有变化。

2、堆栈随机化

程序运行时随机选择堆栈的基址,与映像随机化不同的是堆栈的基址不是在系统启动时确定的,而是在程序打开时确定的,也就是说同一个程序任意两次运行时的堆栈基址都不相同。

3、PEB与TEB随机化

微软在XP SP2之后不再使用固定的PEB基址0x7FFDF000和TEB基址0x7FFDE000,而是使用具有一定随机性的基址。

TEB存放在FS:0和FS:[0x18]处,PEB存放在TEB偏移0x30的位置

绕过ASLR

1、未启用ASLR的模块

Adobe Flash Player ActiveX

Adobe 在Flash Player 10以后开始全面支持微软的安全特性

2、部分覆盖进行定位内存地址

映像随机化只是对映像加载基址的前两个字节做随机化处理,这样我们就可以在一定范围内控制程序,同时,又由于程序虽然随机化了基址,但是指令相对于基址的偏移并未发生变化,所以可以通过在相对基址较小范围处找到跳板指令,即可绕过ASLR。

3、Heap Spray

Heap Spray并没有官方的定义,理论上知识漏洞攻击的一部分,只是一种辅助技术,需要结合其他的栈溢出或堆溢出等等各种溢出技术才能发挥作用。

总的来说,Heap Spray是在shellcode前面加上大量的slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。然后结合其他的漏洞攻击技术控制程序流,使得程序执行到堆上,最终将导致shellcode的执行。

最简单的方式其实就是通过堆申请大量内存,例如200M就可以占领内存中0x0c0c0c0c的位置,然后再这些内存中放入滑板指令和shellcode,当控制程序流程执行到0x0c0c0c0c时,就有机会执行到shellcode。

正如这种方法原理所述,并不需要考虑所谓的基址,自然而然可以绕过ASLR。

还有一个原因就是,虽然基址随机化,但是基址随机化的范围并不会到达0x0c0c0c0c这样的高地址,所以这种方式绕过ASLR是完全可以的。

Java applet heap spray同样可以实现绕过,不同点就是,Java applet仅能申请100M的空间,但已经足够。

SEHOP

SEH是以单链表的形式存放在栈中的,而在这个链表的末端是程序的默认异常处理函数,专门负责处理前面的SEH链都不能处理的异常。

SEHOP的核心任务就是检查这条SEH链的完整性,在程序转入异常处理前,SEHOP会检查SEH链上最后一个异常处理函数是否为系统固定的终极异常处理函数,如果是,则说明这个SEH链没有被破坏,程序可以执行当前的异常处理函数。如果不是,则说明SEH链被破坏,而不会去执行异常处理函数。

绕过SEHOP

理论上来说,有三种途径:

  1. 不去攻击SEH,去攻击函数返回地址或者虚函数等。
  2. 利用未启用SEHOP模块
  3. 伪造SEH链

前两种不用多说,都属于前面说过的内容

伪造SEH链

伪造条件:

  1. 跳板指令指向的地址必须位于当前栈中,而且必须能够被4整除。
  2. 该地址存放的异常处理记录作为异常处理链的最后一项,必须要指向终极异常处理函数,所以必须要知道FinalExceptionHandler指向的地址。
  3. 突破SEHOP检查后,溢出程序还必须搞定SafeSEH。

显然是非常困难的,但是当ASLR未开启,或者处于未开启SafeSEH的模块里,则有可能伪造成功。

堆保护机制

PEB random:

PEB的随机化给DWORD SHOOT攻击增加了难度。

Safe Unlink:

拆卸双向链表时,会对指针指向的内容是否是相应的堆块做安全检查。

heap cookie:

与栈中的Security Cookie类似,微软在堆中也引入了cookie,用于检测堆溢出的发生,cookie被布置在堆首部原堆块的segment table的位置,占一个字节大小。

元数据加密:

块首中的一些重要数据在保存时会与一个4字节的随机数进行异或运算,在使用这些数据时,需要在进行一次异或运算来还原。

堆攻击方式

1、利用chunk重设大小攻击堆

基本思路:Safe Unlink只会对从FreeList[n]上拆卸chunk时对双链表进行有效性检验,但是如果把一个chunk插入到FreeList[n]的时候,并没有进行检验,因此可以利用。

发生插入操作的情况:

  1. 内存释放后chunk不再使用会被重新链入链表。
  2. 当chunk的内存空间大于申请的空间时,剩余的空间会被建立成一个新的chunk,链入链表。

第二种情况下,有可利用的机会:

具体流程如下:

1、将FreeList[0]上最后一个chunk的大小与申请空间的大小进行比较,如果chunk的大小大于等于申请的空间,则继续分派,否则扩展空间。

2、从FreeList[0]的第一个chunk依次检测,直到找到第一个符合要求的chunk,然后将其从链表中拆卸下来。

3、分配好空间后,如果chunk中还有剩余空间,剩余空间会被建立成一个新的chunk,插入到链表中。

第一步没有利用机会,第二步和第三步结合起来便可以利用:

我们在第二步时如果将后一个chunk的指针覆盖,前一个chunk正常分配,在第二步时可以检测出堆结构已经被破坏。

虽然检测到堆结构出问题,但是程序依然会将第三步进行下去,也就是说将我们修改后的堆块加入到链中,从而可以导致任意地址写。

从而当我们再次申请堆时,申请的地址则有可能指向我们所设计的地址,从而达到我们的目的。

利用Lookaside表

Safe Unlink是对双链表进行的检验,而对单链表并没有防护措施,所以如果我们将单链表的后指针覆盖为任意地址,在我们下次申请堆块时,就有可能申请到我们所写的地址,从而可能导致其他后果,比如可以修改异常处理链等。

posted @ 2018-03-11 22:25  Clingyu  阅读(2451)  评论(0编辑  收藏  举报