Android System Securing, Hardening, & Privacy

Gaining Arbitrary Kernel Read/Write on Android ARM64: Exploit Primitive Development

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Android ARM64 Kernel Exploitation

Achieving arbitrary kernel read/write (R/W) capabilities is the holy grail for kernel exploit developers. On Android ARM64 systems, this primitive unlocks the highest level of control, allowing an attacker to bypass security mechanisms, modify system behavior, and ultimately gain root privileges or persist malicious code. The journey to arbitrary R/W is complex, fraught with mitigations like Kernel Address Space Layout Randomization (KASLR), Supervisor Mode Execution Prevention (SMEP) or its ARM equivalent, Privileged Execute Never (PXN), and Write-XOR-Execute (W^X) pages, alongside Pointer Authentication Codes (PAC) on newer ARM architectures. This article will demystify the process, outlining how various limited primitives can be chained to achieve the coveted arbitrary kernel R/W.

The Journey to Arbitrary Read/Write

A typical kernel exploit chain on modern Android ARM64 often involves several distinct stages:

  1. Information Leakage: Bypassing KASLR by leaking a kernel text or heap address.
  2. Limited Write Primitive: Exploiting a vulnerability (e.g., an out-of-bounds write, use-after-free, or type confusion) to perform a controlled, but limited, write operation within kernel space.
  3. Arbitrary Read/Write Primitive Development: Leveraging the limited write to hijack a kernel data structure or function pointer, enabling full control over kernel memory.

Our focus here is on the third stage: converting a limited write into a robust arbitrary R/W primitive, assuming KASLR has already been bypassed.

Achieving Kernel Information Leakage

Before any meaningful write can occur, KASLR must be defeated. This requires an information leak that discloses a kernel memory address. Common sources include:

  • Uninitialized kernel stack or heap memory disclosures.
  • Leaks of kernel pointers from specific kernel data structures.
  • Side-channel attacks or timing vulnerabilities.

For example, a vulnerable ioctl handler might accidentally copy uninitialized kernel stack memory to userspace, revealing a kernel text address. Once a kernel text address is known, the base address of the kernel image can be calculated, effectively bypassing KASLR. Similarly, leaking a heap pointer can aid in heap spray techniques.

// Conceptual C code: Vulnerable ioctl leading to info leak (kernel stack address)void* kbuf;long vulnerable_ioctl_handler(struct file *filp, unsigned int cmd, unsigned long arg) {    switch (cmd) {        case IOCTL_LEAK_ADDRESS:            // Suppose kbuf is an uninitialized local variable on kernel stack            // or contains a leftover kernel pointer from a previous operation.            // A more realistic leak would involve a specific struct field.            copy_to_user((void __user *)arg, &kbuf, sizeof(void*)); // LEAK!            break;        // ... other cases ...    }    return 0;}

From Limited Write to Arbitrary Write

This is the pivotal step. Given a vulnerability that allows a controlled but limited write (e.g., an out-of-bounds write of a small, fixed value, or overwriting a single pointer), the goal is to transform this into the ability to read and write any kernel address. A potent technique involves corrupting a pointer within a kernel object that has a well-defined structure and associated function pointers.

A popular target on Linux (and thus Android) is the seq_operations structure, used by the seq_file interface. If we can overwrite a function pointer within a seq_operations table (or a pointer to a seq_operations table), we can redirect kernel execution. For instance, redirecting the .show or .read callback to a user-controlled address.

// Conceptual C code: Overwriting a seq_operations pointerstruct seq_operations {    // ... other pointers ...    int (*show)(struct seq_file *, void *);    // ... other pointers ...};struct my_vulnerable_object {    // ... other fields ...    struct seq_operations *ops; // Target pointer for corruption    // ...};// Assuming we have a limited write primitive:// k_addr_of_my_vulnerable_object_ops = known kernel address of my_vulnerable_object->ops// user_controlled_code_addr = address in userspace with our payload// limited_write(k_addr_of_my_vulnerable_object_ops, user_controlled_code_addr);

After overwriting my_vulnerable_object->ops with a pointer to a user-controlled fake seq_operations table, or directly overwriting a function pointer within an existing seq_operations table, any subsequent kernel call to that function (e.g., seq_read or seq_show) will execute code at our chosen userspace address. This userspace code can then be crafted to perform arbitrary reads and writes.

Developing Arbitrary Read Primitive

Once we can divert kernel execution to userspace, we can craft a gadget (a small piece of assembly or C code) in userspace that acts as our arbitrary read primitive. The kernel will execute this gadget when the hijacked function pointer is called. This gadget will typically take an address as input (perhaps from a register or an indirect memory location the kernel accesses) and return its content.

// Conceptual Userspace C code for arbitrary read via hijacked kernel executionvoid arbitrary_read_gadget(unsigned long addr, unsigned long *val) {    // This function will be called by the kernel with 'addr' as the target address.    // It needs to copy the content of 'addr' to a user-controlled buffer.    // Actual implementation depends on how arguments are passed (registers, stack).    // This assumes kernel passes `addr` and `val` (a user pointer for result).    *val = *(unsigned long *)addr; // Kernel reads from addr and stores in user_ptr_val}long userspace_arbitrary_read(unsigned long target_kernel_address) {    unsigned long result = 0;    // Trigger the kernel function that calls our hijacked pointer    // e.g., open /proc/my_vulnerable_object, then read from it.    // The 'arbitrary_read_gadget' will be invoked.    // The gadget must communicate the result back, e.g., via shared memory.    // Simplified: Imagine the gadget writes result to a known userspace address.    // Call the hijacked kernel function, passing target_kernel_address as an argument    // that the hijacked function will process.    trigger_kernel_hijack_with_read_target(target_kernel_address, &result);    return result;}

Developing Arbitrary Write Primitive

Similarly, for an arbitrary write, a userspace gadget is needed. This gadget will receive the target address and the value to write, performing the write operation when executed by the kernel.

// Conceptual Userspace C code for arbitrary write via hijacked kernel executionvoid arbitrary_write_gadget(unsigned long addr, unsigned long val) {    // This function will be called by the kernel with 'addr' and 'val'.    // It writes 'val' to 'addr'.    *(unsigned long *)addr = val; // Kernel writes val to addr}void userspace_arbitrary_write(unsigned long target_kernel_address, unsigned long value_to_write) {    // Trigger the kernel function that calls our hijacked pointer    // e.g., open /proc/my_vulnerable_object, then write to it.    // The 'arbitrary_write_gadget' will be invoked.    // Call the hijacked kernel function, passing target_kernel_address and value_to_write.    trigger_kernel_hijack_with_write_target(target_kernel_address, value_to_write);}

The exact mechanism for passing arguments to and retrieving results from these userspace gadgets when they are invoked by the kernel is crucial. This often involves careful manipulation of registers in the context of the kernel call or using pre-arranged userspace memory regions that the kernel can read/write to.

Post-Exploitation: Root and Beyond

With arbitrary kernel R/W, the path to root privileges is clear. The most common technique is to find the current process’s task_struct in kernel memory, locate its cred and real_cred pointers, and then replace the existing cred structure with a new one that grants root privileges (UID 0, GID 0, all capabilities). This is typically done by calling the kernel functions prepare_kernel_cred(0) (to get a root cred structure) and then commit_creds() (to apply it to the current task).

// Conceptual Userspace C code leveraging arbitrary write for rootprivilegesvoid get_root_privileges() {    unsigned long root_cred_struct_addr;    unsigned long current_task_struct_addr;    // 1. Find the address of prepare_kernel_cred and commit_creds in kernel memory    //    (Requires symbol lookup, or prior leak/hardcoding after KASLR bypass)    unsigned long prepare_kernel_cred_addr = find_kernel_symbol(

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