Edge Inline Segment Use After Free
Author:Qixun Zhao(aka @S0rryMybad && 大寶) of Qihoo 360 Vulcan Team
在今個月的微軟補丁裡,修復了我報告的4個漏洞,我將會一一講解這些漏洞.因為我寫的這些文章都是用我的空餘時間寫的,因為每天還有大量的工作和需要休息,而且這個部落格也是一個非公的私人部落格,所以我不能保證更新的時間.
這篇文章講的是CVE-2018-8367,大家可以從這裡看到這個bug的 ofollow,noindex">patch . 這是一個品相非常好的UaF,所以利用起來比較簡單.同時這也是JIT中一個比較特別的漏洞,因為它需要其他模組的配合,希望能為大家以後尋找JIT漏洞的時候帶來一些新思考
測試版本:chakraCore-master-2018-8-5 release build(大概是這個日期下載的就可以了)
關於Chakra的垃圾回收(GC)
要找到chakra中的UaF漏洞,首先需要了解chakra引擎的gc是怎麼執行的.簡單來說,chakra用到的gc演算法是標記-清除(mark-sweep)演算法,更詳細的細節我推薦大家可以看看 <<垃圾回收的演算法與實現>> .
這個演算法的核心在於有一堆需要維護的根物件(root),在chakra中根物件主要包括顯式定義的指令碼變數(var/let/const)還有棧上和暫存器的變數.演算法從根物件開始掃描,並且遞迴掃描他們中的子物件(例如array物件中的segment就是array的子物件),直到遍歷完所有的根物件和子物件.標記階段完成後,如果沒有標記的物件就是代表可以釋放的,這時候記憶體管理器就會負責回收這些物件以用來以後的分配.
Chakra的gc演算法主要有兩個缺陷,第一是沒有區分數字和物件,所以利用這個特性通過在棧上分配大量數字,然後通過側通道攻擊可以獲得某個物件的地址.(這是我之前聽過的一種攻擊方法,不知道現在修補沒有)
第二個缺陷在於,gc演算法遍歷子物件的時候,是需要知道父物件的大小的,不能越界遍歷標記(例如
父物件是array的時候,需要知道array的大小去遞迴標記). 所以如果一個物件A中假如有一個指標指向另一個物件B的內部(非物件開始位置),A中的指標是不會執行標記操作的.假如除了物件A中的指標沒有其他任何指標指向物件B,B物件是會被回收的.這樣在A物件中就有一個B物件的懸掛指標(Dangling Pointers) .
尋找UaF漏洞的關鍵就在於是否能創造一個物件中存在一個指標,指向另一個物件內部,而一般能出現這種情況的物件是String和Array.以前出現的類似漏洞有: CVE-2017-11843
Array Inline Segment 與Array.prototype.splice
在建立陣列的時候,如果陣列的size小於一定的值,陣列的segment是會連著陣列的meta data,也就是儲存陣列資料的segment緊跟陣列後面.記憶體佈局如圖所示:
let arr = [1.1,2.2,3.3,4.4,5.5,6.6]; Array.isArray(arr);

可以看到0x11c45a07700這個segment的地址是緊跟在array(起始地址0x11c45a076c0)後面,這樣的情況稱為inline segment.
另一方面Array.prototype.splice這個介面是用來從一個數組刪除一定數量的element,然後放到新建立的陣列上,具體可以參看 JavaScript/Reference/Global_Objects/Array/splice" target="_blank" rel="nofollow,noindex">MDN
這裡chakra在處理splice這個runtime介面的時候,裡面存在一個優化的模式,假如要刪除的element的start和end剛好等於一個segment的start和end(如上圖就是index[0]到[5]),就會把這個segment的地址直接賦值到新建立的陣列中(function|ArraySpliceHelper|):

但是這裡它需要避免把一個inline segment整個移動到另一個array中,否則就會出現上文提到的那種情況,一個物件中有一個指標指向另一個物件的非開頭位置.

而判斷是否|hasInlineSegment|的邏輯就是:

檢查head segment的length是否少於|INLINE_CHUNK_SIZE|,這裡是64. 換句話說,現在如果我們要找一個UaF的漏洞,我們需要做的是建立一個head segment的length大於64的array .
JIT PLEASE HELP ME
要想得到這樣的一個array是很不容易的,因為在很多建立array的函式中,他們都會檢查要建立的大小是否大於64,如果大於64,則會把head segment單獨分配在另外一個buffer,不會再緊跟在array後面.
但是,一個chakra引擎是由很多很多人編寫的,在我看來,編寫JIT模組的人應該與runtime模組的人是不一樣的,同樣編寫JIT的人會比較不熟悉runtime模組上的一些邏輯與細節.而在現實中,很多漏洞往往就是因為不同人員之間協同開發,導致一些跨模組的漏洞出現,這個漏洞就是典型的JIT開發人員不熟悉runtime模組導致.
在今年一月份修復CVE-2018-0776,開發人員引入了這個漏洞: patch 和 GPZ Report
由於JIT的開發人員不熟悉runtime的邏輯,不清楚在建立陣列的時候headsegment的length不能大於64,導致打破了chakra中的一些慣例.我們可以看到|BoxStackInstance|中建立陣列的時候,根本沒有判斷需要建立的head大小就直接建立了一個inline segment array.(這裡我們需要deepcopy為true才可以)
所以到底什麼是BoxStackInstance,在什麼情況下才能呼叫這個建立陣列?
在JIT中,有一個優化機制叫做escape analysis,用於分析一個變數是否逃出了指定的範圍,假如一個變數的訪問用於只在一個function裡面,我們完全可以把這個變數當成區域性變數,分配在棧上,在函式返回的時候自動釋放.這樣的優化可以減少記憶體的分配和gc需要管理的物件,因為這裡物件的生命週期是和函式同步的,並不是由gc管理.
但是這裡有一個問題,但是某些特殊情況下,我們需要把這個棧上的變數重新分配到堆上,這時候就需要呼叫BoxStackInstance重新建立物件.
情況一就是bailout發生的時候,因為需要OSR回到直譯器層面執行,這時候就沒有所謂的native函式棧概念,只剩下虛擬機器的虛擬棧,否則物件會引用已釋放的棧空間.這種情況deepcopy為false,並不滿足我們的需要.
情況二就是通過arguments訪問一個棧變數,因為arguments可能是一個堆物件,它是由gc管理的,它裡面必定不能包含棧分配的物件.這時候deepcopy為true.
至此,漏洞的大概原理已經分析清楚.
I WANT TYPE CONFUSION
通過上面的分析,我相信大家結合GPZ的case差不多已經知道如何構造poc了:

獲得了這樣一個evil array後,先轉換成物件陣列,然後我們用它來呼叫splice,把它的inline segment 賦值到另一個物件中:

然後把其他所有引用這個inline segment的array的物件全部清除掉,這時候這個帶有inline segment的array將會被回收,由於|evil|指向這個inlinesegment的pointer是指向這個array的內部,所以並不會影響這個array被回收

然後通過gc函式,對噴大量的float array,同時觸發垃圾回收,這時候evil物件陣列的segment區域將會被大量float array佔據:

至此,我們已經成功將這個UaF的漏洞轉成var array和float array的type confusion漏洞,至於剩下怎麼fake與leak,大家可以參考我們上一篇的blog,這裡就不再重複了.

總結
這裡的修復也很簡單,就是利用BoxStackInstance重新建立陣列的時候判斷array是否inline segment array,如果不是,則把head segment分配到另外的地方,以免影響splice裡面|hasInlineSegment|的判斷.
從這個案例我們可以知道,在一個龐大的專案開發的時候,往往由很多不同的人員開發,而他們往往對自己平時不接觸的模組比較陌生,這樣在打一些補丁或者修改程式碼的時候很可能會引入一些跨模組的漏洞.
除此以外,我們也可以想到,在檢視JIT漏洞的時候,我們不能僅僅只盯著在JIT編譯出來的程式碼或者把JIT獨立出來思考,可能一些JIT的問題需要配合runtime或者其他模組才能形成一個完成的安全漏洞.這也為以後尋找JIT漏洞的時候帶來一些新思路—-結合其他的模組思考.
Thank you for your time.