Android Software Reverse Engineering & Decompilation

Dynamic Instrumentation on Android: Building a Custom ART Hooker from Scratch

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Dynamic Instrumentation and ART Hooking

Dynamic instrumentation, the process of altering a program’s execution flow at runtime, is a powerful technique in Android software reverse engineering, security analysis, and performance profiling. While tools like Frida and Xposed offer high-level APIs for hooking, understanding the underlying mechanisms of Android’s runtime (ART) is crucial for developing custom, stealthier, or more specialized instrumentation frameworks. This article dives deep into ART internals, guiding you through the process of building a fundamental custom ART method hooker from the ground up.

Understanding Android Runtime (ART) Internals

The Android Runtime (ART) is the managed runtime used by Android and its core mission is to execute application code. Unlike its predecessor Dalvik, ART uses Ahead-Of-Time (AOT) compilation by default, converting Dalvik bytecode (DEX) into native machine code (OAT format) during app installation. However, it also incorporates Just-In-Time (JIT) compilation for hot paths, ensuring optimal performance.

ART Method Representation: The ArtMethod Object

At the heart of ART’s execution model is the ArtMethod object. Every method (Java method, native method) within an application’s loaded DEX files is represented by an instance of ArtMethod. This structure contains vital information about the method, including:

  • dex_code_item_offset_: Offset to the method’s bytecode in the DEX file.
  • access_flags_: Method modifiers (public, static, native, etc.).
  • declaring_class_: Pointer to the ArtClass object this method belongs to.
  • entrypoint_from_quick_compiled_code_: The most critical field for hooking, this points to the native machine code entrypoint for the method.
  • ptr_sized_fields_.dex_cache_resolved_methods_: For resolved methods.

The exact layout of the ArtMethod object can vary significantly between Android versions, making cross-version hooking a challenging task.

Method Invocation Flow

When a Java method is invoked, ART looks up its corresponding ArtMethod object. Depending on whether the method has been AOT-compiled, JIT-compiled, or needs to be interpreted, ART directs execution to the appropriate entrypoint. For AOT/JIT compiled code, this is typically the native address stored in entrypoint_from_quick_compiled_code_.

Why Build a Custom ART Hooker?

While frameworks like Frida provide excellent capabilities, there are scenarios where a custom approach is beneficial:

  • **Stealth and Evasion**: Many anti-tampering solutions detect known hooking frameworks. A custom, purpose-built hooker can be harder to detect.
  • **Granular Control**: Direct manipulation of ART internals allows for highly specific and optimized hooking strategies not easily achievable with high-level APIs.
  • **Research and Learning**: Understanding ART at a low level provides invaluable insights into Android’s execution model, essential for advanced security research.
  • **Specific ART Versions**: Tailoring hooks for specific Android versions or device architectures.

Core Concepts for ART Hooking

The fundamental idea behind ART hooking is to redirect the method’s execution flow. This typically involves:

  1. **Locating the Target ArtMethod**: Finding the specific ArtMethod object for the method you wish to hook.
  2. **Modifying its Entrypoint**: Changing the entrypoint_from_quick_compiled_code_ field to point to your custom hook function.
  3. **Creating a Trampoline**: A small piece of code that saves the original context, executes your hook, and then jumps back to the original method’s execution (or continues elsewhere).

Locating ArtMethod Objects

Finding an ArtMethod can be done in several ways:

  • **Through JNI**: If you have a JNIEnv*, you can use GetMethodID or GetStaticMethodID to get a jmethodID, which is often a direct pointer to the ArtMethod object.
  • **Symbol Resolution**: Using dlsym to find exported ART functions in libart.so, then traversing internal ART structures. This is highly version-dependent.
  • **Memory Scanning**: For very specific scenarios, scanning memory for known method signatures or ArtMethod patterns.

For simplicity, let’s assume we can get the ArtMethod* via JNI:

// Example: Getting ArtMethod* for a Java method via JNI (simplified) ArtMethod* getArtMethod(JNIEnv* env, jclass clazz, const char* methodName, const char* methodSignature) {    jmethodID methodId = env->GetMethodID(clazz, methodName, methodSignature);    if (methodId == nullptr) {        methodId = env->GetStaticMethodID(clazz, methodName, methodSignature);    }    return reinterpret_cast<ArtMethod*>(methodId); }

Approximating the ArtMethod Structure

To manipulate ArtMethod, we need a C++ representation. This is an approximation and will differ across ART versions:

// Simplified ArtMethod structure (Android 9/10/11 common base) // Actual structure is much more complex and version-dependent struct ArtMethod {    // Some common fields, order and size vary greatly    uint32_t dex_code_item_offset_; // 0x0    uint32_t access_flags_;         // 0x4    uint16_t dex_method_index_;     // 0x8    uint16_t method_index_;         // 0xA    uint32_t hotness_count_;        // 0xC    void* declaring_class_;         // 0x10 (ArtClass*)    void* entrypoint_from_quick_compiled_code_; // 0x18 (The method's compiled native entrypoint)    // ... other fields and padding};

The `entrypoint_from_quick_compiled_code_` field is our target for modification.

Building the Basic Hooker: Step-by-Step

1. Modifying Memory Protection

The memory pages containing ArtMethod objects are usually read-only. We need to change their protection to read-write-execute (RWX) before modifying the entrypoint. This is done using mprotect.

#include <sys/mman.h> #include <unistd.h> bool make_rwx(void* addr, size_t len) {    long page_size = sysconf(_SC_PAGE_SIZE);    void* page_start = (void*)((uintptr_t)addr & ~(page_size - 1));    return mprotect(page_start, len, PROT_READ | PROT_WRITE | PROT_EXEC) == 0; }

2. Overwriting the Entrypoint

Once the memory is writable, we can replace the original entrypoint with the address of our hook function.

// Assume target_method_art is your ArtMethod* and hook_func is your custom C++ function void install_hook(ArtMethod* target_method_art, void* hook_func) {    if (!make_rwx(target_method_art, sizeof(ArtMethod))) { // Might need to adjust size        // Handle error    }    // Save original entrypoint for later (trampoline)    void* original_entrypoint = target_method_art->entrypoint_from_quick_compiled_code_;    // Overwrite with our hook function    target_method_art->entrypoint_from_quick_compiled_code_ = hook_func;    // Flush cache if needed (especially on older architectures/kernels)    __builtin___clear_cache(reinterpret_cast<char*>(target_method_art), reinterpret_cast<char*>(target_method_art) + sizeof(ArtMethod)); }

3. Implementing the Hook Function (Trampoline)

Your hook function needs to mimic the calling convention of the original method, process arguments, and then optionally call the original method. This usually involves inline assembly or highly platform-specific C++ for saving/restoring registers and stack frames. For simplicity, we’ll demonstrate a conceptual C++ hook that eventually calls back to the original method (which now needs to be called via the saved original entrypoint).

// Define a global/static variable to store the original entrypoint void* gOriginalEntrypoint = nullptr; // Example: A simple hook for a method like `int com.example.MyClass.add(int a, int b)` extern

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