Windows溢出保护原理与绕过方法概览
By : riusksk(泉哥)
Blog: http://riusksk.me
Data: 第1版:2010/10/26
第2版:2011/3/26
前言
从20世纪80年代开始,在国外就有人开始讨论关于溢出的攻击方式。但是在当时并没有引起人们的注意,直至后来经一些研究人员的披露后,特别是著名黑客杂志Phrack上面关于溢出的经典文章,引领许多人步入溢出研究的行列,从此关于缓冲区溢出的问题才为人们所重视。随着溢出研究的深入,网上开始出现很多关于溢出攻击教程,揭露了许多溢出利用技术,特别是经典的call/jmp esp,借此溢出攻击案例层出不穷。这也引起了微软的重视,他们在windows系统及VC++编译器上加入了各种溢出保护机制,以试图阻止这类攻击,可惜每次公布溢出保护机制之后,不久就有人公布绕过方法。MS每次都称某保护机制将成为溢出利用的末日,可惜每次都被终结掉。既而,黑客与微软之间的溢出斗争一直持续着。更多关于windows溢出的历史,可参见由Abysssec安全组织编写的文章《Past,Present,Future of Windows Exploitation》。在本篇文章中主要揭露了windows平台上的各种溢出保护机制原理以及绕过方法,具体内容参见下文。
一、GS编译选项
原理
通过VC++编译器在函数前后添加额外的处理代码,前部分用于由伪随机数生成的cookie并放入.data节段,当本地变量初始化,就会向栈中插入cookie,它位于局部变量和返回地址之间:
┏━━━━━━━━┓内存低地址
┃ 局部变量 ┃▲
┣━━━━━━━━┫┃
┃security_cookie ┃┃
┣━━━━━━━━┫┃栈
┃ 入栈寄存器 ┃┃生
┣━━━━━━━━┫┃长
┃ SEH节点 ┃┃方
┣━━━━━━━━┫┃向
┃ 返回地址 ┃┃
┣━━━━━━━━┫┃
┃ 函数参数 ┃┃
┣━━━━━━━━┫┃
┃ 虚函数表 ┃┃
┗━━━━━━━━┛内存高地址
经GS编译后栈中局部变量空间分配情况:
1 | sub esp,24h |
在函数尾部的额外代码用于在函数返回时,调用security_check_cookie()函数,以判断cookie是否被更改过,当函数返回时的情况如下:
1 | mov ecx,dword ptr [esp+20h] |
在缓冲区溢出利用时,如果将恶意代码从局部变量覆盖到返回地址,那么自然就会覆写cookie,当检测到与原始cookie不同时(也就是比较上面408040h与4010B2h两处cookie值的比较),就会触发异常,最后终止进程。
绕过方法:
1.猜测/计算cookie
Reducing the Effective Entropy of GS Cookies
至从覆盖SEH的方法出现后,这种方法目前已基本不用了,它没有后面的方法来得简便。
2.覆盖SEH
由于当security_check_cookie()函数检测到cookie被更改后,会检查是否安装了安全处理例程,也就是SEH节点中保存的指针,如果没有,那么由系统的异常处理器接管,因此我们可以通过(pop pop ret)覆盖SEH来达到溢出的目的。但对于受SafeSEH保护的模块,就可能会导致exploit失效,关于它的绕过在后续部分再述。
辅助工具:OD插件safeSEH、pattern_create、pattern_offset、msfpescan、memdump
3.覆盖虚表指针
堆栈布局:[局部变量][cookie][入栈寄存器][返回地址][参数][虚表指针]
当把虚表指针覆盖后,由于要执行虚函数得通过虚表指针来搜索,即可借此劫持eip。
二、SafeSEH
原理
为了防止SEH节点被攻击者恶意利用,微软在.net编译器中加入/safeseh编译选项引入SafeSEH技术。编译器在编译时将PE文件所有合法的异常处理例程的地址解析出来制成一张表,放在PE文件的数据块(LQAJ)一C0N—FIG)中,并使用shareuser内存中的一个随机数加密,用于匹配检查。如果该PE文件不支持safeSEH,则表的地址为0。当PE文件被系统加载后,表中的内容被加密保存到ntdl1.dll模块的某个数据区。在PE文件运行期间,如果发生异常需要调用异常处理例程,系统会逐个检查该例程在表中是否有记录:如果没有则说明该例程非法,进而不执行该异常例程。
绕过方法
1.利用堆地址覆盖SEH结构
在禁用DEP的进程中,异常分发例程允许SEH handler位于某些非映像页面,除栈空间之外。这也就意味着我们可以把shellcode放置在堆中,并通过覆盖SEH跳至堆空间以执行shellcode,这样即可完全绕过safeseh保护。
2.利用SafeSEH保护模块之外的地址
对于目前的大部分windows操作系统,其系统模块都受SafeSEH保护,可以选用未开启SafeSEH保护的模块来利用,比如漏洞软件本身自带的dll文件,这个可以借助OD插件SafeSEH来查看进程中各模块是否开启SafeSEH保护。除此之外,也可通过直接覆盖返回地址(jmp/call esp)来利用。另一种方法,如果esp +8 指向EXCEPTION_REGISTRATION 结构,那么你仍然可以寻找一个pop/pop/ret指令组合(在加载模块的地址范围之外的空间),也可以正常工作。但如果你在程序的加载模块中找不到pop/pop/ret 指令,你可以观察下esp/ebp,查看下这些寄存器距离nseh 的偏移,接下来就是查找这样的指令:
1 | call dword ptr[esp+nn] / jmp dword ptr[esp+nn] |
如果遇到以上指令是以NULL字节结尾的,可将shellcode放置在SEH之前:
• 在nseh 上放置向后的跳转指令(跳转7 字节:jmp 0xfffffff9);
• 向后跳转足够长的地址以存放shellcode,并借此执行至shellcode;
• 把shellcode 放在用于覆盖异常处理结构的指令地址之前。
三、Safe Unlinking
原理
在Windows XP SP2之后,堆分配器在从空闲链表中移除堆块时使用safe unlinking进行保护,防止堆溢出被利用。在使用flink和blink指针前,它会验证是否满足以下条件:Entry->Flink->Blink == Entry->Blink->Flink == Entry,以防止攻击者使flink或blink指向任意内存地址,进而消除在执行unlink操作时写入任意4字节数据的机会。
绕过方法:
1.利用旁视列表(lookaside list)
旁视列表(《软件调试》),也叫快表(《0day安全:软件漏洞分析技术》),它是一张链表,共包含128 项,每一项对应于一个单向链表。每个单向链表都包含了一组固定大小的空闲块,堆块的大小从16 字节开始随索引递增依次增加8字节。最后一个索引(127)包含了大小为1024 字节的空闲堆块。每个堆块包含了8 个字节的块首,用于管理这个堆块。返回给调用者的最小堆块是16 字节。这样,旁视列表前端分配器没有使用索引为0的项,因为这个项对应于大小为8 个字节的空闲堆块。由于在safe unlinking过程中,快表被忽略了,当在快表中分配一块空闲块后,若将该空闲块从链表中移除,则该块的flink指针会写入块首,而系统并未对flink指针的有效性进行验证,这样就导致在分配下一个同大小的堆块时,它将会把flink指针返回给新分配的块。如果攻击者能够覆盖快表中的链表头,那么就可以用任意地址来替换flink指针,并在分配新块时写入任意字节,最后返回被我们修改的地址的值。这一攻击方式最早是由Matt Conover在CanSecWest 2004黑客大会上公布的《Windows Heap ExploitationWin2KSP0 through WinXPSP2)》
实现步骤如下:
1 | p = HeapAlloc(n); |
利用以下值篡改 p[0](任一堆地址):
1 | p->Flags = Busy (防止偶然发生堆块合并) |
但在Windows Vista之后,快表被低碎片堆(Low-Fragmentation Heap)所代替了,上面的攻击方式就不再适用了。
2.heap spary
Heap Spary技术最早是由SkyLined于2004年为IE的iframe漏洞写的exploit而使用到新技术,目前主要作为浏览器攻击的经典方法,被大量网马所使用。Heap Spary技术是使用js分配内存,所分配的内存均放入堆中,然后用各带有shellcode的堆块去覆盖一大片内存地址,Javascript分配内存从低址向高址分配,申请的内存空间超出了200M,即大于了0x0C0C0C0C时,0x0C0C0C0C就会被覆盖掉,因此只要让IE执行到0x0C0C0C0C(有时也会用0x0D0D0D0D这一地址)就可以执行shellcode,这些堆块可以用NOP + shellcode 来填充,每块堆构造1M大小即可,当然这也不是固定。这样当nop区域命中0x0c0c0c0c时,就可执行在其后面的shellcode。下面是一个简单模板:
1 | <html> |
四、Heap Cookie及其加密
原理
在heap header中加入cookie值,原理与栈中的cookie类似,用于检测堆溢出的发生,cookie被放置在堆首部分原堆块的segment table的位置,占1字节大小,其计算公式如下:
1 | (AddressOfChunkHeader / 8) XOR Heap->Cookie = Cookie |
即堆块头部地址除以8,然后跟Heap管理结构中的cookie相异或就得到了cookie值。
绕过方法
1.猜测/计算cookie
由于cookie只有1字节,因此共有256种可能存在的值,如果通过暴力猜测的话,也是存在被破解的可能。
2.heap spary
具体利用方法同上,这里不再赘述。
三、DEP
原理
数据执行保护 (DEP) 是一套软硬件技术,能够在内存上执行额外检查以防止在不可运行的内存区域上执行代码。在 Microsoft Windows XP Service Pack 2、 Microsoft Windows Server 2003 Service Pack 1 、Microsoft Windows XP Tablet PC Edition 2005 、Microsoft Windows Vista 和 windows 7 中,由硬件和软件一起强制实施 DEP。DEP 有两种模式,如果CPU 支持内存页NX 属性, 就是硬件支持的DEP。只有当处理器/系统支持NX/XD位(禁止执行)时,windows才能拥有硬件DEP,否则只能支持软件DEP,相当于只有SafeSEH保护。
绕过方法:
1.ret2lib
其思路为:将返回地址指向lib库中的代码,而不直接跳转到shellcode 去执行,进而实现恶意代码的运行。可以在库中找到一段执行系统命令的代码,比如system()函数,用它的地址覆盖返回地址,此时即使NX/XD 禁止在堆栈上执行代码,但库中的代码依然是可以执行的。函数system()可通过运行环境来执行其它程序,例如启动Shell等等。另外,还可以通过VirtualProtect函数来修改恶意代码所在内存页面的执行权限,然后再将控制转移到恶意代码,其堆栈布局如下所示:
1 | ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ |
由于后期系统dll加入了ASDL保护,因此我们可以选用未开启ASLR的第三方DLL文件,示例如下(这里使用迅雷IE插件):
1 | XunleiBHO7.1.6.2194.dll(DEP/ NO ASLR) |
栈空间布局:
1 | 41414141 垃圾字节(共3088字节) |
在一次实际利用中,我使用了COMODO主动防御软件中的guard32.dll来定位VirtualProtect函数:
1 | #1002CA33 -FF25 E8F30310 JMP DWORD PTR DS:[1003F3E8] [Module : guard32.dll] |
关于ret2lib技术的更多信息可参考资料:http://www.infosecwriters.com/text_resources/pdf/return-to-libc.pdf
2.利用TEB突破DEP
在之前的《黑客防线》中有篇文章《SP2下利用TEB执行ShellCode》,有兴趣的读者可以翻看黑防出版的《缓冲区溢出攻击与防范专辑》,上面有这篇文章。该作者在文中提到一种利用TEB(线程环境块)来突破DEP的方法,不过它受系统版本限制,只能在XP sp2及其以下版本的windows系统上使用,因为更高版本的系统,其TEB地址是不固定的,每次都是动态生成的。该方法的具体实现方法如下:
(1)将返回地址覆盖成字符串复制函数的地址,比如lstrcpy,memcpy等等;
(2)在返回地址之后用目标内存地址和shellcode地址覆盖,当执行复制操作时,就会将shellcode复制到目标内存地址,该目标内存地址位于TEB偏移0xC00的地方,它有520字节缓存用于ANSI-to-Unicode函数的转换;
(3)复制操作结束后返回到shellcode地址并执行它。
此时其堆栈布局如下:
1 | ┏━━━━━━━┓ |
3.利用NtSetInformationProcess关闭DEP
关于此方法最原始的资料应该是黑客杂志《uninformed》上的文章《Bypassing Windows Hardware-enforced Data Execution Prevention》,另外也可以看下本人之前翻译的《突破win2003 sp2中基于硬件的DEP》,此方法的主要原理就是利用NtSetInformationProcess()函数来设置KPROCESS 结构中的相关标志位,进而关闭DEP,KPROCESS结构中相关标志位情况如下:
1 | 0:000> dt nt!_KPROCESS -r |
当DEP 被启用时,ExecuteDisable 被置位,当DEP 被禁用,ExecuteEnable 被置位,当Permanent 标志置位时表示这些设置是最终设置,不可更改。代码实现:
1 | ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE; |
具体实现思路(以我电脑上VirtualBox虚拟机下的xp sp3为例):
1) 将al设置为1,比如指令mov al,1 / ret,然后用该指令地址覆盖返回地址:
1 | 0:000> lmm ntdll |
由于上面的ret 4,因此要再向栈中填充4字节(比如0xffffffff)以抵消多弹出的4字节,如果选择的指令刚好是ret则无须再多填充4字节。
2) 跳转到ntdll!LdrpCheckNXCompatibility中的部分代码(从cmp al,1 开始,可通过windbg下的命令uf ntdll!LdrpCheckNXCompatibility来查看其反汇编代码),比如以下地址就需要用0x7c93cd24来覆写堆栈上的第二个地址:
1 | ntdll!LdrpCheckNXCompatibility+0x13: |
3) 上面跳转后来到这里:
1 | 0:000> u 7c95f70e |
4) 上面跳转后来到:
1 | 0:000> u 7c93cd2f |
5) 上面跳转后来到:
1 | 0:000> u 7c956831 |
在这里调用函数ZwSetInformationProcess(),而其参数也刚好达到我们关闭DEP的各项要求.
6) 最后跳转到函数结尾:
1 | 0:000> u 7c93cd6d |
最后的堆栈布局应为:
1 | ┏━━━━━━━━━━━━━━━━┓ |
如果在禁用NX后,又需要读取esi或ebp,但此时它们又被我们填充的数据覆盖掉了,那么我们可以使用诸如push esp/pop esi/ret或者push esp/pop ebp/ret这样的指令来调整esi和ebp,以使关闭DEP后还能够正常执行。
辅助工具:ImmDbg pycommand插件(!pvefindaddr depxpsp3 + !findantidep)
4.利用SetProcessDEPPolicy来关闭DEP
适用在:Windows XP SP3,Vista SP1 和Windows 2008。
为了能使这个函数有效,当前的DEP 策略必须设成OptIn 或者OptOut。如果策略被设成
AlwaysOn(或者AlwaysOff),然后SetProcessDEPPolicy 将会抛出一个错误。如果一个模块
是以/NXCOMPAT 链接的,这个技术也将不会成功。最后,同等重要的是,它这能被进程调
用一次。因此如果这个函数已经被当前进程调用(如IE8,当程序开始时已经调用它),它
将不成功。
Bernardo Damele 写了一篇关于这一技术的博文《DEP bypass with SetProcessDEPPolicy()》
函数原型如下:
1 | BOOLWINAPI SetprocessDEPPolicy( |
DWORD dwDWORD dw这个函数需要一个参数,并且这个参数必须设置为0,以此禁用当前进程的DEP。
为了在ROP 链中使用这个函数,你需要在栈上这样设置:
●指向SetProcessDEPPolicy 的指针
●指向shellcode 的指针
●0
指向shellcode 的指针用于确保当SetProcessDEPPolicy()执行完ROP链后会跳到shellcode。
在XP SP3 下SetProcessDEPPolicy 的地址是7C8622A4(kernel32.dll)
5.利用WPN与ROP技术
ROP(Return Oriented Programming):连续调用程序代码本身的内存地址,以逐步地创建一连串欲执行的指令序列。
WPM(Write Process Memory):利用微软在kernel32.dll中定义的函数比如:WriteProcess Memory函数可将数据写入到指定进程的内存中。但整个内存区域必须是可访问的,否则将操作失败。
具体实现方法参见我之前翻译的文章《利用WPN与ROP技术绕过DEP》:http://bbs.pediy.com/showthread.php?t=119300
6.利用SEH 绕过DEP
启用DEP后,就不能使用pop pop ret地址了,而应采用pop reg/pop reg/pop esp/ret 指令的地址,指令pop esp 可以改变堆栈指针,ret将执行流转移到nseh 中的地址上(用关闭NX 例程的地址覆盖nseh,用指向pop/pop/pop esp/ret 指令的指针覆盖异常处理器)。
辅助工具:ImmDbg插件!pvefindaddr
四、ASLR
原理
ASLR(地址空间布局随机化)技术的主要功能是通过对系统关键地址的随机化,防止攻击者在堆栈溢出后利用固定的地址定位到恶意代码并加以运行。它主要对以下四类地址进行随机化:
(1)堆地址的随机化;
(2)栈基址的随机化;
(3)PE文件映像基址的随机化;
(4)PEB(Process Environment Block,进程环境块)地址的随机化。
它在vista,windows 2008 server,windows7下是默认启用的(IE7除外),非系统镜像也可以通过链接选项/DYNAMICBASE(Visual Studio 2005 SP1 以上的版本,VS2008 都支持)启用这种保护,也可手动更改已编译库的dynamicbase 位,使其支持ASLR 技术(把PE 头中的DllCharacteristics 设置成0x40 -可以
使用工具PE EXPLORER 打开库,查看DllCharacteristics 是否包含0x40 就可以知道是否支持ASLR 技术)。另外,也可以使用Process Explorer来查看是否开启ASLR。启用ASLR后,即使你原先已经成功构造出exploit,但在系统重启后,你在exploit中使用的一些固定地址就会被改变,进而导致exploit失效。
绕过方法:
1.覆盖部分返回地址
对比下windows7系统启动前后OD中loaddll.exe的各模块基址,启动前:
1 | 可执行模块 |
系统重启后:
1 | 可执行模块 |
由此可见,各模块基址的高位是随机变化的,而低位是固定不变的,这里loaddll.exe不受ADSL保护,所以其基址没有随机化,如果是Notepad.exe就有启用ASLR,还有其它经链接选项/DYNAMICBASE编译的程序也会启用ASLR。因此我们可以让填充字符只覆盖到返回地址的一半,由于小端法机器的缘故,其低位地址在前,因此覆盖到的一半地址刚好处于低位,而返回地址的高位我们让它保持不变,所以我们必须在返回地址之前的地址范围内(相当于漏洞函数所在的255字节空间地址)查找出一个可跳转到shellcode的指令,比如jmp edx(关键看哪一寄存器指向shellcode)。除此之外,我们还必须将shellcode放在返回地址之前,不然连返回地址的高位也覆盖掉了,这是不允许的。纵观此法,相当的有局限性,如果漏洞函数过短,可能就没有我们需要的指令了,这时就得另寻他法了。
2.利用未启用ASLR的模块地址
这与之前绕过SafeSEH的方法类似,直接在未受ASLR保护的模块中查找跳转指令的地址来覆盖返回地址或者SEH结构,比如上方的可执行模块列表中的loaddll.exe地址就是固定不变,因此我们借助其地址空间中的指令来实现跳板。这个可以通过Process Explorer或者ImmDbg命令插件来查看哪些可执行模块未受ASDL保护!ASLRdynamicbase或者(!pvefindaddr noaslr):来查看哪些进程模块启用ASLR保护。
3.heap spary
具体利用方法同上,这里不再赘述。
4.利用内存信息泄漏
通过获取内存中某些有用的信息,或者关于目标进程的状态信息,攻击者通过一个可用的指针就有可能绕过ASLR。这种方法还是十分有效的,主要原因如下:
(1)可利用指针检测对象在内存中的映射地址。比如栈指针指向内存中某线程的栈空间地址,或者一静态变量指针可泄露出某一特定DLL/EXE的基址。
(2)通过指针推断出其他附加信息。比如栈桢中的桢指针不仅提供了某线程栈空间地址,而且提供了栈桢中的相关函数,并可通过此指针获得前后栈桢的相关信息。再比如一个数据段指针,通过它可以获得其在内存中的映像地址,以及单数据元素地址。若是堆指针还可获得已分配的数据块地址,这些信息在程序攻击中还是着为有用的。
在Vista系统的ASLR中,信息泄漏的可用性更广了。如果攻击者知道内存中某一映射地址,那么他不仅可获取对应进程中的DLL地址,连系统中运行的所有进程也会遭殃。因为其他进程在重新加载同一DLL时,是通过特定地址上的_MiImageBitMap变量来搜索内存中的DLL地址的,而这一bitmap又被用于所有进程,因此找到一进程中某DLL的地址,即可在所有进程的地址空间中定位出该DLL地址。
5.利用SystemCall
(1)在SystemCall 地址0x7ffe0300上是没有被随机化的,下面是我在win7 中文旗舰版上的情况:
1 | 0:000> dt _KUSER_SHARED_DATA 0x7ffe0000 |
(2)– Windows 用户模式进入内核模式时:
1 | 0:000> u ZwCreateProcess |
– 通过手工构造System Call的参数
– 并且用System Call的技术来绕过DEP&ALSR
(3)IE MS08-078 exploit with SystemCall on windows
– 通过堆喷射的方法在内存中填充SystemCall的地址
– 在exploit中使用SystemCall地址
1 | .text:461E3D30 mov eax, [esi] //eax==0x0a0a11c8 |
以上代码等同于调用NtUserLockWorkStation
1 | mov eax,11c8h |
(4)System call on x64
– 7ffe0300 不再存放KiFastSystemCall的地址
– 通过call dword ptr fs:[0C0h]指令来代替系统调用的方法
1 | 0:000> u NtQueryInformationToken |
五、SEHOP
原理
微软在Microsoft Windows 2008 SP0、Microsoft Windows Vista SP1和Microsoft Windows 7中加入了另一种新的保护机制SEHOP(Structured Exception Handling Overwrite Protection),它可作为SEH的扩展,用于检测SEH是否被覆写。SEHOP的核心特性是用于检测程序栈中的所有SEH结构链表的完整性,特别是对最后一个SHE结构的检测。在最后一个SEH结构中拥有一个特殊的异常处理函数指针,指向一个位于ntdll中的函数ntdll!FinalExceptHandler()。当我们用jmp 06 pop pop ret 来覆盖SEH结构后,由于SEH结构链表的完整性遭到破坏,SEHOP就能检测到异常从而阻止shellcode的运行
绕过方法
伪造SEH链表
由于SEHOP会检测SEH链表的完整性,那么我们可以通过伪造SEH链表来替换原先的SEH链表,进而达到绕过的目的。具体实现方法:
(1)查看SEH链表结构,可借助OD实现,然后记住最后一个SEH结构地址,以方便后面的利用;
(2)用JE(0x74) + 最后一个SEH结构的地址(由于地址开头是00,故可省略掉,可由0x74替代,共同实现4字节对齐)去覆盖nextSEH;
(3)用xor pop pop ret指令地址去覆盖SEH handle,其中的xor指令是用于将ZF置位,使前面的JE = JMP指令,进而实现跳转;
(4)在这两个SEH结构之前写入一跳转指令(JMP+8),以避免数据段被执行;
(5)在这两个SEH结构之间全部用NOP填充,如果两者之间还有其它SEH结构的话;
(6)将shellcode放置在最后一个SEH结构之后,即ntdll!FinalExceptHandler()函数之后。
此时的堆栈布局如下:
1 | ┏━━━━━━━━━━━━┓ |
更多信息可参见我之前翻译的《绕过SEHOP安全机制》
结论
本文简单地叙述了windows平台上的各类溢出保护机制及其绕过方法,但若结合实例分析的话,没有几万字是不可能完成的,因此这里概览一番,读者若想获得相关的实例运用的资料,可参考文中提及一些paper,特别是由看雪论坛上dge兄弟翻译的《Exploit编写系列教程6》以及黑客杂志《Phrack》、《Uninformed》上的相关论文。微软与黑客之间的斗争是永无休止的,我们期待着下一项安全机制的出现……