导语:cve-2015-2545漏洞样本通过使用特殊构造的EPS文件嵌入到Wolrd中,通过运行World来完成任意执行代码,此样本还逃过了EMET的检测。
1. 概述
这是一种MSOffice漏洞,允许通过使用特殊的 Encapsulated PostScript (EPS)图形文件任意执行代码。这种漏洞于2015年3月被发现,漏洞未修补情况持续了4个月。之后,微软发布了修复补丁(MS15-099),解决了这一安全问题。
漏洞发布时间:2015-09-08
漏洞更新时间:2015-09-08
影响系统
Microsoft Office 2007 SP3 Microsoft Office 2010 SP2 Microsoft Office 2013 SP1 Microsoft Office 2013 RT SP1 Microsoft Office for Mac 2011 Microsoft Office for Mac 2016 Microsoft Office Compatibility Pack SP3
漏洞信息
Microsoft Office是一款微软发布的办公处理应用套件。
Microsoft Office处理EPS文件存在内存破坏,允许攻击者构建恶意文件,诱使应用解析,可使应用程序崩溃或执行任意代码。
用户可参考如下厂商提供的安全补丁以修复该漏洞:https://technet.microsoft.com/library/security/ms15-099
2. 样本来源
通过`推特`搜索`cve-2015-2545`得到相关信息,筛选信息得到其哈希值`哈希值:375e51a989525cfec8296faaffdefa35`,之后Google搜索其哈希值,最后在`https://www.hybrid-analysis.com/`得到样本。
3. 分析
3.1 漏洞成因以及利用
当EPS在处理`字典类型(dict)`的`copy操作`时,将会接受拷贝方的`键值对`,并且将自身空间数据全部删除,然后再重新分配一个空间进行数据拷贝(正常情况下字典拷贝时只对要拷贝的元素进行操作,而不影响其它元素)。而EPS在处理`forall操作`时,当处理类型为`字典(dict)`时,`forall`逐个处理字典(dict)中的每个`键值对`,`forall`会获得`当前键值对`的`内容`以及一个`ptrNext指针`,并指向下一个要处理的键值对,并将键(key)和值(value)的内容放到`操作栈`中,然后进行`forall的处理过程(proc)`,处理完后`仍保留ptrNext指针`以此来处理下一个键值对。如果在`forall`处理过程中有字典的`拷贝(copy)`操作,copy操作会将`键值对`条目`全部删除`,而forall的ptrNext指针仍`存在`,这时`ptrNext`就变成一个`野指针`,只要精心构造指针指向的数据,就可以达到利用效果。该样本利用方式为通过该野指针最终构造出一个起始地址为0x0,大小为0x7fffffff的`string对象`,这样就可以在该空间内作`任意`的`读写`操作以及后期的`ROP,Shellcode`的利用操作。
3.2 测试环境
1. Microsoft Windows [版本 6.1.7601] 旗舰版sp1 简体中文 x86 2. Windows Debugger Version 6.12.0002.633 X86 3. IDA6.8.150423 (32-bit) 4. OllyDbg 1.0 5. Notepad++ 6. Microsoft Office Word 2007(12.0.6612.1000) SP3 MSO(12.0.6607.10000)
3.3 调试方向
此样本从3个方面来进行调试分析,第一个调试方向:定位最简单可以得到信息的位置`ROP`,第二个方向则为重点方向:产生`野指针的位置`,第三个方向:`Rop`+`Shellocde`,共3个方向进行调试分析。
3.4 调试分析-CreateFile入手[第一个方向]
1.通过Windbg打开样本,直接运行,可以发现样本在临时目录文件夹下进行创建文件操作,创建文件顺序:plugin.dll > igfxe.exe,之后,直接想到的是在`ZwCreateFile`下断点【此断点位置不太好,因为word在打开的时候不断的跑CreateFile,但通过追溯可以找到自己想要的内容】。
图1
2.回溯找到`ROP`,`图2`call则为`rop`call,测试方式,自己在相应call下断点,单步跟踪即可,关于rop,换栈空间的操作以及后面的shellcode这里先不详细分析,自己可以单步跟踪即可。
图1
图2
3.5 调试分析-野指针产生过程[第二个方向]
3.5.1 简易例子解释UAF
此过程比较复杂,需要了解“PostScript”语法,可以下载<<PLRM2.pdf>>来学习;大家还要了解下关于`UAF`的简单的知识,何为`UAF`?就是字面意思“释放后重新使用“,例子如下:
#include<stdio.h> #include<stdlib.h> #include<malloc.h> void *pfunc1() { printf("testn"); } typedef struct Object1_struct{ int flag; void (*pfunc1)(); char message[4]; }OBJECT1; typedef struct Object2_struct{ int flag; int flag2; char *welcome; }OBJECT2; int main() { int i; OBJECT1 *pObject1; OBJECT2 *pObject2; pObject1 = (OBJECT1 *)malloc(sizeof(OBJECT1));//init struct pObject1->flag = 1; //pObject1->pfunc1(); //pObject1->message = "this is first create!"; free(pObject1); /*forget pObject1 = NULL*/ for(i=0;i<1000;i++) { pObject2 = (OBJECT2 *)malloc(sizeof(OBJECT2));//heap spray pObject2->flag=2; pObject2->flag2=4; pObject2->welcome = "AAAA"; } /*fill pointer*/ if(pObject1 != NULL) pObject1->pfunc1(); return 0; }
3.5.2 优化PostScript
了解上面的知识之后我们需要定位漏洞出现的位置,我们知道此漏洞出现的位置是在forall操作里面发生的,这个时候可以看下word文件中镶嵌的iamge1.eps文件,从文件可以看出代码都被挤压成一起了,很不方便观看这些代码,只能手动对关键代码更改`[图2]`所示
图1
图2
3.5.3 定位EPS样本文件关键位置
从代码上我们最直观发现的是`<00000000ff030000030000000000000000000000444444440005000000000000000000>`这一串东西,具体是什么我们现在先不需要考虑,然后我们可以看到forall操作{}里好多其他代码,其中就包含了copy的操作,这个位置就是导致漏洞产生的地方,那我们怎么去定位呢?动态调试,栈回溯的方式这个有点坑,大概你跟一天也跟不到吧,因为里面操作特别多,要知道这里面每个操作都是一个函数,所以回溯的方法不太可取,这个时候就需要用到IDA,直接使用IDA打开漏洞模块,打开之后我们应该想到,既然是操作,那么应该跟函数一样有带字符串的名字,那么我们是不是可以通过IDA–>shitf+f12【字符串窗口】–>查找forall这个函数呢?我们用`alt+t`查找下,果然,有了我们想要的东西`图1`,我们跟进去,发现有个地方在引用`图2`,我们继续跟进去,直接定位到了`forall函数“图3`,同理可得,我们可以定位到`copy`的函数与其他关键函数,然后动态调试跟踪即可。
图1
图2
图3
3.5.4 关键函数断点位置
首先我们在关键几个位置下断点,通过IDA得到偏移地址,直接Windbg or OD 定位相应位置即可,这里我用的是Windbg下断点,下面列出的断点是我调试了很多次所记录下的断点,大部分的断点我都给下了主要为了了解下代码流程方便观看,其实刚刚开始调试只需要在forall一处下断点即可,先了解其结构在观察其做法,我们先断第一个`forall`的位置
<`EPSIMP32!RegisterPercentCallback+0x4526`> 0 e 6822bcc6 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x4664 ".printf "forall_value_key_pNext"" 1 e 6823cbe4 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x15582 ".printf "dict1_dict2_Copy"" 2 e 6823a4f0 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x12e8e ".printf "dict1_dict2_copy_2"" 3 e 6821a0e8 0001 (0001) 0:**** EPSIMP32+0x1a0e8 ".printf "_copy_delete "" 4 e 6823cd89 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x15727 ".printf"copy_memcpy "" 5 e 6823ca58 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x153f6 ".printf"_exch_fun_"" 6 e 6823d592 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x15f30 ".printf"_put_fun_"" 7 e 6823d73c 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x160da ".printf"_PutInterVal_fun_"" 8 e 68230313 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x8cb1 ".printf"_BytesavailabelFun_"" 9 e 6823d8f5 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x16293 ".printf"_StringFun_"" 10 e 682312e6 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x9c84 ".printf"_ShowFun_"" 11 e 6823d830 0001 (0001) 0:**** EPSIMP32!RegisterPercentCallback+0x161ce ".printf"memcpy_putinterval_"" 12 e 6820cff5 0001 (0001) 0:**** EPSIMP32+0xcff5 ".printf"memcpy_put_bitshift_and_add_""
3.5.5 关于forall操作函数
我们通过ida可以观察forall是个`条件分支的语句`,通过查询《PLRM2.PDF》得到关于forall的解释,大约有4种情况,不过可以看出“array proc”与“packedarray proc”是一样的类型都是数组类型,其它两种类型分别是字典类型与字符串类型,验证方式直接在image.eps文件中添加实验代码即可验证。
图1
图2
3.5.6 字典对象处理流程
关键的函数为forall在处理dict的过程,我们在第三个语句块首部分下断点<`EPSIMP32!RegisterPercentCallback+0x4664`>,观察断点,入栈了3个参数,调用一函数,那么我们需要思考下这三个参数是什么,通过查询<<PLRM2.PDF>>,我们得到关键性的资料,[`图2`]上面说如果第一个操作是字典类型,那么它会入栈一个`key`和一个`数据`,这里类似哈希表的形式,通过一个key可以迅速得到对应的数值;为什么`图1`我写`pNext`,通过`图3`我们可以判断其内容是否为null,则`lea eax, [ebp-14]`eax取出来的是个指针,所以这里这个名字主要是方便识别其为指针所定。
图1
图2
图3
3.5.7 对象结构解析
关于第一个CALL所做的事情,大致可以分为3个方面,给PNEXT附值,给KEY赋值,给VALUE赋值,这里说的简单,里面的功能CALL需要自己手动调试下做好记录更加方便理解是如何赋值的过程,这里直接说下是如何得到的这些数值,首先看下`ECX`所对应的内容`图1`所示,其中`+0X8`偏移的位置是循环遍历的次数,相当于一个`INDEX`也可以说是个`界限值`,里面的CALL循环条件的范围就是 `400`, 其中`+0XC`的位置`8`表示`KEY`的个数,为什么,如`图2`所示,那么关于`+0X0`偏移的位置则是个`基址`的意思,所有的结果都是通过[基址+偏移*X]的形式得到相应的数据,`图3`表示第一个`KEY1`中的内容,同时`2B4`则为下标,通过基址+下标*4即可获取第一个KEY1对应内容的地址,这0x28个字节就是个结构体,大致内容如下所示,正好可以跟我们的数据一一对应,不过这里还是需要`多跟几遍`才会更加明白`结构`的情况,当跑完第一个CALL之后的内存情况如图4所示。
图1
图2
图3
struct { dword * pNext; //指向下个结构体 dword dwIndex; //下标 ps_obj key; //key ps_obj value; //数据 }kv_pair_element; struct PostScript object { dword type;//类型 dword attr; //属性 dword value1; //数据 dword value2; //数据 }ps_obj;
图4
00030000[类型] 00000000[属性] 000001ff[数据] 04f32ea4[数据] 00000300[类型] 00000000[属性] 04f69db0[数据] 04f32ea4[数据] 04f8fd98[pNext]
3.5.8 释放空间函数分析
第二call,第三call,为同一call,在对key与value进行操作,copy到其他位置,之后的`PROC`则为重点call,此call则执行`forall`的所有的操作,我们只需要在copy函数的位置下断点即可得到出发漏洞的位置<copy:`EPSIMP32!RegisterPercentCallback+0x15582`>,然后我们单步跟踪即可,这里可以先用ida观察下copy的大致流程,然后通过动态的方式更好的去跟踪我们想要的数据,由于copy的函数块比较大,动态调试起来也不是特别容易,我这里跟的时候是每个call都跟进去了,很费时间,但肯定可以找到想要的函数数据的,这里我直接给下偏移<`EPSIMP32!RegisterPercentCallback+0x12e8e`>,如`图1`所示为释放过程的函数<`EPSIMP32+0x1a0e8`>,这个释放的过程可以仔细观察下,还记得最开始的时候的this指针所指向的内容吗?[[this]],记得第一个call的时候,我们获取KEY1-KEY8都是存在以[[this]]为基址,然后加上一个偏移得到这些数据,第一次的循环delete也是从[[this]]为基址开始遍历,判断是否为null,不是空则delete,那么最后这正片空间都被delete了,这个的范围也是我们刚刚进入第一个call的时候的一个下标`400`,循环结束之后,delete this,并且清空`+0x0`, `+0x8`,就是清空key的个数。
图1
图2(释放:dict1 copy dict2—>释放dict2过程)
3.5.9 copy字典对象
之后会进行拷贝的操作,要把dict1–>copy到dict2中,则会new一段空间,而这段空间刚刚好为之前所释放的[[this]],这里new的是`0x1000`大小,并且通过`memset`清空刚刚申请的空间中的内容`图1,2`所示,之后会继续new0x28空间大小,为什么?因为`dect1–>copy–>dect2`中,diect1只有`1个key`,则在0x1000中所填充的数据也就是1个4字节地址,所以需要new 0x28大小来保存数据图3所示,copy完数据`图4`所示。
图1
图2(新new空间与释放的dict2一致)
图3
图4(new0x28大小空间:内容pNext,index, 类型,属性,数据,数据,类型,属性,数据,数据)
3.5.10 填充构造数据
之后则为putinterval操作的过程,这个就会填充我们的pNext指针所指向的数据空间,断点位置<`EPSIMP32!RegisterPercentCallback+0x160da`>,然后会进行`memcpy`的操作,把我们数据正好填充到我们之前的pNext所指向的空间中,这样会whlile循环会再次跑一边,而这次的 key,vale则是我们自己所构造的指定数据,结构如下所示。
图1
00000000[pNext] 000003ff[index] 00000003[类型] 00000000[属性] 00000000[key.valu1] 44444444[key.value2] 00000500[类型] 00000000[属性] 00000000[value.1] 0462e0b0[value.2]//这里是原来数据,并非复制过来的数据
3.5.11 pNext指向填充构造数据空间
观察下一次循环的数据布局情况如`图1`所示,`图1`断点<`EPSIMP32!RegisterPercentCallback+0x4664`>,经过第一个call的时候,则key与value都被更改成了我们构造好的数值如`图2`所示,此时的`pNext`为0也就是下一次不用循环了,其中key和value都被赋值了我们之前构造好的,其中的`0462e0b0`是之前释放空间前的数据,不是我们构造的所带的数据,这个数据可以说是个泄露的地址;其中我们从eps文件格式上面或者动态调试上面看也可以大概猜测 `00000500`为字符串类型
图1
图2
3.5.12 构造stirng类型长度为0x7ffffff及ROP PostScript代码
为了快速定位位置,我们可以在相应`memcopy`的位置下断点,这样方便我们定位数据的位置,最终构造出大小为0x7fffffff的可读写字符串空间。在该空间内作任意读写操作,做后续的处理(ROP/SHELLCODE),其中shellcdoe pe copy大小为`0x00000430`,和`0x0001a010`大小,最后通过`bytesavailable`跑`ROP`,其中`ROP`是直接通过PostScript语法来搜索得到的如`图2`所示。
图1(string类型字符串00000000-7ffffffff大小)
图2(搜索ROP链)
3.6 调试分析-ROP/SHELLCODE[第三个方向]
3.6.1 如何绕过EMET
Rop很简单,直接单步跟踪即可,指令都是EPSIMP32.FLT模块中的指令,单步跟踪观察即可,如`图1`所示,关于`0xd7`十进制`215`为NtProctectVirtulMemory函数的调用号码,可以通过`PChunter`这个软件打开SSDT表即可,如`图2,3`所示,这样做是为什么呢?我们继续往下面跟进观察如`图4`所示,跑到了`ZwCreateEvent+5`的位置了,这个位置一看就是要3环进0环的的地方,这就是绕过了EMET检测,由于EMET在系统的关键API进行了检测也就是`mov eax,调用号`这里进行了`jmp xxxx`检测,所以样本的做法为了绕过这个检测,就需要找没有被EMET hook的的函数,正好,ZwCreteEvent没有被hook那么就直接调用到这里来改我们的内存保护。
图1
图2
图3
图4
3.6.2 shellcode
Shellcode的功能主要为释放一个plugin.dll并加载执行。
4. POC/利用
关于POC,由于是forall中出现的问题,所以样本出现问题的位置:dict{}forall,并且其中只有copy才会触发漏洞所以POC的PostScript的语法是,这里列举的是伪代码。
dict {dict1 dict2 copy....} forall ---------------------------------------------------------------------------------- %%创建dict2并填充它 %%几个键值对 / dict2 5 dict def dict2 begin / k1 1000 array def / k2 1000 array def ... dict2 end %%创建dict1并填充它 %%一个键值对在k1 / dict1 3 dict def dict1 begin / k1 1000 array def dict1 end %%在dict2上开始forall枚举 dict2 { dict1 dict2 copy ....... 0 <00000000ff0300000005000000000000000000002000e01303000000000000000000000044444444> putinterval } forall
4.2 简易弹框
关于利用,纵观已知网上的cve-2015-2545的样本,用的利用方式都是在最初的样本模块上面进行加以改造,所以只需要对其shellcode的方面进行更改即可,`图1`则为更改样本弹出简单的对画框
<6A006A006A006A00B811EA2177FFD06A006AFFB8B51FFF75FFD090> putinterval
图1
5. Word插入EPS
Word在插入EPS格式文件之后,第一运行完其目录`wordmedia`中的`image1.eps`会变成`.wmf`格式文件,如果想每次都运行完`.eps`之后不会变成`.wmf`格式的文件,需要通过修改压缩包里面的一些内容即可,如`图1,2, 3`所示。
第一个文件:rels/document.xml 追加内容:
<Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.eps"/><Relationship Id="rId9" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="ooxWord://word/media/image1.eps" TargetMode="External"/></Relationships>
图1
第二个文件:world/document.xml 追加内容:
<w:p><w:r><w:pict><v:shape style="width:1;height:1"><v:imagedata r:id="rId8" r:href="rId9"/></v:shape></w:pict></w:r></w:p>
图2
第三个文件:[Content_Types].xml:
<Default ContentType="application/postscript" Extension="eps"/>
6.漏洞防护
分析都调试完成,明白其漏洞发生的位置,那么我们需要去防御cve-2015-2545漏洞,防御可以分为两个方面来完成:
1.静态防御(yara规则)
2.动态防御
6.1 静态防御
6.1.1 github yara
1.GitHub上面有关于cve-2015-2545的yara规则检测,但是自己观看了一下,可以看出规则写的不像是针对eps/doc写的,而是对dll,exe进行的yara规则编写,自己进行测试,如下图所示。
图1(未检测出来样本)
图2(检测出来样本生成的文件)
6.1.2 自己写的yara
自己写关于eps的文件的检测,需要对其里面的eps文件规则进行检测,自己简易写的yara规则[针对的是EPS文件]如下`图1`所示,检测出来恶意构造的EPS文件。
/* Yara Rule Set Author: Boluo Date: 2017年3月28日19:16:16 Identifier: CVE-2015-2545 */ /* Rule Set ----------------------------------------------------------------- */ rule cve_2015_2545 : BASE { meta: description = "Check The EPS File" author = "Boluo" date = "2017年3月28日19:16:16" hash1 = "1f9b7d8e692a1c9fadbdd05b794e8c49502323b073b44becaae5eee5e2186fc4" strings: $s1 = {3C 30 30 30 30 30 30 30 30 66 66 30 33 30 30 30 30 30 33 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 34 34 34 34 34 34 34 34 30 30 30 35 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30} condition: $s1 }
图1(检测eps文件为恶意)
6.2 动态防御
动态防御通过hook两个点来进行防御,第一个hook点的位置为`pNext`进入PROC前其中0x28的内容保存一下,第二个hook点的位置为`pNext`在进入PROC后中0x28的内容是否发生变化进行hook,之后进行比较其内容进行判断即可,之所以不能在循环遍历删除的位置检测是因为PostScript中只要`dict1 dict2 copy`都会先把dict2的内容全部释放掉然后再申请空间,不能直观判断是否为cve-2015-2545类型漏洞样本,在释放空间的位置进行检测相当于给其打了补丁而不能直观检测为cve-2015-2545类型漏洞;测试结果如下图所示。
图1(动态检测)
7. 结语
该漏洞及利用样本的威胁程度非常高,可以在多种环境下成功利用,并且其构造的ROP链及shellcode能够绕过多款安全性增强工具的检测。所以需要做好防范工作。
8.参考资料
1.http://seclab.dbappsecurity.com.cn/?p=603 2.http://bbs.pediy.com/thread-216045.htm 3.http://bbs.pediy.com/thread-216046.htm 4.https://www-cdf.fnal.gov/offline/PostScript/PLRM2.pdf 5.https://www.fireeye.com/content/dam/fireeye-www/blog/pdfs/twoforonefinal.pdf 6.https://github.com/Yara-Rules/rules/tree/master/CVE_Rules 7.https://wenku.baidu.com/view/55adb37c8e9951e79b8927ed.html