CVE-2015-1641(ms15-033)漏洞分析与利用
本文已发表于“安全客”,转载请注明出处。
这一期的漏洞分享给大家带来的是CVE-2015-1641的学习总结,这个漏洞因其较好的通用性和稳定性号称有取代CVE-2012-0158的趋势。该漏洞是个类型混淆类漏洞,通过它可以实现任意地址写内存数据,然后根据漏洞的特点,再结合一些典型的利用手法可以达到任意代码执行。
漏洞原理
这个漏洞的常见样本是rtf文档格式的文件,这点和下文的漏洞利用有关,主要原因是rtf方便构造利用组件(当然这并不绝对)。然而,漏洞的原理其实和rtf文档格式无关,而是与office的open xml文档格式的实现有关。这种文档格式常见的word文档拓展名就是docx,实际上是一个使用open xml组织文档内部资源后的zip压缩包。
实际上,该漏洞的rtf样本中,一般包含3个docx格式的文件组件,其中第2个文件用作触发漏洞组件,其他用作exp组件(依然并不绝对)。上面3个zip包就是从rtf文件sample里提取出来的,至于如何提取这里简单说一下,word文档里有个插入对象的功能,可以插入另外的word文档文件,这个样本就是插入了3个docx文档进去然后主文档保存为rtf文档格式,此时这3个插入的docx文件对象在主文件中是一段16进制数据,对应3个文件的16进制编码,所以可以通过一个正则表达式使用Notepad++之类编辑器从主文件中提取16进制编码:“objdata [0-9a-fr\n]+”,然后再借助一些十六进制编辑器如010edit保存为如上的3个docx/zip文件。之后就可以开始分析漏洞原理了,先将第二个目标文件去掉zip后缀使用office打开,此时word程序会直接崩溃,并且在调试器里可以看到崩溃点是一个赋值语句且ecx为一个稳定的内存地址值,其指向的范围是漏洞利用使用到的一个为了绕过aslr的模块msvcr71.dll:
然后从文件角度来看,加上zip后缀解压如下
:
其中,word目录下的document.xml为组织文档资源的首要文件,一般文档的文本内容也在里面,而从这个文件里我们就能找到触发这个漏洞的主要内容:
可以看出调试器里出现的崩溃点ecx值被直接unicode编码在了smartTag标签的element属性值里头了,并且条件满足的情况下(msvcr71模块事先已加载),后续将会进行内存拷贝,且拷贝的目标地址为根据ecx计算出来的一个值,而拷贝的数据为0xffffe696(即子标签moveFromRange*的ID值4294960790):
从而,通过文件里那样构造的内容,主要控制两个变量值便可简单实现任意内存地址写数据的功能。当然,我们也比较关心的一个重点是,这段构造内容的原理是什么?可以看到这段内容是一组open xml闭合标签,最外层是smartTag标签,最里层是moveFromRange*标签。分别查阅msdn文档的相关信息,可以了解到这些标签的详细说明,这里关注到moveFromRange*标签的displaceByCustomXml属性说明:
从上图可以看出,该属性指定被替换的一个custom xml标签元素,换句话理解就是说moveFromRange*标签的该属性指定了其上级标签的一个customXml对象要被替换。然而,从样本内容上我们并没有看到customXml标签,仔细观察了一下customXml标签和smartTag标签的相关说明后才发现,这两个标签元素不仅功能作用具有一定的相似性,其内部属性的结构也比较有意思地保持一致:
可以想象这对同一模板出来的双胞胎标签,被他的缔造者微软分配到了不同的岗位,以至于有时候微软自己都没认清他们谁是谁。实际上,类型混淆漏洞正是由此而发,上文看到的调试器崩溃位置,便是word程序解析到moveFromRange*标签时,准备将其内部id移送到其上级元素smartTag(/customXml)的对象“空间”里头。通过回溯跟踪这一过程并进行对比,如果是正常情况下(上级标签为customXml),移送前会进行一次内存分配再将其拷贝至新的内存空间;而如果是混淆的情况下,由于两者对象本质的差异性,此时直接将id值移送到smartTag对象已有的内部空间里,以下为两种情况的代码跟踪序列对比图:
由于两种标签的内部属性成员具备一定的相似性导致可以类型混淆,语法上通过了内部检查,但是实际解析过程中,对象的内部缺乏严格的校验,导致混淆成smartTag对象后,解析moveFromRange*标签时认为接替需要的内存空间已存在,就直接使用错误的位置进行拷贝过程,造成了这个可被利用的安全性漏洞。
构造触发漏洞的POC
根据上述原理,漏洞发生的场景是word程序在解析内部自定义xml(customXml)标签存在替换标记的情况下,原本moveFromRange*标签是要将标记id传递给上级customXml对象,然而由于customXml和其兄弟标签smartTag存在一定的相似性,导致在customXml标签被替换成smartTag的时候发生类型混淆,造成内存拷贝漏洞。下面介绍如何构造触发这个漏洞的POC样本,首先我们明确一点,为了实现任意内存地址写,我们需要控制的两个变量分别是混淆后smartTag标签的element属性值和moveFromRange*标签的id值,它们分别控制了将要覆写的内存地址和内存数据,逆向跟踪一下上述的崩溃点函数:
该函数原本在解析moveFromRange*标签时被调用,此时类型混淆后ppObj指向smartTag的element成员的内存,进一步跟踪内部copy_func函数:
可见内部对ppObj指向的内存对象进行了一次检查(必要条件),然后就计算要拷贝的真正目标地址,最后才调用call_memcpy进行拷贝,整个拷贝过程可简化成以下公式:
根据此公式,我们只需要事先准备一块编排计算好目标地址的16字节内存,并将其起始地址pObj编码给smartTag的element,便能实现任意地址写内存的功能。这里,为了方便,直接将pObj赋一个不存在的内存地址,如0x0c0c0c0c,这样就会因为内存读取异常而像上面一样崩溃在同一个位置,只不过ecx的值将为0x0c0c0c0c:
static void Main(string[] args)
{
string fileName = @"poc.docx";
if (File.Exists(fileName) == true)
{
File.Delete(fileName);
}
using (WordprocessingDocument myDocument =
WordprocessingDocument.Create(fileName, WordprocessingDocumentType.Document))
{
//创建文档并插入一行文字
MainDocumentPart mainPart = myDocument.AddMainDocumentPart();
mainPart.Document = new Document();
Body body = mainPart.Document.AppendChild(new Body());
Paragraph para = body.AppendChild(new Paragraph());
Run run = para.AppendChild(new Run());
run.AppendChild(new Text("Hello, World!"));
//插入混淆对象
//CustomXmlRun cxr = body.AppendChild(new CustomXmlRun()); //正常对象
SmartTagRun cxr = body.AppendChild(new SmartTagRun()); //混淆对象
cxr.Uri = "urn:schemas:contacts";
cxr.Element = "\u0c0c\u0c0c"; //calc_dst:0x0c0c0c0c
//混淆的子对象
PermStart ps = new PermStart();
PermEnd pe = new PermEnd();
MoveFromRangeStart mfrs = new MoveFromRangeStart();
MoveFromRangeEnd mfre = new MoveFromRangeEnd();
ps.Id = 1;
//ps.EditorGroup = RangePermissionEditingGroupValues.Everyone;
pe.Id = 1;
mfrs.Id = "134744072"; //*src:0x08080808
mfrs.Name = "abc";
//mfrs.DisplacedByCustomXml = DisplacedByCustomXmlValues.Next;
mfre.Id = "134744072";
mfre.DisplacedByCustomXml = DisplacedByCustomXmlValues.Previous;
//插入混淆的子对象触发漏洞例程
cxr.Append(ps);
cxr.Append(mfrs);
cxr.Append(mfre); //触发混淆漏洞
//cxr.Append(pe);
/*最终成功混淆的xml如下,若实验时替换成正常对象标签CustomXml后提示xml错误,需手工自制包含正常SmartTag标签后再进行替换实验
* <w:smartTag w:uri="urn:schemas:contacts" w:element="ఌఌ">
* <w:permStart w:id="1" />
* <w:moveFromRangeStart w:name="abc" w:id="1" />
* <w:moveFromRangeEnd w:displacedByCustomXml="prev" w:id="1" />
* </w:smartTag>
*/
}
}
上面是本人使用微软提供的open xml sdk编写的C#测试代码,简单配置好环境后就可以运行程序对生成的poc.docx进行实验测试了,直接打开后崩溃图如下。从该文档提取document.xml便能找到以上代码最后注释部分所列的xml内容,发现此时element的值是直接unicode编码显示,和一开始调试样本所使用的编码形式(뵐簸)有所区别,虽然最终同样在内存里遵从unicode编码方式,但是利用这种编码方式却具有一定抗检测优势,比如说它能绕过阿里安全在freebuf发表此漏洞分析最后贴的yara检测规则。最后再提一点说明,就是我们可以重复生成多个类似上面的xml用于实现多个任意内存地址写,然后将它们嵌入到一些正常的文档的document.xml中重新打包,从而为后面的漏洞利用做准备。
漏洞利用
现在,有了一个可以随意写word进程内存的漏洞,就可以进入漏洞利用的环节了,下面主要分析上文一开始所举样本的利用手法。样本的利用过程主要分为:1——加载msvcr71.dll,2——堆喷射布局shellcode,3——利用本漏洞修改msvcr71.dll的对象指针和参数劫持eip,4——执行payload。
1、加载msvcr71.dll
由于本漏洞最终实现的主要功能是任意地址写内存,而我们的目标是先要劫持程序eip,故需要借助一些稳定可利用的对象来进行操作。本例样本所选择的目标msvcr71.dll,借助其稳定的内存地址(编译时没有开启aslr保护)及可利用的对象指针来达到劫持程序的目的。所以这第一步需要先加载msvcr71.dll,针对此目标,样本所使用的方法是内嵌一个ole对象otkloadr.WRAssembly,这个对象被解析时会调用OTKLOADR.DLL,而该模块加载时导入了msvcr71.dll模块。不过要内嵌这个ole对象可能需要一些周折,先看一下本例的方法:
将以上内容直接嵌入rtf之中即可成功引入msvcr71.dll模块,原理是通过ProgID引入otkloadr模块,具体详情可以参考出处。ProgID可以理解为CLSID的别名,对应于一个在系统注册的COM组件,如otkloadr.WRAssembly.1对应的是CLSID为{A08A033D-1A75-4AB6-A166-EAD02F547959}的COM组件,其加载模块路径在注册表如下:
当然,通过CLSID也是可以引入所需模块的,具体的方法可以通过修改docx文档中ActiveX.xml里面的CLSID实现加载需要的功能,这里暂不展开讨论。
2、堆喷射
这一步实际上也可以放在开始,作用就是内存布局shellcode,以便劫持eip后能够顺利执行任意代码。当然,执行堆喷射是比较耗资源效果不是很理想的一种方式,个人觉得应该有更好的利用方式,比如借助本漏洞任意内存写的功能找一块稳定的内存构造shellcode,不过可能会比较麻烦,这里只作提个思路。上文介绍到,样本嵌入了3个docx文档,其中第一个和第三个文档功能类似,都是利用ActiveX作的堆喷射,方法和本人在分享cve-2013-3906时演示的差不多,都是使用的40个ActiveX控件,区别是该样本去掉了其他39个bin文件,让40个ActiveX控件共用第一个bin文件:
这样处理的好处显而易见,不仅减少了空间,嵌入shellcode也更方便一些,不过结果一样就是了,最终都是将shellcode布置到精确的内存地址上:
3、劫持eip
前面两步算是准备工作完成后,就可以利用本文漏洞来进行程序劫持了。从样本上看,文档里主要包含4个smartTag标签,分别对应四次内存拷贝操作(实际上可能多于四次,这和系统环境有关,样本包含兼容性处理,这里不展开叙述)。而4次操作又可以分为两两部分,每两次拷贝实现一个目标地址改写,也就是说4个标签主要是为了改写msvcr71.dll的两个地址。这里之所以需要4次的原因是样本使用msvcr71.dll本身的内存去构造需要计算的目标拷贝地址,所以每两个标签的第一个标签都是为了第二个标签构造真正的目标地址做准备:
如上是前两个标签构造的拷贝过程,使用msvcr71本身的内存来构造要覆写的对象指针地址。实际上,最终4个标签的作用是覆写msvcr71.dll的“.data”数据区段里的某对象的两个dword值,其中第一个为指向FlsGetValue调用的函数地址,第二个是该函数调用的参数值。可以想到,一旦这两个值被修改成ROP相关的地址值,后面调度线程时,程序流程可以被成功劫持:
4、执行payload
至此,劫持了程序eip,后续shellcode代码也精确布置到了指定内存位置,剩下的也就是通过ROP分配可执行内存,然后解密加载payload了。这里ROP链就不提供了,简单提一下shellcode的流程:遍历当前进程打开的文件句柄,通过判断文件大小的范围找到该rtf格式攻击文档;从文档尾部提取加载payload的shellcode解密执行,该shellcode依然从文档尾部继续解密一个pe程序到一个目录中执行起来并释放打开一个迷惑性的文档,从而完成内嵌pe执行的功能。
总结
本漏洞值得学习的地方还是有不少的。首先,从漏洞原理出发,需要理解open xml对象类型混淆的内因与外因,通过sdk编程和poc调试,有助于我们了解word程序解析文档的内部执行流程。该漏洞的根本原因是word程序在处理customXml对象相关的标签时没有严格检查对象的类型,导致它可以被近亲smartTag对象冒名顶替。接着,通过这个漏洞可以构造混淆对象的处理过程,重复的进行对象相关属性的内存拷贝,实现任意内存地址覆写的功能,这个过程需要深入地分析对象内部的处理流程,才能准确的计算出如何构造所需的数据。然后有了这个强大的任意内存地址覆写功能,就可以结合各种利用姿势来接管word程序的执行。比如需要绕过aslr,就得想办法加载没有开启该保护的模块,如可以通过嵌入一个ListView控件来加载MSCOMCTL.OCX,而该模块可以利用的对象也有一些,虽然可能使用条件有一些限制,但如果能够互相配合好,相信会收到意想不到的惊喜。最后提一个经常遇到的难点,就是在漏洞利用过程中为了能尽量的“通用化”,经常需要调整一些量值,可能不同平台有所差异,这种情况经常比较吃力不讨好,所以最好是能够变换一下思路,至于如何变换,我也只是有点思路,就是往高级的方向去思考……剩下的就靠大家多多贡献了!
7 条评论
评论 从: ashu [游客]
评论 从: danny [游客]
我手动替换rtf文档中的docx文件时一直出错,只替换对应的十六进制编码的内容好像无效,现实的依然是以前的docx文档的内容,但是变成了图片类似的……谢谢
评论 从: danny [游客]
你好,请问怎么将带poc的docx文件插入到rtf文件中,如果直接用word的插入功能,会导致崩溃。还有怎么手动构造堆喷射的docx? 谢谢
评论 从: 请教 [游客]
我在用C# 编译你文中的自动生成样本那段代码,报一个错
错误 1 未能找到类型或命名空间名称“SmartTagRun”(是否缺少 using 指令或程序集引用?)
我不知道这个需要引用什么,C#没弄过,谢谢
你需要安装一下open xml sdk,就按这关键字去找资料,下载个安装包引入就行了。
表单载入中...
提取出{\object…}闭合的部分然后组成新文件这步有一个问题,就是很多漏洞的触发单文件拖进rtf文件就会崩溃,无法有效提出{\object…}闭合的部分,这个问题请问楼主是如何解决的呢?