Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/binary-exploitation/chrome-exploiting.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,87 @@ After corruption we possess a fully-featured **renderer R/W primitive**.

---

## 3.1 Pointer-Compressed V8 Escape Chain: Caged R/W → Native Leak → Stack Pivot

An alternative V8-sandbox escape does **not** require unrestricted renderer read/write. A historical Chrome 150 chain combined a compiler-assisted address disclosure, a missing generational-GC barrier, an `ExternalString` bounds abuse, and a JSPI/JS Dispatch Table (JDT) metadata mismatch. The useful lesson is the separation of capabilities: one primitive can disclose a native module address while a different bug supplies native control flow.<sup>[[10]](#references)[[11]](#references)</sup>

### Cage and tagged-value model

On 64-bit pointer-compressed V8 builds, many tagged heap references are stored as 32-bit offsets from a cage base. The low bit distinguishes a Smi (`0`) from a heap-object reference (`1`), and the 32-bit offset limits ordinary forged heap references to a 4 GB address range.<sup>[[3]](#references)</sup>

```text
compressed heap reference: [ 31-bit cage offset | 1 ]
compressed Smi: [ signed integer | 0 ]
```

The V8 sandbox assumes that an attacker may already corrupt the entire in-sandbox address space. Native resources and trusted call targets are therefore represented through indirection such as external, trusted, code, and dispatch tables rather than writable raw pointers in ordinary heap objects. Consequently, `addrof` and arbitrary cage read/write are intermediate primitives, not a sandbox escape by themselves.<sup>[[4]](#references)</sup>

### Web-compatible tiering and compressed `addrof`

Shell-only helpers such as `%OptimizeFunctionOnNextCall` are unavailable to a malicious webpage. A browser exploit can instead collect feedback and request optimized compilation by repeatedly calling the vulnerable function through synchronous native events; pauses give concurrent compilation time to finish.<sup>[[5]](#references)[[10]](#references)[[11]](#references)</sup>

```javascript
async function tierViaEvents(listener, type) {
const target = document.createElement("span");
const event = new Event(type);
target.addEventListener(type, listener);
for (let i = 0; i < 2000; i++) target.dispatchEvent(event);
await new Promise(r => setTimeout(r, 250));
for (let i = 0; i < 30000; i++) target.dispatchEvent(event);
await new Promise(r => setTimeout(r, 250));
}
```

If the optimized bug yields an out-of-bounds byte read (for example through `charCodeAt`), place many copies of the same object reference next to the string and scan the disclosure as little-endian 32-bit words. A candidate that is odd and repeated across the sprayed slots is a strong compressed-address oracle: oddness matches the heap-object tag, while repetition rejects unrelated pointer-looking data. Treat the result as a per-process cage offset, never as a reusable address.<sup>[[5]](#references)[[10]](#references)[[11]](#references)</sup>

### Missing old-to-young barrier → fake array

A useful generational-GC bug occurs when an old object stores a young heap reference without recording it in the remembered set. A minor collection then sees no root for the young allocation, reclaims it, and leaves the old object holding a stale tagged address. In the RegExp example, incrementing `lastIndex = 1073741823` crossed the maximum positive Smi and produced a young `HeapNumber`, but a Smi-specialized store omitted the necessary barrier.<sup>[[6]](#references)[[7]](#references)</sup>

The representation invariant was reached by making `matchAll` observe global flags on a pseudo object while its species constructor returned a real non-global RegExp. The fixed regression trigger is compact:<sup>[[6]](#references)[[7]](#references)</sup>

```javascript
const re = /(?:)/;
function Species() { return re; }
const pseudoRe = {
flags: "g",
lastIndex: 1073741823,
constructor: { [Symbol.species]: Species },
};
RegExp.prototype[Symbol.matchAll].call(pseudoRe, "").next();
```

After minor collections, spray compatible allocations so controlled bytes reclaim the stale `HeapNumber` slot as a packed-double `JSArray`. Its compressed header is `[map, properties, elements, length]`; selecting `elements` and an oversized `length` turns normal array indexing into read/write over the cage. Compressed fields are four-byte aligned but JS doubles are eight bytes, so helpers must select the correct 32-bit half and preserve the adjacent half.<sup>[[7]](#references)[[10]](#references)[[11]](#references)</sup>

Do not declare the primitive stable after a crash or one fake-object access. Verify a reversible sequence against a sacrificial double: read its raw bits, write a recognizable value, observe it through normal JavaScript, read the same bits back, and restore the original value. Then migrate long-lived data into non-compacting large-object-space backings: a double backing for wide cage access, a genuine tagged backing for GC-updated `addrof`/`fakeobj` references, and a carrier backing for a later native stack.<sup>[[10]](#references)[[11]](#references)</sup>

### Bypass protected pointers by corrupting access semantics

An `ExternalString` keeps its native resource behind a protected External Pointer Table handle, so overwriting the in-cage field does not directly forge a native pointer. However, its JavaScript-visible length is also in-cage metadata. Enlarging only the length preserves the legitimate handle but makes string operations read beyond the actual native resource. Groomed external resources can then expose a repeated native vtable pointer; subtracting its build-specific static offset discloses the randomized Chrome image base and defeats ASLR for the renderer.<sup>[[10]](#references)[[11]](#references)</sup>

This pattern generalizes beyond strings: when a protected table prevents pointer replacement, audit every attacker-writable field that controls the **bounds, type, lifetime, or operation** performed through a valid handle. A native disclosure is sufficient if a separate primitive later controls execution; native arbitrary write is not mandatory.<sup>[[4]](#references)[[10]](#references)[[11]](#references)</sup>

### JSPI/JDT cleanup mismatch → native stack

JSPI wraps WebAssembly imports and exports so execution can suspend on a Promise and later resume. Suspended computations leave genuine internal resume state reachable from Promise reactions; with caged `fakeobj`, the hidden `WasmResume` handlers can be materialized as JavaScript values.<sup>[[9]](#references)[[10]](#references)[[11]](#references)</sup>

The vulnerable JDT path allowed the fixed-arity `WasmResume` code and its dispatch metadata to disagree about stack cleanup. `WasmResume` cleans the receiver plus one explicit argument, while a mismatched donor entry could describe a different trusted parameter count. Calling the recovered handler with an attacker-shaped receiver and arguments shifted the effective return slot into attacker-influenced stack data.<sup>[[8]](#references)[[10]](#references)[[11]](#references)</sup>

```text
JDT/caller cleanup count != WasmResume fixed cleanup count
return slot resolves to controlled data
pop rsp ; ret → attacker carrier backing
```

After leaking the image base, place exact-build gadget and PLT addresses in the carrier and pivot with a gadget such as `pop rsp; ret`. An `open` → `read` → `write` ROP chain reuses executable code already mapped by Chrome, preserving W^X. Check the platform ABI, 16-byte stack alignment, and tagged-pointer bias: a tagged receiver may address one byte past the naturally aligned carrier, requiring the byte stream to be shifted without damaging neighboring fields.<sup>[[10]](#references)[[11]](#references)</sup>

> [!WARNING]
> This crosses the **in-process V8 sandbox** into native renderer control. It is not a Chrome process-sandbox escape. In the cited challenge the browser was started with `--no-sandbox`, so the OS sandbox boundary was deliberately absent; offsets, layouts, and gadgets were also exact-build values.<sup>[[10]](#references)[[11]](#references)</sup>

---

## 4. Stage 3 – Renderer → OS Sandbox Escape (CVE-2024-11114)

The **Mojo** IPC interface `blink.mojom.DragService.startDragging()` can be called from the Renderer with *partially trusted* parameters. By crafting a `DragData` structure pointing to an **arbitrary file path** the renderer convinces the browser to perform a *native* drag-and-drop **outside the renderer sandbox**.<sup>[[1]](#references)</sup>
Expand Down Expand Up @@ -195,5 +276,14 @@ linux-kernel-exploitation/af-unix-msg-oob-uaf-skb-primitives.md

- [1] [101 Chrome Exploitation — Part 0 (Preface)](https://opzero.ru/en/press/101-chrome-exploitation-part-0-preface/)
- [2] [Chromium sandbox design](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/design/sandbox.md)
- [3] [V8 - Pointer Compression](https://v8.dev/blog/pointer-compression)
- [4] [V8 sandbox source documentation](https://github.com/v8/v8/blob/main/src/sandbox/README.md)
- [5] [V8 fix - Keep safe-integer check in ToNumber Word32 lowering](https://github.com/v8/v8/commit/fba00590cb03c58aa01fe18dd8f0cfedf0e94077)
- [6] [V8 fix - Hard-check that `index + 1` is a Smi in `AdvanceStringIndex`](https://github.com/v8/v8/commit/de11d56041d5aa9c5e69b031990ad17068dbef7a)
- [7] [Salvatore Gulizia - From Regex to RCE](https://github.com/Serotav/Writeups/blob/main/v8/CVE-2026-15776-From-Regex-To-Rce.md)
- [8] [V8 fix - Prevent fixed-arity JSPI code in mismatched JDT entries](https://github.com/v8/v8/commit/752405a70a5c8696196c5c29b1349b439eb881fd)
- [9] [V8 - Introducing the WebAssembly JavaScript Promise Integration API](https://v8.dev/blog/jspi)
- [10] [v8CTF M150 exploit PoC and reproduction notes](https://github.com/unknownhad/v8ctf-m150-exploit)
- [11] [Himanshu Anand - I had some free time, so I tried to pwn V8](https://blog.himanshuanand.com/2026/08/i-had-some-free-time-so-i-tried-to-pwn-v8/)

{{#include ../banners/hacktricks-training.md}}