作者: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

左側為簡單對象,右側為復合對象。簡單對象都是原子實體,類型、屬性和值不可逆轉地結合在一起,不能改變。但復合對象的值與對象本身是分開的,對象本身存儲于操作棧中,具體的結構如下:

// PostScript Object
struct PostScript object
{
    dword    type;      //對象類型      
    dword    attr;      
    dword    value1;    //指向對象所屬變量名稱
    dword    value2;    //若為簡單對象,直接指向值;若為復合對象,指向存儲的值的結構
}ps_obj;

其中部分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的值,指向了操作棧,查看后發現有兩個對象在棧中,第一個為string l63,第二個為array l61

繼續分析,會獲取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 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1704/