作者:Qixun Zhao(aka @S0rryMybad && 大寶) of Qihoo 360 Vulcan Team
作者博客:https://blogs.projectmoon.pw/2018/08/17/Edge-InlineArrayPush-Remote-Code-Execution/
前言
回顧之前Edge出現的漏洞,在root cause屬于回調的問題當中(不一定有用到回調的root cause就是回調的問題),雖然出現的漏洞有很多個,但是無非可以分為三種情況:
第一種,GlobOpt階段的bailoutKind沒有加入或者處理不當,對應的一個例子: CVE-2017-11837
第二種,Lower階段新加入的指令沒有update GolbOpt階段的bailoutkind,對應的一個例子: CVE-2017-8601
最后一種是進行回調的時候沒有update implicitCallFlags,導致GlobOpt和Lower階段的工作全部白費:CVE-2017-11895
前段時間一直在學習與研究一些新的方向iOS的越獄(以后有機會也會寫一系列關于越獄的文章),沒有怎么關注JIT的代碼.mosec之后回來看一看JIT相關的代碼,發現漏洞可能沒以前好找了,但是也不是沒有.在最近我報告了一系列的Edge 漏洞給微軟,在此后的一段時間我將會陸續分享這一系列的關于JIT的與以往不太一樣的漏洞, 這些漏洞品相都是相當好, 并且最后能RCE的.
作為我一系列Edge JIT 漏洞的第一篇,這次我選擇的是原理最簡單的一個洞作為分享(屬于單個opcode的問題),當然也因為漏洞修補的時間剛好.在前幾天的微軟補丁中,修復了我兩個Edge的漏洞,其中這篇就是CVE-2018-8372,另外一個并沒有assign CVE,但是在代碼中已經修復,在以后的文章中會提到.
這一系列的文章需要讀者對js或者瀏覽器漏洞有一定的研究基礎,因為我們只會關注于JIT本身,而不會過多關注js和瀏覽器的一些基礎概念.

JIT優化eliminate duplicate check
試想如下一段JS代碼JIT會怎么處理:
Function jit(){
arr[1] = 1.1;
arr[2] = 2.2;
}
我們都知道js是一門動態語言,所以一開始肯定是先檢查arr的類型,然后再進行賦值,但是由于opcode的原子性,在第二行語句的時候假如沒有優化,肯定也會再次檢查一次arr的類型.但是聰明的讀者肯定也會發現,第二行的檢查是沒有必要的,剛剛才檢查過,為啥又要檢查一次啊,cpu閑的蛋疼啊,而且Edge還想你設置成默認瀏覽器呢,還要比V8快呢,怎么能這么多冗余的指令呢?于是這時候chakraCore就要引入JIT的其中一個優化措施,消除冗余的檢查.下面我們看看最終經過GlobOpt階段的IR是怎樣的:

我們可以清楚看到,第二句JS代碼沒有|BailOnNotArray|,也就是沒有了類型的檢查.
但是也不是什么時候也能消除檢查的,在中間存在回調的時候就不能消除檢查:
Arr[1] = 1.1;
Object.property; => callback
Arr[1] = 2.2;
這里明顯中間有一個回調,所以我們是需要把chakra中已經保存的type信息去掉,這時候chackaCore就引入了一個kill機制,其中一個相關的處理代碼是在|GlobOpt::CheckJsArrayKills|.在審閱這個函數的時候,InlineArrayPush opcode引起了我的注意:

代碼注釋已經說得很清楚,假如array的type與element type一致,就不要把type信息去掉,這是一個比較激進的優化,而InlineArrayPush opcode通過調用Array.prototype.push生成的.簡單來說就是,假如生成這么一段代碼:
arr[1] = 2.2;
arr.push(value);
arr[1] = 3.3;
假如arr的type信息是float array,value的type信息是float,前面保存的arr type信息就不會被kill.換言之,在|arr[1] = 3.3;|中就不會生成|baitOnNotArray| IR,沒有了type類型的檢查代碼.是的,這樣就非常快了,比v8還快,但是安全嗎?需要注意的是,在push里面不能觸發回調,因為InlineArrayPush會生成|BailOutOnImplicitCallsPreOp|,如果觸發回調是會bailout的.
接下來要思考的問題就非常直觀了,將一個float數值push到一個float array里面,在不觸發回調的前提下,真的不會改變array的類型嗎???
JavaScript undefined
在JS里面,有一個特殊的值undefined,表示這個變量未初始化.試想這樣一個數組|arr = [1.1,,3.3];|,不考慮prototype的情況下,當我們訪問arr[1]的時候就會返回一個值undefined.這里就有一個疑問了,這個undefined的值在內存里究竟這么表示,所以我們先看一下這個arr的內存表示:

可以看到,arr[1]在內存的值是0x8000000280000002,這時候敏銳的讀者可能就會想到了,這個值是在浮點數的表示范圍內啊(詳情查看IEEE 754),通過轉換,我們可以知道這個值對應浮點數-5.3049894784e-314.所以為了區分-5.3049894784e-314和undefined,chakraCore在float Array的|setItem|有一個特殊的處理:

當把這個值傳入setItem,就會進行數組的轉換,變成var array,而在push函數里面,是通過調用setItem函數處理的.所以回到最初的問題:|將一個float數值push到一個float array里面,在不觸發回調的前提下,真的不會改變array的類型嗎???|.答案是否定的.
Please DON’T kill my NativeArrays >_<
但是,通過如下pattern,我發現我的array type信息還是被kill了,即使我保證了arr的type信息是float array,value的type信息是一個float:
Arr[1] = 1.1;
Arr.push(value);
Arr[1] = 3.3;

在push下面的arr賦值還是生成了bailOnNotArray.通過研究發現,在上面還有一個語句引起了我的注意,push語句會把missingValues相關的type信息刪掉:

我們來看看這個MissingValues 的kill是怎么處理的:

原來如果chakraCore覺得arr的type信息中沒有MissingValues,在經過push后,還是會把arr的type信息刪去,當然這個十分容易bypass,只需要令|valueInfo->HasNoMissingValues|返回false,就會進入continue語句.換句話說,就是需要我們傳入的arr中帶有MissingValues.所以最后的PoC也就呼之欲出:

From Bug To Remote Code Execution
在這一系列的文章當中,我們對于利用的講解步驟都只是會講解到達成兩個漏洞利用原語,第一是任意對象地址泄露,第二是任意地址對象fake.當有了這兩個原語以后到最后的RCE網絡上已經有大量公開的參考資料,大家可以自行參考查閱.
在PoC中,我們已經達到了第二種的漏洞利用原語,但是對于第一種的利用還是需要點技巧.因為在這個bug中,我們不能觸發回調,而且只能插入一個固定的double float -5.3049894784e-314,所以很難泄露任意一個對象的地址.想挑戰的讀者可以先行嘗試一些怎么利用,或者可以直接繼續閱讀查看怎么編寫代碼.
試想一下,在經過push語句后,|arr|對象的類型已經變成var array,換言之就是arr現在可以通過任何關于var array的檢查.所以現在|arr|可以用于var array的賦值,而這個賦值是可以任意的一個對象.而我們在JIT的profile階段必須要給對應的Symbol transfer一個var array.數組的賦值對應的字節碼是StElem_A,這里唯一需要注意的就是不要觸發這個Opcode
的任何bailout.下面查看|oarr[2] = leak_object|生成的對應IR信息:

可以看到,只需要array的type信息符合(oarr必須是var array,上文已經提到,|arr|通過push已經轉換成var array),MissingValue信息符合,index不大于數組的長度,即可當成一個正常的var array賦值.通過賦值以后,現在arr[2]上已經有一個對象的地址,通過return arr[2]即可得到該對象的地址.但是這個return的語句對應的native code的|RegOpnd|是float類型,而不是var,所以會直接把對象地址以浮點數的方式返回給我們,從而泄露該對象的地址,因為|arr|數組現在JIT的profile信息中還是一個double array.
這部分可能有點難以理解,下面我們結合PoC和注釋進一步理解:

在每次JIT開始之前,都會經歷一個profile階段,用于收集對象的類型信息用于JIT時候生成相關的類型檢查與訪問代碼.在Profile階段,我傳入了一個NoMissingValues為false的float array,所以|arr[0]|和|arr[2]|的讀寫都是以float形式訪問,換句話說,如果arr數組中存在對象的地址則可以通過|return arr[2]|成功讀取出來.但是必須在第一句|arr[0]|通過類型檢查,也就是arr一開始必須為float array類型.
其次,我傳入了一個var array類型的數組|oarr|,所以|oarr[2] = leak_object|會把需要泄露的對象地址賦值到oarr[2]中,但是必須通過類型檢查,也就是oarr在訪問的時候必須為var array類型.
在漏洞觸發的最后一次調用中,|arr|和|oarr|其實是同一個數組,在|arr[0] = 1.1|中,此時arr是float array,通過檢查,賦值成功.通過|arr.push(value)|觸發漏洞,改變數組類型,變成var array類型.在第三行代碼|oarr[2] = leak_object|,因為|arr|和|oarr|是同一個數組,所以oarr當前為var array類型,通過檢查,賦值成功.
最后一句是最關鍵的代碼,我們可以看到,|arr[0] = 1.1|和|return arr[2]|中有兩行代碼,這兩行代碼必須不能kill |arr|的type信息,否則就會重新類型檢查,因為arr已經轉變成var array類型了,如果此時有類型檢查就會檢查失敗然后bailout.上文已經詳細分析了如果arr NoMissingValues為false,|arr.push(value)|是不會kill arr的type信息的.所以現在剩下|oarr[2] = leak_object|這句,對應的opcode是StElemI_A,|CheckJsArrayKills|代碼如下:

我們可以看到,并沒有任何情況會kill array的type信息.所以到最后沒有任何類型檢查,直接以浮點數的方式訪問已經變成var array類型的arr,返回剛剛賦值的對象|leak_object|,將浮點數轉換為16進制,即可得到對象的地址.得到這兩個原語以后,距離RCE就不遠了.
總結
在這個bug中,我們可以看到,不需要觸發任何的回調,最后我們成功利用了這個bug達到任意對象地址泄露和任意地址對象fake.關于這個bug的利用我個人覺得還是有點技巧的,而這個bug的根本原因就是開發人員忘記了push中的一些特殊情況而導致的過激優化,我們需要時刻記著,在保持性能優化的同時,也要注重安全.
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/677/
暫無評論