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