您现在所在的位置:首页 >> 通知公告 >> 通知公告 >>
发布日期:2014年11月19日
CVE-2013-2551漏洞成因与利用分析

1. 简介

VUPEN在Pwn2Own2013上利用此漏洞攻破了Win8+IE10,5月22日VUPEN在其博客上公布了漏洞的细节。它是一个ORG 数组整数溢出漏洞,由于此漏洞的特殊性,使得攻击者可以通过整数溢出修改数组的长度,获取任意内存读写的能力,最终绕过ASLR并获取控制权限实现远程代 码执行。

在漏洞公布不久,就有人写出了对应的metasploit利用代码,不过其代码只支持Win7+IE8环境。通过分析,想要在 Win7+IE10下成功利用此漏洞需要解决几个关键问题,并运用几个技巧。本文首先参考已有的分析文章,根据自己的理解对漏洞成因再次进行综合分析,记 录分析过程及结果,方便小组成员学习。然后针对IE8、IE10两个环境,对漏洞利用所使用的技术及技巧进行总结。

 

2. 实验环境

操作系统:Win7 SP1 (6.1.7601.17514)

浏览器:IE10.0.9200.16540

漏洞编号:CVE-2013-2551

 

3. 漏洞分析

这个漏洞是由于负责VML解析的模块VGX.DLL,在处理<v:stroke>标签的 dashstyle.array.length属性时,没有对传入的参数进行完备验证而导致的整数溢出。攻击者利用这个漏洞能够对任意地址进行读写操作 ——通过读取敏感内存信息、改写对象虚函数表,就能够完美绕过重重内存防护机制实现任意代码执行。

 

3.1. VML

VML(Vector Markup Language)矢量标记语言是SVG的前身,它仍然被IE10所支持,通过它可以在页面中表现二维矢量图形。值得说明的是IE10在默认情况下是不支 持VML的,但通过在HTML中加入有关对文档模式的声明,可以使IE10正常解析VML标签。VML被vgx.dll(C:\Program Files\Common Files\Microsoft Shared\VGX)所实现,可以通过附件中的VML.html来简单了解VML的使用。

 

3.2. 漏洞成因

VML为shape元素实现了一个Stroke子元素,Stroke子元素有一个DashStyle属性。DashStyle可以是一个常数也可以是一个自定义的类型。例如下面的使用方法:

当通过JS读取dashstyle属性时,其内部会调用”vgx!COAStroke::get_dashstyle()”,随 后”vgx!COAStroke::get_dashstyle()”调用子函 数”vgx!COAShapeProg::GetOALineDashStyle()”并最终返回一个COALineDashStyle对象。

dashstyle属性的功能是被COALineDashStyle对象实现的,COALineDashStyle对象的方法如下,其中get_value/put_value是针对dashstyle为常数的情况。

如果dashstyle属性是自定义类型,调用COALineDashStyle::get_array()方法,将会在其子函数 COAShapeProg::GetOALineDashStyleArray()中返回一个COALineDashStyleArray对象。

 

COALineDashStyleArray对象用来管理自定义类型的dashstyle属性,其方法如下:

设置或者读取dashstyle.array.length属性时,对应的put_length或get_length函数就会被调用;利用数组下标访问dash.array属性时,对应的put_item或get_item函数就会被调用。

 

使用JS语句对dashstyle属性赋值时,例如:stroke.dashstyle = “1 2 3 4″,函数vgx!ParseDashStyle会被调用,将控制流转向_MsoFCreateArray函数来创建一个ORG数组。在 _MsoFCreateArray中调用_MsoFInitPx函数根据数组成员个数来、进行内存分配,每个数组成员占四字节内存,所以ORG数组的缓冲 区大小为数组成员个数×4(byte)。在对dashstyle.array.length属性赋值时,数组成员的个数会改变,在put_length函 数中会根据新设置的值来重新为ORG数组成分配内存。然而在put_length中存在一个整数溢出漏洞,漏洞相关代码如下:

 

 

 

可以看出漏洞成因就在于put_length函数判断当前长度current_length和desired_length长度时,使用跳转指 令是用于判断有符号数的jge指令,而数组长度是一个unsiged int类型的变量。因此当传入desired_length的值为有符号数0xFFFFFFFF时,会导致跳转至loc_1008B844处,进而调用 ORG::DeleteRange。执行流程 为:ORG::DeleteRange–>MsoDeletePx–>MsoFRemovePx。

进入MsoFRemovePx函数时,arg_4=current_length-desired_length,arg_8=desired_length。

 

在MsoFRemovePx函数内部,最终将desired_length设置成了current_length,使得在没有重新分配内存的情况下将ORG数组长度current_length改写成0xFFFF。因此,可以通过此时的ORG数组获得越界访问。

 

要想成功触发漏洞,须使用JS将dashstyle.array.length的值设置为0xFFFFFFFF,但是如果直接使用语句 dashstyle.array.length = 0xFFFFFFFF。会提示错误有”溢出”发生,将0xFFFFFFFF改为(0-1)就能够成功触发漏洞了。

“漏洞成因”结合“信息泄露”的源代码在附录poc (CVE-2013-2551).html中给出,请参见附件。

 

4. 漏洞利用

 

4.1. Bypass ASLR

 

4.1.1.  信息泄露

VML shape的_vgRuntimeStyle属性由COARuntimeStyle对象负责处理,它包含很多方法,如下

 

使用JS语句设置或者读取_vgRuntimeStyle.marginLeft属性时,对应的COARuntimeStyle::put_marginLeft()或者COARuntimeStyle::get_marginLeft()函数就会被调用。

 

如果是第一次访问marginLeft/rotation属性,那么在put_marginLeft/put_rotation函数中会调用 CVMLShape::GetRTSInfoàCParserTag::GetRTSInfo来创建一个COARuntimeStyle对象,该对象大小 为0xAC(实际分配0xB0)。

而marginLeft属性的值对应的字符串指针就保存在该COARuntimeStyle对象的0x58偏移处。同样get_marginLeft函数在读取marginLeft属性时,也是通过访问这个对象偏移0x58位置上的字符串指针来完成的。

 

COARuntimeStyle::put_marginLeft()

 

因此,我们可以设置堆内存,使得ORG数组在一个COARuntimeStyle对象前面。然后通过ORG数组用任意值(这里使用 0x7ffe0300)越界重写marginLeft属性的字符串指针,再使用COARuntimeStyle::get_marginLeft()将任 意内存地址(0x7ffe0300)处的内容读取出来。0x7ffe0300处保存着ntdll!KiFastSystemCall的地址,通过此地址减 去相应偏移即可得到当前ntdll.dll的内存基址。

也可以通过ORG数组越界读随后COARuntimeStyle对象的前4字节(虚函数表指针),最终减去相应偏移即可得到当前vgx.dll的内存基址。

信息泄露部分的源代码见附件poc (CVE-2013-2551).html

 

4.1.2. 定位shellcode

定位shellcode有两种方法可控选择,使用的最多的方法就是堆喷射,但是它并不是一种完美的利用方法。完美的利用方法是通过漏洞读取出shellcode的地址。我使用两种方法分别实现了IE10下的漏洞利用。

4.1.2.1. 通过堆喷射布局shellcode

使用堆喷射可以方便的布局我们的shellcode在指定的内存处。在IE8下,堆喷射使用常见的heapLib技术。IE10下,heapLib不再奏效,我们可以通过DEPS技术实现堆喷射。具体技术资料及源码很多,此处不再赘述。

4.1.2.2. 通过漏洞定位shellcode

由于此漏洞的特殊性,使其具有任意地址读的能力。参见4.1.1信息泄露部分,我们可以将shellcode作为 COARuntimeStyle的marginLeft属性值,再通过触发漏洞使用ORG数组越界读取出shellcode的地址。其原理与“信息泄露” 一致,主要在于代码实现,请参见“CVE-2013-2551_MS13-037(IE10noHeapSpray).rb”。

 

4.2. 获取控制权

 

当通过JS读取_anchorRect属性时,其内部会调用”vgx!COAShape::get__anchorRect()”,最终创建并返回一个0x10大小的COAReturnedPointsForAnchor对象。

 

 

因此有这样一种利用场景:创建大量的COAReturnedPointsForAnchor对象,在其中为Dashstyle属性创建具有4个 元素的ORG数组使其内存布局刚好位于大量COAReturnedPointsForAnchor对象之间。然后利用漏洞通过ORG数组越界改写其后 COAReturnedPointsForAnchor对象的虚函数表指针,从而获取控制权。具体步骤如下:

 

1) 创建大量的COAShape元素(v:shape)

2) 遍历每个COAShape的_anchorRect属性,这样就可以创建COAReturnedPointsForAnchor元素,在遍历的过程中,某 个地方给dashstyle属性赋值,这样会创建ORG对    象,为保证ORG对象和COAReturnedPointsForAnchor对象在同 一个堆块并且地址连续,ORG的元素数目为4个,正好 4×4 = 0x10。

3) Dashstyle.array.length = -1,触发漏洞。由于漏洞被触发,Dashstyle.array的长度被修改为0xffff,可以越界写内存

4) dashstyle.array.item(6) = 0x0c0c0c0c,修改ORG后面的COAReturnedPointsForAnchor虚表指针。Item(6)是为了跳过堆首部的8个字节。

5) 释放_anchorRec元素,当被修改虚表指针的元素被删除时,会通过虚表调用Release函数,这样就可以被攻击者获得控制权。

 

4.3. Bypass DEP

 

4.3.1. IE8

首先来看当被修改虚函数表指针的COAReturnedPointsForAnchor对象将要被释放时,索引其Release虚函数的过程。

 

如上图所示,(98行)ecx保存着对象UserPtr地址,并将其首4字节(虚函数表指针)传递给eax,然后(100行)调用虚函数表的第 三个函数。而在“获取控制权”阶段我们已经将对象的首4字节改写成了0x0c0c0c0c,因此它将在0x0c0c0c0c处的伪造虚函数表中索引虚函数 表并调用。

在IE8下通过常规的heapLib技术进行堆喷射,将shellcode布局在0x0c0c0c0c处。并在0x0c0c0c0c处伪造虚函数表,构造ROP chain。

如上图所示,0x0c0c0c0c处就是根据之后虚函数被调用顺序,构造的ROP chain,将切换堆栈的指令(xchg eax,esp)的地址放在了0x0c0c0c0c + 0x08处,这样当程序接下来调用虚基表中的第3个虚函数Release时,就在伪造的虚函数表0x0c0c0c0c处索引第三个函数地址,并执行 xchg eax,esp; retn指令

注意,当调用“第三个虚函数”时,eax保存着虚函数表指针(此时为0x0c0c0c0c),ecx保存着对象UserPtr指针。其随后的执行流程如下:

1) 通过xchg eax,esp指令可将堆栈切换到0x0c0c0c0c上,使得esp指向0x0c0c0c0c处。随后执行retn指令时,将会在新的堆栈(0x0c0c0c0c)上索引返回地址并

跳转执行。

2) 执行0x0c0c0c0c处第一个4字节地址所指向的指令——retn。继续在堆栈上索引返回地址。

3) 执行0x0c0c0c0c处第二个4字节地址所指向的指令——pop ebx; retn。意在跳过0x0c0c0c0c处的第三个4字节,并将第四个4字节的值作为返回地址并执行。

4) 第四个4字节为实际为ntdll!ZwProtectVirtualMemory函数地址,其返回地址、调用参数已经硬编码到随后的shellcode中。其目的在于修改0x0c0c0c40处0x400字

节大小的内存空间的属性,将其修改成可执行页面。

5) ntdll!ZwProtectVirtualMemory执行完毕后从堆栈上索引返回地址,跳转到0x0c0c0c40处继续执行,最终绕过了DEP,实现了任意代码执行。

 

4.3.2. IE10

在IE10下,我们将面对两个关键问题。首先是以前常用的堆喷射技术(heapLib)不再奏效;其次是编译器做了调整,在索引虚函数时eax 不再保存虚函数表指针,转而保存对象UserPtr指针,并由ecx来保存虚函数表指针。其造成的结果是指令串xchg ecx,esp; retn;对应的字节码变长(变为87E1C3,而xchg eax,esp; retn为94C3),使得在内存中很难搜索到能够满足类似要求的指令串,从而无法轻易控制esp。(IE10下相关代码如下图所示)

 

针对IE10,我找到了可替代的堆喷射方法——DEPS,关于DEPS的技术细节及实现请参见参考资料链接。

而当在ntdll.dll中尝试搜索实现类似功能的指令串(xchg ecx,esp; retn;)无果后,需要重新理清思路。想明白我们能控制什么,尽最大可能利用它们仍然可以绕过阻碍。

4.3.2.1. 堆喷射下ROP chain构造

在IE10下使用DEPS技术仍然可以进行堆喷射,并把我们的shellcode部署在指定内存空间,demo中将shellcode部署在 0x0c0c2228处。此时我们重点关注如何在call dword ptr [ecx+8]时控制堆栈,使得esp切换到指定的内存空间处(0x0c0c2228)

在IE8下,我们通过漏洞使用ORG数组越界改写了对象的虚函数表指针,最终对象在释放过程中将索引我们伪造的虚函数,并在一个ROP gadget中实现对堆栈的控制。但IE10下保存虚函数表指针的寄存器变成了ecx,保存对象指针的寄存器变成了eax,此时无法在一个ROP gadget中实现类似xchg ecx,esp; retn的功能。

此时,我们面对的情况为:保存对象指针的eax、保存对象虚函数表指针的ecx、一个具有越界读写对象能力的ORG数组。

因此,可以使用ORG数组进一步越界改写对象头部,利用容易找到的xchg eax,esp; retn指令串先将堆栈切换到对象内存空间上。然后再通过其他的ROP gadget将堆栈切换到shellcode处(0x0c0c2228)。实现对堆栈esp的完全控制。

首先通过mona.py搜索ntdll.dll中所有可能的gadget后。在其搜索结果中寻找可用的ROP gadget。最终效果如下:

 

由于我们使用了堆喷射技术对shellcode进行布局,因此我们能够确切的知道其内存位置(0x0c0c2228),在调用 ntdll!ZwProtectVirtualMemory修改shellcode内存空间可执行权限时基本不存在问题,可以通过硬编码设置当前堆栈上的 返回地址、参数值等。如下图所示:

 

4.3.2.2. 无堆喷射下ROP chain构造

  • 获取shellcode地址

在4.1.1信息泄露部分,已经介绍了利用COARuntimeStyle对象及其marginLeft属性来读取任意地址处的值。如果将 shellcode作为COARuntimeStyle的marginLeft属性,再通过触发漏洞,可以很容易获得shellcode的地址,这样就可 以不使用heap spray,实现完美的利用。利用方法如下:

其中#{js_code}为shellcode,marginLeftAddress即为读取到的shellcode地址。

 

  • 构造ROP chain

由于shellcode地址是动态获取的,因此在构造ROP chain(尤其是在堆栈上为ntdll!ZwProtectVirtualMemory构造返回地址及参数)时不能使用硬编码。构造ROP chain是一项考验并激发创造力的体力活,其中mona.py为我们创造了条件,提供了很多便利。

在为其构造ROP chain时运用了几个小技巧,下面一一讲解,首先来看函数的定义

其总体思想是将ntdll!ZwProtectVirtualMemory函数调用的函数地址、返回地址、参数依次传递给edi、esi、 ebp、esp、ebx、edx、ecx。然后使用pushad; retn指令串将其布局在堆栈上并调用,调用pushad; retn指令串前的要求寄存器环境为:

 

因为ntdll!ZwProtectVirtualMemory函数的参数中有指针,参数NumberOfBytesToProtect即为一 个指针,指向要修改保护属性的字节数。在ROP chain中可以使用push esp; pop; pop retn的指令串来动态获得当前的地址,例如下图122行代码所示,其ROP gadget将NumberOfBytesToProtect的地址传递给了esi,最终传递给了ebx

ntdll!ZwProtectVirtualMemory的参数BaseAddress也是一个指针,指向要修改内存属性的内存基址。调用 pushad; retn前,要使esp指向想要修改内存属性的内存基址也需要一点小技巧,我的实现如下图。143行代码首先获取当前堆栈的地址,然后将其加0x40字 节,并写入其值所指的内存中。即指针所指的内存空间保存着指针的地址。

最后,真正的返回地址(esi)要指向最终的payload,它将会被布局在ROP chain之后,即pushad; retn指令串之后的+4字节处(pushad; retn指令串之后的4字节保存BaseAddress)。因此,还需要将eax加4字节再传递给esi。

 

由于在shellcode中不能出现/u0000,因此在指定NewAccessProtection时,通过加法指令溢出寄存器,使其变为0x00000040。其实现过程如下:

 

至此,构造ROP chain时所运用的几个技巧已经讲解完毕。此处也可以将shellcode分开存放,即将payload作为COARuntimeStyle的 marginLeft属性存放;将伪造的虚函数表及用于改写payload所在页面属性的ZwProtectVirtualMemory参数布局在 ntdll.dll的.rsrc处,即可省去复杂的ROP构造;

构造ROP chain虽然是体力活,但也考验并激发创造力。有点像走一根有障碍的独木桥,想明白自己能控制什么,尽最大可能利用它们绕过阻碍,最终展现那奇妙可能性时的成就感妙不可言。

 

5. 总结

网上也已经有多篇文章对CVE-2013-2551漏洞成因与利用方法进行了分析说明。个人认为它是学习漏洞利用技术非常好的一个例子,通过这一个漏洞可以了解学习到漏洞利用过程中的多种技术与技巧。

                                                                                                               ——信息系统及安全对抗实验中心 韦伟

6. 参考资料

[1] Advanced Exploitation of Internet Explorer 10 / Windows 8 Overflow (Pwn2Own 2013):http://www.vupen.com/blog/20130522.Advanced_Exploitation_of_IE10_Windows8_Pwn2Own_2013.php

[2] CVE-2013-2551利用技巧分析:http://hi.baidu.com/4b5f5f4b/item/33d5c21e33ed12181994ec6c

[3] CVE-2013-2551利用分析:http://zenhumany.blog.163.com/blog/static/171806633201403114815739/