Android Hacking, Sandboxing, & Security Exploits

Exploiting Use-After-Free (UAF) on Android ARM64: A Full Exploit Development Walkthrough

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Use-After-Free on Android ARM64

Use-After-Free (UAF) vulnerabilities represent a critical class of memory corruption bugs that can lead to arbitrary code execution, privilege escalation, and sensitive data exfiltration. On Android, particularly on ARM64 architectures, exploiting UAFs presents unique challenges and opportunities due to the platform’s security mitigations, specific memory allocators, and the ARM64 instruction set. This article delves into the intricate process of identifying, analyzing, and exploiting a UAF vulnerability on an Android ARM64 target, providing a comprehensive walkthrough for aspiring exploit developers and security researchers.

A UAF occurs when an application continues to use a pointer to memory that has already been freed. Once memory is freed, it can be reallocated and used by another part of the program. If the original pointer is subsequently dereferenced, it might read data belonging to the new object, write data to it, or even execute code by overwriting function pointers, leading to unpredictable behavior and, often, a crash. However, with careful manipulation, this ‘unpredictable behavior’ can be weaponized into a powerful exploitation primitive.

The Anatomy of a UAF Vulnerability

Identifying the Flaw

Identifying a UAF often involves meticulous code review, dynamic analysis with fuzzers, or advanced static analysis tools. Consider a simplified C++ scenario where a custom object `MyObject` is allocated, used, and then prematurely freed, while a global or persistent pointer still references it.

class MyObject {public:    void (*callback_func)();    char data[24];    MyObject() { callback_func = nullptr; }    ~MyObject() { }};MyObject* global_obj_ptr = nullptr;void vulnerable_function() {    MyObject* obj = new MyObject();    global_obj_ptr = obj; // Store a global reference    // ... use obj ...    delete obj; // Premature free!    // ... more code ...}void later_function_call() {    if (global_obj_ptr) {        // UAF! global_obj_ptr now points to freed memory        // This memory might have been reallocated by now.        global_obj_ptr->callback_func(); // Dereferencing freed memory!    }}

In this example, after `obj` is deleted, `global_obj_ptr` becomes a dangling pointer. If `later_function_call()` is invoked, it attempts to call a function pointer from memory that could now belong to an entirely different object, or even be unmapped, leading to a crash or a controllable overwrite.

Android’s Memory Allocators (jemalloc)

Android primarily uses `jemalloc` as its default memory allocator for processes. `jemalloc` is designed for high performance and scalability but introduces specific patterns for memory allocation and deallocation. It manages memory in various ‘bins’ based on chunk sizes. When a chunk is freed, it’s returned to a free list within its size bin. The key to UAF exploitation is understanding that a freed chunk of a certain size can be reclaimed by a subsequent allocation request of the same size, allowing an attacker to place controlled data into the freed region.

Crafting the Exploit: A Step-by-Step Guide

Exploiting a UAF typically involves a sequence of carefully orchestrated steps to transition from a memory corruption primitive to arbitrary code execution.

Step 1: Heap Grooming (Heap Feng Shui)

Heap grooming is the art of manipulating the heap’s memory layout to achieve a predictable state for exploitation. The goal is to ensure that when our target object is freed, a subsequent allocation request of the same size lands precisely in its place. This often involves:

  • Spraying: Allocating a large number of objects of specific sizes to fill the free lists and ensure a contiguous block.
  • Hole Punching: Freeing specific objects strategically to create ‘holes’ in the heap where the target freed object will reside.
  • Isolation: Allocating ‘guard’ objects around the target to prevent other legitimate allocations from interfering.

For our `MyObject` example (size 32 bytes including header, assuming 8-byte alignment), we might allocate many dummy objects of similar sizes to ‘prime’ the heap:

// Example heap grooming logic (conceptual)std::vector<char*> dummy_objs;for (int i = 0; i < 1000; ++i) {    dummy_objs.push_back(new char[24]); // Allocate many 24-byte data buffers to fill bins}

Step 2: Triggering UAF and Memory Reclamation

Once the heap is groomed, the next step is to trigger the UAF by freeing the target `MyObject`. Immediately after, we need to reclaim the freed memory with attacker-controlled data. This controlled data will be crafted to achieve our next primitive.

// 1. Trigger the UAF by calling vulnerable_function()vulnerable_function(); // global_obj_ptr now points to freed memory// 2. Reclaim the freed memory with our fake object structure//    A new object (e.g., another char[24] or a specially crafted struct) //    will now occupy the memory previously held by MyObject.char* fake_object_data = new char[24]; // Reclaims the 24-byte data partmemset(fake_object_data, 0x41, 24); // Fill with A's for now

The critical part is that `global_obj_ptr` still points to this region, but it now contains our `fake_object_data` (or a specially crafted fake `MyObject`).

Step 3: Gaining an Arbitrary Read/Write Primitive

With memory reclamation, we can now manipulate the data that `global_obj_ptr` points to. If `MyObject` had a function pointer (like `callback_func`), we can overwrite this function pointer with an address of our choice. This gives us an Arbitrary Code Execution (ACE) primitive, or at least a controlled call.

Alternatively, if `MyObject` contained a data pointer, we could overwrite that pointer to point to an arbitrary memory address. Then, any subsequent write operation through `global_obj_ptr->data_ptr` would become an arbitrary write to the address we supplied. This ‘fake object’ technique is potent.

Let’s assume we want to overwrite `callback_func`. We craft a `fake_object_data` that places our desired code address where `callback_func` would be:

// Assume we want to jump to address 0xdeadbeef0000 (shellcode address)uint64_t target_shellcode_addr = 0xdeadbeef0000; // Placeholdermemset(fake_object_data, 0, 24); // Clear itmemcpy(fake_object_data, &target_shellcode_addr, sizeof(uint64_t)); // Overwrite callback_func// Now, when later_function_call() is called:later_function_call(); // global_obj_ptr->callback_func() will jump to 0xdeadbeef0000!

Step 4: Achieving Code Execution with ARM64 Shellcode

Once we can divert execution flow, the next step is to execute our own shellcode. On ARM64, understanding calling conventions and system call mechanisms is crucial.

ARM64 Calling Conventions

  • Function arguments are passed in registers `x0` to `x7`.
  • Return values are placed in `x0`.
  • `x30` is the Link Register (LR), holding the return address for `BL` (Branch with Link) calls.
  • `SP` is the Stack Pointer.
  • System calls are typically invoked using the `SVC #0` instruction, with the system call number in `x8` and arguments in `x0-x7`.

Example ARM64 Shellcode (execve)

A common goal is to spawn a shell by calling `execve`. Here’s a basic conceptual ARM64 shellcode for `execve("/system/bin/sh", NULL, NULL)`:

    .global _start_shellcode_execve_sh    _start_shellcode_execve_sh:        // Create a string "/system/bin/sh" on the stack        ADR X0, #filename        MOV X1, #0              // Arg2: argv (NULL)        MOV X2, #0              // Arg3: envp (NULL)        MOV X8, #221            // Syscall number for execve (Android ARM64)        SVC #0                  // Call system callfilename:    .ascii "/system/bin/sh
"

To execute this, we’d need to ensure our `target_shellcode_addr` points to the start of this shellcode, which must be placed in an executable memory region. This often involves either ROP (Return-Oriented Programming) to chain existing gadgets to map executable memory and copy shellcode, or finding existing `mprotect` calls within the target process.

Debugging and Mitigations

Debugging Techniques

During exploit development, robust debugging is essential. Tools like `gdb` (often `gdbserver` on the device) and `IDA Pro` (for static and dynamic analysis) are invaluable. Key commands include:

  • `info registers`: View current register states.
  • `x/i $pc`: Disassemble instructions at the Program Counter.
  • `b *address`: Set a breakpoint.
  • `watch *address`: Set a memory watchpoint to detect writes or reads to a specific address.
  • `vmmap`: View memory mappings and permissions.

Android’s Security Mitigations

Android incorporates several security mitigations to make exploitation harder:

  • ASLR (Address Space Layout Randomization): Randomizes base addresses of libraries and the heap, making it difficult to predict memory locations. Info leaks are often required to bypass ASLR.
  • DEP (Data Execution Prevention / W^X): Prevents code execution from data segments and vice-versa. This means directly jumping to shellcode in the heap is usually blocked unless an `mprotect` call can be used to change memory permissions.
  • PAC (Pointer Authentication Codes): On newer ARMv8.3+ architectures, PACs cryptographically sign pointers to prevent their arbitrary modification. This adds a significant layer of difficulty to function pointer overwrites.
  • KFENCE/HWASAN: Kernel/Hardware-assisted Address Sanitizers can detect memory errors like UAF, making exploitation more challenging for developers and users.

Despite these, UAF vulnerabilities remain exploitable. Bypassing ASLR often involves an information leak. Circumventing W^X typically requires ROP chains to call `mprotect` or equivalent system calls. PAC, while formidable, has also seen public bypasses.

Conclusion

Exploiting Use-After-Free vulnerabilities on Android ARM64 is a complex yet rewarding endeavor that demands a deep understanding of memory management, the ARM64 architecture, and Android’s security landscape. From carefully grooming the heap to crafting precise shellcode and navigating system mitigations, each step requires meticulous planning and execution. This walkthrough provides a foundational understanding of the techniques involved, emphasizing that while the platform continues to evolve with stronger defenses, robust security practices in software development remain the most crucial line of defense against such sophisticated attacks.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner