1.前言
本文通过阅读几位大牛的文章、演讲报告、exploit编写教程等技术文档不断总结修改而成,列举了当前Windows下常见漏洞利用缓解技术的基本原理及绕过方法,具体技术细节则不详细描述,大家可以通过参考文献或其他文章进一步学习。由于本人能力有限,文中可能还存在不少错误,我会不断回顾并完善,也欢迎读者指出错误,共同学习进步。
2.Windows漏洞利用缓解技术简介
自从Windows 成为主流操作系统以后,针对Windows平台的漏洞利用技术不断发展,而微软也不断运用新的漏洞利用缓解技术来封堵漏洞利用技术。根据被引入Windows的时间顺序,先后引入的漏洞利用缓解技术有:GS(Control Stack Checking Calls)、SafeSEH(Safe Structured Exception Handler)、Heap Protection、DEP(Data Execution Prevention)、ASLR(Address Space Layout Randomization)等。
下面用一个例子对这几种保护机制及其绕过方法做进一步说明
int main(int argc, char **argv)
{
Char buf[64];
__try
{
memcpy(buf, argv[1], atol(argv[2]));
}
__except(EXCEPTION_CONTINUE_SEARCH)
{
}
return 0;
}
这个例子的漏洞显而易见,下面让我们在开启不同保护机制的情况下解释如何利用这个例子的堆栈溢出漏洞。
- 无攻击缓解措施
如果在编译链接时不开启任何保护机制,我们只需要溢出buf,改写返回地址,使其指向可预测的shellcode地址。这里我使用jmp esp指令(内存中任何一条jmp esp指令,在当前进程自己的dll中寻找最佳)的地址覆盖main函数的返回地址,并用shellcode进一步覆盖随后的堆栈空间。
当main函数返回时,会从栈中弹出返回地址(此时已经是jmp esp的地址)并跳转至其处继续执行。当jmp esp指令被执行后,eip指向了当前堆栈,此时程序执行流程被转到了已经溢出的堆栈中,shellcode便得到了执行。
- GS
GS(Control Stack Checking Calls)是一种针对堆栈溢出的漏洞利用缓解技术,它主要通过Stack Cookies和Variable Reordering两种技术对堆栈进行保护。
Stack Cookies是由编译器提供的一种运行时堆栈溢出检测技术,开启/GS选项的编译器通过在函数头部与尾部增加代码块的方式来检测堆栈溢出。当函数执行时,这段代码会在堆栈本地变量与函数返回地址之间保存一个随机值(即stack cookies)。函数返回时会检查堆栈中存放的随机值是否被修改,如果被修改则意味着发生了堆栈溢出,从而终止程序运行,阻止恶意代码执行。
Variable reordering是一种在编译阶段通过重新排列变量顺序来最大限度降低堆栈溢出破坏的技术。它重新排列本地变量,将string buffer类型的变量放置在比其他类型变量更高的堆栈地址空间上,防止攻击者通过溢出string buffer改写其他本地变量,在stack cookies被检测前获取控制权。同时,Variable reordering还将函数参数中的指针、string buffer复制到本地变量上方额外分配的堆栈地址空间上,函数内部不再使用原先位于返回地址后方的参数。
下面的示意图是被GS保护的函数堆栈布局
Figure 2‑1
针对开启GS保护的程序,可以通过溢出buf,改写堆栈上的SHE record,因为GS并没有对堆栈上的SHE record进行保护。同时攻击者在stack cookie被检查前触发一个异常,即可通过改写的SHE record获取控制权。
这里,我们确保修改后的SHE record –> Handler为指向pop pop ret指令串的地址(注意,需要确保此指令串位于没有SafeSEH保护的模块中,否则异常分发函数将对其进行验证),而将SHE record –> nextSEH改写成jumpcode(short jmp X)。
当函数发生异常(可以通过buf溢出大量的数据,使得strcpy访问到堆栈的底端)从而索引SEH异常处理函数时,将执行pop pop ret,这串指令从堆栈中弹出nextSEH的地址(此时nextSEH处为跳转指令jumpcode)给EIP,执行jumpcode后将跳转到shellcode,使其获取执行权限。
- GS & SafeSEH
SafeSEH(Safe Structured Exception Handler)是一种针对SHE异常处理的漏洞利用缓解技术,用于阻止攻击者通过改写堆栈上的Exception Handler Record来获取控制权。它包括针对SEH异常处理函数的验证(SHE Handler validation)和针对SEH链完整性的验证(SEH chain validation)两部分。
SHE Handler validation:使用(/SAFESEH)链接选项生成的可执行文件,会在头部包含一个记录所有有效异常处理函数的SafeSEH table,当异常发生时ntdll.dll中的异常分发处理函数会验证堆栈上的Exception Handler Record指向SafeSEH table中一个有效的异常处理函数。如果异常分发处理函数检测出Exception Handler Record被攻击者改写并指向了其它地方,则会终止程序的运行。
SEH chain validation是针对SEH链完整性的验证保护技术,通常这种技术被称为SEHOP(Structured Exception Handler Overwrite Protection),它是在Windows Server 2008被引入的一项保护机制。如果系统开启了SEHOP保护机制,会将ntdll.dll中的FinalExceptionHandler作为最后一个异常处理函数设置到每个SHE链的尾部。当异常发生时,异常分发处理函数会遍历整个异常处理链,确保最后一个Exception Handler Record的nextSEH指针为0xFFFFFFFF,Handler始终指向FinalExceptionHandler,否则将终止程序的运行。
针对开启SafeSEH保护的程序,可以在改写Exception Handler Record时,进一步伪造整个SEH链,从而绕过检测。具体细节不在这里阐述,请参考其他技术文档。
- GS & DEP
DEP(Data Execution Prevention)试图从根本上阻止shellcode的执行。在开启DEP的系统中,代码只能在被标记为execution的页面上执行,因此可以阻止攻击者执行位于堆栈、堆或者数据段中的shellcode。当EIP指向non-execution属性的页面时,系统将直接终止程序执行。可以通过链接选项(/NXCOMPAT)使生成的可执行文件开启DEP保护。
目前,常见的绕过DEP的技术主要分两类,第一类是借用第三方应用程序及组件,利用这些程序可以申请同时支持readable、writeable、executable属性内存页的特点,结合Heap Spray技术实现shellcode的布局与执行。JIT Spray就属于这类技术;第二类是采用代码重用的方法实现对DEP的绕过,包括利用系统API改写内存页属性、调用执行系统命令或加载可执行文件(EXE、DLL等)引入外部代码、改写安全配置(IE浏览器的SafeMode标志位等)。其中利用系统API改写内存页属性是最为常见的利用方法。
针对这个例子,我们利用漏洞改写Exception Handler Record,此时修改SHE record –> Handler指向pop/pop/pop esp ret指令串的地址。并用可以改写页面属性的系统API地址(VirtualProtect、ZwProtectVirtualMemory等)来改写nextSEH。当发生异常时,pop/pop/pop esp ret将被执行,其中pop esp将改变堆栈指针,使esp其指向nextSEH。因此ret指令执行后,将跳转到用于改写页面属性的系统API处。通过系统API将页面改写为execution属性,最后跳转到shellcode处继续执行。
- GS & DEP & SafeSEH
针对同时开启GS、DEP、SafeSEH的应用程序,实现漏洞利用的原理与上面类似,不同的是还需要伪造SEH链,这里不再详细叙述。
- GS & DEP & SafeSEH & ASLR
ASLR(Address Space Layout Randomization)是从Vista和Windows Server 2008起引入的一项重要的漏洞利用缓解技术。系统在加载支持ASLR技术的可执行程序、动态链接库等映像文件时,会对其加载位置进行随机化处理,使得重启后每次加载进内存的基址不同。因此利用程序编写者就无法找到稳定可靠的jmp esp、pop pop ret、xchg eax,esp等指令地址,更无法定位系统API的位置,特别是在DEP、ASLR同时开启的情况下,会大大增加漏洞利用的难度。
ASLR会对加载的PE文件、堆、堆栈、PEB、TEB等的位置进行随机化处理。可以通过链接选项(/DYNAMICBASE)使生成的可执行文件开启ASLR保护。
面对ASLR和DEP,许多攻击者需要解决两个关键问题,首先是如何通过漏洞获取到模块(目标进程、系统或其他DLL)基址,由此才有可能利用系统API进行代码重用;其次是定位攻击者布局在堆栈、堆上的shellcode地址(通常由stack pivot、ROP chain、payload等组成)。
目前,常见的获取模块基址的方法有两种,第一种是查看系统或应用程序运行时是否加载了non-ASLR的模块,是否有其他外部的non-ASLR模块可以被引入当前进程空间,比如Java 6运行环境JRE1.6的msvcr71.dll、Office 2010/2007的hxds.dll(当在URL中使用ms-help://时引入)等。这些non-ASLR模块每次加载到进程内存空间时的基址都是固定不变的,因此可以在他们导入表中索引系统DLL的基址,在其代码空间中搜索可执行指令小配件(gadget)等;第二种是通过内存信息泄露漏洞获取有关内存布局、目标进程相关的状态信息等。例如可以通过静态变量的指针、虚函数表指针等暴露出目标进程、系统或其他DLL的基址,从而进一步获取相关API地址及有用的gadget等。(后文会对bypass ASLR的相关技术进一步讨论)
为了定位攻击者布局在堆栈、堆上的shellcode地址,最常见的方法是Heap spray技术,通过连续大量的内存申请,将shellcode布局在指定的内存地址上。这种方法存在稳定性不高、消耗时间和资源多、容易被检测的缺点;因此,最好的方法还是使用漏洞读取出shellcode地址,其限制条件为漏洞类型,需要一个具有任意地址读写的漏洞来配合利用。
针对这个例子的漏洞场景,攻击者的可控程度较低,无法在GS & DEP & SafeSEH & ASLR漏洞利用缓解技术都开启的情况下实现利用。但是,当类似这样的一个漏洞场景发生在浏览器、Adobe Flash、Adobe Reader、Office等应用软件中时,结合信息泄露类漏洞,攻击者则会实现完美利用。因为攻击者可以使用JavaScript、ActionScript、VBScript等脚本语言进行内存操作(内存申请与释放、创建特殊对象等)从而使得利用变得可行。
由上面的简介我们知道,即便Windows已经逐步引入了新的漏洞利用缓解技术来阻止恶意代码运行,但这些漏洞利用缓解技术只是提高了编写漏洞利用程序的难度,并不能完全阻止恶意代码的运行。通过研究,仍然可以通过已知或未知的方法对其进行利用。
当前,IE浏览器、Adobe Flash、Adobe Reader、Office等是人们日常办公、娱乐生活中经常使用的应用软件。因此,恶意代码编写者常常针对它们,利用挖掘到的漏洞进行攻击,使得恶意代码传播面广、影响范围大。业内安全厂商、研究人员也都集中精力针对这些软件的漏洞挖掘、利用技术进行分析与研究,下面主要针对IE浏览器,具体讲解ASLR & DEP绕过技术的原理。
3.绕过ASLR
FireEye在其博客上曾发表一篇文章——《ASLR Bypass Apocalypse in Recent Zero-Day Exploits》,通过举例说明了最近0day漏洞利用中所使用的三类绕过ASLR的技术:Using non-ASLR modules、Modifying the BSTR length/null terminator、Modifying the Array object。从本质上来讲第二类和第三类技术一样,都是通过一系列的内存破坏(Memory Corruption),最终泄露出关键模块的内存基址,并获取控制权。下面参考FireEye的文章分别对三类技术的原理进行介绍。
3.1. 利用non-ASLR模块
这是一种最简单也最常见的对抗ASLR保护的技术,因为现在仍然有许多应用程序及其模块在编译时没有开启ASLR保护(/DYNAMICBASE链接选项)。系统每次都会在固定的内存基址上加载它们,恶意代码编写者便可以从这些non-ASLR模块中挑选指令(gadget)、索引系统函数,实现关闭DEP、获取控制权等。
常见的未开启ASLR保护的模块有Java 6运行环境JRE1.6的msvcr71.dll, office 2007/2010的HXDS.DLL。HXDS.DLL会在浏览器加载一个带有“ms-help://”的URL时被加载进内存,但是微软已经通过补丁修补了这个问题,现在的HSDS.DLL已经开启了ASLR保护。而Java 7运行环境JRE 1.7也已经全部开启了ASLR保护。
随着越来越多的应用程序及其模块使用ASLR保护,今后使用这种利用方法通用性将越来越低。
3.2. 修改BSTR的长度或终止符
3.2.1. BSTR
BSTR是一种字符串数据类型,一种Pascal-Style字符串(明确标示字符串长度)和C-Style字符串(以结尾)的混合物,主要应用于COM、交互功能等。它是一种复合的数据类型,由一个长度前缀,数据字符串和一个终止符组成,如下图所示:
其中数据字符串string为UNICODE编码,但是也可以存储其他非\x00\x00的字符串。
3.2.2. 利用方法
这种利用方法首次出现是在Pwn2own 2010上,它只适用于那些可以重写内存的特殊类型的漏洞,例如缓冲区溢出、任意地址写、增加或减少内存指针处的内容。
当某个内存破坏漏洞可以满足修改任意4字节内存的最终效果时,便可以利用它来修改指定位置处的BSTR的长度前缀,从而使得此BSTR可以访问它原始界限以外的内存。由此最终可以获取适合构造ROP链的链接库的精确位置,从而实现了绕过ASLR。当通过此方法绕过ASLR后,便可通过同样的内存破坏漏洞改变执行流程获取控制权。
例如下面的一个情形,首先在内存中连续创建一定数量且大小相同的BSTR,它们在内存中的布局如Figure 3‑2所示。
释放其中的一个BSTR,然后立刻申请相同大小的一个object,相同大小是指object最终在内存中所占用的大小与BSTR在内存中占用的大小一致。由于前端堆Low Fragmentation Heap的作用,申请的object很可能会被分配到刚刚释放的BSTR内存空间上。因此此时的内存布局如图所示。
此时修改Object前面BSTR的长度,就可以通过越界读,读取到object对象的虚函数表指针,最终计算出相关模块的内存基址。
很多时候内存破坏类漏洞可控程度并不高,不足以修改4字节(不能用来修改BSTR的长度),只能对内存指针处的1、2字节进行修改。在这种情况下我们可以通过修改BSTR的终止符,从而将string与后面的object连接起来。随后访问修改后的BSTR,object也会作为BSTR的一部分被访问到,攻击者便可以计算出相关模块的基址。
3.3. 修改Array对象长度
3.3.1. 利用方法
这种修改Array对象长度的利用方法与修改BSTR长度的利用方法类似,同样需要那些可以重写内存的内存破坏类型的漏洞。但是从攻击者的角度来看,可以通过这种利用方法实现绕过ASLR的漏洞更具用户友好性,因为一旦Array对象的长度被修改,攻击者就可以通过数组获取任意内存读写的能力,同时更容易控制程序的执行流程,实现代码执行。下面通过VUPEN在Pwn2Own2013上利用的CVE-2013-2551漏洞为例,描述此方法的利用原理。
这个漏洞是由于负责VML解析的模块VGX.DLL,在处理<v:stroke>标签的dashstyle.array.length属性时,没有对传入的参数进行完备验证而导致的整数溢出漏洞。攻击者利用这个漏洞能够对任意地址进行读写操作——通过读取敏感内存信息、改写对象虚表,就能够完美绕过重重内存防护机制实现任意代码执行。
首先说明一下,VML中使用JS语句对dashstyle属性赋值时,例如:stroke.dashstyle = “1 2 3 4″,函数vgx!ParseDashStyle会被调用,将控制流转向_MsoFCreateArray函数来创建一个ORG数组。在_MsoFCreateArray中调用_MsoFInitPx函数根据数组成员个数来进行内存分配,每个数组成员占四字节内存,所以ORG数组的缓冲区大小为数组成员个数×4(byte)。在对dashstyle.array.length属性赋值时,数组成员的个数会改变,在其内部调用的vgx!COALineDashStyleArray::put_length函数中会根据新设置的值来重新为ORG数组分配内存。然而在put_length中存在一个整数溢出漏洞,使得可以不通过新的内存分配而将dashstyle的Array对象长度设置为0xffff。此漏洞的利用原理如下。
在堆上分配连续的 COARuntimeStyle对象,每个对象占用0xB0字节,并在中间通过设置dashstyle穿插进一个ORG数组。控制数组的大小,使其也为0xB0字节。此时内存布局如图所示。
利用漏洞修改dashstyle的长度,使得可以通过ORG数组越界访问随后的数据。 ORG数组的长度被修改后,循环给每个 COARuntimeStyle的marginLeft属性赋值,在循环中同时利用已经溢出的ORG 数组去读取指定偏移处的值(即读取ORG 数组随后 COARuntimeStyle对象保存marginLeft属性的地址),如果其值大于0,说明此时索引到ORG 数组之后的那个 COARuntimeStyle对象。此时利用ORG 数组越界写 COARuntimeStyle对象中保存marginLeft属性的地址的值,修改其为0x7ffe0300。越界写完成后,通过 COARuntimeStyle对象的get_marginLeft来读取marginLeft属性。于是 COARuntimeStyle就会将0x7ffe0300处的值读取出来,而0x7ffe0300处正好保存着系统调用入口函数ntdll!KiFastSystemCall的地址,通过计算便可得到ntdll的内存基址,由此便绕过了ASLR。
此处也可以通过ORG数组越界读随后COARuntimeStyle对象的前4字节(虚函数表指针),最终减去相应偏移即可得到当前vgx.dll的内存基址。
4. 绕过DEP
DEP技术在XP SP3被引入,但仅依靠GS & DEP & SafeSEH技术仍不足以阻止所有的恶意代码。第2节中已经简要介绍了常见的两类Bypass DEP技术。目前0day漏洞利用中常采用代码重用的方法实现对DEP的绕过,包括利用系统API改写内存页属性、调用执行系统命令或加载可执行文件(EXE、DLL等)从进程空间外引入代码、改写安全配置(例如IE浏览器的SafeMode标志位[6]等)。
其中,利用系统API改写内存页属性和加载可执行文件(EXE、DLL等)从进程空间外引入代码是最为常见的利用方法,其核心是ROP技术。它将堆栈切换到攻击者伪造的堆栈上,利用精巧构造的堆栈实现DEP的绕过。下面将主要说明ROP的技术原理。(注:TK提出的LdrHotPatchRoutine技术等方法不需要使用ROP)
4.1. ROP技术
ROP的全称为Return Oriented Programming,它是目前最常用的绕过DEP的技术。它利用了攻击者可以控制程序执行时的数据这样一个事实,攻击者向进程内存空间中注入一个伪造的调用堆栈,然后执行一个stack pivot(堆栈反转)指令,使得堆栈指针转换到伪造的调用堆栈上,伪造的堆栈可以被认为是记录着因果链(ROP chain,由一个个ROP Gadget组成,下文具体解释)。
在正常情况下,当一个函数返回时,正常的堆栈中保存着函数的返回地址,即父函数调用子函数时的下一条指令的地址。然而当堆栈指针转换到伪造的调用堆栈后,当函数返回时将从伪造堆栈中获取返回地址,此时的返回地址是指向进程空间中的一个ROP Gadget。
ROP Gadget代表进程空间中任何有用的可执行指令串(位于可执行页面上),并且这些可执行指令串都以 ret指令结尾,从而使得指令串执行完毕后仍然从伪造的堆栈中获取返回地址(下一个ROP Gadget的地址)。 要保证目标程序每次运行时,伪造堆栈中ROP Gadgets的地址都是可预期且可靠的,攻击者必须先绕过ASLR,获取相关模块的内存基址,然后根据指令偏移(硬编码,每个模块中的指令偏移固定不变)得到确定的ROP Gadget地址,或者通过动态搜索的方法在内存空间中搜索ROP Gadget构建ROP chain。
在伪造堆栈上构造一系列ROP Gadget的目的就是针对要利用的漏洞布置“有用”的环境(例如调整堆栈指针、设置寄存器值等),因为最后一个ROP Gadget将指向VirtualAlloc、VirtualProtect、WriteProcessMemory、ShellExecueEx、LoadLibrary等函数的地址,它们将用于修改指定内存页为可执行或者从进程空间外引入代码并执行。最终使得堆栈、堆上的数据变成可执行或直接加载运行可执行文件,从而绕过DEP的保护。
ROP利用方法之所以能够工作是因为攻击者欺骗系统使用攻击者可控的数据,而这些数据也并不需要得到执行权限,攻击者只需要控制EIP接下来执行的地址从而执行需要的指令和函数。当伪造的堆栈被注入进程空间后(通过Heap spray[4]在指定地址处布局shellcode、通过漏洞获取shellcode的地址),ROP方法的执行流程[5]简单总结如下:
1) 执行“stack pivot”指令(例如xchg eax, esp; ret等),将堆栈指针转换到伪造堆栈上。
2) 从伪造堆栈的顶部开始执行ROP Gadget
a) 执行一些“有用”的指令串(push/pop/add/sub…)
b) 执行ret指令(如果伪造堆栈上的返回地址处仍然是一个ROP Gadget,则继续跳转至执行;如果返回地址是一个函数地址如VirtualProtect等,则利用伪造堆栈上布置好的参数执行函数。执行完毕后即可跳转至伪造堆栈上设置好的返回地址,运行payload)
5. 通用绕过方法
已经有研究者公布出了更具通用性的绕过ASLR、DEP的方法,例如Dion Blazakis提出的JIT spray(Just In Time Compilation),于旸(tombkeeper)发现的LdrHotPatchRoutine等。但是研究者也发现,在自然环境下,从来没有0day攻击使用这些方法对抗ASLR,原因是这些方法公开以后,就很快会被修补掉。下面简单介绍一下其实现原理,技术细节请参考其他技术文档。
5.1. JIT spray技术
JIT Spray 是一种利用“及时编译”的特性来绕过ASLR、DEP的利用方法。一个“及时编译器”根据定义生成代码作为它的数据。因此它的目的就是生成可执行数据。JIT编译器是一种不能运行在“no-executable-data”环境下的程序,因此,JIT编译器会免除DEP。而JIT Spray攻击就是用JIT编译器“生成的代码(利用代码)”来进行堆喷射的一种攻击方式。
5.2. LdrHotPatchRoutine 技术
从Windows NT 4到Windows 8,SharedUserData的位置一直固定在地址0x7ffe0000,并且在x86下0x7ffe0300处也一直保存这KiFastSystemCall的地址。
而x64下7ffe0350则保存着LdrHotPatchRoutine的地址,在它内部会索引LdrLoadDll来加载外部的DLL,从而可以实现任意代码的执行。
5.3. DEV技术
DEV技术最早由yuange提出,其基本思想是利用脚本语言的先天优势,即脚本语言是解释执行的,代码、shellcode都是数据。通过漏洞修改关键数据结构,最终实现作为脚本的shellcode的执行。关于DEV的技术细节请参见参考文献[7]。
5.4. Undocuments
相信还有其他未公开的通用方法,能够像LdrHotPatchRoutine技术、JIT spray技术、DEV技术一样不需要通过精心的内存布局,从而实现绕过ASLR、DEP。这些未公开的技术具有更高的价值。
6. 参考文献
[1] Bypassing Browser Memory Protections-Setting back browser security by 10 years [EB/OL]. Alexander Sotirov, Mark Dowd. 2008.
[2] The info leak era on software exploitation [EB/OL]. Fermin J. Serna. 2012.
[3] ASLR Bypass Apocalypse in Recent Zero-Day Exploits [EB/OL]. Xiaobo Chen. 2013.
[4] Heap Feng Shui in JavaScript. [EB/OL]. Alexander Sotirov. 2007
[5] ROP (Return Oriented Programming) [EB/OL]. Neil Sikka. 2014.
[6] Subverting without EIP. [EB/OL]. MALLOCAT. 2014
[7] APT 高级漏洞利用技术. [EB/OL]. yuange. 2014
原创文章,作者:BFS,如若转载,请注明出处:https://www.isclab.org.cn/2014/11/12/windows%e6%bc%8f%e6%b4%9e%e5%88%a9%e7%94%a8%e6%8a%80%e6%9c%af%e6%80%bb%e7%bb%93/