【V8】UMDCTF2025 literally-1984/1985 WriteUp
字数 2210 2025-08-29 22:41:32

UMDCTF2025 V8漏洞利用分析:literally-1984/1985

前言

本文详细分析UMDCTF2025中两道V8 CTF题目(literally-1984和literally-1985)的漏洞原理与利用技术。这两道题目本质上是相同的,1985只是1984的临时修复版,核心漏洞利用思路一致。

漏洞分析

Patch分析

Part 1: Int32加法优化修改

修改文件:machine-operator-reducer.cc
修改函数:MachineOperatorReducer::ReduceInt32Add(Node* node)

关键修改:

  • 添加了特殊优化规则:当加法操作的两个操作数都是2时,直接返回5
  • 影响范围:Turbofan优化阶段的Javascript层面的2+2运算

Part 2: 边界检查修改

修改内容:

  • 将边界检查条件从if (v8_flags.turbo_typer_hardening)改为if (v8_flags.turbo_typer_hardening && 2 + 2 == 5)
  • 由于源代码中的2+2不受Part1修改影响,这个条件永远为false
  • 结果:new_flags永远不会拥有CheckBoundsFlag::kAbortOnOutOfBounds标志位

CheckBoundsFlag::kAbortOnOutOfBounds的作用:

  • 当数组访问越界时强制终止程序
  • 去除该标志后,越界访问不会触发错误终止

漏洞原理

结合两个patch:

  1. Javascript层面的2+2=5(通过Turbofan优化)
  2. 禁用边界检查的强制终止

这使得我们可以:

  • 构造特定算术表达式绕过边界检查
  • 实现数组越界访问

漏洞利用

初始PoC构造

基础利用函数:

function test() {
    let arr = [1.1, 2.2, 3.3];
    let idx = 2 + 2; // 在Turbofan优化后变为5
    return arr[idx];
}

通过外层循环触发Turbofan优化后,可以成功泄露内存值。

内存布局分析

越界访问泄露的内存包含:

  • JSArray对象的map和properties域
  • 通过分析泄露值可以确定内存布局

越界访问扩展

通过乘法运算扩大越界范围:

function test() {
    let arr = [1.1, 2.2, 3.3];
    let idx = (2 + 2) * 2; // 优化后为5*2=10
    return arr[idx];
}

任意地址读写(AAR/AAW)尝试

初始思路:

  • 覆盖elements指针实现任意地址读写
  • 但会遇到新的CHECK校验elements对象的map

解决方案:

  • 使用JSTypedArray替代常规数组
  • 利用其data_ptrbase_pointer + external_pointer)实现任意地址读写

JSTypedArray利用

JSTypedArray关键结构:

  • data_ptr直接指向数据存储空间
  • 无需满足特定对象结构
  • 可以通过越界读写覆盖base_pointerexternal_pointer

实现AAW/AAR的步骤:

  1. 通过越界访问修改JSTypedArray的长度和elements长度
  2. 定位优化完成时机(通过检查长度变化)
  3. 构造稳定的越界读写对象数组
  4. 覆盖data_ptr相关字段实现任意地址读写

执行流控制

Wasm实例分析

关键结构:

  • wasm.Instance对象包含trusted_data
  • trusted_data中的jump_table_start指向RWX内存区域
  • 类似于glibc的PLT/GOT结构

利用步骤

  1. 寻找真实汇编地址

    • 正常调用一次wasm函数
    • 使用%DebugPrint()找到函数真实代码地址(懒加载后)
  2. 记录shellcode偏移

    • 计算shellcode距离原始jump_table_start的偏移
    • 使用addrof()arb_read()进行内存泄露
  3. 覆盖start_jump_table

    • 关键点:必须在首次调用前完成覆盖
    • 一旦函数被调用过,覆盖将无效
    • jump_table_start覆盖为shellcode地址

Shellcode构造

使用JIT-Spray技术:

  • 通过浮点数构造shellcode
  • 利用Turbofan优化将浮点数编译为可执行代码

完整利用流程

  1. 触发Turbofan优化使2+2=5
  2. 构造越界访问泄露内存信息
  3. 创建JSTypedArray对象
  4. 通过越界访问修改JSTypedArray属性
  5. 实现稳定的任意地址读写原语
  6. 准备wasm实例和shellcode
  7. 覆盖wasm实例的jump_table_start
  8. 首次调用wasm函数触发shellcode执行

优化后的EXP

// 触发2+2=5优化
function triggerOpt() {
    let arr = [1.1, 2.2, 3.3];
    let idx = 2 + 2;
    return arr[idx];
}

// 构造越界访问
function buildOOB() {
    let corrupting = [1.1, 2.2, 3.3];
    let corrupted = [4.4, 5.5, 6.6];
    
    function test() {
        let idx = (2 + 2) * 2; // 优化后为10
        corrupted[idx] = 0x41414141; // 越界写入
    }
    
    // 触发优化
    for (let i = 0; i < 100000; i++) {
        test();
    }
    
    return {corrupting, corrupted};
}

// 实现addrof原语
function addrof(obj) {
    // 利用越界读取对象地址
    // ...
}

// 实现任意读
function arb_read(addr) {
    // 通过修改JSTypedArray的data_ptr实现
    // ...
}

// 实现任意写
function arb_write(addr, value) {
    // 通过修改JSTypedArray的data_ptr实现
    // ...
}

// 主利用函数
function exploit() {
    // 1. 触发优化
    for (let i = 0; i < 100000; i++) {
        triggerOpt();
    }
    
    // 2. 构造越界访问
    let {corrupting, corrupted} = buildOOB();
    
    // 3. 创建JSTypedArray
    let typedArray = new Float64Array(10);
    
    // 4. 实现任意地址读写
    // ...
    
    // 5. 准备wasm实例
    let wasmCode = new Uint8Array([...]);
    let wasmModule = new WebAssembly.Module(wasmCode);
    let wasmInstance = new WebAssembly.Instance(wasmModule);
    
    // 6. 获取jump_table_start地址并覆盖
    let jump_table_addr = /* 通过addrof和偏移计算 */;
    arb_write(jump_table_addr, shellcode_addr);
    
    // 7. 触发执行
    wasmInstance.exports.main();
}

exploit();

关键注意事项

  1. 首次调用规则

    • wasm函数的jump_table_start覆盖必须在首次调用前完成
    • 一旦函数被调用过,覆盖将不会影响执行流
  2. 内存对齐

    • 所有内存操作需要注意对齐要求
    • 浮点数与整数的转换要正确处理
  3. 优化触发

    • 确保足够次数的循环触发Turbofan优化
    • 可以通过检查结果验证优化是否生效
  4. 偏移计算

    • 不同V8版本的偏移可能不同
    • 需要通过%DebugPrint()动态确定

总结

本漏洞利用的关键点在于:

  1. 利用Turbofan优化实现算术运算的异常行为
  2. 结合边界检查的禁用实现越界访问
  3. 通过JSTypedArray的data_ptr机制绕过常规数组的限制
  4. 精确控制wasm实例的跳转表实现代码执行

这种利用方式展示了现代JIT引擎中类型混淆与优化器交互的复杂安全问题,对于理解V8引擎的安全机制具有重要意义。

UMDCTF2025 V8漏洞利用分析:literally-1984/1985 前言 本文详细分析UMDCTF2025中两道V8 CTF题目(literally-1984和literally-1985)的漏洞原理与利用技术。这两道题目本质上是相同的,1985只是1984的临时修复版,核心漏洞利用思路一致。 漏洞分析 Patch分析 Part 1: Int32加法优化修改 修改文件: machine-operator-reducer.cc 修改函数: MachineOperatorReducer::ReduceInt32Add(Node* node) 关键修改: 添加了特殊优化规则:当加法操作的两个操作数都是2时,直接返回5 影响范围:Turbofan优化阶段的Javascript层面的2+2运算 Part 2: 边界检查修改 修改内容: 将边界检查条件从 if (v8_flags.turbo_typer_hardening) 改为 if (v8_flags.turbo_typer_hardening && 2 + 2 == 5) 由于源代码中的2+2不受Part1修改影响,这个条件永远为false 结果: new_flags 永远不会拥有 CheckBoundsFlag::kAbortOnOutOfBounds 标志位 CheckBoundsFlag::kAbortOnOutOfBounds 的作用: 当数组访问越界时强制终止程序 去除该标志后,越界访问不会触发错误终止 漏洞原理 结合两个patch: Javascript层面的2+2=5(通过Turbofan优化) 禁用边界检查的强制终止 这使得我们可以: 构造特定算术表达式绕过边界检查 实现数组越界访问 漏洞利用 初始PoC构造 基础利用函数: 通过外层循环触发Turbofan优化后,可以成功泄露内存值。 内存布局分析 越界访问泄露的内存包含: JSArray对象的map和properties域 通过分析泄露值可以确定内存布局 越界访问扩展 通过乘法运算扩大越界范围: 任意地址读写(AAR/AAW)尝试 初始思路: 覆盖elements指针实现任意地址读写 但会遇到新的CHECK校验elements对象的map 解决方案: 使用JSTypedArray替代常规数组 利用其 data_ptr ( base_pointer + external_pointer )实现任意地址读写 JSTypedArray利用 JSTypedArray关键结构: data_ptr 直接指向数据存储空间 无需满足特定对象结构 可以通过越界读写覆盖 base_pointer 和 external_pointer 实现AAW/AAR的步骤: 通过越界访问修改JSTypedArray的长度和elements长度 定位优化完成时机(通过检查长度变化) 构造稳定的越界读写对象数组 覆盖 data_ptr 相关字段实现任意地址读写 执行流控制 Wasm实例分析 关键结构: wasm.Instance 对象包含 trusted_data trusted_data 中的 jump_table_start 指向RWX内存区域 类似于glibc的PLT/GOT结构 利用步骤 寻找真实汇编地址 : 正常调用一次wasm函数 使用 %DebugPrint() 找到函数真实代码地址(懒加载后) 记录shellcode偏移 : 计算shellcode距离原始 jump_table_start 的偏移 使用 addrof() 和 arb_read() 进行内存泄露 覆盖start_ jump_ table : 关键点:必须在首次调用前完成覆盖 一旦函数被调用过,覆盖将无效 将 jump_table_start 覆盖为shellcode地址 Shellcode构造 使用JIT-Spray技术: 通过浮点数构造shellcode 利用Turbofan优化将浮点数编译为可执行代码 完整利用流程 触发Turbofan优化使2+2=5 构造越界访问泄露内存信息 创建JSTypedArray对象 通过越界访问修改JSTypedArray属性 实现稳定的任意地址读写原语 准备wasm实例和shellcode 覆盖wasm实例的jump_ table_ start 首次调用wasm函数触发shellcode执行 优化后的EXP 关键注意事项 首次调用规则 : wasm函数的jump_ table_ start覆盖必须在首次调用前完成 一旦函数被调用过,覆盖将不会影响执行流 内存对齐 : 所有内存操作需要注意对齐要求 浮点数与整数的转换要正确处理 优化触发 : 确保足够次数的循环触发Turbofan优化 可以通过检查结果验证优化是否生效 偏移计算 : 不同V8版本的偏移可能不同 需要通过%DebugPrint()动态确定 总结 本漏洞利用的关键点在于: 利用Turbofan优化实现算术运算的异常行为 结合边界检查的禁用实现越界访问 通过JSTypedArray的data_ ptr机制绕过常规数组的限制 精确控制wasm实例的跳转表实现代码执行 这种利用方式展示了现代JIT引擎中类型混淆与优化器交互的复杂安全问题,对于理解V8引擎的安全机制具有重要意义。