Author: Qixun Zhao(aka @S0rryMybad && 大寶) of Qihoo 360 Vulcan Team
作者博客:https://blogs.projectmoon.pw/2018/10/26/Chakra-JIT-Loop-LandingPad-ImplicitCall-Bypass/

前言

第一篇文章的時候,我們提到過關于回調的漏洞一般分為三種情況,其中第一種是GlobOpt階段的|BailOutOnImplicitCall| bailoutKind沒有加入.具體來說就是在GlobOpt階段遍歷處理每一個opcode的時候,chakra會檢測這個opcode是否有必要加上此bailoutKind,如果加上了,在Lower階段生成出的指令中會在指令結束的時候檢測一個implicit call flag,在發生回調的時候這個flag設置為true,JIT生成的檢測指令如果檢測到為true就會bailout,用于在回調發生的時候bailout。

今天我們要介紹的是CVE-2018-8456,這個漏洞的原理有點復雜,我的表達可能會不太清楚,希望大家能堅持看下去:)

Check Or Not Check?

我們知道,在很多情況下,上述的implicit call flag check指令是沒有必要生成的,所以有一個專門的函數決定當前opcode是否生成|BailOutOnImplicitCall| bailoutKind,這個函數是|IsImplicitCallBailOutCurrentlyNeeded|.

我們可以看到高亮的地方,對應的參數是|mayNeedImplicitCallBailOut|,如果為false,這個指令必定不會生成|BailOutOnImplicitCall|,換句話說,如果當前block |IsLandingPad|為true,當前block上所有的指令都不會生成check.這里我們有必要了解一些編譯原理的相關術語block和LandingPad

What is LandingPad

在編譯原理中,CFG(控制流程圖)中的最小單位是block,每一個不同的流程都會分裂出一個block,而block也是JIT中很喜歡進行優化的一個單元(我們可以看到optblock等等的函數),block組成function,opcode組成block.而LandingPad是針對Loop(循環)優化而生成的一個子block,用于存放loop中一定保持不變的變量的相關指令,也就是沒有必要每一次循環都調用的指令.舉個具體的例子,看如下js代碼:

我們知道數組每一次的訪問都是需要load field,type check, bound check等等,然后再賦值,但是這里很明顯type check和bound check都是可以提取到loop body外面,只需要在循環開始的時候檢查一次就可以了,否則會浪費很多時間執行沒必要的檢查,這個在chakra稱為|Loop hoist|.用于存放這些只需要在循環開始運行一次的指令的block就稱為|LandingPad|,在這個區域中生成的任何指令都不會進行implicit call check.

但是很明顯不是每一個opcode都是可以hoist到|LandingPad|的,用于決定函數是否可以hoist的函數是|TryHoistInvariant| => |OptIsInvariant|.簡單來說就是,如果chakra覺得opcode的所有src都是不變量并且opcode帶有CanCSE的屬性,這個opcode就會被hoist.通過簡單審閱一遍,我發現能hoist的指令的條件十分苛刻,主要在CanCSE屬性和要求Src的Type |IsPrimitive|為true這兩點上:

這幾乎把所有能回調的opcode都封掉了.其實這也很正常,如果帶有回調,opcode的dst就肯定不會為不變量.所以這里我們需要轉換思路.

Give me a callback in LandingPad

CSE是一種常見的編譯器優化措施,用于消除一些可以取代的公共子表達式,但是帶有回調的opcode是不可以通過CSE措施消除的,因為帶有回調往往就表示這個opcode產生的結果不會是不變量.既然不能直接hoist,那么我們能不能用已經hoist的指令生成一個新的帶有回調的指令呢,事實證明這個思路是可行的.我在審閱|OptHoistInvariant|的過程中,出現一種情況會生成一個新的opcode(OptHoistInvariant=>OptHoistUpdateValueType):

我們可以看到,|SetConcatStrMultiItemBE|有一個邏輯會生成|Conv_PrimStr|,并且這個opcode帶有|OpOpndHasImplicitCall|屬性,說明它是有可能回調的.聰明的讀者在這里可能也會提問為什么我們不直接插入|Conv_PrimStr|到LandingPad.這里因為要產生回調src必須不為Primitive,但是上文已經提到,如果|IsPrimitive|為false,這個指令是不可以hoist的,所以這里通過這種曲線救國的方法hoist上去.

What is your src’s TYPE, Conv_PrimStr?

接下來是此漏洞最關鍵的地方.

所以為什么通過|SetConcatStrMultiItemBE|hoist上去的|Conv_PrimStr|就可能不為Primitive?首先我們需要看函數|OptHoistUpdateValueType|的邏輯,正如函數名字那樣,因為在hoist的過程中,這個opcode是需要從一個block轉移到另一個block上(LandingPad),所以opcode的src type是需要更新的,因為type check指令可能存在與這兩個block之間,如果hoist到type check指令之前,type要變為Likely.還是舉個例子:

這里我們給var1變量的profile feed一個string類型,然后調用它的|slice|函數,在調用的時候需要type check string, type check完成后,在這個block中余下的地方,var1 這個value的type都是|Definite String|,所以|IsPrimitive|為true,并且其他兩個相加的變量都是常量字符串,這樣可以保證了每一次循環中|let tmp2 = var1 + ‘projectmoon’ + ‘projectmoon’;|tmp2得到的結果都是一樣的,完全可以hoist到loop body外面,從而|SetConcatStrMultiItemBE|會hoist到LandingPad,由于LandingPad在slice調用以前,也就是在type check string之前,所以這個時候的var1變量的type必須從|Definite String|變成|Likely String|.而|OptHoistUpdateValueType|就是專門負責這種情況的:

由于現在var1還沒有經過slice函數調用,也就是沒有經過type check string,所以它可能是非String類型的變量,所以這里還需要加入一個|Conv_PrimStr| opcode,用于把非String類型轉換成String,正如上文提到的(因為SetConcatStrMultiItemBE要求傳入的src都是String),這個指令有可能生成回調,同時它的src type |IsPrimitive|為false,按照chakra的設計,它是不可以出現在LandingPad區域的,但是通過這個trick我們得到了這樣的環境.

構造PoC

有了上文提到的要點后,我們可以開始構造PoC,首先是生成|SetConcatStrMultiItemBE|,這個是通過三個String相加生成(就如上圖的例子),這里我們使用var1 + “constString” + “constString”生成.其次var1變量必須是String類型(IsPrimitive為true),這樣才能hoist到LandingPad.這里我們通過調用var1.slice在String相加之前進行type check,從而保證了var1在|SetConcatStrMultiItemBE|這里的type是|definite String|,這里才能保證hoist成功.

這里還有一個問題就是type check指令的hoist(在chakra master版本有這個問題,正式版本中沒有),由于string.slice()需要進行type check,而chakra也認為這個type check可以hoist到LandingPad中(事實也應該如此),從而會導致我們的變量在LandingPad進入|Conv_PrimStr|之前type check失敗,然后發生bailout,這里我們只需要在loop body中加入arguments變量,就可以阻止string type check opcode的hoist.構造完有問題的循環體后,我們在循環體的前后加入兩個數組的access,在循環體的callback中改變數組的類型,導致type confusion.

最后JIT的函數體如下:

給JIT profile的時候,我們傳入一個string類型給string 參數:

這里傳入一個帶回調的obj觸發漏洞,這里需要注意的是我們不能進入循環體,不然slice函數的string type check會失敗然后bailout,所以start和end都必須為0,但是無論進不進入循環體,LandingPad的指令(回調函數)都是會執行的:

這個特性同時也會產生一些JS層面上的bug,同一段代碼在edge中會觸發回調,在其他主流瀏覽器中不會觸發回調(事實也不應該觸發回調,畢竟我們沒有進入循環體,沒有執行|let tmp2 = string + ‘projectmoon’ + ‘projectmoon’;|語句).

通過這個type confusion我們很容易泄露任何對象的地址和偽造任何對象(參考我們的第一篇文章),有了這兩個原語,距離RCE就不遠了,具體這里就不再敘述,網絡上有大量的公開文章.

總結

我們可以看到,通過不同block之間的優化,我們可以得到一些比較復雜的bug,而這些bug往往隱藏得比較深,fuzz也比較難以得到.也啟發了我們以后在審閱JIT的相關漏洞的時候,不要再單單針對某個opcode,而是通過block甚至function為單元的審核.


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