微软等大型软件厂商为了最大程度地保护系统安全运行,逐渐在操作系统中增加了多种保护机制,致力于减少系统程序漏洞,以及漏洞被触发和利用的可能性。本节主要介绍Windows平台下几种典型的漏洞利用阻断技术,包括/GS、DEP、ASLR及SafeSEH等,同时本节对这些技术的不足也进行了分析,最后对微软为缓解操作系统漏洞被利用而推出的EMET工具进行简要介绍。
Visual Studio 2003中加入了/GS栈溢出检测这个编译选项,微软命名为Stack Canary(栈金丝雀)。金丝雀对空气中的甲烷和一氧化碳浓度的高敏感度使它成为最早的煤矿安全报警器,这里是指,调用函数时将一个随机生成的秘密值存放在栈上,当函数返回时,检查这个堆栈检测仪的值是否被修改,以此判断是否发生了栈溢出。
/GS栈溢出检测选项最初在Visual Studio 2003中默认情况下是关闭的。自Visual Studio 2005以后的版本中,微软将/GS选项改为默认开启,也可以在系统中对其进行禁用设置。如图3-31所示,在Visual Studio 2013中新建一个项目后,可以选择菜单中的“项目”→“项目属性”→“配置属性”→“C/C++”→“代码生成”→“安全检查”找到该选项。
如果编译器启用了/GS选项,那么当程序编译时,它首先会计算出程序的一个安全Cookie(伪随机数,4字节DWORD无符号整型);然后将安全Cookie保存在加载模块的数据区中,在调用函数时,这个Cookie被复制到栈中,位于RET返回地址、EBP与局部变量之间,如图3-31所示;在函数调用结束时,程序会把这个Cookie和事先保存的Cookie进行比较。如果不相等,就说明进程的系统栈被破坏,需要终止程序运行。
在典型的缓冲区溢出中,栈上的返回地址会被数据所覆盖(如图3-32所示),但在返回地址被覆盖之前,安全Cookie早己经被覆盖了,因此在函数调用结束时检查Cookie会发现异常,并终止程序,这将导致漏洞利用的失效。
图3-31 /GS编译选项设置
图3-32 使用/GS选项编译的程序的栈帧结构
/GS保护机制除了在栈中加入安全Cookie之外,在Visual Studio 2008及以后的版本中还增加了对函数内部的局部变量和参数的保护功能,编译器会进行以下操作。
●对函数栈重新排序,把字符串缓冲区分配在栈帧的高地址上,这样当字符串缓冲区被溢出时,也就不能溢出任何本地局部变量了。
●将函数参数复制到寄存器或放到栈缓冲区上,以防止参数被溢出。
从/GS栈溢出检测机制来看,其关键之处就是在栈中加入安全Cookie来保护相关参数和变量,所以对抗这种栈溢出保护机制的直接方法是围绕Cookie值展开的。
(1)猜测Cookie值
Skape曾经在文献 Reducing the Effective Entropy of GS Cookies 中讨论并证明了/GS保护机制使用了几个较弱的熵源,攻击者可以对其进行计算并使用它们来预测(或猜测)Cookie值。但是这种方法只适用于针对本地系统的攻击。
(2)通过同时替换栈中的Cookie和Cookie副本
替换加载模块数据区中的Cookie值(它必须是可写的,否则程序就无法在运行中动态更新Cookie),同时用相同的值替换栈中的Cookie,以此来绕过栈上的Cookie保护。
(3)覆盖SEH绕过Cookie检查
/GS保护机制并没有保护存放在栈上的SEH结构。因此,如果能够写入足够的数据来覆盖SEH记录,并在Cookie检查之前触发异常,那么可以控制程序的执行流程。该方法相当于是利用SEH进行漏洞攻击。虽然有SEH保护机制SafeSEH,但SafeSEH也是可以被绕过的,因而可以同时绕过/GS保护机制。
(4)覆盖父函数的栈数据绕过Cookie检查
当函数的参数是对象指针或结构指针时,这些对象或结构存在于调用者的堆栈中,这种情况下可能导致/GS保护被绕过:覆盖对象的虚函数表指针,将虚函数重定向到需要执行的恶意代码,那么如果在检查Cookie前存在对该虚函数的调用,则可以触发恶意代码的执行。
栈溢出漏洞的最常见利用方式是:在栈中精心构造二进制串溢出原有数据结构,进而改写函数返回地址,使其跳转到位于栈中的Shellcode执行。如果使栈上数据不可执行,那么就可以阻止这种漏洞利用方式的成功实施。而DEP就是通过使可写内存不可执行或使可执行内存不可写,以消除类似威胁的。
DEP是微软随Windows XP SP2和Windows 2003 SP1的发布而引入的一种数据执行保护机制。类似DEP这样的内存保护方式较早就出现了,但是叫法不尽相同,较为通用的称呼是NX,即No eXecute。此外,Intel把它这种技术称为Execute Disable或XD-bit;AMD把它称为Enhanced Virus Protection,也写成W^X,意思就是可写或可执行,但二者绝不允许同时发生。
事实上,不可执行堆栈技术并不新鲜,早在多年前,SUN公司(已被甲骨文公司收购)的Solaris操作系统中就提供了启用不可执行栈的选项,这要早于Windows的DEP技术。早在1993年,NT 3.1系统里就有了VirtualProtect函数的使用,在内存页中包含了是否可执行的标志,但是由于当时的处理器不支持对每个内存页上的数据进行不可执行检查,所以这种标志因缺乏硬件支持而实际上并未发挥作用。
2003年9月,AMD率先为不可执行内存页提供了硬件级的支持,即NX的特性;随后Intel也提供了类似的被称为XD(eXecute Disable)的特性。在硬件支持的基础上,微软开始在Windows系统上真正引入了DEP保护机制。
DEP在具体实现上有两种模式:硬件实现和软件实现。如果CPU支持内存页NX属性,就是硬件支持的DEP。如果CPU不支持,那就是软件支持的DEP模式,这种DEP不能直接组织在数据页上执行代码,但可以防止其他形式的漏洞利用,如SEH覆盖。Windows中的DEP tabsheet会表明是否支持硬件DEP。
根据操作系统和Service Pack版本的不同,DEP对软件的保护行为是不同的。在Windows的早期版本及客户端版本中,只为Windows核心进程启用了DEP,但此设置已在新版本中改变。
在Windows服务器操作系统上,除了那些手动添加到排除列表中的进程外,系统为其他所有进程都开启了DEP保护,而客户端操作系统使用了可选择启用的方式。微软的这种做法很容易理解:客户端操作系统通常需要能够运行各种软件,而有的软件可能和DEP不兼容;在服务器上,在部署到服务器前都经过了严格的测试(如果确实是不兼容,仍然可以把它们放到排除名单中)。
此外,Visual Studio编译器提供了一个链接标志(/NXCOMPAT),可以在生成目标应用程序时使程序启用DEP保护。
DEP技术使得在栈上或其他一些内存区域执行代码成为不可能,但是执行已经加载的模块中的指令或调用系统函数则不受DEP影响,而栈上的数据只需作为这些函数/指令的参数即可。从已有的技术来看,要绕过DEP保护可以有以下几种选择。
●利用ret-to-libc执行命令或进行API调用,如调用WinExec实现执行程序。
●将包含Shellcode的内存页面标记为可执行,然后再跳过去执行。
●通过分配可执行内存,再将Shellcode复制到内存区域,然后跳过去执行。
●先尝试关闭当前进程的DEP保护,然后再运行Shellcode。
从Windows Vista开始,微软向其新版本操作系统中引入了地址空间布局随机化(Address Space Layout Randomization,ASLR)保护机制。其原理很简单:通过对堆、栈和共享库映射等线性区域布局的随机化,增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止漏洞利用的目的。例如,同一版本的Windows XP上系统里DLL模块的加载地址是固定的,那么攻击者只需针对不同操作系统版本进行分别处理即可,但是使用ASLR之后,攻击者必须在攻击代码中进行额外的地址定位操作,才有可能成功利用漏洞,这在一定程度上确保了系统安全。
ASLR保护机制进行随机化的对象主要包括以下几个方面。
1)映像随机化:改变可执行文件和DLL文件的加载地址。
2)栈随机化:改变每个线程栈的起始地址。
3)堆随机化:改变已分配堆的基地址。
对于地址空间布局随机化ASLR机制,微软从可执行程序编译时的编译器选项和操作系统加载时地址变化两个方面进行了实现和完善。
在Visual Studio 2005 SP 1及更高版本的Visual Studio编译器中,均提供了连接选项/DYNAMICBASE。使用了该连接选项之后,编译后的程序每次运行时,其内部的栈等结构的地址都会被随机化。
(1)对本地攻击者无能为力
系统每次重启,ASLR就会在整个系统中生效。如果使用了特定DLL的所有进程都卸载该DLL,那么在下次加载时它能被加载到新的随机地址,但是,如果系统中的DLL总是由多个进程加载,导致只有在操作系统重启时才能再次随机化。
因此,虽然ASLR对于来自网络的攻击,比如像蠕虫之类的攻击行为都能很好地防止,可是,对于在本地计算机的攻击却显得无能为力。对于本地攻击,攻击者很容易获得所需要的地址。
(2)造成内存碎片的增多
ASLR的运行时间越长,其带来的内存碎片也会越多,如果不及时进行清理,有可能会影响系统的效率,特殊情况下甚至会降低系统的稳定性。
当然,为了减少虚拟地址空间的碎片,操作系统把随机加载库文件的地址空间限制为8位,即地址空间为256,而且随机化发生在地址前两个有意义的字节上。
例如,对于地址:0x12345678,其存储方式如图3-33所示。
图3-33 地址存储方式
当启用了ASRL技术后,只有4 3和2 1是随机化的。在某些情况下,攻击者可以利用或者触发任意代码:当利用一个允许覆盖栈里返回地址的漏洞时,原来固定的返回地址被系统放在栈中;而如果启用ASLR,则地址被随机处理后才放入栈中,比如返回地址是0x12345678(0x1234是被随机部分,5678始终不变)。如果可以在0x1234XXXX(1234是随机的,并且操作系统已经把它们放在栈中了)空间中找到有用的跳转指令,如JMP ESP等,则只需要用这些找到的跳转指令的地址的低字节替换栈中的低字节即可,该方法也称为返回地址部分覆盖法。
(3)利用没有采用/DYNAMICBASE选项保护的模块作跳板
当前很多程序和DLL模块未采用/DYNAMICBASE连接选项进行分发,这就导致即使系统每次重启,也并非对所有应用程序地址空间分布都进行了随机化,仍然有模块的基地址没有发生变化。利用程序中没有启用ASLR的模块中的相关指令作为跳板,使得这些跳转指令的地址在重启前后一致,故用该地址来覆盖异常处理函数指针或返回地址即可绕过ASLR。
为了防止SEH机制被攻击者恶意利用,微软通过在.Net编译器中加入/SafeSEH连接选项,从而正式引入了SafeSEH技术。
SafeSEH的实现原理较为简单,就是编译器在链接生成二进制IMAGE时,把所有合法的异常处理函数的地址解析出来制成一张安全的SEH表,保存在程序的IMAGE数据块里面,当程序调用异常处理函数时会将函数地址与安全SEH表中的地址进行匹配,检查调用的异常处理函数是否位于该表中。如果IMAGE不支持SafeSEH,则表的地址为0。
安全结构化异常处理(Safe Structured Exception Handling,SafeSEH)保护机制的作用是防止覆盖和使用存储栈上的SEH结构。如果使用/SafeSEH链接器选项编译和链接一个程序,那么对应二进制的头部将包含一个由所有合法异常处理程序组成的表,当调用异常处理程序时会检查这张表,以确保所需的处理程序在这张表中。这项检查工作是作为ntdll.dll中的RtlDispatchException例程的一部分来完成的,它会执行以下测试。
●确保异常记录位于当前线程的栈上。
●确保处理程序的指针没有指回栈。
●确保处理程序已经在经授权处理程序列表中登记。
●确保处理程序位于可执行的内存映像中。
由此可以看出,SafeSEH保护机制对于保护异常处理程序而言相当有效,但稍后将看到,它也并非绝对安全。
SafeSEH是一种非常有效的漏洞利用防护机制,如果一个进程加载的所有模块都支持SafeSEH的IMAGE,覆盖SafeSEH进行漏洞利用就基本不可能。Windows 7下绝大部分的系统库都支持SafeSEH的IMAGE,但Windows XP/2003等绝大部分系统库不支持Windows的IMAGE。当进程中存在一个不支持SafeSEH的IMAGE时,整个SafeSEH的机制就很有可能失效。此外,由于支持SafeSEH需要.Net的编译器支持,现在仍有大量的第三方程序和库未使用.Net编译或者未采用/safeSEH链接选项,这就使得绕过SafeSEH成为可能。
(1)利用未启用SafeSEH的模块作为跳板进行绕过
对于目前的大部分Windows操作系统,其系统模块都受SafeSEH保护,可以选用未开启SafeSEH保护的模块来利用,比如漏洞软件本身自带的dll文件。在这些模块中寻找特定的某些跳转指令如pop/POP/ret等,用其地址进行SEH函数指针的覆盖,使得SEH函数被重定位到这些跳转指令,由于这些指令位于加载模块的IMAGE空间内,且所在模块不支持SHE,因此异常被触发时,可以执行到这些指令,通过合理安排shellcode,那么就有可能绕过SafeSEH机制,执行shellcode中的功能代码。
(2)利用加载模块之外的地址进行绕过
利用加载模块之外的地址进行绕过,包括从堆中进行绕过和从其他一些特定内存绕过。从堆中绕过,源于这样的缺陷:如果SEH中的异常处理函数指针指向堆区,则通常可以执行该异常处理函数,因此只需将shellcode布置到堆区就可以直接跳转执行。此外,如果在进程内存空间中的一些特定的、不属于加载模块的内存中找到跳转指令,则仍然可以用这些跳转指令的地址来覆盖异常处理函数的指针,从而绕过SafeSEH。
增强缓解体验工具包(Enhanced Mitigation Experience Toolkit,EMET)是微软推出的一套用来缓解漏洞攻击、提高应用软件安全性的增强型体验工具。与前面几种保护机制不同,EMET并不随Windows操作系统一起发布或预装,而是用户可自行选择安装,通过配置可实现对指定应用的增强型保护,但由于操作系统版本的差异性,不同版本的操作系统上所能提供的增强型保护机制也不尽相同,且目前仅支持Windows XP SP3及以上版本。
目前,微软官网提供EMET 5.5版本的下载(http://www.microsoft.com/emet),不过2018年7月31日以后,微软将不再提供该工具的更新支持,微软建议使用安全性更强的Windows 10系统。
EMET的基本保护功能介绍如下。
(1)增强型DEP
自Windows XP SP3起,操作系统内建支持DEP,但对于特定应用程序而言,则还与生成时所使用的编译连接选项有关,同时还需要结合OPT-in和OPT-out的配置来使DEP生效。而EMET则能够通过在指定应用中强制调用SetProcessPolicy来打开DEP保护,使其生效。
(2)SafeSEH的升级版——SEHOP
SEHOP正是看到了SafeSEH被绕过的可能性,从而增加的一项针对目标程序的运行时防护方案——在分发异常处理函数前,动态检验SEH链的完整性。需要说明的是:SEHOP技术在较新的操作系统中已经内建支持了,EMET对于这些版本的操作系统则更多的是一个完善和增强。
(3)强制性ASLR
在前面针对ASLR的介绍中已经提及,ASLR的防护能力的有效性和程序生成时是否采用/DYNAMICBASE连接选项有关,因此EMET对此进行了增强——EMET能够对生成时未使用/DYNAMICBASE连接选项的模块进行加载基址的强制随机化。实现思路则是对于那些动态加载的模块或延迟载入的模块,强制占用其首选基址,从而迫使DLL模块选择其他基址通过重定位的方式实现模块加载。
(4)HeapSpray防护
在前面的章节中,对HeapSpray攻击进行了介绍,针对这种很重要的攻击方式,EMET通过采用强制分配内存、占用常用攻击地址的方式来迫使HeapSpray攻击中的内存分配失效,从而挫败攻击。
体验微软目前提供的/GS、DEP和ASLR漏洞利用阻断技术。
1)在Visual Studio 2013中新建一个Win32控制台应用程序,体验/GS保护机制的作用。
2)了解Windows系统中DEP功能的启用情况。
3)启用ASLR服务。
在Visual Studio 2013中新建一个Win32控制台应用程序,附加选项中将“安全开发生命周期(SDL)检查”取消设置。
运行结果如图3-34所示。由于默认开启了/GS编译选项,系统可以有效地检测到溢出并抛出异常,从而防止缓冲区溢出。
图3-34 【例3-7】运行结果
分别查看使用/GS选项和未使用/GS选项编译的VulnerableFunc函数反汇编代码,如表3-2所示。
表3-2 使用/GS选项和未使用/GS选项编译的反汇编代码比较
观察表3-2中使用/GS选项编译的VulnerableFunc函数反汇编代码可以发现,在函数调用发生时,向栈帧内压入一个额外的随机DWORD值,也就是安全Cookie,该值位于EBP之前,系统还将在内存区域中存放该值的一个副本。
在函数返回之前,系统执行一个额外的安全验证操作,称为Security Check Cookie。在检查过程中,系统比较栈中存放的安全Cookie和原先副本的值。本程序中由于name数组中的字符个数多于output字符数组的长度,产生了溢出,破坏了安全Cookie值,导致与原先副本值不吻合,说明栈核中的安全Cookie已被破坏,即栈中发生了溢出。编译器给出出错提示。
出于性能优化的考虑,Visual Studio会评估程序中哪些函数需要保护,通常只有当一个函数中包含字符串缓冲区或使用_alloc函数在栈上分配空间时,编译器才在栈中设置安全Cookie。此外,当缓冲区少于5个字节时,在栈中也不保存安全Cookie。
如图3-35所示,在Windows 8.1系统中,打开“控制面板”,依次选择“系统和安全”→“系统”→“高级系统设置”→“高级”→“性能”页框中的“设置”,就能看到“数据执行保护”选项卡。
在Windows系统中,Windows Server 2008及以后版本默认情况下启用ASLR,但它仅适用于动态链接库和可执行文件。Windows Server 2008及以上版本都启用ASLR服务,但可以在项目属性中修改/dynamicbase属性,使其取消ASLR保护。
本例在Visual Studio 2013中通过开启和关闭/DYNAMICBASE选项作示例。首先关闭/DYNAMICBASE选项,如图3-36所示。
图3-35 Windows系统中DEP功能设置
图3-36 关闭/DYNAMICBASE选项
在Visual Studio 2013中新建一个Win32控制台应用程序,附加选项中取消选择“安全开发生命周期(SDL)检查”复选框。输入下列代码。
这段程序的目的是输出kerner32.dll基址、loadlibrary函数的入口地址,以及应用程序本身一个函数printAddress()的入口地址。
在不开启ASLR的情况下,选择“调试”→“开始执行(不调试)”命令,可以查看程序初次运行结果,如图3-37a所示,重启系统后,运行结果如图3-37b所示。
图3-37 关闭ASLR情况下的运行结果对比
a)初次运行结果 b) 重启系统后的运行结果
由图3-37可以看出,即使程序本身没有使用ASLR,Kernel32.dll加载地址也发生了变化,这是因为Kernel32.dll库已经选择了被ASLR保护,但是应用程序自身printAddress()函数的地址是固定的。
在开启ASLR的情况下,初次运行结果如图3-38a所示,重启系统后,运行结果如图3-38b所示。
图3-38 开启ASLR情况下运行结果对比
a)初次运行结果 b) 重启系统后的运行结果
由图3-38可以看出,应用程序自身函数printAddress()的加载地址随着系统重启发生了变化,即一旦使用了/DYNAMICBASE选项,生成的程序在运行时就会受到ASLR机制的保护。
拓展阅读
读者要想了解更多Windows内存攻击与保护的方法,可以阅读以下书籍资料。
[1]王清.0 day安全:软件漏洞分析技术[M].2版.北京:电子工业出版社,2011.
[2]DanielRegalado.灰帽黑客:正义黑客的道德规范、渗透测试、攻击方法和漏洞分析技术[M].4版.李枫,译.北京:清华大学出版社,2016.