首页
社区
课程
招聘
1
[原创] CVE-2024-0517 漏洞分析
发表于: 2024-4-21 20:17 8971

[原创] CVE-2024-0517 漏洞分析

2024-4-21 20:17
8971

前言


这个漏洞发生在 MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct 函数中,考虑之前分析的 CVE-2023-4069 也是发生在该函数中,所以打算把该漏洞也分析了。该漏洞主要发生在折叠分配时,未考虑内存空间分配与初始化之间的操作可能导致触发 gc,从而导致 UAF

环境搭建

1
2
git checkout d8fd81812d5a4c5c3449673b6a803279c4bdb2f2
gclient sync -D

漏洞分析

还是从 patch 入手:

1
2
3
4
5
6
7
8
9
10
11
12
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index ad7eccf..3dd3df5 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -5597,6 +5597,7 @@
           object = BuildAllocateFastObject(
               FastObject(new_target_function->AsJSFunction(), zone(), broker()),
               AllocationType::kYoung);
+          ClearCurrentRawAllocation();
         } else {
           object = BuildCallBuiltin(
               {GetConstant(current_function), new_target});

可以看到补丁代码非常简单,就是添加了个 ClearCurrentRawAllocation 函数:

1
2
3
void MaglevGraphBuilder::ClearCurrentRawAllocation() {
  current_raw_allocation_ = nullptr;
}

该函数的功能为将 current_raw_allocation_ 指针清空

这里补丁代码打在了 TryBuildFindNonDefaultConstructorOrConstruct 函数中,其上层调用链为:

1
2
VisitFindNonDefaultConstructorOrConstruct
    TryBuildFindNonDefaultConstructorOrConstruct

VisitFindNonDefaultConstructorOrConstruct 其实我们在之前分析 CVE-2023-4069 时就详细分析过,其主要就是处理 FindNonDefaultConstructorOrConstruct 节点的,但是这里还是放一下代码分析吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
  ValueNode* this_function = LoadRegisterTagged(0); // target
  ValueNode* new_target = LoadRegisterTagged(1);    // new_target
 
  auto register_pair = iterator_.GetRegisterPairOperand(2);
  // 先调用 TryBuildFindNonDefaultConstructorOrConstruct
  if (TryBuildFindNonDefaultConstructorOrConstruct(this_function, new_target, register_pair)) {
      return;
  }
  // 失败则调用 Builtin_FindNonDefaultConstructorOrConstruct
  CallBuiltin* result =
      BuildCallBuiltin({this_function, new_target});
  StoreRegisterPair(register_pair, result);
}

这里会先调用 TryBuildFindNonDefaultConstructorOrConstruct 尝试进行图创建:

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
bool MaglevGraphBuilder::TryBuildFindNonDefaultConstructorOrConstruct(
    ValueNode* this_function, ValueNode* new_target,
    std::pair result) {
  // See also:
  // JSNativeContextSpecialization::ReduceJSFindNonDefaultConstructorOrConstruct
  // 【1】获取 target constant
  compiler::OptionalHeapObjectRef maybe_constant = TryGetConstant(this_function);
  if (!maybe_constant) return false;
  // 获取 map 和原型链上的对象
  compiler::MapRef function_map = maybe_constant->map(broker());
  compiler::HeapObjectRef current = function_map.prototype(broker());
 
  // TODO(v8:13091): Don't produce incomplete stack traces when debug is active.
  // We already deopt when a breakpoint is set. But it would be even nicer to
  // avoid producting incomplete stack traces when when debug is active, even if
  // there are no breakpoints - then a user inspecting stack traces via Dev
  // Tools would always see the full stack trace.
  // 遍历原型链
  while (true) {
    // 遍历 __proto__
    // 如果原型对象不是 JSFunction,则遍历结束
    if (!current.IsJSFunction()) return false;
    // 当前原型对象 current_function
    compiler::JSFunctionRef current_function = current.AsJSFunction();
 
    // If there are class fields, bail out. TODO(v8:13091): Handle them here.
    if (current_function.shared(broker()).requires_instance_members_initializer()) {
      return false;
    }
 
    // If there are private methods, bail out. TODO(v8:13091): Handle them here.
    if (current_function.context(broker()).scope_info(broker()).ClassScopeHasPrivateBrand()) {
      return false;
    }
    // 获取函数类型 kind
    FunctionKind kind = current_function.shared(broker()).kind();
    // 如果是派生默认构造函数,则直接跳过
    if (kind != FunctionKind::kDefaultDerivedConstructor) {
      // The hierarchy walk will end here; this is the last change to bail out
      // before creating new nodes.
      if (!broker()->dependencies()->DependOnArrayIteratorProtector()) {
        return false;
      }
      // 【2】获取 new_target constant
      compiler::OptionalHeapObjectRef new_target_function = TryGetConstant(new_target);
      // 如果是顶层默认构造函数,则进行相关处理
      if (kind == FunctionKind::kDefaultBaseConstructor) {
        // Store the result register first, so that a lazy deopt in
        // `FastNewObject` writes `true` to this register.
        StoreRegister(result.first, GetBooleanConstant(true));
 
        ValueNode* object;
        // new_target_function 存在且是 JSFunction
        // 并且 new_target_function 具有一个有效的 initial_map
        //  即 initial_map.constructor ==? target
        if (new_target_function && new_target_function->IsJSFunction() &&
            HasValidInitialMap(new_target_function->AsJSFunction(), current_function)) {
             //【3】为对象分配空间
              object = BuildAllocateFastObject(
                  FastObject(new_target_function->AsJSFunction(), zone(), broker()),
                  AllocationType::kYoung);
        } else {
          object = BuildCallBuiltin({GetConstant(current_function), new_target});
          // We've already stored "true" into result.first, so a deopt here just
          // has to store result.second. Also mark result.first as being used,
          // since the lazy deopt frame won't have marked it since it used to be
          // a result register.
          current_interpreter_frame_.get(result.first)->add_use();
          object->lazy_deopt_info()->UpdateResultLocation(result.second, 1);
        }
        StoreRegister(result.second, object);
      } else {
        StoreRegister(result.first, GetBooleanConstant(false));
        StoreRegister(result.second, GetConstant(current));
      }
 
      broker()->dependencies()->DependOnStablePrototypeChain(
          function_map, WhereToStart::kStartAtReceiver, current_function);
      return true;
    }
 
    // Keep walking up the class tree.
    // 遍历下一个 __proto__
    current = current_function.map(broker()).prototype(broker());
  }
}

可以看到这里我们可以将其分为快速路径和慢速路径,快速路径主要就是利用 new_target.initial 直接进行对象创建,慢速路径则退回到内建函数 FastNewObject,这里我们主要看快速路径,快速路径为 【1】->【2】->【3】,而 【3】 也是漏洞代码所在处,所以需要满足以下条件:

  • 1、TryGetConstant(this_function)
  • 2、TryGetConstant(new_target)
  • 3、new_target.initial.constructor === target

这里想要到达想要到达漏洞逻辑,得绕过这三个判断,前面两个还是之前的方式插入 CheckValue 节点绕过,第三个就不多说了,new_target 是派生构造函数即可,或者顶层默认构造函数也????,比较简单

最后为分配对象的语句如下,也是漏洞代码所在处:

1
2
3
object = BuildAllocateFastObject(
        FastObject(new_target_function->AsJSFunction(), zone(), broker()),
        AllocationType::kYoung);

然后跟进 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
ValueNode* MaglevGraphBuilder::BuildAllocateFastObject(FastObject object, AllocationType allocation_type) {
  SmallZoneVector properties(object.inobject_properties, zone());
  for (int i = 0; i < object.inobject_properties; ++i) {
    // MaglevGraphBuilder::BuildAllocateFastObject(FastField value, AllocationType allocation_type)
    properties[i] = BuildAllocateFastObject(object.fields[i], allocation_type);
  }
  // elements
  // MaglevGraphBuilder::BuildAllocateFastObject(FastFixedArray value, AllocationType allocation_type)
  ValueNode* elements = BuildAllocateFastObject(object.elements, allocation_type);
 
  DCHECK(object.map.IsJSObjectMap());
  // TODO(leszeks): Fold allocations. 尝试折叠分配,allocation 就是分配空间的指针
  ValueNode* allocation = ExtendOrReallocateCurrentRawAllocation(object.instance_size, allocation_type);
  // 设置对象的 map,主要就是添加一个 StoreMap 节点
  BuildStoreReceiverMap(allocation, object.map);
  // 设置 Properties 为 EmptyFixedArray,添加 StoreTaggedFieldNoWriteBarrier 节点
  AddNewNode(
      {allocation, GetRootConstant(RootIndex::kEmptyFixedArray)}, JSObject::kPropertiesOrHashOffset);
   
 if (object.js_array_length.has_value()) {
    // 如果 js_array_length 有值,则初始化 length
    // 添加 StoreTaggedFieldNoWriteBarrier 节点 或 StoreTaggedFieldWithWriteBarrier 节点
    BuildStoreTaggedField(allocation, GetConstant(*object.js_array_length), JSArray::kLengthOffset);
  }
  // 设置 Elements
  // 添加 StoreTaggedFieldNoWriteBarrier 节点 或 StoreTaggedFieldWithWriteBarrier 节点
  BuildStoreTaggedField(allocation, elements, JSObject::kElementsOffset);
  // 设置属性
  for (int i = 0; i < object.inobject_properties; ++i) {
    BuildStoreTaggedField(allocation, properties[i], object.map.GetInObjectPropertyOffset(i));
  }
  return allocation;
}

这里可以看到分配空间调用了 ExtendOrReallocateCurrentRawAllocation 函数,其会尝试折叠分配:

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
ValueNode* MaglevGraphBuilder::ExtendOrReallocateCurrentRawAllocation(
                                    int size, AllocationType allocation_type) {
  // 【1】
  if (!current_raw_allocation_ || // current_raw_allocation_ 为空
      current_raw_allocation_->allocation_type() != allocation_type || // 分配类型不一致
      !v8_flags.inline_new)     // 头一次分配
  {        
    // 分配 size 空间,节点为 AllocateRaw
    current_raw_allocation_ = AddNewNode({}, allocation_type, size);
    return current_raw_allocation_;
  }
  // 如果上面三个条件都不满足,则会走到这里
  // 即 current_raw_allocation_ 不为空,且分配类型一致,且不是头一次分配
  int current_size = current_raw_allocation_->size();
  // 【2】检查是否可以折叠分配
  //    如果折叠分配后空间太大,则单独分配,并更新 current_raw_allocation_
  if (current_size + size > kMaxRegularHeapObjectSize) {
    return current_raw_allocation_ = AddNewNode({}, allocation_type, size);
  }
  // 【3】折叠分配,current_size 应当大于 0
  DCHECK_GT(current_size, 0);
  int previous_end = current_size; // previous_end 即当前对象的起始位置
  current_raw_allocation_->extend(size); // 扩展当前分配空间
  // FoldedAllocation 节点,这里只记录 current_raw_allocation_ / previous_end 即可
  // 该对象的位置为:current_raw_allocation_ + previous_end
  return AddNewNode({current_raw_allocation_}, previous_end);
}

先来说下什么是折叠分配?顾名思义,当我们在进行内存分配时,可能每次分配一小块内存,比如下面场景:


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-7-1 13:29 被XiaozaYa编辑 ,原因:
收藏
免费 1
支持
分享
赞赏记录
参与人
雪币
留言
时间
PLEBFE
为你点赞~
2024-5-31 01:30
最新回复 (17)
雪    币: 5317
活跃值: (3443)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
2

Google Chrome V8 CVE-2024-0517 Out-of-Bounds Write Code Execution这标题意思是越界写,不是UAF,这个如果没弄清的话估计用不出来。                                                                                                     
我看了这篇文章,还没有自己调试,我的理解是,因为优化过程中没考虑到垃圾回收的情况,所以那个指针Offset+12没有清零。

但是发生垃圾回收到了新堆块以后,Offset+12之前的对象+4,+8偏移的对象被垃圾回收清除,并不会转移到新的堆块,这时再用新堆块+Offset12索引的话,就会越界写破坏到了别的对象。

如果我的理解是正确的,这个漏洞利用不需要占据释放后的内存,只需要知道垃圾回收后的内存布局,然后越界修改就行了。


最后于 2024-4-22 10:42 被苏啊树编辑 ,原因:
2024-4-22 10:36
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
3
本质就不是一个UAF吗?gc时把那块内存回收了,而后面赋值时重写了那块内存。可能我没搞明白,等下我看看能不能找管理把这篇文章先下了
2024-4-22 12:52
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
4
苏啊树 Google&nbsp;Chrome&nbsp;V8&nbsp;CVE-2024-0517&nbsp;Out-of-Bounds&nbsp;Write& ...
感觉你应该是对的,根据 Maglev IR 来看,current_raw_allocation_  应该是保存在栈上的。后面进行初始化时是直接从栈上拿的 current_raw_allocation_  指针,所以确实应该只是一个越界写......我就说咋这么难写。。最开始是自己分析的,所以把他当成 UAF 了,参考文章我没细读,就看了看利用的部分,感谢大佬指正,我后面调试一下,然后在改一改吧
2024-4-22 12:57
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
5

确实是从栈上获取的 current_raw_allocation_ 指针

2024-4-22 13:02
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
6
但我还是感觉是一个 UAF,,,,
2024-4-22 13:11
0
雪    币: 5317
活跃值: (3443)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
7
XiaozaYa 但我还是感觉是一个 UAF,,,,[em_5]

栈上的指针指向前后因为垃圾回收前后指向的值应该发生了变化,这个情况在MaglevIR层面上好像看不到,垃圾回收后在这个IR指令层面从栈拿出来后那个地址已经指向了垃圾回收后的地址,但是问题是在垃圾回收之后,他前面的对象已经被回收了,这时正确的处理应该是使用offset为0的地址索引,但是有漏洞的版本使用offset+12,越界写到了其他对象,造成了崩溃。

就是你截图的 31/32FoldAllocation[+12]这个指令,是有问题的。

所以利用时候应该是越界写相邻的数组大小这种思路,而不是UAF的利用方法,不是依靠占位,我估计这就是你一直没有成功利用的原因。。。

最后于 2024-4-22 13:37 被苏啊树编辑 ,原因:
2024-4-22 13:31
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
8
嗯....仔细的看了下EXODUS上的解析文章,似乎有点明白了:触发 gc 后,原始分配的空间会被移动,但是移动后分配的空间只是第一个已经初始化对象的空间,所以当第二个折叠分配对象初始化时则导致了越界写,所以这里需要让目标对象分配在 this 对象的下面才能完成覆写(:我的 exp 一直不成功的原因就是目标对象一直在 this 对象的上面,导致覆盖写数据失败....所以这里其实还是要占据 this 下面的空间。也是我是时候深入 gc 了
2024-4-22 13:57
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
9
苏啊树 XiaozaYa 但我还是感觉是一个 UAF,,,,[em_5] 栈上的指针指向前后因为垃圾回收前后指向的值应该发生了变化,这个 ...
从  0 开始写也是错误的应该,因为 gc 后根本没有为第二个对象分配空间。。。就不应该优化
2024-4-22 14:02
0
雪    币: 5317
活跃值: (3443)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
10
XiaozaYa 从 0 开始写也是错误的应该,因为 gc 后根本没有为第二个对象分配空间。。。就不应该优化
可以打好补丁再看看指令变化验证一下
2024-4-22 14:48
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
11
苏啊树 可以打好补丁再看看指令变化验证一下
是的,感谢大佬的指正,后面会在重新好好调一调
2024-4-22 14:59
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
我调试了一下,如果在super之前分配一个对象x,在super之后分配一个对象y,在调用super的时候,不会把对象x算作在折叠分配中,但会把y和this的分配一起进行折叠。x->super->y这个过程中,在super存在gc,感觉目的好像是为了让x的对象重新排布在this后面,然后通过之前折叠的y,从而可以越界写x的内容
2024-4-27 20:35
0
雪    币: 70
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
999
13

我的理解,第一次折叠分配初始化后,与第二次分配初始化前,如果触发了GC,会把第一次分配移动到新的内存区域中,而第二次分配因为没有被初始化GC会把他清理掉导致初始化第二次分配的时候会从第一次分配的也就是内存起始地址的12偏移处开始写入(假设第一次分配12bytes),因为第二次分配被清理没有给他预留内存,所以此处存放的并不是第二次分配的对象所以导致写到了其他对象字段

最后于 2024-4-28 11:12 被999编辑 ,原因:
2024-4-28 10:46
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
14
岚沐 我调试了一下,如果在super之前分配一个对象x,在super之后分配一个对象y,在调用super的时候,不会把对象x算作在折叠分配中,但会把y和this的分配一起进行折叠。x->super-& ...
问题是我调试触发 gc后 x 对象并不在 this 对象后面,所以导致覆写失败
2024-4-28 14:40
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
15
999 我的理解,第一次折叠分配初始化后,与第二次分配初始化前,如果触发了GC,会把第一次分配移动到新的内存区域中,而第二次分配因为没有被初始化GC会把他清理掉导致初始化第二次分配的时候会从第一次分配的也就是 ...
我后面调试确实就是这样的,从 new_space -> old_space,没有给第二个对象分配空间。但是写利用有个问题就是:如果把目标对象分配在 this 对象的后面,这样才能有效覆写关键数据
2024-4-28 14:44
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
16
岚沐 我调试了一下,如果在super之前分配一个对象x,在super之后分配一个对象y,在调用super的时候,不会把对象x算作在折叠分配中,但会把y和this的分配一起进行折叠。x->super-& ...
嗯....这里 gc 的目的不是这个
2024-4-28 14:47
0
雪    币: 70
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
999
17
XiaozaYa 我后面调试确实就是这样的,从 new_space -> old_space,没有给第二个对象分配空间。但是写利用有个问题就是:如果把目标对象分配在 this 对象的后面,这样才能有效覆写关键数据
issue tracker上有利用代码,实在写不出来利用的话或许可以参考参考
2024-4-28 17:32
0
雪    币: 5930
活跃值: (2935)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
18
999 issue tracker上有利用代码,实在写不出来利用的话或许可以参考参考
那个我很早就调过了,没调通(:把 %NativeFunction 替换为循环触发后,内存布局不好控制(:就这样吧,我想开了
2024-4-30 16:18
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册