作者:啟明星辰ADLab

1. 研究背景

Chakra是一個由微軟為Microsoft Edge瀏覽器開發的JavaScript引擎。它在一個獨立的CPU核心上即時編譯腳本,與瀏覽器并行。本文主要對Chakra引擎中JIT編譯優化過程中的數組類型混淆漏洞進行分析。

JavaScript引擎的性能對整個瀏覽器的影響至關重要, JIT編譯優化是為了提高Chakra引擎性能。當在循環語句中反復執行同一段腳本代碼時,如果解釋器反復執行相關的字節碼,效率會很低。JIT可以將源代碼直接生成機器指令,在下一次執行時直接執行機器指令。在Chakra中只有當目標函數或者循環語句被頻繁調用時才會啟用JIT編譯,JIT編譯后生成了相應的機器指令,下一次調用到這個語句或是函數時就會直接執行機器指令。

一旦JIT生成完成,程序就可以直接調用JIT生成的機器指令。因為JIT是直接編譯為機器指令的,所以需要預先假定操作目標的類型。如果不滿足JIT的假設的話,此JIT代碼就不能執行,否則就會發生類型混淆的錯誤。因此JIT代碼中設計了bailout功能,一旦發現不滿足假設就進行bailout,bailout會放棄執行JIT代碼轉回使用解釋器繼續執行字節碼。

img

2. 數組類型混淆思路

Chakra數組可以分為三類,分別是NativeIntArray、NativeFloatArray和VarArray。NativeIntArray和NativeFloatArray數組轉化成VarArray數組過程中會將數組中的原數據通過異或0xfffc000000000000轉化為VarArray中的數據。也就是說VarArray會通過數組中元素的高位來判斷數組中的元素是數據還是對象。

NativeIntArray和NativeFloatArray之間混淆一般不能帶來安全問題,但是當這二者和VarArray混淆之后就會出現數據和對象無法區分的問題。

先看一段簡單代碼。

img

這段代碼在JIT優化后的表現形式是這樣的。

img

如果在xxx操作過程中將NativeArray的類型改變成了VarArray,并且JIT的優化過程并沒有檢測到這種變化的話,2.3023e-320就會被當作float數據存放進入VarArray的元素中,由于這個過程中數組的變化是始料未及的,所以2.3023e-320并沒有通過與0xfffc000000000000異或而變成一個可以被VarArray識別的float,所以VarArray對象在讀取該元素時會將其當成一個對象來處理。

為了實現數組的類型混淆,xxx操作主流的思路有兩種,一種是通過沒有檢測的回調來修改數組的類型,第二種是通過合理的函數來修改數組的類型。下面通過一些實例進行簡要分析。

2.1. 思路一:通過回調修改數組類型

先來看一個簡單的例子,通過回調修改數組類型。

imgfunc的JIT主要片段如下:

img

根據上述代碼,可以看到call rax之后并沒有驗證數組a是否合法就直接進行了賦值。那么如何改變數組a的類型呢?我們來看最后一次對func的調用。

img

漏洞腳本將一個對象直接賦值給了參數c,并且在這個對象上掛了一個valueOf回調,c要賦值給typed數組b,而b中的元素只能是Uint32類型,所以JIT會對參數c進行一個轉換(用到ToInt32),這會觸發c的valueOf回調,在回調函數中通過a[0]={}給數組a賦值,這會將a由NativeFloatArray變成VarArray,而后續代碼因為沒有檢查a數組改變所以繼續將其當作NativeFloatArray賦值造成了類型混淆。

補丁后代碼如下。

img

一般來說,Chakra引擎在對JIT中的回調進行優化時會考慮一個叫做ImplicitCallFlags的標志位,通過這個標志位,就可以檢測用戶函數是否可能被調用,如果是的話就會啟動bailout或進行相關檢測。但是這種機制存在一些問題,比如ImplicitCallFlags標志位到底在什么位置會被置位,它是否能保護所有存在回調函數的位置?

一個典型的例子:CVE-2017-11802

img

這個漏洞比較簡單,存在于RegexHelper::StringReplace函數中,regexp的replace方法,可以定義一個回調函數,但是在其實現中并沒有對回調函數進行保護,也就是說可以直接在regexp的replace方法中修改數組類型而不被JIT檢測到。

img

該漏洞的補丁也比較簡單,通過對兩處調用回調的位置添加ExecuteImplicitCall驗證,就可以修補該漏洞。這個補丁同時修補了一處位于JavascriptArray::ArraySpeciesCreate中的由于創建新對象而導致的回調。

img

這種機制在實現和優化過程中有沒有瑕疵呢?下面來看另一個例子CVE-2018-0840

img

這是一個直接對ExecuteImplicitCall函數進行對抗的漏洞,其問題本身在于ExecuteImplicitCall函數的實現,其代碼片段如下。

img

函數首先會執行implicitCall然后才會更新ImplicitCallFlags,單純從函數本身來考慮好像沒什么問題,但是這里面忽略了一個可能就是回調在執行過程中如果出現了一個異常該怎么處理,POC中的typeof實現位于JavascriptOperators::TypeofElem函數中,和漏洞有關的代碼如下。

img

回調會通過ExecuteImplicitCall函數進行調用,但是回調函數會觸發一個異常,該異常會被TypeofElem捕獲,也就是說ExecuteImplicitCall函數中更新ImplicitCallFlags的操作被跳過了,由于標志位沒有被更新,所以優化過程中的相應排錯機制也就沒有被生成,最終導致了漏洞的產生。

另外一個問題是CVE-2018-8556,通過補丁信息可以知道漏洞存在于GlobOptBailOut.cpp的MayNeedBailOnImplicitCall函數中,從名字可以推測,這個函數主要負責判斷JIT優化過程中是否對ImplicitCall生成bailout代碼。

img

在該函數對對象的length屬性進行獲取的操作中,判斷返回值的邏輯出現了問題。

img

從邏輯上看,string和滿足IsAnyArray并且不等于ObjectWithArray的對象都是可以通過驗證的,也就是說typedarray也是滿足條件的。

img

img

如果要給對象獲取length的操作加回調或者過濾操作,對象的length屬性的configurable特性必須為true,string和array的length都符合這個假定,但是typedarray卻是個例外,所以可以通過給typedarray的length屬性加回調的操作,去執行用戶定義的代碼來觸發類型混淆漏洞。

2.2. 思路二:通過合理的函數調用修改數組類型

接下來看第二種思路,通過合理函數調用來觸發數組類型改變。在一些函數處理中,由于功能原因會調用ToVarArray函數對數組類型進行改變。

下面舉例說明。

img

opt函數的JIT優化代碼如下:

img

可以看到,在call rax之后并沒有進行數組類型的檢測就直接賦值了,那么這個call中到底發生了什么呢?這個call調用了JavascriptOperators::OP_InitProto函數來初始化proto,在最后一次opt調用時,將array當作proto給了屬性鏈,在對屬性鏈賦值時,如果賦值參數是一個Native數組的話會將其轉換為VarArray(調用了ToVarArray函數)。其調用函數棧如下。

img

此時數組的類型已經發生了改變而JIT并沒有檢查到這一點所以產生了漏洞。

再來看一個較為復雜點的例子CVE-2018-0835

img

該漏洞存在于JavascriptArray::ReverseHelper函數中,函數會調用JavascriptArray::FillFromPrototypes,該函數通過遍歷prototype來填充array。

img

在程序中,函數確保prototype中的array不能是NativeArray。

img

也就是說,如果prototype是NativeArray數組則會被程序轉換為VarArray,如果能夠使一個數組的prototype為NativeArray,就可以通過數組的Reverse方法將其prototype的NativeArray轉換為VarArray。不過這里還有一個問題就是如何確保prototype是NativeArray,一般情況下如果一個數組被當作prototype,則它會被轉化為VarArray。

在JavascriptArray::EntrySort中存在如下代碼。

img

如果arr是一個NativeArray,它首先會變成一個VarArray執行sort回調,再變回NativeArray,如果能夠在回調中將這個arr賦給prototype,之后它的類型又會變回來,這樣就可以得到一個類型混淆漏洞。

2.3. 思路三:MissingItem

CVE-2018-0953同樣也是通過函數調用修改數組類型,這個漏洞特別之處在于引出了另一個關注點,即數組的MissingItem。MissingItem是一個數值,在64位程序上等于0x8000000280000002。Chakra引擎在數組創建的時候會使用這個值對數組元素進行初始化,表示數組中該元素還未進行賦值,另外數組還會保留一個標志位(NoMissingValues)來標志此數組是否有未被賦值的元素。

先看看下面這段代碼。

img

當執行數組的賦值操作,調用了NativeArray的SetItem函數,SetItem函數實現如下。

img

當給NativeArray賦值時,如果這個值等于MissingItem,可以將NativeArray轉化為VarArray。優化邏輯假設對數組進行賦值是一個很安全的操作,只要傳入參數不是一個對象那么就不會改變數組類型,但是并沒有考慮到如果賦值的值等于MissingItem的話會引起數組類型的變化,正是這種疏忽導致了漏洞的發生。

這個漏洞本身非常好理解,但是MissingItem本身又引出了一連串的問題。該漏洞的補丁程序修補了通過OP_SetElementI來調用SetItem的情況,但是這樣修補遠遠不夠,因為對該函數調用的位置其實非常多,于是找漏洞的思路變成了尋找為NativeArray賦值的各種路徑的問題。

CVE-2018-0953的漏洞發現者lokihardt在補丁修補后又提出兩種思路來繞過補丁,第一個是通過arraypush來調用SetItem。

觸發漏洞代碼如下:

img因為通過push對數組進行插入的操作會調用SetItem,所以數組改變的情況仍舊會存在。

img

第二個思路是先直接修改數組的元素,再通過cancat來修改數組類型。漏洞觸發代碼如下:

imgPOC首先通過set修改了數組中元素的值。

img對應的JIT代碼是這樣的。

img在修改了數組元素后,創造了一個有MissingItem但是HasNoMissingValues的array。

接著腳本調用了trigger函數,由于數組的HasNoMissingValues標志位為真,下圖代碼中的條件是滿足的。

img

因為數組有了MissingItem,所以可以進行到如下分支。

img

InternalFillFromPrototype函數會對buggy數組prototype鏈上所有對象調用EnsureNonNativeArray,也就是說會對arr調用EnsureNonNativeArray,這樣就可以修改其數組類型,但是JIT引擎并不知道arr類型已經改變,所以會導致類型混淆。

針對此問題,Chakra的工作人員開始大規模的檢查NativeArray的input,在LowerStElemC、GenerateProfiledNewScObjArrayFastPath、GenerateHelperToArrayPopFastPath等諸多函數上添加了MissItem的檢測(由于修補函數較多,這里就不一一列舉了,詳情請參考地址https://github.com/Microsoft/ChakraCore/commit/91bb6d68bfe0455cde08aaa5fbc3f2e4f6cc9d04)。

但是,通過如下代碼調用的OP_Memset函數并沒有對value進行檢查,仍舊可以用來構造擁有MissingItem但是HasNoMissingValues的array,并通過concat來得到一個類型混淆漏洞。

img

值得一提的是,在11月的補丁中Chakra直接對concat方法做了嚴格的處理,從情況上推測應該是找到了新的方法來將MissingItem寫入array,但由于網上沒找到相應的信息,再加上補丁并沒有對將值寫入array的代碼進行修補,反而限制了concat,所以也無法判斷具體情況。

img

2.4. 思路四:將數組偽裝成對象

最后一種思路,通過迷惑Chakra引擎,使其在生成JIT代碼過程中錯誤的將NativeArray當作其他對象,以至于沒有在恰當的位置添加檢查代碼。

公開的例子是CVE-2018-8466

img

Chakra使用JavascriptArray::GetArrayForArrayOrObjectWithArray來判斷對象是否是array,其邏輯如下所示。

img

通過CrossSite class來wrap一個對象的時候會替換該對象的虛表,所以被wrapping的數組將不會被識別為數組,這將導致無法在正確的地方生成對數組類型的檢查并產生類型混淆漏洞。

補丁除了驗證虛表是否是array對象之外,還檢查了對象是否是被CrossSite wrap的數組。

img

另一個例子是CVE-2018-8542,其補丁在ValueType::MergeWithObject中。

img

該函數主要用于合并兩個對象,可以看到補丁添加了驗證,用于確定兩個對象中是否有數組,再觀察一下沒打過補丁的問題代碼,如果兩個對象都不是UninitializedObject,則合并為Object對象,大致可以獲知漏洞產生的原因,在執行到這句的時候如果兩個對象中有一個是數組,在合并時數組會被當作對象來處理,優化過程中引擎把合并的數組當作了對象,那么對數組類型是否改變的檢測當然就不被需要,于是最終導致了類型混淆。

img

3. 總結

在過去一年左右,JIT編譯優化過程中的類型混淆是Chakra漏洞挖掘方面的一個主要關注點。從早期的利用未被保護的回調和正常函數來修改數組類型,再到尋找驗證過程中的邏輯問題,利用數組的MissingItem特性,將數組偽裝成其他類型對象思路,我們可以看到隨著研究者對Chakra引擎的深入研究,漏洞產生的位置已經從簡單的對象方法慢慢向JIT優化代碼生成過程中產生的各種邏輯和判斷問題靠攏,漏洞挖掘的門檻也有了顯著的提升。


啟明星辰積極防御實驗室(ADLab)

ADLab成立于1999年,是中國安全行業最早成立的攻防技術研究實驗室之一,微軟MAPP計劃核心成員。截止目前,ADLab通過CVE發布Windows、Linux、Unix等操作系統安全或軟件漏洞近400個,持續保持國際網絡安全領域一流水準。實驗室研究方向涵蓋操作系統與應用系統安全研究、移動智能終端安全研究、物聯網智能設備安全研究、Web安全研究、工控系統安全研究、云安全研究。研究成果應用于產品核心技術研究、國家重點科技項目攻關、專業安全服務等。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/768/