一個指針引發(fā)的“血案”
腳本引擎開發(fā)者在設(shè)計GC(Garbage Collect,簡稱GC)時追蹤指針不善導(dǎo)致的UAF(Use-After-Free),是一類常見的漏洞。本文通過一個例子來向讀者介紹這類漏洞的成因與分析思路。
漏洞描述
CVE-2018-8353是谷歌的Ivan Fratric發(fā)現(xiàn)的一個jscript漏洞,該漏洞在2018年8月被修復(fù)。這是一個UAF漏洞,Ivan Fratric在披露頁清晰地描述了該漏洞的成因:
通俗一點說就是RegExp類的lastIndex成員沒有被加入GC追蹤列表,如果給它賦值,在GC時會導(dǎo)致lastIndex處存儲的指針變?yōu)閼掖怪羔槨:罄m(xù)再訪問lastIndex時,即造成一個典型的Use-After-Free場景。
jscript模塊目前已發(fā)現(xiàn)多個類似漏洞,例如CVE-2017-11793,CVE-2017-11903,CVE-2018-0866,CVE-2018-0935,CVE-2018-8353,CVE-2018-8653,CVE-2018-8389,CVE-2019-1429
本文試圖通過CVE-2018-8353一窺這類漏洞的成因,并在此基礎(chǔ)上分析谷歌PoC中的信息泄露利用代碼。讀者將會看到一個GC導(dǎo)致的UAF如何被轉(zhuǎn)化為高質(zhì)量的信息泄露漏洞。
?
PoC
以下為Ivan Fratric給出的PoC,下一小節(jié)將通過該PoC分析漏洞成因。
UAF
@0Patch團隊已通過補丁分析發(fā)現(xiàn),x86下lastIndex位于RegExpObj對象的+A8偏移處,如下:
現(xiàn)在RegExpObj::Create函數(shù)內(nèi)下斷點,在RegExpObj對象創(chuàng)建完成后,對其偏移+A8處下一個硬件寫入斷點,這個偏移處存儲一個VAR結(jié)構(gòu)體,此結(jié)構(gòu)體在x86下大小為0x10。重點觀察+B0處的數(shù)據(jù)變化。
為了更清晰地解釋成因,筆者并沒有開啟頁堆,但開啟了用戶模式下堆申請的棧回溯,以下為調(diào)試日志:
重占位
到這里已經(jīng)獲得了一個非常好的UAF,接下來的問題是:如何使用它?
從調(diào)試日志中可以看出,用來存儲VAR變量的內(nèi)存塊是從GcBlockFactory::PblkAlloc申請的,x86下其申請大小固定為0x648(《Garbage Collection Internals of JScript》這篇文章有解釋為什么x86下這個大小是0x648):
如果要重用被釋放的內(nèi)存,得在GC后迅速用大小為0x648的內(nèi)存申請去占用之。如何做到?
一個比較好的方法是借助NameList。jscript對象在創(chuàng)建成員變量時,如果成員變量的名稱過長(谷歌的文章中說這個長度閾值為4),會在NameList::FCreateVval函數(shù)內(nèi)單獨申請內(nèi)存,以存儲對應(yīng)的成員變量,并且會以第一個成員名稱的長度去申請?zhí)囟ù笮〉膬?nèi)存,而相關(guān)計算公式是固定的。
通過逆向調(diào)試,可以得到x86下的計算公式:
現(xiàn)在,令alloc_size=0x648,解上述方程,可得到x=0x178(0n376)。于是可以通過下面的代碼重用被釋放的內(nèi)存:
在調(diào)試器中觀察驗證重用:
從UAF到信息泄露
前一小節(jié)已經(jīng)在合適的時機控制了被Free的內(nèi)存,接下來要哦那個過這個UAF漏洞實現(xiàn)信息泄露,以得到被重用內(nèi)存的起始地址。
NameList::FCreateVval點
NameList::FCreateVval函數(shù)內(nèi)在申請成員變量名內(nèi)存時,若成員名長度超過一定值,就會額外申請內(nèi)存去存儲這些名稱。第一個成員名可以用來控制申請的內(nèi)存大小,相關(guān)計算過程已經(jīng)在前面說明。后面的成員名稱只要長度合適,就可以在第一個成員名稱初始化時申請的內(nèi)存中使用剩余的部分,從而用來布控內(nèi)存。
在x86環(huán)境下,通過逆向NameList::FCreateVval函數(shù),發(fā)現(xiàn)每個成員名稱前面會額外留0x30大小的空間作為頭部,用于初始化各種數(shù)據(jù)。每次成員名稱進行申請時,還會按照下圖的計算公式按4字節(jié)對齊并保存與返回相關(guān)偏移:
整個計算公式比較復(fù)雜,但設(shè)計思路很簡單,筆者在這里描述一遍,讀者大致了解即可:x86下,第一個成員名初始化時,先申請(2x+0x32)*2+4的內(nèi)存大小,得到內(nèi)存后,最初的0x30作為頭部使用,用來初始化各種數(shù)據(jù),包括本次字符串長度,指向下一個成員名頭的指針(這個指針會后面的成員名初始化時被更新),然后因為是第一個成員,按照公式直接加4字節(jié)進行對齊,所以從前面的調(diào)試日志也可以看到,第一個成員名從+0x34開始被復(fù)制。只要第一次申請的內(nèi)存空間夠,第二個成員名按照base+offset+4的方式進行內(nèi)存地址獲取,然后前0x30又是頭部,接著再開始復(fù)制,以此類推。
?
泄露被重用內(nèi)存首地址
接下來是泄露被重用內(nèi)存的首地址。
由于被重用的內(nèi)容之前存儲著lastIndex引用的VAR數(shù)據(jù),所以只要用長度及內(nèi)容合適的字符串設(shè)計類成員名稱,就可以控制指定地址處的VAR結(jié)構(gòu)。
從這里開始,使用Ivan Fratric在附件中給出的infoleak.html代碼,為便于展示,去除了部分注釋:
name1用來申請大小為0x648的內(nèi)存。name2可調(diào)節(jié),用來對齊。name3用來指定類型,以泄露特定偏移處的一個指針,這個后面再會提及。name4用來布控0x1337對應(yīng)的VAR,用于jscript代碼中的條件判斷。
上面的小節(jié)中只關(guān)心了name1,現(xiàn)在開始來具體設(shè)計name4,name3,name2。
-
鎖定偏移值
首先得計算垂懸指針指向的VAR結(jié)構(gòu)在被重用內(nèi)存的偏移值。Ivan Fratric的適配的是x64的版本,原poc在筆者的環(huán)境中運行后0x1337對應(yīng)的i為十進制的115。
x64與x86的原理一致,以x86的版本進行說明。既然x64環(huán)境中對應(yīng)的i為115。x32環(huán)境中,也以115為例進行偏移計算。在上述代碼中在第115個RegExpObj對象創(chuàng)建時下斷點,相關(guān)方法在前面UAF小結(jié)已經(jīng)描述,這個偏移很容易計算得到。
筆者的環(huán)境中這個偏移每次固定為0x3d8,如下:
-
設(shè)計name
現(xiàn)在來設(shè)計name,在每個成員名稱初始化時,都會有0x30的頭部,在這個頭部的+0x24處是一個指針(這個指針要到初始化下一個成員名時才會被初始化),指向下一個變量名的0x30頭部,下圖中字體為紅色的即為這些指針。如果能讀取其中一個指針,減去其相對內(nèi)存起始地址的偏移,就可以得到被重用內(nèi)存的首地址。
下圖中字體顏色為橙黃的是被拷貝的成員名稱,每個名稱最后會多拷貝兩個0x00。字體顏色為藍色的是每個成員名稱的實際長度(轉(zhuǎn)化為unicode后的長度)。字體顏色為紅色上面已經(jīng)進行解釋。字體背景為灰色的一個個0x30內(nèi)存區(qū)域為name2、name3、name4三個成員名的頭部。
字體背景為黃色高亮的區(qū)域,實驗時發(fā)現(xiàn)會與name3的值相同(意思就是給3得3,給5得5)。后面需要借助這個值來讀取它后面偏移8字節(jié)的一個紅色指針。
-
最后一個注意點
因為要泄露某個紅色指針,所以x86下必須保證這個紅色指針之前8字節(jié)處的type為long型,這可以通過設(shè)計name3來實現(xiàn)?,F(xiàn)在的問題是:VAR與某個特定的lastIndex對應(yīng)起來?
幸運的是,通過調(diào)試觀察發(fā)現(xiàn),當連續(xù)申請VAR結(jié)構(gòu)時,一個個大小為0x10的VAR似乎是從高內(nèi)存往低內(nèi)存次第排列。筆者用下圖來通俗地解釋一下VAR的分布(name2中b的數(shù)量被用來調(diào)節(jié)這里的對齊):
所以,在x86下,如果找到了0x1337對應(yīng)的regexps[i].lastIndex,就可以通過讀取regexps[i+5].lastIndex來泄露相關(guān)指針,減去固定偏移就得到被重用內(nèi)存的起始地址了。如下:
到這里已經(jīng)將這個UAF漏洞轉(zhuǎn)為了信息泄露,泄露出一塊(aaa...部分)完全可控的內(nèi)存的首地址。如果讀者之前看過筆者之前的一篇文章,就會明白這里已經(jīng)將CVE-2018-8353轉(zhuǎn)換為和CVE-2017-11906具有相同功能的信息泄露漏洞。
?
從信息泄露到RCE
此類信息泄露漏洞與其他堆溢出漏洞一起使用可以實現(xiàn)遠程代碼執(zhí)行。筆者將這個漏洞的利用代碼稍加改動,并配合CVE-2017-11907一起使用,可以在未打補丁的機器上完成概念驗證。
考慮到CVE-2018-8653或CVE-2019-1429這類在野0day的利用方式,應(yīng)該是用了更高級的利用手法,通過UAF直接實現(xiàn)了任意地址讀寫,通過單個UAF即可實現(xiàn)遠程代碼執(zhí)行,并不需要其他漏洞進行輔助。
此類UAF漏洞后面一定還會出現(xiàn),請大家做好防范工作。
?
參考文章
Issue 1587: Windows: use-after-free in JScript in RegExp.lastIndex
Garbage Collection Internals of JScript