作者:墨云科技VLab Team
原文鏈接:https://mp.weixin.qq.com/s/CBj03EPBlsGe689Lc74qHQ
Google于2022年4月11日更新了Chrome的100.0.4896.88,其中修復了由@btiszka在3月18日報告的正則表達式模塊的UAF漏洞;6月28日,Google紕漏了該漏洞的具體細節,目前該漏洞已被修復并公開了技術細節,本文將從技術角度分析漏洞的成因和修復方式。
要理解這個漏洞,需要對V8的垃圾回收機制有一定的了解,本文首先簡單介紹V8的垃圾回收機制,然后結合具體漏洞PoC代碼分析漏洞成因。
V8垃圾回收機制
垃圾回收一直是V8引擎的優化重點,是多種復雜優化策略組合形成的機制,其本質采用的標記跟蹤回收算法,在堆布局上使用分代布局,大致可以分為新生代和老年代,具體的回收策略可分為Major GC(Mark-Compact)和Minor GC(Scavenger)。這里僅對兩種策略的關鍵階段做簡單介紹,詳細實現可以從參考文檔和源代碼進行學習。
Major GC(Mark-Compact)
V8的主要GC負責對整個堆區的垃圾進行回收,可分為標記、清除、整理三個階段,其中清除階段釋放無用內存,整理階段將已使用內存移動壓實,算法的重點在標記階段。
標記階段中,收集器需要發現并標記所有的活動對象。收集器從維護的一組根對象開始,跟隨指針迭代發現更多的對象,通過持續標記新發現的對象并跟隨指針,直到沒有需要標記的對象為止。

V8使用三色標記法來標記對象,每個對象通過兩個標記位和一個標記列表來實現標記,兩個標記位標識三種顏色:白色(00)、灰色(10) 和黑色(11)。最初所有對象都是白色的,當收集器發現白色物體并將其推送到標記列表時,它會變成灰色。當收集器從標記工作列表中彈出對象并訪問其所有字段時,灰色對象變為黑色。當不再有灰色對象時,標記完成,所有剩余的白色物體都無法到達,可以安全地回收。

Minor GC(Scavenger)
次要GC主要工作在新生代空間中,可以分為標記、疏散和指針更新三個階段,這些階段都是交錯執行的,沒有嚴格的先后順序。Scavenger將新生代的空間分為From-Space和To-Space,這兩個空間可以互相交換,新分配的對象都會出現在From-Space,在標記和回收完成后的疏散階段,Scavenger會將依然存活的對象移動到To-Space緊密排列,然后交換From-Space和To-Space,開始下一輪GC。

這里需要特別介紹寫屏障(Write-Barrier)機制,它是此漏洞發生的關鍵原因。Write-Barrier維護了一組從舊對象到新對象的列表,一般是老年代指向年輕代中的對象的指針,使用這個引用列表可以直接進行標記,不需要跟蹤整個老年代。

可以看到,Write-Barrier將一個關聯的可訪問的value對象標記為灰色,并放入marking_worklist中,后續的標記程序可以不需要再遍歷老年代中的對象,直接從該列表開始進行標記。
漏洞分析
Chrome V8命令執行漏洞(CVE-2022-1310)出現在V8引擎的正則表達式模塊,作者在報告中提到的漏洞PoC部分關鍵代碼如下:
var re = new RegExp('foo', 'g');
re.exec = function() {
gc(); // move `re` to oldspace using a mark-sweep gc
delete re.exec; // transition back to initial regexp map to pass HasInitialRegExpMap
re.lastIndex = 1073741823; // maximum smi, adding one will result in a HeapNumber
RegExp.prototype.exec = function() {
throw ''; // break out of Regexp.replace
}
return ...;
};
try {
var newstr = re[Symbol.replace]("fooooo", ".$"); // trigger
} catch(e) {}
gc({type:'minor'});
%DebugPrint(re.lastIndex);
通過對比PoC和分析源碼,當在JS代碼中調用re[Symbol.replace]函數時,V8引擎使用Runtime_RegExpReplaceRT函數進行處理,函數中的異常退出分支會調用RegExpUtils::SetAdvancedStringIndex,該函數最終將re.lastIndex加1并寫回re對象中。

可見,上述函數功能約等于re.lastIndex += 1,對于類似的代碼邏輯,在底層語言中通常需要考慮邊界值,防止出現數據溢出。V8中的Number類型分為Smi和HeapNumber,Smi代表了小整數,和對象中的指針共享存儲空間,通過值的最低位是否為0來區分類型,超出Smi表示范圍的值會在堆中創建HeapNumber對象來表示,在32位環境下,Smi值的范圍為-2^30到2^30 - 1。

根據上述邏輯,當我們對RegExp對象賦值re.lastIndex=1073741823,并進入Runtime_RegExpReplaceRT函數邏輯時,由于加1后的值1073741824超過Smi的表示范圍,V8引擎在堆中重新申請了一個HeapNumber對象來存儲新的lastIndex值,此時,該RegExp對象的lastIndex屬性不再是一個Smi數,而是一個指向堆中HeapNumber對象的指針。如下圖所示:

在之前的垃圾回收中已經介紹,V8的Minor GC的Write-Barrier機制需要對將新生代內存中的新建對象置灰并添加到標記列表中,以省略對老年代對象的遍歷。但函數SetLastIndex在處理RegExp對象存在初始化Map情況的代碼分支中,默認lastIndex是一個Smi值并使用SKIP_WRITE_BARRIER標記跳過了寫屏障。因此,當re.lastIndex變成了HeapNumber對象,又沒有被Write-Barrier標記,那么在GC發生時,該對象就會被當作可回收對象被釋放,釋放后re.lastIndex屬性指針就變成了一個懸垂指針,指向了一個已釋放的堆空間,再次嘗試訪問這個對象空間,就產生了Use-After-Free漏洞。

該漏洞(CVE-2022-1310)是一個典型的UAF漏洞,觸發后可以通過堆噴重新分配釋放后的內存空間達到利用的目的,但由于GC時間和堆噴的不穩定性,會給漏洞利用增加一定難度。在漏洞報告中,作者也給出了完整的利用代碼,感興趣可通過參考文檔中的issue 1307610的完整報告繼續研究。

總結
漏洞(CVE-2022-1310)出現的根本因為是V8在處理Number類型數據時,沒有考慮Smi值溢出的情況,致使新分配的HeapNumber對象破壞了Write-Barrier機制造成UAF,最終導致了任意代碼執行,修復方案也非常簡單,將SKIP_WRITE_BARRIER標記改成UPDATE_WRITE_BARRIER即可。

該漏洞最早在2020年6月25日就有安全研究員發布了相關信息,直到2022年4月才被修復,目前漏洞細節和利用代碼均已經被公開,由于V8引擎影響范圍較廣,請大家積極升級相關軟件至最新版本。
參考資料
https://bugs.chromium.org/p/chromium/issues/detail?id=1307610
https://v8.dev/blog/trash-talk
https://v8.dev/blog/concurrent-marking
https://chromium.googlesource.com/v8/v8/+/bdc4f54a50293507d9ef51573bab537883560cc8%5E%21/
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1955/
暫無評論