Let the Cache Cache and Let the WebAssembly Assemble: Knockin' on Chrome's Shell

Unknown

Black Hat USA 2024 · Day 1 · Briefing

Overview

This talk, presented at Black Hat USA, details a sophisticated exploit chain that successfully compromised Google Chrome and Microsoft Edge during the Pwn2Own Vancouver 2024 competition. Delivered by two security researchers from Palo Alto Networks, the presentation delves into a critical V8 vulnerability leading to an arbitrary read/write primitive, followed by an innovative V8 sandbox escape technique. The significance of this research lies in its demonstration of a novel approach to bypass the V8 sandbox, especially in light of recent architectural changes that removed raw pointers, a common target for previous exploits.

Watch on YouTube

Visual summary for Let the Cache Cache and Let the WebAssembly Assemble: Knockin' on Chrome's Shell by Unknown
Visual summary for Let the Cache Cache and Let the WebAssembly Assemble: Knockin' on Chrome's Shell by Unknown

Key moments

  1. 0:00 Introduction to Chrome/Edge pwn and talk agenda
  2. 1:30 V8 sandbox impact on exploit chains, pointer removal
  3. 2:40 Understanding V8 object layout, maps, and property descriptors
  4. 4:40 Mechanics of JavaScript for-in loops and enum cache
  5. 6:00 Step-by-step execution of the vulnerability Proof-of-Concept
  6. 7:20 Initiating exploitation: reliably triggering JIT in Chrome

Let the Cache Cache and Let the WebAssembly Assemble: Knockin' on Chrome's Shell

Speakers: Unknown

Conference: Black Hat USA

YouTube: https://www.youtube.com/watch?v=9aA0ajufsu4

Overview

This talk, presented at Black Hat USA, details a sophisticated exploit chain that successfully compromised Google Chrome and Microsoft Edge during the Pwn2Own Vancouver 2024 competition. Delivered by two security researchers from Palo Alto Networks, the presentation delves into a critical V8 vulnerability leading to an arbitrary read/write primitive, followed by an innovative V8 sandbox escape technique. The significance of this research lies in its demonstration of a novel approach to bypass the V8 sandbox, especially in light of recent architectural changes that removed raw pointers, a common target for previous exploits.

The speakers guide the audience through the intricate process of identifying and exploiting a vulnerability within Chrome's V8 JavaScript engine, specifically targeting how it handles object properties, map transitions, and for...in loops. They then unveil their method for escaping the V8 sandbox, a crucial security boundary designed to contain V8 engine compromises. This talk is essential for browser security researchers, developers, and anyone interested in the cutting edge of offensive and defensive techniques against modern web browsers, showcasing the continuous cat-and-mouse game between exploit developers and browser security teams.

The successful exploitation at Pwn2Own underscores the practical impact and severity of the disclosed vulnerabilities. The researchers' ability to adapt to the V8 sandbox's evolving defenses, particularly the removal of raw pointers, highlights a new frontier in browser exploitation. Their work provides invaluable insights into the internal workings of the V8 engine and the challenges of securing complex software environments against highly skilled adversaries.

Background

▶ Watch: Introduction to Chrome/Edge pwn and talk agenda (0:00)

Exploiting Google Chrome traditionally involves a multi-stage process, beginning with a V8 vulnerability to achieve memory corruption, which is then transformed into an arbitrary read/write primitive. Historically, this primitive would directly grant renderer code execution. However, this still required a separate Chrome sandbox bypass or an OS kernel vulnerability to achieve code execution outside the browser's main process.

The introduction of the V8 sandbox significantly altered this exploit landscape. Released in beta in March 2024, the V8 sandbox aims to contain V8 engine compromises by eliminating raw pointers within the V8 heap. Instead, objects within the V8 heap are referenced using offsets from a sandbox base address. This means that even if an attacker gains control over an offset, they are confined to the V8 heap and cannot directly reference memory outside it. For objects outside the V8 heap, such as trusted code metadata, bytecode, or executable JIT code, the V8 security team replaced raw pointers with index-to-pointer tables. This architectural shift necessitated new exploitation techniques for sandbox escapes, as previous methods relying on raw pointer manipulation became obsolete. The speakers explicitly stated that for Pwn2Own, they needed to find a new way to escape the sandbox after the beta release.

To understand the vulnerability, it's crucial to grasp several V8 internal concepts. JavaScript objects in V8 are represented in memory with specific structures. Each object has a map, which defines its shape or type, including details like the number of properties and a reference to a descriptor array. The descriptor array is a memory structure that holds information about an object's properties, such as their keys, storage indices (whether they are in-object properties or stored in a separate property store), and their types (e.g., SMIs for small integers). As properties are added or modified, V8 dynamically creates new maps and descriptor arrays, managing transitions between them to optimize memory usage and performance, often sharing descriptor arrays between objects with similar property sets.

Another key concept is the for...in loop and the enum cache. When a for...in loop iterates over an object's properties, V8 creates an enum cache in memory. This cache is designed to speed up property enumeration by pre-filtering enumerable properties from non-enumerable ones. The enum cache contains structures like an indices array and a keys array, sized according to the number of enumerable properties at the time of its initialization. Property values within the loop are typically accessed via the get key property bytecode, handled by the built-in function get key property handler. The interaction between dynamic object shape changes (map transitions) and the static nature of the enum cache proved to be a fertile ground for the vulnerability exploited in this talk.

Key Findings

▶ Watch: Understanding V8 object layout, maps, and property descriptors (2:40)

The central discovery presented in this talk is a type confusion vulnerability within the V8 JavaScript engine, specifically impacting how for...in loops interact with dynamic object property changes and the enum cache. This vulnerability, when triggered, allows for an out-of-bounds (OOB) read primitive, which is then leveraged to achieve arbitrary read/write capabilities within the V8 sandbox.

The core of the vulnerability lies in a race condition or desynchronization between the enum cache and the actual map/descriptor array state of an object. The enum cache is initialized at the beginning of a for...in loop, capturing the object's property structure at that moment. However, if a callback function executed within the loop modifies the type of a property on a related object, it can trigger a map update and a change in the descriptor array. This change, if not properly reflected or invalidated in the already initialized enum cache, leads to a mismatch. When the get key property handler subsequently attempts to access properties using the stale enum cache information, it might interpret data incorrectly, resulting in a type confusion or accessing memory outside the intended bounds.

Crucially, the speakers developed a novel V8 sandbox escape technique. Since the V8 sandbox beta release eliminated raw pointers, traditional escape methods became ineffective. The researchers' breakthrough involved utilizing their arbitrary read/write primitive, obtained through the enum cache vulnerability, to manipulate the new sandbox primitives. Specifically, they targeted the index-to-pointer tables that V8 uses to reference executable objects like JIT code outside the V8 heap. By gaining control over these tables or the JIT code itself, they effectively bypassed the V8 sandbox, achieving renderer code execution. This demonstrates a significant advancement in browser exploitation, adapting to and overcoming the latest security mitigations implemented by the V8 security team.

Technical Deep Dive

▶ Watch: Mechanics of JavaScript for-in loops and enum cache (4:40)

The technical foundation of this exploit chain rests on a meticulous understanding of V8's internal object representation and optimization mechanisms.

V8 Object Model and enum cache

V8 objects are structured in memory to optimize access. An object typically contains in-object properties (for frequently accessed, fixed properties) and a pointer to a property store for additional or dynamically added properties. Every object also has a map, which is critical for defining its shape and types. The map contains metadata such as number of descriptors and a pointer to a descriptor array. The descriptor array, in turn, stores detailed information about each property, including its key, index (indicating whether it's an in-object property or in the property store), and type (e.g., SMI for small integers, double for floating-point numbers).

When an object's properties change (e.g., adding a new property, changing a property's type), V8 performs map transitions. This often involves creating a new map and a new descriptor array. To save memory, V8 might reuse or link descriptor arrays between objects that share similar property sets.

The for...in loop optimization is central to the vulnerability. When JavaScript executes for (let key in obj) { ... }, V8 constructs an enum cache. This cache is designed to accelerate property enumeration by pre-calculating and storing an indices array and a keys array of enumerable properties. The size of these arrays is determined by the number of enumerable properties of the object at the time the enum cache is initialized. Property access within the loop is then handled by optimized bytecode, specifically the get key property bytecode, which delegates to the built-in function get key property handler. This handler relies on the information stored in the enum cache and the object's current map.

The Vulnerability: enum cache Desynchronization

The vulnerability is triggered by a specific sequence of events within a for...in loop:

  1. Initial Setup: An object (let's call it object2) is created with a certain set of properties (e.g., 'A' and 'B'). An enum cache is then initialized for object2 as part of a for...in loop. At this point, the enum cache's indices array and keys array will have a size reflecting object2's initial two properties.
  2. Callback Execution and Type Change: Inside the for...in loop, a callback function is invoked. Within this callback, a property of another object (object3) is modified. For example, object3.C is changed from a small integer (SMI) to a double.
  3. Map and Descriptor Array Update: This type change on object3.C triggers a map update for object3. If object3's map (map3) was linked to object2's map (map2) via a transition tree (e.g., map2 transitioned to map3), then map2 also gets updated. This update causes map2 to now point to a new descriptor array (e.g., descriptor array 4) that correctly describes property 'C' as a double.
  4. Desynchronization: The crucial point is that while map2 and its associated descriptor array have been updated, the enum cache for object2 (which was initialized before the callback) still holds stale information. It continues to refer to the old descriptor array or assumes the old property types and layout.
  5. Out-of-Bounds Read: When the get key property handler subsequently attempts to access a property of object2 within the for...in loop, it uses the outdated information from the enum cache. This desynchronization causes the handler to read beyond the legitimate bounds of the descriptor array or property store, leading to an out-of-bounds (OOB) read. This OOB read is the foundational primitive for the subsequent exploit stages.

Exploitation: From OOB Read to Arbitrary Read/Write

To transform the OOB read into a controllable primitive, the attackers followed these steps:

  1. Stable JIT Triggering: Exploitation of V8 vulnerabilities often requires the target code to be Just-In-Time (JIT) compiled by the TurboFan compiler. The speakers highlighted that while native syntax can trigger JIT in research, it's not practical in the real world. Instead, they focused on achieving stable JIT compilation using carefully crafted JavaScript loops. TurboFan's JIT compiler employs heuristics for inlining functions and optimizing code, relying on factors like optimizing thresholds, call frequencies, runtime profiles, and critically, code density. By optimizing code density and execution patterns, they ensured the vulnerable code path was JIT-compiled reliably.
  2. Controlling the OOB Read Index: To make the OOB read useful, its target memory location needed to be controlled. This was achieved through precise heap spraying and object layout manipulation:
  • They set up a series of objects (e.g., object1, object2, object3, object4) with specific numbers of items (e.g., 19, 10, 11).
  • This setup resulted in the indices array of the enum cache having a size of one.
  • By carefully controlling the number of items in object2 (e.g., nine items), they could manipulate the OOB read index, effectively dictating how far past the intended boundary the read would occur.
  1. Heap Grooming for Controlled Data: Within the callback function, immediately after the index array and enum cache were allocated, they allocated a string in memory. Through careful offset computation, they positioned this controlled string value at a specific index (e.g., index seven) relative to the OOB read.
  2. Arbitrary Read Primitive: With this setup, the OOB read could now read bytes from their controlled string. By repeatedly triggering the vulnerability and manipulating the offset, they established an arbitrary read primitive. This primitive is crucial for leaking sensitive memory addresses, such as the base address of the V8 sandbox or pointers to other critical structures.
  3. Arbitrary Write Primitive: Once an arbitrary read is established and base addresses are known, an arbitrary write primitive can typically be constructed. While not explicitly detailed in the transcript, common techniques involve corrupting metadata of objects (like array lengths or map pointers) to turn a controlled read into a controlled write. This arbitrary read/write capability, operating within the V8 sandbox, is the foundation for the subsequent sandbox escape.

V8 Sandbox Escape: Index-to-Pointer Table Manipulation

The V8 sandbox, as implemented in its beta release, fundamentally changes how objects are referenced.

  • Within the V8 Heap: All references between objects are offsets from the sandbox base address, not raw pointers. This prevents an attacker from using a compromised offset to directly access memory outside the V8 heap.
  • Outside the V8 Heap: For trusted objects (like code metadata, bytecode) and executable objects (like JIT code), raw pointers are replaced by index-to-pointer tables. These tables are arrays where an index refers to a pointer to the actual object in memory outside the V8 heap.

The speakers' "new way" to escape the sandbox, as hinted by the talk title "Let the Cache Cache and Let the WebAssembly Assemble: Knockin' on Chrome's Shell," likely involved manipulating these index-to-pointer tables or directly corrupting JIT code itself. With the arbitrary read/write primitive obtained from the enum cache vulnerability, they could:

  1. Leak Sandbox Base Address and Table Locations: Use the arbitrary read to find the sandbox base address and the memory locations of the index-to-pointer tables.
  2. Identify JIT Code Pointers: Locate entries in the index-to-pointer tables that point to JIT-compiled code (e.g., WebAssembly JIT code).
  3. Corrupt JIT Code/Table Entries: Use the arbitrary write to modify either the JIT code directly in memory (e.g., injecting shellcode) or to alter an entry in an index-to-pointer table to point to attacker-controlled executable memory. This would allow them to hijack control flow and execute arbitrary code outside the V8 sandbox, achieving renderer code execution. The mention of "WebAssembly Assemble" strongly suggests WebAssembly's JIT'd code as a target for this final stage.

This multi-stage attack chain demonstrates a sophisticated understanding of V8's modern architecture and the ingenuity required to bypass its latest security mitigations.

Demo / Proof of Concept

▶ Watch: Step-by-step execution of the vulnerability Proof-of-Concept (6:00)

The speakers concluded their presentation with a live demonstration of the exploitation on both Google Chrome and Microsoft Edge. While the specific visual details of the demo are not elaborated in the transcript, it would have showcased the full exploit chain in action, starting from triggering the V8 vulnerability to ultimately achieving renderer code execution. Typically, such a demonstration involves opening a specially crafted webpage that executes the JavaScript exploit, leading to a visible indication of compromise, such as launching an arbitrary application (like Calculator) or displaying a message box, thereby proving the successful bypass of both the V8 sandbox and the broader Chrome sandbox. This practical demonstration validated the theoretical findings and the effectiveness of their novel sandbox escape technique.

Defensive Implications

▶ Watch: Initiating exploitation: reliably triggering JIT in Chrome (7:20)

The Pwn2Own Vancouver 2024 exploit against Chrome and Edge carries several critical defensive implications for browser vendors and users alike:

  1. Rapid Patching and Updates: The most immediate implication is the necessity for browser vendors (Google for Chrome, Microsoft for Edge) to swiftly patch the identified V8 vulnerability. Users must prioritize updating their browsers to the latest versions as soon as patches are released to mitigate this specific threat. The vulnerability likely received a CVE identifier and was addressed shortly after Pwn2Own.
  2. V8 Sandbox Evolution: The talk highlights the ongoing evolution of the V8 sandbox and the continuous cat-and-mouse game between security researchers and browser security teams. The V8 team's removal of raw pointers was a significant mitigation, but attackers are already adapting. This underscores the need for layered security, continuous innovation in sandbox design, and proactive threat modeling against new bypass techniques.
  3. Increased Scrutiny of JIT and Memory Management: The vulnerability's roots in for...in loop optimizations, enum cache behavior, and map transitions point to complex interactions within the V8 engine's JIT compilation and memory management. Defenders must enhance scrutiny of these highly optimized, performance-critical components for subtle logic flaws that can lead to type confusion or OOB issues. Stricter type checks, more robust enum cache invalidation mechanisms, and improved compiler sanitization techniques are crucial.
  4. Hardening Index-to-Pointer Tables: The sandbox escape technique targeting index-to-pointer tables or JIT code indicates that these new indirection mechanisms, while improving security over raw pointers, can become new targets. Defenses should consider additional protections for these tables, such as stronger integrity checks, non-writable memory regions for the tables themselves, or more advanced control flow integrity (CFI) mechanisms that specifically protect JIT-generated code and its metadata.
  5. Memory Safety and Type Consistency: The underlying issue is a violation of memory safety and type consistency. Investing in fuzzing, static analysis, and formal verification tools specifically designed for complex JavaScript engine components can help uncover similar vulnerabilities before they are exploited in the wild. Continued efforts to introduce memory-safe languages or stricter memory management paradigms within critical browser components remain a long-term goal.
  6. Layered Security for Renderer Process: While the V8 sandbox is a critical layer, it's not the only one. The full exploit chain still requires a Chrome sandbox bypass (or OS kernel vulnerability) to achieve full system compromise. This reinforces the importance of maintaining robust outer-layer browser sandboxes and OS-level security features (like ASLR, DEP, CFI) as defense-in-depth measures.

Key Takeaways

  • The V8 sandbox, despite its recent enhancements (like removing raw pointers), remains a target for sophisticated attackers, necessitating continuous evolution of defensive measures.
  • A novel type confusion vulnerability in V8's for...in loop and enum cache mechanism allowed for an out-of-bounds read primitive.
  • The vulnerability stemmed from a desynchronization between the enum cache's stale information and dynamic map/descriptor array updates triggered within a loop's callback.
  • Exploitation required careful heap grooming, stable JIT compilation (leveraging TurboFan's code density heuristics), and precise control over the out-of-bounds read index.
  • The V8 sandbox was bypassed by manipulating index-to-pointer tables or JIT code, demonstrating a new post-raw-pointer sandbox escape technique.
  • Successful exploitation at Pwn2Own Vancouver 2024 against Chrome and Edge underscores the real-world impact and the high level of skill required to compromise modern browsers.

About the Speaker(s)

The talk was delivered by two security researchers from Palo Alto Networks. They are actively engaged in both offensive and defensive security research, contributing to the understanding and mitigation of advanced threats against complex software systems like web browsers.

All talks from Black Hat USA 2024