原创 Joey 2021-09-07 12:46
对Office EPS文件解析漏洞成因和利用分析
前言
第一次分析 EPS 类漏洞,对于 PostScript 格式十分陌生,通过查阅PostScript LANGUAGE REFERENCE了解 PostScript 格式。调试 EXP 来自 kcufld 师傅的eps-CVE-2017-0261,EXP 在 Office 2007 可以正常运行,但在 Office 2010 以上版本需要配合提权漏洞逃逸沙箱后完成利用。
调试环境
调试是直接使用 kcufld 师傅的 eps 加载器进行调试,EPSIMP32.FLT 版本信息如下:
OS: Win7 x64 SP1Office: Ofiice 2007 x86Image name: EPSIMP32.FLTImageSize: 0x0006E000File version: 2006.1200.4518.1014Product version: 2006.1200.4518.0
PostScript 格式简介
先介绍下 PostScript 基本的数据结构:
SIMPLE OBJECTS | COMPOSITE OBJECTS |
---|---|
boolean | array |
fontID | dictionary |
integer | file |
mark | gstate |
name | packedarray |
null | save |
operator | string |
real |
左侧为简单对象,右侧为复合对象。简单对象都是原子实体,类型、属性和值不可逆转地结合在一起,不能改变。但复合对象的值与对象本身是分开的,对象本身存储于 dict 对象中,具体的结构如下:
// PostScript Objectstruct 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 如图所示:
在 ida 中定位到 forall 操作符的代码:
使用 windbg 找到对应偏移后下断:sxe ld EPSIMP32;g;bp EPSIMP32+2b928;g;
运行到图中所示位置时查看 edi 的值,指向了 forall 的 dict 对象,接着查看*pnext,发现存储了两个对象,第一个为 string l63,第二个为 array l61
继续分析,会获取 l63 和 l61 对象到栈中,并确认 l63 的类型为 string 后,跳转到获取 string 类型元素部分
获取值的过程会因为 type 的不同而有所变化,具体如图所示:
通过调试可以更加直观的看到通过 value2 获取 string 的方式:
接着循环获取 string 中的每一个元素并执行函数:
此时传入 deferred_exec 的参数为 eax,查看传入参数:
0:000> bp EPSIMP32+2ba06 //call deferred_exec0:000> gBreakpoint 1 hiteax=0018fd78 ebx=00000000 ecx=00291280 edx=00000001 esi=00425770 edi=00000000eip=718fba06 esp=0018fd54 ebp=0018fdbc iopl=0 nv up ei pl nz na po nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202EPSIMP32!RegisterPercentCallback+0x4604:718fba06 e8d8abffff call deferred_exec (718f65e3)0:000> dd eax L4 //查看传入的参数为数组0018fd78 00030000 00000000 0049ea98 0048f40c0:000> dd poi(poi(poi(poi(poi( 0018fd78 +c))+24))+28) //查看数组中存储的内容0049e2c0 00000500 00000100 00495408 0048ee98 //数组中存放着字符串对象0049e2d0 12d85688 8000f194 00000020 000001000049e2e0 0049dc40 0048f198 12d8568f 800000000049e2f0 00490023 000007c8 00000300 000001000049e300 12d856b2 8000f19c 00000026 000001000049e310 0049dc60 0048f1a0 12d856b1 800001000049e320 00420029 0048f1a4 00000003 000000000049e330 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 hiteax=0018fd78 ebx=00000000 ecx=00291280 edx=00000003 esi=00425770 edi=00000001eip=718fba06 esp=0018fd54 ebp=0018fdbc iopl=0 nv up ei pl nz na pe nccs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206EPSIMP32!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 000001000049e2e0 0049dc40 0048f198 12d8568f 800000000049e2f0 00490023 000007c8 00000300 000001000049e300 12d856b2 8000f19c 00000026 000001000049e310 0049dc60 0048f1a0 12d856b1 800001000049e320 00420029 0048f1a4 00000003 000000000049e330 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:
于是重新调试,下断在此,分析参数:
虽然 type 为 0x10 的操作符对象存储在 Systemdict 中无法查看,但是通过其他字符和数字还是能够确定该语句就是 l50。当执行该语句后,原本 l63 指向的 string 结构将被替换成存放 l52 内容的 string 结构:
可以看到此时原本存放 l63 的 string 结构已经变成了 l52。
在 get 函数下断,跳转到 forall 下的/l64 l57 56 get def
语句查看 l57 的值:
可以证实 l57 中存放的就是从 l63 中获取到的字符,该 forall 的作用就是泄露被释放的 string 结构指向的字符串。
接着获取 l57 中的值,并进行一些处理,通过 ifelse 判断系统位数,若 l77 等于 l52 的长度+1,那么 l99 的值为 1 代表系统为 64 位,否则 l99 为 0,代表系统为 32 位:
可以看到在 32 位的调试环境下,l77 的值为 0,因此会将 5 个 0 压入操作栈中,并赋值给 l95 到 l99:
至此,漏洞原理部分分析完毕,接下来分析漏洞利用部分。
漏洞利用
第二次执行 forall 代码如下:
和第一次执行十分类似,因此就不深入分析。查看执行完 forall 后 stringl63 的变化:
查看 l63 中的值,发现是一个 string 结构,于是查看字符串,内容正是 l102 中存储的 l36 的字符串
接着通过l90 0 l92 putinterval
将 l63 中指向的第一个 4 字节的内容改为 0x02b14ad4,该值指向 l36 中四字节之后的内容
经过多次修改,字符串修改为如下状态,修改的值会在第三次漏洞触发时使用到:
接着查看 l137 获取的是 l63 中 0x4 处的值,l138 获取的是 l63 中 0x20 处的值,l103 的值为 1
第二次漏洞触发部分分析完毕,接下分析第三次漏洞触发构造读写原语的部分。
构造读写原语
l142 中存储的是将 l138 放入到 l193 的 0x24 位置的后的字符串:
接着使用 forall 操作符遍历 l63 数组,当遍历到第 54 个元素时,恢复快照。此时 array l63 被释放,接着复制 l142 字符串,使得 array l63 对象被 l142 字符串对象覆盖:
此时查看被覆盖后的 l63 中最后一次会被获取的值:
说明最后一次会获取一个 array 对象,继续深入查看该对象发现存储了一个字符串,该字符串起始地址为 0x00000000,大小为 0x7fffffff:
通过该字符串,可读写内存中 0x00000000-0x7fffffff 的任意地址,实现了读写原语的构造,最终将字符串对象存储在 l201 中。
构建 ROP 链
通过字符串 l201 获取 EPSIMP32 的基地址为:0x74750000,并存入 l314 中:
接着通过 EPSIMP32 的导入表获取 kernel32.dll 的基地址并存放于 l315 中:
随后开始利用读写原语搜索内存中的 gadget 用于构造 ROP 链:
将构造好的 ROP 链放入伪造的文件对象中,并将对象放置在 l159 的 2 号元素中,将恶意 pe 文件和 shellcode 组成的字符串放置在 l159 的 3 号元素中:
最终调用 closefile 操作符关闭伪造的文件对象,在关闭过程中会执行call [eax+8]
使得跳转到构造好的 ROP 链中:
至此,整个漏洞的原理和利用分析完毕,剩下的行为部分不再分析。
总结
该样本漏洞利用的十分巧妙,通过 UAF 将原本正常的数组对象替换为指向构造好的能够读写任意内存的字符串对象。通过字符串对象实现了读写任意内存并构造 ROP 链的目的,并最终将构造好的 ROP 字符串对象修改为文件对象,利用 cloasefile 操作符跳转到 ROP 链中。
尽管微软已经关闭了 Office 对于 EPS 文件的支持,但该格式文件仍然能被 Adobe Illustrator 打开,如果深入研究该类型文件可能仍有出现漏洞的可能。
参考链接
[1]PostScript LANGUAGE REFERENCE: https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf
[2]eps-CVE-2017-0261:
https://github.com/kcufId/eps-CVE-2017-0261
[3]CVE-2017-0261及利用样本分析:
https://bbs.pediy.com/thread-261442.htm
[4]EPS Processing Zero-Days Exploited by Multiple Threat Actors: https://www.fireeye.com/blog/threat-research/2017/05/eps-processing-zero-days.html