Android Software Reverse Engineering & Decompilation

Advanced ARM64 NDK Exploit Primitives: Heap Spraying & UAF on Android

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Advanced ARM64 NDK Exploitation

Exploiting native code on Android, particularly NDK binaries compiled for ARM64 architecture, presents a unique set of challenges and opportunities. While modern Android versions incorporate robust security mitigations, understanding core exploit primitives like User-After-Free (UAF) vulnerabilities and heap spraying remains critical for advanced research and ethical hacking. This article delves into these two powerful techniques, demonstrating how they can be combined to achieve arbitrary read/write or even code execution on ARM64 Android devices.

We will explore the underlying concepts, provide conceptual code examples relevant to NDK development, and discuss practical considerations for developing exploits in this complex environment. A solid grasp of ARM64 assembly, C/C++ memory management, and Android’s native runtime is assumed.

Understanding User-After-Free (UAF) Vulnerabilities

A User-After-Free (UAF) vulnerability occurs when a program continues to use a pointer to memory that has already been freed. This can lead to a variety of issues, from crashes to information disclosure, and critically, arbitrary code execution. On modern systems, especially with heap allocators like jemalloc (commonly used on Android), a freed memory chunk can be reallocated to a different object, allowing an attacker to control the data at a previously used address.

Anatomy of a UAF

Consider the following simplified C++ NDK code snippet:

class MaliciousObject {public:    void (*callback_func)();    char buffer[256];    MaliciousObject() {        callback_func = nullptr;        memset(buffer, 0, sizeof(buffer));    }    void execute() {        if (callback_func) {            callback_func();        }    }};void vulnerable_function() {    MaliciousObject* obj = new MaliciousObject();    // ... some operations ...    delete obj; // obj is freed, but pointer 'obj' still holds the address    // ... some unrelated code ...    // Later, potentially attacker-controlled code uses 'obj' pointer again    obj->execute(); // UAF! The memory 'obj' points to might have been reallocated}

In this example, after obj is deleted, the memory it occupied is returned to the heap. If obj->execute() is called afterward, the program attempts to dereference a stale pointer. If another object is allocated into that same memory slot before the `execute` call, an attacker might control the content of that memory, including the `callback_func` pointer, leading to arbitrary code execution.

Heap Spraying: Controlling the Heap Layout

Heap spraying is a technique used to fill specific regions of the heap with attacker-controlled data. Its primary goal in exploitation is to reliably place a desired payload (e.g., shellcode, fake object structures, or controlled pointers) at a predictable memory location or to increase the probability that a subsequent allocation will land in an attacker-controlled chunk. This becomes particularly powerful when combined with UAFs, as it allows an attacker to dictate what data occupies the freed UAF chunk.

Why Heap Spraying?

Heap allocators are complex and non-deterministic to some extent. Factors like allocation size, allocation patterns, and multithreading can influence where memory chunks are placed. Heap spraying aims to mitigate this unpredictability by flooding the heap with many identical or similar-sized objects. When a UAF occurs and the freed chunk is available, a carefully sized and timed heap spray can ensure that an attacker-controlled object occupies that specific freed slot.

On ARM64, pointer sizes are 8 bytes. This means that if we are trying to overwrite a vtable pointer or a function pointer, we need to ensure our sprayed objects are the correct size to fill the target UAF chunk and contain our 64-bit address.

Basic Heap Spraying Concept

A typical heap spray involves allocating numerous objects of a specific size:

// Example of a conceptual heap spray loopvoid perform_heap_spray(size_t chunk_size, int num_chunks, void* data_to_spray) {    for (int i = 0; i < num_chunks; ++i) {        char* spray_chunk = new char[chunk_size];        if (spray_chunk) {            // Fill the chunk with attacker-controlled data            memcpy(spray_chunk, data_to_spray, chunk_size);            // Store pointers to prevent them from being immediately freed            // In a real exploit, these might be stored in a global vector            // or kept alive by other means.            // e.g., g_spray_objects.push_back(spray_chunk);        }    }}

The `data_to_spray` would contain the attacker’s payload, such as a pointer to shellcode or a controlled vtable structure.

Combining UAF and Heap Spraying on ARM64 Android

The true power emerges when UAF and heap spraying are combined. The general exploit flow on ARM64 Android would be:

  1. Identify and trigger a UAF vulnerability in an NDK binary.
  2. Immediately after the vulnerable object is freed, perform a heap spray. The goal is to allocate new objects that are precisely the same size as the freed UAF object.
  3. Through careful timing and sizing, one of the sprayed objects will likely occupy the memory location previously held by the UAF object.
  4. When the stale pointer of the UAF object is subsequently dereferenced (e.g., calling a method on a C++ object, or accessing data members), it will now operate on the attacker-controlled sprayed data.

Example: VTable Hijacking via UAF & Spray

Consider our MaliciousObject example with a virtual function. If it were a C++ object with a vtable, a UAF could allow us to overwrite its vtable pointer.

// Original vulnerable class (modified for virtual function)class VulnerableClass {public:    virtual void virtual_method() {        // Default implementation    }    // ... other members ...};void exploit_scenario() {    VulnerableClass* vuln_obj = new VulnerableClass();    // ... use vuln_obj ...    delete vuln_obj; // UAF point! Memory is freed.    // Attacker's turn: Heap spray    // Allocate many objects of the same size as VulnerableClass.    // These 'fake' objects contain a controlled vtable pointer    // pointing to attacker-controlled data (e.g., shellcode or ROP chain).    // `fake_vtable_ptr` would point to a crafted vtable in attacker-controlled memory.    // `shellcode_address` would be the address of our shellcode.    unsigned long fake_vtable[] = {shellcode_address}; // Simplified    char spray_data[sizeof(VulnerableClass)];    memcpy(spray_data, &fake_vtable, sizeof(unsigned long)); // Overwrite vtable ptr    // Fill rest of spray_data if needed    for (int i = 0; i virtual_method(); // UAF triggered!    // If a sprayed object occupied vuln_obj's memory,    // this call will jump to `fake_vtable[0]`, i.e., `shellcode_address`.}

In ARM64, the vtable pointer is typically the first 8 bytes of the object. By spraying objects of the same size as `VulnerableClass` and placing our `fake_vtable_ptr` at the beginning of our spray data, we can redirect the `virtual_method` call to an arbitrary address.

Practical Considerations for ARM64 Android

  • Heap Profiling: Use tools like `jemalloc`’s profiling features (if enabled) or `gdb-multiarch` with appropriate scripts to observe heap behavior and identify suitable chunk sizes.
  • Memory Layout: Android’s ASLR (Address Space Layout Randomization) will randomize library base addresses and stack/heap locations. Heap spraying helps with relative predictability within the heap, but absolute addresses (for shellcode, ROP gadgets) often require an information leak first.
  • Allocation Primitives: Not all allocations are equal. Depending on the NDK library, `malloc`/`free`, `new`/`delete`, or custom allocators might be in use. Understanding the specific allocator helps in precise heap feng shui.
  • Timing: The timing between `free` and subsequent `alloc` (spray) is crucial. Race conditions can make exploitation unreliable without careful synchronization.
  • Payloads: Shellcode for ARM64 needs to be carefully crafted, considering register usage (e.g., `x0-x7` for arguments), system call numbers, and alignment. ROP chains are often used to bypass DEP/NX.

Conclusion

Advanced NDK exploit primitives like User-After-Free and heap spraying remain fundamental techniques for compromising native Android applications on ARM64. While UAF provides the initial memory corruption primitive, heap spraying transforms an unreliable memory state into a predictable environment, paving the way for reliable exploitation. Understanding these concepts, along with a deep dive into ARM64 architecture and Android’s native runtime, is essential for anyone involved in security research, penetration testing, or vulnerability analysis on the Android platform. Mastering these techniques opens doors to uncovering and mitigating complex security flaws in production software.

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