Introduction to ART and JIT Compilation
The Android Runtime (ART) is the managed runtime used by Android and its core mission is to execute application code. While initially designed with Ahead-Of-Time (AOT) compilation in mind, ART also incorporates a Just-In-Time (JIT) compiler. The JIT compiler optimizes frequently executed ("hot") code paths at runtime, translating Dalvik bytecode into highly optimized native machine code. This dynamic compilation improves performance but introduces a potent attack surface for exploit development: JIT spraying.
JIT spraying is a technique that exploits the predictable nature of JIT-generated code to bypass Address Space Layout Randomization (ASLR) and achieve arbitrary code execution. Attackers can craft specific bytecode sequences that, when JIT-compiled, produce predictable native instruction patterns in memory. By repeatedly triggering the JIT compilation of these sequences, an attacker can flood large regions of memory with their controlled "gadgets" or "sleds".
Understanding JIT Spraying Fundamentals
The core idea behind JIT spraying is to fill memory with attacker-controlled native code. Unlike traditional heap spraying where arbitrary data is placed, JIT spraying leverages the JIT compiler itself to transform controlled bytecode into executable native code. This compiled code typically consists of two main parts: a long sequence of "NOP" (No Operation) or other benign, predictable instructions (the "sled"), followed by a jump instruction to the attacker’s actual shellcode. The sled increases the probability of hitting the attacker’s code if the exact address of the shellcode is unknown but its general vicinity is predictable.
The predictability of JIT output stems from the compiler’s deterministic behavior for simple, repetitive code patterns. When the JIT encounters a method that’s frequently called, it compiles it. If we can control the bytecode of such methods, we can influence the resulting native code.
Prerequisites and Environment Setup
To embark on JIT spraying, you’ll need a suitable environment:
- Rooted Android Device or Emulator: Essential for debugging and memory inspection.
- Android SDK with Platform Tools: Provides
adbfor device interaction. - AOSP Source Code (Optional but Recommended): For deep dives into ART and JIT internals.
- Debugging Tools:
gdbserverandgdbfor native debugging, orFridafor dynamic instrumentation. - Basic ARM64 Assembly Knowledge: Understanding how instructions translate to bytecode.
Step-by-Step: Crafting Your JIT Spray Payload
1. Identifying JIT-able Code Patterns
The JIT compiler prioritizes methods that are executed frequently and are relatively simple. Loops containing basic arithmetic or logical operations are prime candidates. For example, a method performing a series of additions or subtractions within a tight loop is highly likely to be JIT-compiled.
Consider this Java code snippet:
public class JitSprayTarget { private static volatile long sink = 0; public void sprayMethod(int iterations) { for (int i = 0; i < iterations; i++) { sink += (long) (i * 2 - i / 3); // Simple arithmetic that gets JIT-compiled } }}
2. Analyzing JIT-Generated Assembly
The next crucial step is to understand what ARM64 assembly ART’s JIT compiler generates from your chosen Java method. You can observe this using tools like Frida or by leveraging debugging features within AOSP.
Using Frida, you can hook the `art::jit::JitCodeCache::InsertCode` function to intercept newly compiled code, or attach to the running process and examine the code cache directly. Alternatively, if working with AOSP, you can recompile ART with verbose JIT logging or use `dex2oat` flags to dump assembly.
A typical JIT-compiled method will show a prologue, the actual method logic, and an epilogue. Our goal is to find predictable sequences. Simple arithmetic operations often compile into `ADD`, `SUB`, `MOV` instructions, and possibly `NOP`s if instruction scheduling introduces gaps. For instance, a method designed to generate NOP sleds might look like:
// Conceptual JIT-generated ARM64 assembly snippet (simplified)0x...: NOP0x...: NOP0x...: NOP0x...: NOP0x...: NOP0x...: B target_address // Branch to our shellcode (or another part of the sled)
3. Designing the Spray Payload
Once you understand how certain bytecode patterns translate to native code, you can design your Java method to generate the desired spray. The objective is to create a long sequence of ‘safe’ instructions (e.g., NOPs) that will form the sled, followed by a jump instruction to your actual shellcode (which would be placed elsewhere, perhaps through data spraying or another JIT method).
A common strategy involves using specific integer constants or operations that predictably map to a `NOP` (e.g., `MOV X0, X0` or other redundant operations) or a branch instruction when JIT-compiled. The exact Java code will depend on the ART JIT version and optimization level. For example, a series of redundant arithmetic operations might compile down to multiple `NOP`-like instructions due to optimization passes.
public class NopSledGenerator { public void generateNopSled(int count) { for (int i = 0; i < count; i++) { // These operations might be optimized into NOPs or very simple instructions // depending on the JIT compiler's specific version and optimization level. // Research is required to find reliable patterns for a target ART version. int a = i * 1; int b = a + 0; if (b == 0) { // Dead code path, but still contributes to method complexity/size } } // Hypothetical: A specific sequence here might generate a branch instruction // For example, calling a native method or a reflection call if its address is known. }}
4. Triggering the JIT Compilation and Spray
To populate memory with your JIT-compiled sled, you must repeatedly call the target method. ART’s JIT compiler has heuristics to determine when a method is "hot" enough to compile. This typically involves a certain number of invocations.
To reliably spray, you would create many instances of `JitSprayTarget` (or `NopSledGenerator`) and call their `sprayMethod` (or `generateNopSled`) repeatedly. Each instance might be associated with a different memory region. This repetition forces the JIT compiler to compile multiple copies of your chosen method, effectively spraying the JIT code cache with your sleds.
// Example Java code to trigger JIT sprayfinal int NUM_SPRAY_OBJECTS = 1000; // Adjust based on memory and performance needsfinal int ITERATIONS_PER_CALL = 10000; // To make methods 'hot'List<JitSprayTarget> sprayObjects = new ArrayList<>();for (int i = 0; i < NUM_SPRAY_OBJECTS; i++) { JitSprayTarget obj = new JitSprayTarget(); sprayObjects.add(obj); obj.sprayMethod(ITERATIONS_PER_CALL); // Trigger JIT compilation}System.gc(); // May help in some cases to stabilize memory, but also might clear it
During this process, monitor the `art_jit_zygote_code_cache` or `art_jit_app_code_cache` memory regions using `cat /proc/self/maps` or a debugger to observe the growth of JIT-compiled code.
5. Leveraging a Memory Corruption Vulnerability
JIT spraying itself doesn’t directly give you code execution. It’s typically paired with a separate memory corruption vulnerability (e.g., an out-of-bounds write, use-after-free, or type confusion) that allows you to hijack control flow. Once you have a primitive to overwrite an instruction pointer, a return address, or a function pointer, you can redirect execution to one of your sprayed NOP sleds.
Because the sled occupies a large, predictable region, even if ASLR prevents you from knowing the exact address of your shellcode, landing anywhere on the sled will eventually slide execution to your desired payload at the end of the sled.
Exploit Demonstration (Conceptual)
1. **Prepare Shellcode:** Place your actual payload (e.g., `execve(‘/system/bin/sh’)`) in a known, accessible memory region, or embed it within another JIT-sprayed method. This could be done via data spraying or by crafting a specific JIT payload.2. **JIT Spray:** Execute the Java `NopSledGenerator` multiple times to fill the JIT code cache with your sleds and a jump to your shellcode.3. **Trigger Vulnerability:** Exploit your memory corruption bug to overwrite a critical pointer (e.g., a function pointer in a vtable, or a return address on the stack).4. **Redirect Execution:** Set the overwritten pointer’s value to an address within the JIT-sprayed NOP sled.5. **Achieve Code Execution:** When the program attempts to execute the overwritten pointer, control flow is transferred to your NOP sled, which then branches to your shellcode, achieving arbitrary code execution.
Mitigations and Defenses
ART and Android have implemented several mitigations against JIT spraying and similar exploitation techniques:
- Code Cache Permissions: JIT code caches are typically marked as `PROT_EXEC | PROT_READ` but not `PROT_WRITE` after compilation, making direct modification difficult.
- Pointer Authentication Codes (PAC): On ARMv8.3-A and newer, PAC can protect return addresses and other critical pointers from being easily overwritten without triggering an exception.
- Control Flow Integrity (CFI): ART’s CFI mechanisms aim to ensure that indirect jumps and calls only target valid, expected destinations, which would ideally prevent jumps into JIT-sprayed code.
- Fine-Grained ASLR: Increased randomization of memory regions makes it harder to predict the location of JIT-compiled code.
- JIT Compiler Hardening: Ongoing efforts to make JIT output less predictable or to limit the types of instructions that can be generated from user input.
Conclusion
JIT spraying remains a powerful technique in the arsenal of Android exploit developers. By understanding how ART’s JIT compiler operates and how bytecode translates to native machine code, attackers can craft sophisticated payloads to bypass ASLR and achieve arbitrary code execution. While modern Android versions incorporate numerous mitigations, the dynamic nature of JIT compilation continues to present unique challenges and opportunities for security researchers.
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 →