Office EPS 文件解析漏洞分析
2021-09-10 18:32:00 Author: paper.seebug.org(查看原文) 阅读量:53 收藏

作者:Joey@天玄安全实验室
原文链接:https://mp.weixin.qq.com/s/PnKo3nL9h3jcws16fxtY2A

前言

第一次分析EPS类漏洞,对于PostScript格式十分陌生,通过查阅PostScript LANGUAGE REFERENCE了解PostScript格式。调试EXP来自kcufld师傅的eps-CVE-2017-0261,EXP在Office 2007可以正常运行,但在Office 2010以上版本需要配合提权漏洞逃逸沙箱后完成利用。

调试环境

调试是直接使用kcufld师傅的eps加载器进行调试,EPSIMP32.FLT版本信息如下:

OS:                 Win7 x64 SP1
Office:             Ofiice 2007 x86
Image name:         EPSIMP32.FLT
ImageSize:          0x0006E000
File version:       2006.1200.4518.1014
Product version:    2006.1200.4518.0

PostScript格式简介

先介绍下PostScript基本的数据结构:

SIMPLE OBJECTS COMPOSITE OBJECTS
boolean array
fontID dictionary
integer file
mark gstate (LanguageLevel 2)
name packedarray (LanguageLevel 2)
null save
operator string
real

左侧为简单对象,右侧为复合对象。简单对象都是原子实体,类型、属性和值不可逆转地结合在一起,不能改变。但复合对象的值与对象本身是分开的,对象本身存储于dict对象中,具体的结构如下:

// PostScript Object
struct PostScript object
{
    dword    type;      //对象类型      
    dword    attr;      
    dword    value1;    //指向对象所属变量名称
    dword    value2;    //若为简单对象,直接指向值;若为复合对象,指向存储的值的结构
}ps_obj;

//字典‘key-value’对象的定义
struct Dictionary_key_value {
    dword *pNext;   //指向存放PostScript Object的地址
    dword dwIndex;  
    ps_obj key;
    ps_obj value;
} dict_kv;

其中部分type的值与类型的映射如下:

type值 数据类型
0x0 nulltype
0x3 integertype
0x5 realtype
0x8 booleantype
0x10 operatortype
0x20 marktype
0x40 savetype
0x300 nametype
0x500 stringtype
0x900 filetype
0x30000 arraytype
0x70000 packedarraytype
0x0B0000 packedarraytype
0x110000 dicttype
0x210000 gstatetype

接着介绍下漏洞中使用到的比较关键的操作符的意义:

操作符 示例 解析
forall array proc forall 枚举第一个操作数的元素,为每个元素执行过程 proc。如果第一个操作数是数组、压缩数组或字符串对象,则 forall 将一个元素压入操作数堆栈,并对对象中的每个元素执行 proc,从索引为 0 的元素开始并依次执行。
dup any dup ---> any any 复制操作数堆栈上的顶部元素。 dup 只复制对象;复合对象的值不是复制而是共享的。
putinterval array1 index array2 putinterval 用第三个操作数的全部内容替换第一个操作数的元素的子序列。被替换的子序列从第一个操作数的 index 开始;它的长度与第三个操作数的长度相同。
put/get array index any put/get 替换/获取第一个操作数的一个元素的值。如果第一个操作数是一个数组或一个字符串,put/get将第二个操作数视为一个索引,并将第三个操作数存储在索引所确定的位置,从0开始计算。
save /save save 保存当前VM状态快照,一个快照只能使用一次。
restore save restore 丢弃本地VM中自相应保存以来创建的所有对象,并回收它们占用的内存;将本地VM中所有复合对象的值(字符串除外)重置为保存时的状态;关闭自相应保存以来打开的文件,只要这些文件在local VM 分配模式有效时打开。

了解了上述背景后,开始分析漏洞。

漏洞成因

通过使用forall操作符获取创建的字符串对象,并在第一次循环时使用restore操作符释放字符串对象,随后创建新的字符串对象使得原本存储旧字符串对象的结构被新复合对象代替。若故意构造大小为0x27的字符串对象,则字符串被释放后会多出0x28的内存空间,此时立即创建新的字符串对象,则该内存会用来存储指向新字符串的string结构。随后通过改变forall的函数,获取指向新字符串的结构。

漏洞文件中一共触发了三次漏洞,第一次是获取了被释放的string的字符用于判断系统是32位还是64位。第二次触发故意构造大小为0x27的string对象,用于获取指向恶意string的结构。第三次则利用第二次构造的特殊string结构创造了一个起始地址为0x00000000,大小为0x7fffffff的字符串,构造了能够读写任意地址内存的读写原语。接着利用读写原语搜索内存中函数地址构造ROP链。最终创建了一个文件对象,在调用closefile操作符时跳转执行ROP完成漏洞利用。

查看poc.eps文件,第一次调用forall如图所示:

image-20210903105452944

在ida中定位到forall操作符的代码:

image-20210902173157063

使用windbg找到对应偏移后下断:sxe ld EPSIMP32;g;bp EPSIMP32+2b928;g;

image-20210831150138806

运行到图中所示位置时查看edi的值,指向了forall的dict对象,接着查看*pnext,发现存储了两个对象,第一个为string l63,第二个为array l61

image-20210831152552619

继续分析,会获取l63和l61对象到栈中,并确认l63的类型为string后,跳转到获取string类型元素部分

image-20210831161042602

获取值的过程会因为type的不同而有所变化,具体如图所示:

image-20210901172253284

通过调试可以更加直观的看到通过value2获取string的方式:

image-20210831171553055

接着循环获取string中的每一个元素并执行函数:

image-20210831192324185

此时传入deferred_exec的参数为eax,查看传入参数:

0:000> bp EPSIMP32+2ba06          //call    deferred_exec
0:000> g
Breakpoint 1 hit
eax=0018fd78 ebx=00000000 ecx=00291280 edx=00000001 esi=00425770 edi=00000000
eip=718fba06 esp=0018fd54 ebp=0018fdbc iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
EPSIMP32!RegisterPercentCallback+0x4604:
718fba06 e8d8abffff      call    deferred_exec (718f65e3)
0:000> dd eax L4        //查看传入的参数为数组
0018fd78  00030000 00000000 0049ea98 0048f40c
0:000> dd poi(poi(poi(poi(poi( 0018fd78 +c))+24))+28)   //查看数组中存储的内容
0049e2c0  00000500 00000100 00495408 0048ee98           //数组中存放着字符串对象
0049e2d0  12d85688 8000f194 00000020 00000100
0049e2e0  0049dc40 0048f198 12d8568f 80000000
0049e2f0  00490023 000007c8 00000300 00000100
0049e300  12d856b2 8000f19c 00000026 00000100
0049e310  0049dc60 0048f1a0 12d856b1 80000100
0049e320  00420029 0048f1a4 00000003 00000000
0049e330  12d856b4 80000080 0000002c 00000100
0:000> db poi(poi(poi(poi(poi( 0049e2c0 +c))+24))+20) L10   //查看字符串的内容为l56 cvx exec
00495940  20 6c 35 36 20 63 76 78-20 65 78 65 63 20 00 00   l56 cvx exec ..
0:000> g        //第二次执行deferred_exec
(5c8.144): C++ EH exception - code e06d7363 (first chance)
(5c8.144): C++ EH exception - code e06d7363 (first chance)
Breakpoint 1 hit
eax=0018fd78 ebx=00000000 ecx=00291280 edx=00000003 esi=00425770 edi=00000001
eip=718fba06 esp=0018fd54 ebp=0018fdbc iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
EPSIMP32!RegisterPercentCallback+0x4604:
718fba06 e8d8abffff      call    EPSIMP32+0x265e3 (718f65e3)
0:000> dd poi(poi(poi(poi(poi( 0018fd78 +c))+24))+28)   //查看数组的内容
0049e2c0  00000500 00000100 00495438 0048eeac           //数组中存放着字符串对象
0049e2d0  12d85688 8000f194 00000020 00000100
0049e2e0  0049dc40 0048f198 12d8568f 80000000
0049e2f0  00490023 000007c8 00000300 00000100
0049e300  12d856b2 8000f19c 00000026 00000100
0049e310  0049dc60 0048f1a0 12d856b1 80000100
0049e320  00420029 0048f1a4 00000003 00000000
0049e330  12d856b4 80000080 0000002c 00000100
0:000> db poi(poi(poi(poi(poi( 0049e2c0 +c))+24))+20) L10   //查看字符串的内容为l53 cvx exec
00495958  20 6c 35 33 20 63 76 78-20 65 78 65 63 20 00 00   l53 cvx exec ..

从调试的结果可以得知,该函数执行的正是forall。在第一次执行时,l61中待执行的命令是l56 cvx exec,在第二次执行时,l61中的内容已经被换成了l53 cvx exec与调试结果相符。

接着深入函数分析,发现函数内部嵌套了deferred_exec:

image-20210901193647205

于是重新调试,下断在此,分析参数:

image-20210901201303745

虽然type为0x10的操作符对象存储在Systemdict中无法查看,但是通过其他字符和数字还是能够确定该语句就是l50。当执行该语句后,原本l63指向的string结构将被替换成存放l52内容的string结构:

image-20210901211439785

可以看到此时原本存放l63的string结构已经变成了l52。

在get函数下断,跳转到forall下的/l64 l57 56 get def语句查看l57的值:

image-20210903144034585

可以证实l57中存放的就是从l63中获取到的字符,该forall的作用就是泄露被释放的string结构指向的字符串。

接着获取l57中的值,并进行一些处理,通过ifelse判断系统位数,若l77等于l52的长度+1,那么l99的值为1代表系统为64位,否则l99为0,代表系统为32位:

image-20210903164816672

可以看到在32位的调试环境下,l77的值为0,因此会将5个0压入操作栈中,并赋值给l95到l99:

image-20210903165138520

至此,漏洞原理部分分析完毕,接下来分析漏洞利用部分。

漏洞利用

第二次执行forall代码如下:

image-20210903171728048

和第一次执行十分类似,因此就不深入分析。查看执行完forall后stringl63的变化:

image-20210904155018524

查看l63中的值,发现是一个string结构,于是查看字符串,内容正是l102中存储的l36的字符串

image-20210904155250267

接着通过l90 0 l92 putinterval将l63中指向的第一个4字节的内容改为0x02b14ad4,该值指向l36中四字节之后的内容

image-20210904161420028

经过多次修改,字符串修改为如下状态,修改的值会在第三次漏洞触发时使用到:

image-20210907120914829

接着查看l137获取的是l63中0x4处的值,l138获取的是l63中0x20处的值,l103的值为1

image-20210904170623340

第二次漏洞触发部分分析完毕,接下分析第三次漏洞触发构造读写原语的部分。

构造读写原语

l142中存储的是将l138放入到l193的0x24位置的后的字符串:

image-20210904185419955

接着使用forall操作符遍历l63数组,当遍历到第54个元素时,恢复快照。此时array l63被释放,接着复制 l142字符串,使得array l63对象被l142字符串对象覆盖:

image-20210906105724780

此时查看被覆盖后的l63中最后一次会被获取的值:

image-20210906111515943

说明最后一次会获取一个array对象,继续深入查看该对象发现存储了一个字符串,该字符串起始地址为0x00000000,大小为0x7fffffff:

image-20210906112556232

通过该字符串,可读写内存中0x00000000-0x7fffffff的任意地址,实现了读写原语的构造,最终将字符串对象存储在l201中。

构建ROP链

通过字符串l201获取EPSIMP32的基地址为:0x74750000,并存入l314中:

image-20210906151059868

接着通过EPSIMP32的导入表获取kernel32.dll的基地址并存放于l315中:

image-20210906162228059

随后开始利用读写原语搜索内存中的gadget用于构造ROP链:

image-20210906164333935

将构造好的ROP链放入伪造的文件对象中,并将对象放置在l159的2号元素中,将恶意pe文件和shellcode组成的字符串放置在l159的3号元素中:

image-20210906200817931

最终调用closefile操作符关闭伪造的文件对象,在关闭过程中会执行call [eax+8]使得跳转到构造好的ROP链中:

image-20210906201456526

至此,整个漏洞的原理和利用分析完毕,剩下的行为部分不再分析。

总结

该样本漏洞利用的十分巧妙,通过UAF将原本正常的数组对象替换为指向构造好的能够读写任意内存的字符串对象。通过字符串对象实现了读写任意内存并构造ROP链的目的,并最终将构造好的ROP字符串对象修改为文件对象,利用cloasefile操作符跳转到ROP链中。

尽管微软已经关闭了Office对于EPS文件的支持,但该格式文件仍然能被Adobe Illustrator打开,如果深入研究该类型文件可能仍有出现漏洞的可能。

参考链接

[1] PostScript LANGUAGE REFERENCE

[2] eps-CVE-2017-0261

[3] CVE-2017-0261及利用样本分析

[4] EPS Processing Zero-Days Exploited by Multiple Threat Actors


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



文章来源: http://paper.seebug.org/1704/
如有侵权请联系:admin#unsafe.sh