Introduction to Android CFI and Obscure Architectures
Control-Flow Integrity (CFI) is a crucial security mechanism designed to prevent common memory corruption vulnerabilities from being exploitable by ensuring that program execution follows a pre-determined, valid path. In modern Android, CFI is extensively implemented through LLVM’s compiler-based instrumentation, protecting indirect calls and jumps. While formidable on mainstream ARM64 architectures, older or less common architectures still found in some Android devices (like MIPS or older ARM variants) can present unique challenges and potential weaknesses in CFI implementations. This article delves into the intricacies of crafting a custom CFI bypass, focusing on a hypothetical scenario on an ‘obscure’ MIPS-based Android environment, highlighting the specific architectural considerations that might enable such an exploit.
Understanding CFI involves recognizing its two primary enforcement points: forward-edge (indirect calls, jumps) and backward-edge (return addresses). LLVM CFI, as used in Android, primarily focuses on forward-edge integrity by ensuring that indirect calls and jumps only target valid, type-compatible destinations. This is achieved by inserting runtime checks that validate the target address against a whitelist of known valid function entry points, often by checking a unique CFI signature or type ID.
Understanding MIPS Architecture and CFI Challenges
MIPS (Microprocessor without Interlocked Pipeline Stages) architecture, while less prevalent in newer Android devices, historically powered many embedded and low-cost smartphones. Its RISC instruction set and distinct calling conventions offer a different attack surface compared to ARM. Key aspects for exploitation include:
- Delay Slot: MIPS branch instructions have a delay slot, meaning the instruction immediately following a branch is executed *before* the branch takes effect. This is a critical detail for ROP or custom shellcode.
- Register Usage: MIPS uses registers like
$ra(return address),$sp(stack pointer), and$gp(global pointer), along with argument registers$a0-$a3and temporary registers$t0-$t9. Indirect calls often usejalr $rs(jump and link register) orjr $rs(jump register). - No Execute (NX) Bit: Like other modern CPUs, MIPS supports NX, preventing execution from data segments.
The challenge in bypassing CFI on MIPS lies in subverting the runtime type checks. A common CFI implementation strategy involves encoding type information into the target function’s prologue or a dedicated metadata table. If an attacker can inject an arbitrary address into an indirect call register, the CFI check will still validate its type. Our goal is to find a way around this validation.
Identifying a CFI Weakness: The Unvalidated Function Pointer
Consider a hypothetical scenario where an older, custom library (liblegacy.so) compiled for a MIPS Android system uses a function pointer for a callback mechanism. Due to specific compilation flags, a legacy toolchain, or an oversight, this particular function pointer is placed in a writable data section and *not* fully instrumented by CFI. This could happen if the pointer’s type is ambiguous to the compiler or if it’s part of a data structure that CFI instrumentation doesn’t correctly cover.
Let’s assume we’ve identified a vulnerability that allows us to write an arbitrary 4-byte value (MIPS is 32-bit) to a specific memory location. We’ve also located a global function pointer, void (*my_callback_ptr)(), within liblegacy.so at a known address (e.g., 0xXXXXXXXX), which is later invoked via jr $t9 (where $t9 holds the value of my_callback_ptr).
The Vulnerable Code (C)
// liblegacy.c snippet demonstrating vulnerability targetvoid (*my_callback_ptr)() = NULL; // Global, potentially uninstrumented pointervoid set_callback(void* func_ptr) { my_callback_ptr = (void(*)())func_ptr; // Writable, potentially bypassing CFI instrumentation}// ... later in the code ...void trigger_callback() { if (my_callback_ptr) { my_callback_ptr(); // Indirect call target for our bypass }}
If `set_callback` is called with attacker-controlled data, and `my_callback_ptr` falls outside the scope of strict CFI validation (e.g., due to its initialization, or the linker not associating it with a specific function type for CFI metadata generation), we have a primitive.
Crafting the MIPS CFI Bypass
Our strategy is to overwrite my_callback_ptr with the address of a ROP gadget that executes arbitrary code without hitting a CFI enforcement point for its *own* indirect call. Since my_callback_ptr is called directly, we need a gadget that either branches to our shellcode or performs a useful action like `system()` if we can control its argument.
Step 1: Information Gathering
Using an information leak (e.g., format string bug, uninitialized memory read), we’d need:
- The base address of
liblegacy.soto calculate the offset tomy_callback_ptr. - The address of a useful gadget (e.g., `system()` from `libc.so`, or an existing ROP gadget).
Let’s assume we leak `liblegacy.so` base at `0x70000000` and `libc.so` base at `0x71000000`. We find that `my_callback_ptr` is at `0x7000A000` and `system` is at `0x71030000`.
Step 2: Finding a Suitable Gadget (MIPS ROP)
A simple ROP gadget for our scenario might be a `jr $ra` (jump to return address) followed by a `nop` (delay slot), or an instruction that loads an argument and then calls `system`. For a `system()` call, we need to control `$a0`. If we can’t directly control `$a0` before the `jr $t9` call, we need a gadget that moves our string address into `$a0`.
Let’s assume we find a gadget within liblegacy.so or libc.so:
0x70012340: lw $a0, 0($sp) # Load argument from stack0x70012344: jal system # Call system()0x70012348: nop # Delay slot
This gadget is ideal: if we make my_callback_ptr point to 0x70012340, and control the stack ($sp), we can execute `system(our_string)`. If we can’t control the stack, we need a simpler `jr $t9` bypass.
For a basic CFI bypass using `jr $t9` to an arbitrary function (like `system`), we just need to place `system`’s address directly into `my_callback_ptr`.
Step 3: Executing the Bypass
Assuming we can write `0x71030000` (address of `system`) into `0x7000A000` (address of `my_callback_ptr`), and we can also place a pointer to a string like `/system/bin/sh` onto the stack such that it’s picked up by `$a0` during `system`’s execution (or if the vulnerability allows us to set `$a0` directly), the exploit flow would be:
- Leak Addresses: Obtain `liblegacy.so` base and `libc.so` base.
- Calculate `my_callback_ptr` address: `liblegacy_base + offset_to_my_callback_ptr`.
- Calculate `system()` address: `libc_base + offset_to_system`.
- Overwrite `my_callback_ptr`: Use the write primitive to set `my_callback_ptr` to the address of `system()`.
- Trigger `trigger_callback()`: This will now execute `system()` with whatever `my_callback_ptr` was pointing to.
Illustrative MIPS Assembly Trace
Original indirect call path (hypothetical, CFI checked):
# Assume $t9 holds 0x12345678 (valid function)lw $t0, 0x10($t9) # Load CFI metadata check value... (CFI check against valid types)...jr $t9 # Jump to 0x12345678nop
Our bypassed path:
# Attacker overwrites my_callback_ptr with system() address (0x71030000)jalr $t9 # (equivalent to my_callback_ptr() in C)nop # The delay slot instruction is executed# Now $t9 contains 0x71030000 (address of system())# The original CFI check path is bypassed because the vulnerable pointer was not instrumented.# Execution continues at 0x71030000, calling system().
This bypass relies on a specific vulnerability where the target function pointer is not properly subjected to CFI checks, often due to compilation flags, a legacy code path, or exotic linking configurations that confuse the CFI instrumentation passes.
Practical Exploitation Steps (Conceptual)
On an Android device, these steps would involve:
- ADB Setup: Connect to the device via ADB.
- Process Attachment (GDB): Attach to the vulnerable process using `gdbserver` and `arm-linux-androideabi-gdb` (or `mips-linux-android-gdb` in our case).
- Memory Inspection: Use GDB to inspect memory, verify leaked addresses, and locate `my_callback_ptr`.
- Crafting Payload: Prepare the address of `system()` or a suitable ROP gadget.
- Triggering Write: Execute the vulnerability that overwrites `my_callback_ptr`. This could be a malicious input via `adb shell am start -n …`, a crafted network packet, or a local application.
- Triggering Call: Cause the application to invoke `trigger_callback()`, leading to the execution of our chosen function (e.g., `system(
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 →