长亭百川云 - 文章详情

Office EPS文件解析漏洞分析

天玄安全实验室

69

2024-07-13

前言

第一次分析 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

name

packedarray

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 如图所示:

在 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_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:

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

虽然 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

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2