CVE-2024-2887: A PWN2OWN WINNING BUG IN GOOGLE CHROME

In this guest blog from Master of Pwn winner Manfred Paul, he details CVE-2024-2887 – a type confusion bug that occurs in both Google Chrome and Microsoft Edge (Chromium). He used this bug as a part of his winning exploit that led to code execution in the renderer of both browsers. This bug was quickly patched by both Google and Microsoft. Manfred has graciously provided this detailed write-up of the vulnerability and how he exploited it at the contest.
在 Master of Pwn 获奖者 Manfred Paul 的这篇客座博客中,他详细介绍了 CVE-2024-2887——Google Chrome 和 Microsoft Edge (Chromium) 中都会出现的类型混淆错误。他将这个错误作为他获胜漏洞的一部分,导致在两个浏览器的渲染器中执行代码。谷歌和Microsoft都很快修补了这个错误。曼弗雷德慷慨地提供了这篇关于该漏洞的详细文章,以及他如何在比赛中利用它。


In this blog, I describe a means of exploiting the V8 JavaScript and WebAssembly engine to gain execution of arbitrary shellcode inside the renderer process. This includes a bypass of the V8 memory sandbox (Ubercage), though code execution is still constrained by the process isolation-based browser sandbox. For demonstration purposes, this limitation can be removed by running the browser with the --no- sandbox flag.
在这篇博客中,我描述了一种利用 V8 JavaScript 和 WebAssembly 引擎在渲染器进程中执行任意 shellcode 的方法。这包括绕过 V8 内存沙箱 ( Ubercage),尽管代码执行仍然受到基于进程隔离的浏览器沙箱的限制。出于演示目的,可以通过运行带有标志的 --no- sandbox 浏览器来删除此限制。

Root Cause of the WebAssembly Universal Type Confusion
WebAssembly 通用类型混淆的根本原因

A WebAssembly module may contain a type section that defines a list of custom “heap types”. In the base specification, this is used only to declare function types, but with the adoption of the garbage collection (GC) proposal [PDF], this section can additionally define struct types, allowing for the use of composite, heap-allocated types in WebAssembly.
WebAssembly 模块可能包含一个 type 定义自定义“堆类型”列表的部分。在基本规范中,这仅用于声明函数类型,但随着垃圾回收 (GC) 提案 [PDF] 的采用,本节可以额外定义结构类型,从而允许在 WebAssembly 中使用复合堆分配类型。

Normally, a struct declared in this section may only reference structs that precede it (structs with a lower type index). To support mutually recursive data structures, a feature called recursive type groups is available. Instead of declaring the (potentially) mutually recursive types as individual entries in the type section, a recursive group is declared as a single type section entry. Within this group, individual types are declared, which are thereby allowed to reference each other.
通常,本节中声明的结构只能引用其前面的结构(类型索引较低的结构)。为了支持相互递归的数据结构,可以使用一种称为递归类型组的功能。递归组不是在类型节中将(可能)相互递归类型声明为单个条目,而是声明为单个类型节条目。在此组中,声明了各个类型,从而允许它们相互引用。

With this in mind, consider the function responsible for parsing the type section from the binary WebAssembly format in v8/src/wasm/module-decoder-impl.h:
考虑到这一点,请考虑负责从二进制 WebAssembly 格式解析 type 该部分的 v8/src/wasm/module-decoder-impl.h 函数:

“}” data-block-type=”22″ data-immersive-translate-walked=”71e772d7-5aa9-4c32-a1a4-8d8371275355″>

At (1), the limit kV8MaxWasmTypes (currently equal to 1,000,000) is passed as a maximum to consume_count(), ensuring that at most this many entries are read from the type section. When recursive type groups were added, this check became insufficient. While this code will permit only kV8MaxWasmTypes entries of the type section to be read, each of those can potentially be a recursive type group containing more than one individual type definition.
在 (1) 处,限制 kV8MaxWasmTypes (当前等于 1,000,000)作为最大值传递给 consume_count() ,确保最多从 type 该部分读取这么多条目。添加递归类型组时,此检查变得不够充分。虽然此代码仅 kV8MaxWasmTypes 允许读取 type 该部分的条目,但每个条目都可能是包含多个单独类型定义的递归类型组。

This insufficiency was clearly noticed at the time of this change, as together with recursive type groups a second check was added at (2). Here, for each recursive type group, it is checked that the addition of the constituent types would not exceed the kV8MaxWasmTypes limit.
在进行此更改时,可以清楚地注意到这种不足,因为在(2)处添加了第二个检查,与递归类型组一起。在这里,对于每个递归类型组,检查成分类型的添加不会超过 kV8MaxWasmTypes 限制。

However, this second check is still not enough. While it protects the indices of each type allocated inside a recursive group, the presence of those groups also has implications for types declared outside this group, as each recursive group adds to the total count of declared types.
但是,这第二次检查仍然不够。虽然它保护了在递归组内分配的每种类型的索引,但这些组的存在也会对在此组外部声明的类型产生影响,因为每个递归组都会增加声明类型的总数。

To make this clearer, imagine a type section consisting of two entries: one recursive group containingkV8MaxWasmTypes entries, and following that group, one non-recursive type. The check at (1) is passed, as the section only has two entries. While processing the recursive group, the check at (2) is also passed, as the section has exactly kV8MaxWasmTypes entries. For the following single type, there is no further check: at (3) the type is simply allocated at the next free index. In this case, the index will be kV8MaxWasmTypes, exceeding the usual maximum of kV8MaxWasmTypes-1. If there were more than one non-recursive type at the end of the type section, they would similarly get assigned kV8MaxWasmTypes+1kV8MaxWasmTypes+2, and so forth, as type indices.
为了更清楚地说明这一点,假设一个由两个条目组成的类型部分:一个包含 kV8MaxWasmTypes 条目的递归组,在该组之后,一个非递归类型。(1) 处的检查已通过,因为该部分只有两个条目。在处理递归组时,也会通过 (2) 处的检查,因为该部分具有确切 kV8MaxWasmTypes 的条目。对于以下单一类型,没有进一步的检查:在 (3) 处,该类型只是在下一个自由索引处分配。在这种情况下,索引将为 kV8MaxWasmTypes ,超过通常的最大 kV8MaxWasmTypes-1 值。如果 type 本节末尾有多个非递归类型,则它们同样会被指定 kV8MaxWasmTypes+1 为 、 kV8MaxWasmTypes+2 、 等,作为类型索引。

Impact of the Root Cause
根本原因的影响

Exceeding the maximal number of declared heap types might seem like a very harmless resource exhaustion bug at first. However, due to some internal details of how V8 handles WebAssembly heap types, it directly allows constructing some very powerful exploit primitives.
乍一看,超过声明的堆类型的最大数量似乎是一个非常无害的资源耗尽错误。然而,由于 V8 如何处理 WebAssembly 堆类型的一些内部细节,它直接允许构建一些非常强大的漏洞利用原语。

In v8/src/wasm/value-type.h, the encoding of heap types is defined:
在 中 v8/src/wasm/value-type.h ,定义了堆类型的编码:

“}” data-block-type=”22″ data-immersive-translate-walked=”71e772d7-5aa9-4c32-a1a4-8d8371275355″>

Here, V8 assumes that all user-defined heap types will be assigned indices smaller than kV8MaxWasmTypes. Larger indices are reserved for fixed, internal heap types (beginning with kFunc). This results in our own type declarations aliasing one of these internal types, leading to many opportunities for type confusion.
在这里,V8 假设所有用户定义的堆类型都将被分配小于 kV8MaxWasmTypes 的索引。较大的索引保留给固定的内部堆类型(以 kFunc 开头)。这会导致我们自己的类型声明与这些内部类型之一产生别名,从而导致许多类型混淆的机会。

Universal WebAssembly Type Confusion
通用 WebAssembly 类型混淆

To leverage this encoding ambiguity into a full type confusion, let’s first consider the struct.new opcode, which produces a reference to a new struct created from fields given on the stack. The caller specifies the desired struct type by passing its type index. The relevant check on the type index can be found in v8/src/wasm/function-body-decoder-impl.h:
为了将这种编码歧义转化为完整的类型混淆,让我们首先考虑 struct.new 操作码,它生成对从堆栈上给定的字段创建的新结构的引用。调用方通过传递其类型索引来指定所需的结构类型。类型索引的相关检查可以在以下位置 v8/src/wasm/function-body-decoder-impl.h 找到:

“}” data-block-type=”22″ data-immersive-translate-walked=”71e772d7-5aa9-4c32-a1a4-8d8371275355″>

Following the validation logic into the has_struct() method from v8/src/wasm/wasm-module.h:
按照验证逻辑进入 has_struct() 方法: v8/src/wasm/wasm-module.h

“}” data-block-type=”22″ data-immersive-translate-walked=”71e772d7-5aa9-4c32-a1a4-8d8371275355″>

Since we can make types.size() exceed the usual limit of kV8MaxWasmTypes, we can make the check pass even if when passing an index larger than this value. This allows us to create a reference of an arbitrary internal type that points to the struct we can freely define.
由于我们可以使 types.size() 超过通常的 kV8MaxWasmTypes 限制,即使传递大于此值的索引,我们也可以使检查通过。这允许我们创建任意内部类型的引用,该引用指向我们可以自由定义的结构。

On the other hand, consider now the handling of the ref.cast instruction:

“}” data-block-type=”22″ data-immersive-translate-walked=”71e772d7-5aa9-4c32-a1a4-8d8371275355″>

Here, a type check elimination is performed. If TypeCheckAlwaysSucceeds returns true, then no actual type check is emitted and the value is simply reinterpreted as the target type.
在这里,执行 type 检查消除。如果 TypeCheckAlwaysSucceeds 返回 true,则不会发出任何实际 type 检查,并且该值只是被重新解释为目标 type 。

The function TypeCheckAlwaysSucceeds ultimately calls IsHeapSubtypeOfImpl defined in v8/src/wasm/wasm-subtyping.cc:
该函数 TypeCheckAlwaysSucceeds 最终调用 IsHeapSubtypeOfImpl 定义在: v8/src/wasm/wasm-subtyping.cc

“}” data-block-type=”22″ data-immersive-translate-walked=”71e772d7-5aa9-4c32-a1a4-8d8371275355″>

This means that if our declared type index aliases the constant HeapType::kNone, the type check will always be elided if we cast to any non-function, non-external reference. In combination, we can use this to turn any reference type into any other by the following steps:
这意味着,如果我们声明的类型索引为常量 HeapType::kNone 别名,如果我们转换为任何非函数、非外部引用,则类型检查将始终被省略。结合使用,我们可以通过以下步骤将任何引用类型转换为任何其他引用类型:

  1. In the type section, define a structure type with a single field of type anyref, and make this struct have a type index equal to HeapType::kNone using the bug described above.
    在 type 部分中,定义一个具有 type anyref 字段的结构类型,并使此结构具有与 HeapType::kNone 使用上述 bug 相同的类型索引。

  2. Place a non-null reference value of any type on the top of the stack and call struct.new with the type index set to HeapType::kNone. This will succeed, as has_struct() validates the index against the index established via the previous step.
    将任何类型的非 null 引用值放在堆栈的顶部,并在类型索引设置为 的情况下 HeapType::kNone 进行调用 struct.new 。这将成功,因为 has_struct() 根据通过上一步建立的索引验证索引。

  3. Also, declare a struct with a normal type index lower than kV8MaxWasmTypes with a single field of the target reference type. Call ref.cast with this this struct’s type index. The engine will not perform any type check, as the input value is at this point understood to be reference type HeapType::kNone.
    此外,声明一个结构体的正常类型索引低于 kV8MaxWasmTypes 目标引用类型的单个字段。用这个调用 ref.cast 这个结构体的类型索引。引擎不会执行任何类型检查,因为此时输入值被理解为引用类型 HeapType::kNone 。

  4. Finally, read back the reference stored in the struct by executing struct.get.
    最后,通过执行 struct.get 读回 struct 中存储的引用。

This arbitrary casting of reference types allows transmuting any value type into any other by referencing it, changing the reference type, and then dereferencing it – a universal type confusion.
这种对引用类型的任意转换允许通过引用任何值类型、更改引用类型,然后取消引用它来将任何值类型转换为任何其他值类型,这是一种通用类型混淆。

In particular, this directly contains nearly all usual JavaScript engine exploitation primitives as special cases:
特别是,这直接包含几乎所有常见的 JavaScript 引擎开发原语作为特殊情况:

• Transmuting int to int* and then dereferencing results in an arbitrary read.
int • 转换为 int* 然后取消引用会导致任意读取。

• Transmuting int to int* and then writing to that reference results in an arbitrary write.
• int 转换为 int* 该引用,然后写入该引用会导致任意写入。

• Transmuting externrefto int is the addrOf() primitive, obtaining the address of a JavaScript object.
• 转换为 externref int 是 addrOf() 原始的,获取 JavaScript 对象的地址。

• Transmuting int to externref is the fakeObj() primitive, forcing the engine to treat an arbitrary value as a pointer to a JavaScript object.
• 转换为 int externref 是 fakeObj() 基元,强制引擎将任意值视为指向 JavaScript 对象的指针。

While casting from HeapType::kNone to an externref is not allowed, remember that we are actually operating on one more level of indirection – transmuting to externref involves casting to a reference to a struct containing one externref member.
虽然不允许从 HeapType::kNone an 转换到 an externref ,但请记住,我们实际上是在另一个间接级别上操作 – 转换为 externref 涉及转换为对包含一个 externref 成员的结构的引用。

Note however that these “arbitrary” reads and writes are still contained in the V8 memory sandbox, as all involved pointers to heap-allocated structures are tagged, compressed pointers inside the heap cage, not full 64-bit raw pointers.
但请注意,这些“任意”读取和写入仍包含在 V8 内存沙箱中,因为所有涉及的指向堆分配结构的指针都是标记的、堆笼内的压缩指针,而不是完整的 64 位原始指针。

Integer Underflow Leading to V8 Sandbox Escape
导致 V8 沙盒逃逸的整数下溢

The primitives described above allow for freely manipulating and faking most JavaScript objects. However, all of this happens inside the limited memory space of the V8 sandbox. “Trusted” objects such as WebAssembly instance data cannot yet be manipulated. We will now turn our attention to a bug that can be used to escape the memory sandbox.
上面描述的原语允许自由操作和伪造大多数 JavaScript 对象。然而,所有这些都发生在 V8 沙盒的有限内存空间内。“可信”对象(如 WebAssembly 实例数据)尚无法操作。现在,我们将把注意力转向一个可以用来逃避内存沙盒的错误。

An often-used object for JavaScript engine exploits is ArrayBuffer and its corresponding views, (i.e. typed arrays), as it allows for direct, untagged access to some region of memory.
JavaScript 引擎漏洞利用的一个常用对象是 ArrayBuffer 及其相应的视图(即类型化数组),因为它允许直接、无标记地访问内存的某个区域。

To prevent access to pointers outside the V8 sandbox, sandboxed pointers are used to designate a typed array’s corresponding backing store. Similarly, an ArrayBuffer’s length field is always loaded as a “bounded size access”, inherently limiting its value to a maximum of 235 − 1.
为了防止访问 V8 沙箱外部的指针,沙盒指针用于指定类型化数组的相应后备存储。同样,ArrayBuffer 的长度字段始终作为“有界大小访问”加载,本质上将其值限制为最大 235 − 1。

However, in modern JavaScript, the handling of typed arrays has become quite complex due to the introduction of resizable ArrayBuffers (RABs) and their sharable variant, growable SharedArrayBuffers (GSABs). Both variants feature the ability to change their length after the object has been created with the shared variant being restricted to never shrink. In particular, for typed arrays with these kinds of buffers, the array length can never be cached and must be recomputed on each access.
然而,在现代 JavaScript 中,由于引入了可调整大小的 ArrayBuffers (RAB) 及其可共享变体 SharedArrayBuffers (GSAB),类型化数组的处理变得相当复杂。这两种变体都能够在创建对象后更改其长度,共享变体被限制为永不缩小。特别是,对于具有此类缓冲区的类型化数组,永远无法缓存数组长度,并且必须在每次访问时重新计算。

Additionally, ArrayBuffers also feature an offset field, describing the start of the data in the actual underlying backing store. This offset must be taken into account when computing the length.
此外,ArrayBuffers 还具有一个偏移字段,用于描述实际底层后备存储中数据的开始。在计算长度时,必须考虑此偏移量。

Let’s now look at the code responsible for building a TypedArray’s length access in the optimizing Turbofan compiler. It can be found in v8/src/compiler/graph-assembler.cc. Note that most non-RAB/GSAB cases and the code responsible for dispatching are omitted for simplicity:
现在让我们看一下负责在优化的 Turbofan 编译器中构建 TypedArray 长度访问的代码。它可以在 v8/src/compiler/graph-assembler.cc 中找到。请注意,为简单起见,省略了大多数非 RAB/GSAB 案例和负责调度的代码:

“}” data-block-type=”22″ data-immersive-translate-walked=”71e772d7-5aa9-4c32-a1a4-8d8371275355″>

For arrays backed by a resizable ArrayBuffer, we can see at (1) that the length is computed as floor((byte_length - byte_offset) / element_size). Crucially, there is an underflow check. If byte_offset exceeds byte_length, then 0 is returned instead.
对于由可调整大小的 ArrayBuffer 支持的数组,我们可以在 (1) 处看到长度计算为 floor((byte_length - byte_offset) / element_size) 。至关重要的是,有一个下溢检查。如果 byte_offset 超过 byte_length ,则返回 0。

Curiously though, in the case of a GSAB-backed array, the corresponding underflow check is missing. Thus, if byte_offset is larger than byte_length, an underflow occurs and the subtraction wraps around to something close to the maximum unsigned 64-bit integer 264. As both of these fields are found in the (by now) attacker-controlled array object, we can easily trigger this using the sandboxed arbitrary read/write primitives discussed previously. This results in access to the whole 64-bit address space, as the length computed by this function is used to bound any typed array accesses (in JIT-compiled code).
但奇怪的是,在 GSAB 支持的阵列中,缺少相应的下溢检查。因此,如果 byte_offset 大于 byte_length ,则发生下溢,并且减法将接近最大无符号 64 位整数 264。由于这两个字段都可以在(现在)攻击者控制的数组对象中找到,因此我们可以使用前面讨论的沙盒化任意读/写基元轻松触发它。这会导致对整个 64 位地址空间的访问,因为此函数计算的长度用于绑定任何类型化数组访问(在 JIT 编译的代码中)。

Exploitation for Arbitrary Shellcode Execution
利用任意 Shellcode 执行

Using the two bugs described above, exploitation becomes fairly straightforward. The primitives described in the Universal WebAssembly Type Confusion section directly give arbitrary reads and writes within the V8 memory sandbox. This can then be used to manipulate a growable SharedArrayBuffer to have an offset greater than its length. A previously JIT-compiled read/write function can then be used to access and overwrite data anywhere in the process’s address space. An appropriate target for overwrite is the compiled code of a WebAssembly module, since that resides in an RWX (read-write-execute) page and can be overwritten with shellcode.
使用上述两个错误,利用变得相当简单。通用 WebAssembly 类型混淆一节中描述的原语直接在 V8 内存沙箱中提供任意读取和写入操作。然后,这可用于操纵可生长物 SharedArrayBuffer 的偏移量大于其长度。然后,可以使用先前 JIT 编译的读/写函数来访问和覆盖进程地址空间中任何位置的数据。覆盖的合适目标是 WebAssembly 模块的编译代码,因为它驻留在 RWX(读-写-执行)页面中,可以用 shellcode 覆盖。


Thanks again to Manfred for providing this thorough write-up. He has contributed multiple bugs to the ZDI program over the last few years, and we certainly hope to see more submissions from him in the future. Until then, follow the team on Twitter, Mastodon, LinkedIn, or Instagram for the latest in exploit techniques and security patches.
再次感谢 Manfred 提供这篇详尽的文章。在过去的几年里,他为ZDI程序贡献了多个错误,我们当然希望将来能看到他提交的更多内容。在此之前,请在Twitter,Mastodon,LinkedIn或Instagram上关注团队,以获取最新的漏洞利用技术和安全补丁。

原文始发于Guest Blogger:CVE-2024-2887: A PWN2OWN WINNING BUG IN GOOGLE CHROME

版权声明:admin 发表于 2024年5月10日 下午8:36。
转载请注明:CVE-2024-2887: A PWN2OWN WINNING BUG IN GOOGLE CHROME | CTF导航

相关文章