作者:xd0ol1@知道创宇404实验室

0 引子

在上一篇文章中,我们分析了 Office 文档型漏洞 CVE-2015-1641 的利用,本文将继续对此类漏洞中的另一常见案例 CVE-2015-2545(MS15-099)展开分析。相较而言,这些 Exp 的威胁性更大,例如可采用“Word EPS + Windows EoP”的组合,且很多地方借鉴了浏览器漏洞的利用思路,因此还是很值得我们学习研究的。

1 样本信息

分析中用到的样本信息如下:

SHA256:3a65d4b3bc18352675cd02154ffb388035463089d59aad36cadb1646f3a3b0fc
Size:420,577 bytes
Type:Office Open XML Document

我们将此文件的后缀名改为 zip,解压后可得到如下目录结构:

图0  样本通过 zip 解压后的目录结构

其中,image1.eps 是精心设计的漏洞利用文件,即由 PostScript 语言编写的特殊图形文件,这里 Word 和 PostScript 的关系一定层度上可类比为 IE 浏览器和 JavaScript 的关系,更多关于 PostScript 语言的说明可参考该手册

此外,本文的分析环境为 Win7 x86+Office 2007 SP3,EPSIMP32 模块的版本信息如下:

图1  EPSIMP32 模块的版本信息

2 漏洞原理分析

首先我们看下原理,简单来说就是 Word 程序在解析 EPS(Encapsulated PostScript)图形文件时存在一个 UAF(Use-After-Free)的漏洞,其错误代码位于 EPSIMP32 模块。为了便于理解,我们给出样本中触发此漏洞的那部分 PostScript 代码,当然有经过一定的反混淆处理:

图2  触发漏洞的那部分 PostScript 代码(PoC)

其中操作符 copy 和 forall 的定义如下:

图3  dict 操作时 copy 和 forall 的定义

结合上述代码,我们给出漏洞原理更为具体的描述:当通过 forall 操作 dict2 对象时,将对 dict2 中的 ‘key-value’ 进行迭代处理,且 pNext 指针指向下一对待处理的 ‘key-value’。然而,proc 中存在 dict1 dict2 copy 的操作,此过程会先释放掉 dict2 原有的 ‘key-value’ 空间,之后再申请新空间进行接下来的拷贝,即原先 pNext 指向的 ‘key-value’ 空间被释放了。而后在 putinterval 操作中将重新用到原先 pNext 指向的空间,并向其中写入特定的字符串。因此,在下一次迭代时,pNext 指向的数据就变成了我们所构造的 ‘key-value’。

接着我们来完整分析下此过程,这里给出 PostScript 对象和 dict 下 ‘key-value’ 对象的定义,它们在后面会涉及到:

//PostScript对象的定义
struct PostScript_object {
    dword type;
    dword attr;
    dword value1;
    dword value2;
} ps_obj;

//字典‘key-value’对象的定义
struct Dictionary_key_value {
    dword *pNext;
    dword dwIndex;
    ps_obj key;
    ps_obj value;
} dict_kv;

就每个 PostScript 操作符而言,都有一个具体的处理函数与之对应,我们可以很方便的由 IDA 进行查看,之后通过相对偏移的计算就可以在 OllyDBG 中定位到关键点了:

图4  操作符对应的处理函数

借助如下断点我们将在进程加载 EPSIMP32 模块时断下来:

bp LoadLibraryW, UNICODE [dword ptr [esp + 0x04] + 0x6e] == “EPSIMP32.FLT”

图5  WINWORD 进程加载 EPSIMP32 模块

很自然的我们会想到在 forall 的对应函数上下断,可以得到与 dict 操作迭代处理相关的代码段如下,其中 EPSIMP32 的模块基址为 0x73790000:

图6  dict 在 forall 操作时的迭代处理

此过程包含4个 call 调用,其中第一个 call 用于获取当前要处理的 ‘key-value’ 和指针 pNext,即指向下次处理的 ‘key-value’,而第二个和第三个 call 分别用于将 key 和 value 存储到操作栈上,最后的第四个 call 则用于处理 proc 中的操作。

我们来跟一下,在第一个 call 调用时,ecx 寄存器指向的内容为 dict2 内部 hash-table的 指针、hash-table 的大小以及包含的 ‘key-value’ 个数:

图7  ecx 寄存器指向的 hash-table

此调用执行完成后,我们会得到 keyZ1 和指向 keyZ2 的指针:

图8  keyZ1 及指向 keyZ2 的指针

而当第二个和第三个 call 调用完成后,我们可以看到 keyZ1 的 key 和 value 被存储到了操作栈上:

图9  将 keyZ1 存储到操作栈上

在第四个 call 调用中,对于 proc 的各操作符,首先会获取对应处理函数的地址,而后以虚函数的方式进行调用,相关代码片段如下:

图10  调用操作符的处理函数

这里我们主要关注 copy 操作,由分析可知,在其处理过程中会将 dict2 内部 hash-table 上对应的所有 ‘key-value’ 空间都释放掉,即上述 pNext 指向的 keyZ2 空间被释放掉了,如下给出的是进行该 delete 操作的函数入口:

图11  delete ‘key-value’ 的函数入口

同样,此时入参 ecx 寄存器指向的内容中包含了 dict2 的 hash-table 指针,接下去的操作将逐次释放 keyZ1~keyZ8 的空间,最后 hash-table 也会被释放掉:

图12  释放 dict2 上的 ‘key-value’ 空间

而释放的 keyZ2 空间,即 pNext 指向的空间,将在随后的 putinterval 操作中被重新写入特定的伪造数据:

图13  由 putinterval 操作写入伪造数据

因此,在 forall 的下一次迭代过程中,根据 pNext 指针获取的 ‘key-value’ 就变成了我们所伪造的数据,并且之后同样被存储到了操作栈上:

图14  伪造的 ‘key-value’

3 漏洞利用分析

这里我们接着上一节的内容来继续跟下漏洞的利用,此时伪造的 ‘key-value’ 已经被存储到了操作栈上,下述给出的是本次迭代中 forall 操作所处理的 proc 代码:

图15  第二次迭代时处理的 proc 代码

也就是将操作栈上的 key 和 value 分别赋给 xx_19169 以及 xx_26500,操作完成后得到的 xx_19169 如下:

图16  xx_19169 中的内容

可以看到,xx_19169 的 type 字段为 0x00000003,即表示的是整型,所以对于本文的分析环境来说,接下去的处理过程将会按照 “old version” 的分支来进行:

图17  不同版本执行分支的选择

xx_26500 则是实现漏洞利用的关键,由图18可知它的 type 字段为 0x00000500,表明这是一个string类型,且 value2 字段为泄露出来的指针,在此基础上经过一系列构造后,可得到 string 对象如下:

图18  获取 RW primitives

在 PostScript 中会为每个 string 对象分配专门的 buffer 用于存储实际的字符串内容,其基址及大小就保存在该 string 对象中。就最终样本伪造的 string 对象来说,其 buffer 基址为 0x00000000,且大小为 0x7fffffff,因此借助此对象可以实现任意内存的读写。之后代码会通过获取的 RW primitives 来查找 ROP gadgets,从而创建 ROP 链,同时由 putinterval 操作将 shellcode 和 payload 写入内存:

图19  创建 ROP 链并写入 shellcode 和 payload

之后再通过修改操作符 bytesavailable 处理函数中的如下 call 指针跳转到 ROP 链上:

图20  控制 EIP 跳转到 ROP 链

其中,ROP 链包含的指令如下,可以看到首先进行的是 stack pivot 操作,接着会将 shellcode 所在的页属性置为可执行,最后跳转到 shellcode 的入口:

图21  ROP 链中的操作指令

这里借助了一个小技巧来绕过保护程序对 ZwProtectVirtualMemory 调用的检测,对于 ntdll 模块中的 Nt/Zw 函数,除了赋给 eax 寄存器的 id 不同外,其余部分都是相同的。ROP 链在完成 eax 的赋值后,也就是将 ZwProtectVirtualMemory 函数中的 id 赋给 eax 后,会直接跳过 ZwCreateEvent 函数(该函数未被 hook)的前5字节并执行余下的那部分指令,通过这种方式能实现任意的系统调用而不会被检测到:

图22  绕过保护程序对 ZwProtectVirtualMemory 调用的检测

下面我们再来简单看下 shellcode,和大多数情况一样,它的主要作用就是获取相关的 API 函数,然后创建并执行 payload 文件。样本中 shellcode 的部分数据经过了加密处理,因此会有一个解密的操作:

图23  对 shellcode 中的数据进行解密

而后,代码通过查找 LDR 链的方式来获取 msvcrt 模块的基址:

图24  获取 msvcrt 模块的基址

之后从 msvcrt 模块的导入表中得到函数 GetModuleHandleA 和 GetProcAddress 的入口地址,由 GetModuleHandleA 函数可以获取到 kernel32 模块的句柄,最后再借助 GetProcAddress 调用来逐个获取下述的导出函数地址:

图25  获取相关的 API 函数

紧接着 payload 的内容,即图19所示代码中介于首尾字符串 “5555555566666666” 之间的那部分数据,会被写入到临时目录下的 plugin.dll 文件中,分析可知这是一个恶意的程序:

图26  样本创建的恶意 dll 文件

通过 LoadLibraryA 函数加载该 plugin.dll 模块后,将会在临时目录下另外再释放一个名为 igfxe.exe 的程序,其作用是获取远程文件并执行之:

图27  释放的 igfxe.exe 程序

4 结语

本文基于样本文档分析了 CVE-2015-2545 的利用,然鉴于笔者就 PostScript 语言所知尚少,固有些点也是没能给讲透彻,希望能有更多这类漏洞的分析文章出现。另外,错误之处还望各位加以斧正,欢迎一起交流:P

5 参考

[1] The EPS Awakens
[2] Microsoft Office Encapsulated PostScript and Windows Privilege Escalation Zero-Days
[3] 警惕利用Microsoft Office EPS漏洞进行的攻击
[4] 针对CVE-2015-2545漏洞研究分析
[5] 文档型漏洞攻击研究报告
[6] PostScript Language Reference Manual
[7] How the EPS File Exploit Works to Bypass EMET (CVE-2015-2545)
[8] CVE-2015-2545 ITW EMET Evasion


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/368/