作者:Qixun Zhao(aka @S0rryMybad && 大寶) of Qihoo 360 Vulcan Team
作者博客:https://blogs.projectmoon.pw/2018/09/15/Edge-Inline-Segment-Use-After-Free/
在今個月的微軟補丁里,修復了我報告的4個漏洞,我將會一一講解這些漏洞.因為我寫的這些文章都是用我的空余時間寫的,因為每天還有大量的工作和需要休息,而且這個博客也是一個非公的私人博客,所以我不能保證更新的時間.
這篇文章講的是CVE-2018-8367,大家可以從這里看到這個bug的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,然后放到新創建的數組上,具體可以參看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.
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/704/
暫無評論