Introduction to Android Kernel Exploitation and UAF
Android kernel security is a critical battleground, and understanding kernel vulnerabilities like Use-After-Free (UAF) is paramount for both exploit developers and defenders. A UAF vulnerability occurs when a program continues to use a pointer to freed memory, leading to unpredictable behavior, data corruption, or even arbitrary code execution. Debugging these complex kernel exploits requires a specialized environment, and the combination of QEMU and GDB offers an unparalleled platform for in-depth analysis.
This article will guide you through setting up an advanced debugging environment for the Android kernel, specifically tailored for identifying and troubleshooting UAF vulnerabilities using GDB in conjunction with a QEMU-emulated Android system. We’ll cover environment setup, basic UAF identification techniques, and advanced GDB features essential for kernel exploit development.
Prerequisites and Environment Setup
Before diving into debugging, ensure you have the necessary tools and a suitable environment. We’ll be working with a customized Android kernel built for QEMU.
Required Tools:
- AOSP Build Environment: A Linux-based system with sufficient disk space and RAM to build Android from source.
- QEMU: A powerful emulator capable of running ARM-based Android kernels.
- GDB (GNU Debugger): Specifically, a cross-compiler version (e.g., aarch64-linux-android-gdb) from the Android NDK.
- Android Kernel Source: Downloaded from AOSP repositories.
Building a Debuggable Android Kernel for QEMU
To effectively debug, your kernel must be built with debugging symbols. Navigate to your kernel source directory and configure it.
cd /path/to/android-kernel-source
export ARCH=arm64
export CROSS_COMPILE=/path/to/android-ndk/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-
make defconfig
make menuconfig # Enable debugging options
Inside `menuconfig`, navigate to ‘Kernel hacking’ and enable:
- ‘Kernel debugging’
- ‘Compile the kernel with debug info’ (CONFIG_DEBUG_INFO)
- ‘Enable __GFP_ZERO and PAGE_POISONING’ (useful for detecting UAF)
- ‘Slab debugging (SLUB)’ -> ‘SLUB debugging’ (CONFIG_SLUB_DEBUG)
Save and exit, then build the kernel:
make -j$(nproc)
This will generate `arch/arm64/boot/Image` and `vmlinux` (the unstripped kernel image with symbols).
Launching QEMU with GDB Server
Once your debuggable kernel is built, launch QEMU with an Android ramdisk and a GDB server listening on a specific port.
qemu-system-aarch64
-M virt -cpu cortex-a57 -m 2G -smp 2
-kernel /path/to/android-kernel-source/arch/arm64/boot/Image
-append "console=ttyAMA0,115200 root=/dev/vda rw init=/init androidboot.console=ttyAMA0"
-initrd /path/to/android/ramdisk.img
-drive file=/path/to/android/system.img,if=none,id=system
-device virtio-blk-device,drive=system
-serial mon:stdio
-netdev user,id=mynet -device virtio-net-device,netdev=mynet
-S -s
- `-S`: Halts the CPU at startup, waiting for GDB to connect.
- `-s`: Equivalent to `-gdb tcp::1234`, making QEMU listen for GDB connections on port 1234.
- Replace `/path/to/android/ramdisk.img` and `/path/to/android/system.img` with your actual Android images.
Connecting GDB to QEMU and Basic Debugging
In a separate terminal, launch your `aarch64-linux-android-gdb` and connect to the QEMU instance.
/path/to/android-ndk/toolchains/.../bin/aarch64-linux-android-gdb
/path/to/android-kernel-source/vmlinux
(gdb) target remote localhost:1234
(gdb) continue
You are now connected! GDB will load the kernel symbols from `vmlinux`, and `continue` will start the QEMU VM. You can set breakpoints, inspect registers, and step through kernel code.
Identifying and Troubleshooting UAF Exploits
UAF vulnerabilities are often subtle. They involve a pointer becoming ‘stale’ after its memory has been freed, only to be dereferenced later. Your goal is to catch this dereference and observe the memory state.
Scenario: Custom Kernel Module UAF
Consider a hypothetical kernel module with a UAF flaw:
struct my_data {
int value;
char name[16];
};
static struct my_data *global_data_ptr = NULL;
// Allocate and initialize
static ssize_t alloc_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
global_data_ptr = kmalloc(sizeof(struct my_data), GFP_KERNEL);
if (global_data_ptr) {
global_data_ptr->value = 0xDEADBEEF;
strncpy(global_data_ptr->name, "initial", sizeof(global_data_ptr->name) - 1);
}
return count;
}
// Free the data
static ssize_t free_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
if (global_data_ptr) {
kfree(global_data_ptr);
// global_data_ptr is now a stale pointer!
// A real bug would involve it being used *after* this point without being set to NULL.
}
return count;
}
// Use the data AFTER it might have been freed (hypothetical UAF trigger)
static ssize_t use_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
if (global_data_ptr) {
// If memory at global_data_ptr was freed and re-allocated by something else,
// this read is a UAF!
printk(KERN_INFO "UAF Read: Value = %x, Name = %sn", global_data_ptr->value, global_data_ptr->name);
}
return count;
}
Debugging Steps with GDB:
- Set Breakpoints: Identify the `kmalloc`, `kfree`, and potential ‘use’ sites.
- Trigger Allocation: From the Android shell (via `adb shell`), trigger the `alloc_write` operation (e.g., by writing to a `/dev/mydevice` file).
- Inspect Memory After Allocation: When GDB hits `alloc_write` (or just after), inspect `global_data_ptr`.
- Trigger Free: Continue execution and trigger the `free_write` operation.
- Observe After Free: After `kfree` returns, the memory pointed to by `global_data_ptr` is free. Ideally, `global_data_ptr` should be nulled. If not, it’s stale.
- Heap Spray (to re-occupy freed memory): To make the UAF impactful, you often need to re-occupy the freed memory with attacker-controlled data. This is typically done by triggering other kernel allocations (e.g., creating pipes, sockets, or specific kernel objects) of the same size as the freed object.
- Trigger Use-After-Free: Finally, trigger the `use_read` function. If `global_data_ptr` points to re-occupied memory, you’ll observe your sprayed data instead of the original or an immediate crash.
(gdb) b alloc_write
(gdb) b free_write
(gdb) b use_read
(gdb) p global_data_ptr
(gdb) x/16wx global_data_ptr // Examine 16 words in hex at the address
(gdb) p global_data_ptr // The address won't change, but the content might.
(gdb) x/16wx global_data_ptr // What's there now? Poisoned data? New data from another allocation?
Advanced GDB Techniques for UAF
- Watchpoints: Set a watchpoint on the memory location `global_data_ptr` points to. GDB will halt whenever that memory address is written to or read from. This is incredibly powerful for tracking memory corruption.
(gdb) watch *global_data_ptr
(gdb) watch -l global_data_ptr->value // Watch a specific member
(gdb) b free_write if global_data_ptr != 0
python
# Example: Print memory content before and after free
# gdb.execute("commands 1")
# gdb.execute("x/16wx global_data_ptr")
# gdb.execute("end")
end
Conclusion
Debugging Use-After-Free vulnerabilities in the Android kernel is a challenging but essential skill for security researchers and exploit developers. By leveraging QEMU for emulation and GDB for precise memory inspection and execution control, you can systematically identify, analyze, and troubleshoot these critical flaws. The techniques outlined here – from setting up a debuggable kernel to using advanced GDB features like watchpoints – provide a robust foundation for deeper dives into kernel exploit development and hardening Android’s security posture.
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 →