Presentation - Introduction to Browser Exploitation (Memory Exploitation)

R3zk0n · October 2, 2025

Contents

    Pointer Compression: https://blog.infosectcbr.com.au/2020/02/pointer-compression-in-v8.html Main article: https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/ Secondary article: https://www.freebuf.com/vuls/203721.html

    Web Browser Architecture

    • To exploit browsers we need to understand how modern day Web Browsers work. This is a high level of the general architecture of browsers.

    image

    Source: https://medium.com/web-god-mode/how-web-browsers-work-behind-the-scene-architecture-technologies-and-internal-working-fec601488bfa

    • User Interface: The top bar of the browser that the user can control. Comprises of the URL bar, forward/back/reload options, extensions and settings
    • Browser Engine:
      • Marshals actions between the UI and rendering engine.
      • Handles the interactions from the user interface, such as clicking links, submitting forms, scrolling the page etc.
      • Enforces security policies such as SOP (Same-Origin Policy)
    • Rendering Engine:
      • Actual parsing of the HTML/CSS/XML to display the webpage. It parses HTML using a tokenization algorithm, while CSS and scripts are parsed via top-down or bottom-up parsing. There are two main ones - WebKit (Safari) and Gecko (Firefox) engines
    • Networking:
      • Handles the communication to retrieve resources from other servers.
    • Javascript Interpreter: Reads and executes JavaScript code and sends result to rendering engine. This includes V8 for Chrome, SpiderMonkey for Firefox, Nitro for Safari etc.
    • UI Backend / Platform Integration Layer: Serves as a bridge between the browser and the underlying operating system. This includes the rendering of the actual user interface, provides access to system resources, UI controls and system-level APIs.
    • Data Persistence: Database that uses various web storage APIs such as localStorage, FileSystem to store data locally.

    Multi-Process Architecture

    • To provide a more stable and secure experience to users, browsers implemented multi-process architecture.
    • Stability: If the browser runs as a single process, with all the web content, extensions, rendering in one place, if any component crashes it would affect the entire browser.
    • Security: Isolation of different processes, their levels of access and privilege via sandboxing can provide additional security that ensures malicious web content will be unable to access sensitive resources.

    image

    • Chrome has a process for the browser (main process), a GPU process, and a dedicated process for each tab and extension. With site isolation, each cross-domain frame and the rendered tab will each have their own dedicated process further enhancing security. For Firefox, there are four worker processes which contain tabs.

    Increasing Complexity

    • Due to the increasing complexity of browser exploitation, so too are the rewards. This is a list of the rewards for Pwn2Own 2023 Vancouver:

    image

    • A renderer-only vulnerability is a security flaw in the rendering engine and complementary components to execute arbitrary code execution on the underlying system. To increase the rewards, the individual needs to then escape the sandbox to access the operating system, and then elevate privileges to gain kernel/root access.

    JavaScript Engines

    • A JavaScript Engine comprises of a parser, interpreter and optimizing engine(s). SpiderMonkey, ChakraCore and JSC have variations with multiple optimising engines.
    • JavaScript source code is initially parsed into an Abstract Syntax Tree. This is then interpreted into bytecode and executed by the machine.
    • Both the AST and bytecode are then optimised by keeping track of the code running through the interpreter (profiling) to determine warm and hot areas of code - which are code segments that are run many times. Speculative optimisations are made against the code to improve the efficiency, or are otherwise deoptimised. This is otherwise known as JIT compilation.
    • The name of the V8 interpreter is called Ignition, and the V8 Optimising Compiler is known as TurboFan.

    image

    What is Browser Exploitation

    Built-In Functions

    • https://sensepost.com/blog/2020/intro-to-chromes-v8-from-an-exploit-development-angle/
    • https://v8.dev/docs/builtin-functions
    • Some are implemented in Javascript
    • Some are runtime functions, which are C++ code that can be called using the %-prefix
    • Commonly used for debugging purposes
    • For example: %DebugPrint function is used to provide debugging information of a Javascript object
    • These functions are normally not exposed but using the flag --allow-natives-syntax allows them to be called from Javascript code

    SimpleInstallFunction

    • Takes in 6 values:
    • isolate_ is a reference to the Javascript context of running code
    • proto: This is the prototype object that the new function will be added to.
    • "oob": This is the name of the new function.
    • Builtins::kArrayOob: This is the built-in function that the new function will be associated with.
    • 2: This is the number of arguments that the new function will accept.
    • false: This indicates whether the new function will be a constructor. That is, you cannot use the new keyword to create new functions.

    image

    Ultimately what this means we can all the function like this: Array.oob(arg1, arg2)

    Overexplaining the Vulnerable Code

    // Section for discussing the vulnerable code

    • Array.oob(arg1): If a.oob() then read out of bounds memory, if a.oob(value) then write out of bounds memory.
    • The first argument of a Javascript function is usually “this”, which allows the object to access and modify its own properties.
    • If only “this” is provided, the code would read the floating integer at the array[length]
    • If two arguments are provided, the second argument would be a new value written to array[length]
    • However the issue is that arrays are zero-indexed, while function arguments are one-indexed. This means the values should have been array[length-1], and not doing so leads to an out of bound memory read and write issue for the array index.

    What is beyond an array index?

    Pointer Tagging

    • To distinguish between floating point doubles, SMIs (Small Integers) and pointers in memory, Javascript uses a pointer tagging mechanism - which basically means modifying the least significant bit in memory to a 1 to identify a pointer.
    • In a 64-bit binary representation:
      • Doubles are unchanged as the value is 64-bits
      • SMIs have the first 31-bits as the value, and the least significant bit is set to 0.
      • Pointers have the first 31-bits as the value, and the least significant bit is set to 1.

    32-bit architecture

    image

    64-bit architecture

    image

    64-bit architecture with pointer compression (Since 2020)

    image

    • Memory leaks are in floating point notation, and therefore need to be converted to 64-bit hexidecimal notation

    What exists beyond an array?

    • We first create a floating point array containing two elements.

    image

    • Then we can use the runtime function %DebugPrint to provide additional debugging information about the array

    image

    • There’s a lot of information so we can condense it to view in just memory. This can be done using the gdb “x” command which accesses the memory contents at a given address. We can format the memory by adding 4gx, which prints out the next 4 64-bit hexidecimal values.
    • We will print out the memory location of the JSArray, and also remove 1-bit from the value to account for pointer tagging.

    image

    • The memory addresses of a JSArray are as follows:
      1. 0x000024b70c242ed9: Pointer to JSArray map
      2. 0x00000b2075dc0c71: Properties
      3. 0x0000302fa44cdd59: Elements (which are <FixedDoubleArray[2]>)
      4. 0x0000000200000000: SMIs of value 2 - length (or number of) elements

    We can then look at the memory configuration of elements:

    image

    • The memory addresses of an element are as follows:
      1. 0x00000b2075dc14f9: Pointer to elements map
      2. 0x0000000200000000: SMIs of value 2 - length (or number of) elements
      3. 0x3ff3be76c8b43958: Floating point value 1.234
      4. 0x4002c28f5c28f5c3: Floating point value 2.345

    image

    • From then:
      1. 0x4002c28f5c28f5c3: Pointer to JSArray Map
      2. 0x00000b2075dc0c71: Properties (of JSArray)
    • Therefore, the map pointer of JSArray is directly after the last element of an array. To verify this, we can use the vulnerable Builtin function Array.oob() from the source code. If we don’t pass a value to the function, it will perform an out of boundary read on the value after the second element.

    image

    • 0x12d39174e131: Memory location of JSArray
    • 0x39c1af782ed9: Region of memory that is read by the out of bounds vulnerability
    • 0x39c1af782ed9: Pointer to JSArray Map
    • Note the ftoi() function converts the floating point value to a 64-bit hexidecimal representation

    JavaScript Hidden Classes (Maps/Shapes)

    • To improve efficiency of Javascript, we have what are known as hidden classes. These can also be called maps or shapes.

    Source: https://mathiasbynens.be/notes/shapes-ics

    Objects: Similar to dictionaries, objects contain string keys that are mapped to property attributes. There are some special attributes such as writable, enumerable and configurable, as well as the value.

    image

    Arrays: Similar to objects, with a special “length” property that updates when the array index increases. Array string keys are array indexes.

    image

    • If there are objects that have the same amount of properties, they will also share the same map/shape. This is to prevent creating the same repeated data for each object and save memory.

    To save memory, if two objects/arrays share the same “shape” or layout, such as they have the same property attributes, we only need to store these once. These are known as hidden classes, or in V8 they are known as maps. In V8, an object would have a map/shape that would contain memory offset to property values and special attributes as previously mentioned.

    image

    From an attacker’s perspective, if we can control the map of an object, we can modify the map to create a type confusion.

    JavaScript Array Type Confusion (JATC)

    • Hypothetically let’s say we have an floating point JSArray A and an JSArray of Objects B. These arrays are fundamentally different and therefore should have a different map.
    • If we access A[0] we should receive a floating point value
    • If we access B[0] we should receive an object
    • However if the type and therefore the map were swapped between A and B:
      • For array A, we should have access to a Javascript object with A[0] as the floating point memory address. This is known as addrOf.
      • For array B, we should access the floating point value of an object, which is the memory address of the B[0] object. This is known as ‘fakeObject`.
    • The code primitives are shown below:

    image

    Arbitrary Address Read

    • To read any arbitrary memory address, we can create a floating point JSArray,
    • As mentioned previously, the memory mapping of a JSArray is as follows:
      1. Offset + 0: Map
      2. Offset + 1: Prototype
      3. Offset + 2: Elements
      4. Offset + 3: Length (SMI, Number of elements)

    gef➤ x/10gx JSArray-0x30-1 // Memory location of floating JSArray less 0x30 to reach elements Offset - 0x30: Elements_map Length SMI <- FixedDoubleArray Offset - 0x20: (1) (2)
    Offset - 0x10: (3) (4) Offset + 0x00: JSArray_map JSArray_properties <- JSArray Offset + 0x10: JSArray_elements Length SMI (4)

    image

    1. Define a floating point JSArray var float_arr = [1.234, 2.345, 3.456, 4.567];
    2. Use the out of bounds functions to obtain the memory address of the JSArray map var float_arr_map = float_arr.oob();
    3. Create an identical JSArray but replace the index 0 with the map of JSArray var exploit_arr = [float_arr_map, 2.345, 3.456, 4.567];

    gef➤ x/10gx JSArray-0x30-1 // Memory location of floating JSArray less 0x30 to reach elements Offset - 0x30: Elements_map Length SMI <- FixedDoubleArray Offset - 0x20: JSArray_map (2) <- Location of fake object Offset - 0x10: Arbitrary_read (4) <- Location of fake element pointer Offset + 0x00: JSArray_map JSArray_properties <- JSArray Offset + 0x10: JSArray_elements Length SMI (4)

    image

    1. Use addrof to determine the exact memory of the JSArray "0x"+addrof(exploit_arr).toString(16); equivalent to %DebugPrint(exploit_arr)
    2. Create a fakeobj pointing the memory address to the map of the fake object (Offset - 0x20) var fake = fakeobj(addrof(exploit_arr)-0x20n);
    3. This will mean that the arbitrary read will be at the location of the elements pointer (Offset - 0x10).

    Arbitrary Address Write

    • Similarly, arbitrary write would require:
      • Create a fakeobj pointing the memory address to the map of the fake object (Offset - 0x20) var fake = fakeobj(addrof(exploit_arr)-0x20n);
      • exploit_arr[2] = itof(BigInt(addr)-0x10n);
      • Setting the first element of the fakeobj to be what we want to write over fake[0] = itof(BigInt(data));

    Arbitrary Code Execution

    • In order to achieve arbitrary code execution, the most common way would be to leak libc address and change __free_hook to perform system calls. This is more of a CTF method.
    • libc is the Standard C Library implementation on Unix systems. If we can leak a libc address or function, it is possible to determine the base address of the libc library, and then locate any function we want to execute using offsets.
    • __free_hook is from the Standard C library that changes what the free() function does. The function normally would deallocate memory previously allocated by functions like malloc(), but the __free_hook acts as a pointer to another function to be executed when free() is called instead.
      • In this case, we would want to point the __free_hook to system, thereby executing system commands when free() is called.

    Problems with direct overwriting of memory space

    • Attempting to overwrite the memory space of __free_hook results in a segmentation fault.
      • This could be because the memory is protected and there are no write permissions to the address. But I am unsure.
      • Furthermore this technique would not work in modern versions of V8 engine, because of pointer compression. The primitives would only let you perform arbitrary reads and writes within the V8 heap.
        • Why you ask? Because the elements pointer of a JSArray stores a 32-bit compressed pointer, and if you change it to an arbitrary 32-bit memory address, performing reads and writes using this elements pointer will cause V8 to add the isolate root to the 32-bit address each time, meaning you are stuck within the V8 heap no matter what you do.

    64-bit architecture with pointer compression (Since 2020)

    image

    ArrayBuffers and DataView objects

    • The classic route to achieve remote code execution is to allocate an ArrayBuffer on the V8 heap and overwrite its backing store to an arbitrary 64-bit memory address.
      • Then, performing reads and writes with it using either a TypedArray or a DataView object will grant you an arbitrary r/w primitive within the entire 64-bit address space (Credits: Luke)
    • The ArrayBuffer object is used to represent raw binary data buffer in Javascript. This is otherwise known as a byte array. The ArrayBuffer object contains a pointer reference to a backing store, which is the location where the values are stored.

    image

    • A DataView object is used to direct manipulate the ArrayBuffer and perform read + write operations against it. It is generally used for binary formats such as PNG or XML.
    • Here are the steps required for arbitrary address write for all 64-bit memory addresses:
      1. Create an ArrayBuffer object of 64-bits var buffer = new ArrayBuffer(8);
      2. Create a new DataView object to perform read/write operations on the buffer var dataview = new DataView(buffer);
      3. Determine the location of the buffer var buffer_memloc = addrof(buffer);
      4. Determine the location of the ArrayBuffer backing store (+0x20) var buffer_bakstore = addrof(buffer) + 0x20n;
      5. Override the ArrayBuffer backing store with the __free_hook function arb_write(buffer_bakstore, __free_hook_memory_address);
      6. Override the __free_hook pointer to system dataview.setBigUInt64(0, BigInt(system_memory_address), true)
        • The three values are: offset (0), value and little endianness
      7. Calling console.log('command') would allocate and free the memory of the string, calling system('command')

    Arbitrary Code Execution 1: Determining the memory location of __free_hook and system

    Arbitrary Code Execution 2: WebAssembly RWX Page

    • WebAssembly is a low level binary format that is designed to be executed in web browsers. This generally allows for higher performance.
    • WebAssembly allocates permissions to memory, and usually this is given “RW” - read + write permissions. If we create a WebAssembly function with “RWX” privileges, we can leverage the out of bounds vulnerability to disclose the WebAssembly memory location and write into the memory.

    Pointer Compression

    https://v8.dev/blog/pointer-compression

    • The V8 Heap is a specific region of memory that is used by the Javascript Engine.
    • Used to store Javascript Object and other data structures.
    • The Javascript Engine creates a memory mapping of the V8 Heap at the lowest addresses. It is a contiguous block of memory known as the ‘isolate’. This is different to mmaping a file since V8 Heap stores program data and not disk-file data.
    • Having memory mapping to the lowest memory addresses improves efficiency, as Javascript Engines use a “bottom-up” memory allocation strategy.

    Twitter, Facebook