Android App Penetration Testing & Frida Hooks

Mapping Android Native Libraries: Finding Obfuscated ARM/ARM64 Hook Points using Ghidra & Frida

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Challenge of Native Android Analysis

Android applications increasingly rely on native libraries (.so files) written in C/C++ for performance-critical operations, low-level system interactions, and often, security-sensitive logic like cryptographic routines or anti-tampering checks. While using native code offers advantages, it also introduces a significant challenge for security analysts and penetration testers: these libraries are harder to reverse engineer than Java bytecode, especially when heavily obfuscated. Techniques like stripping symbols, control flow flattening, and virtual machine protection make static analysis difficult, and dynamic instrumentation becomes crucial.

This article delves into an advanced methodology for identifying and hooking obfuscated ARM/ARM64 native functions within Android applications. We will combine the power of Ghidra for meticulous static analysis and Frida for dynamic runtime instrumentation, providing a comprehensive guide to uncover even the most elusive hook points.

Setting Up Your Environment

Before diving into the analysis, ensure you have the necessary tools installed and configured:

  • Ghidra: A powerful open-source software reverse engineering (SRE) suite developed by the NSA. Download and install the latest stable version.
  • Frida: A dynamic instrumentation toolkit that lets you inject snippets of JavaScript or your own library into native apps on Windows, macOS, Linux, iOS, Android, and QNX. Install the Frida client on your host machine (pip install frida-tools) and the Frida server on your Android device (download from GitHub releases, push to /data/local/tmp, and run).
  • ADB (Android Debug Bridge): Essential for interacting with your Android device. Ensure it’s in your system’s PATH.
  • Target Android Application: An application containing native libraries you wish to analyze. For practice, choose an app with a libnative-lib.so or similar.

Static Analysis with Ghidra: Unraveling Native Binaries

Loading the Library into Ghidra

First, extract the target .so library from the APK. You can do this by renaming the APK to .zip, extracting its contents, and finding the library under lib/[arch-abi]/ (e.g., lib/arm64-v8a/libfoo.so). Then, launch Ghidra, create a new project, and import the .so file:

  1. Go to File > Import File… and select your .so.
  2. In the ‘Language’ prompt, select the correct architecture (e.g., AARCH64:LE:64:v8A for ARM64, or ARM:LE:32:v7 for ARMv7). Ghidra usually auto-detects this.
  3. Once imported, double-click the file to open it in the CodeBrowser. When prompted, perform an ‘Auto Analyze’ with default options.

Navigating and Identifying Functions

Ghidra’s CodeBrowser provides several views:

  • Symbol Tree: Lists functions, labels, and external imports. Exported functions are the easiest to spot.
  • Listing View: Shows the disassembled machine code.
  • Decompiler View: Attempts to translate assembly into C-like pseudocode, invaluable for understanding logic.

Our primary goal is to identify functions that are not explicitly exported or easily identifiable. Here’s how:

  • String References: Search for relevant strings (e.g., API keys, URLs, error messages, known function names if not stripped) within the library (Search > For Strings…). Cross-reference any found strings to see where they are used. This often leads directly to interesting functions.
  • External Function Calls (Imports): Look for calls to common system libraries (e.g., memcpy, strlen, malloc, crypto functions like AES_set_encrypt_key, EVP_EncryptUpdate, network functions like connect, send, recv). Functions that frequently call these imports are often good candidates for hooks. Use the ‘Cross-References To’ feature on imported functions to find all locations where they are called.
  • Function Signatures and Control Flow: Analyze function prologues (stack setup, register saving) and epilogues (stack cleanup, register restoring) to identify function boundaries, especially when Ghidra might not fully recognize them. Pay attention to common instruction patterns for cryptographic operations or data manipulation.

Dealing with Obfuscation

Obfuscated native code poses a greater challenge:

  • Stripped Symbols: Most functions will be named FUN_XXXXXXXX. Use the techniques above to rename and understand them.
  • Control Flow Flattening: Linearizes code, replacing direct jumps with indirect ones based on state variables. Analyze the state machine or identify key branches.
  • Indirect Calls: Functions might be called via register values (e.g., `BL X0` or `BL R1`) rather than direct addresses. Track where these registers are loaded from. Use Ghidra’s cross-references on data sections that might hold function pointers.
  • Anti-Disassembly/Anti-Analysis: Techniques like opaque predicates or self-modifying code. These require careful, manual analysis in the Listing View to understand the true execution path.

For example, if you find a function that takes a buffer and a key and calls an AES encryption function, this would be a prime target. Note its starting address, e.g., 0x12345 (relative to the library’s base address). This offset is crucial for Frida.

Dynamic Instrumentation with Frida: Attaching and Hooking

Once you’ve identified a potential function’s offset within the native library using Ghidra, Frida allows you to dynamically hook it at runtime. This bypasses static obfuscation by observing the function’s actual behavior.

Basic Frida Script Structure

A typical Frida script for Android native hooking involves attaching to the target process and then using Module and Interceptor APIs.

Java.perform(function () {    var libName = "libfoo.so"; // The name of your target native library    var targetModule = Module.findBaseAddress(libName);    if (!targetModule) {        console.log("[-] " + libName + " not found in memory.");        return;    }    console.log("[+] " + libName + " base address: " + targetModule);    // --- Hooking an exported function (example) ---    var JNI_OnLoad_ptr = targetModule.add(Module.findExportByName(libName, "JNI_OnLoad").offset);    if (JNI_OnLoad_ptr) {        Interceptor.attach(JNI_OnLoad_ptr, {            onEnter: function (args) {                console.log("[+] JNI_OnLoad called!");            },            onLeave: function (retval) {                console.log("[+] JNI_OnLoad returned: " + retval);            }        });    }    // --- Hooking an obfuscated function by address (main focus) ---    var obfuscated_func_offset = 0x12345; // Replace with the offset found in Ghidra    var obfuscated_func_ptr = targetModule.add(obfuscated_func_offset);    console.log("[+] Attempting to hook obfuscated_func at: " + obfuscated_func_ptr);    Interceptor.attach(obfuscated_func_ptr, {        onEnter: function (args) {            console.log("n[+] obfuscated_func_12345 CALLED!");            // ARM64 registers: x0, x1, x2, x3...            // ARM32 registers: r0, r1, r2, r3...            console.log("  [Argument x0/r0]: " + args[0]);            console.log("  [Argument x1/r1]: " + args[1]);            // If it's a pointer to data, you can read it            // console.log("  [Data at x0/r0]: " + Memory.readCString(args[0]));            // To get a stack trace:            // console.log("  Stack Trace:n" + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("n"));        },        onLeave: function (retval) {            console.log("[-] obfuscated_func_12345 finished. Return value: " + retval);            // You can modify the return value if needed            // retval.replace(0x0);        }    });});

Executing the Frida Script

To run this script:

  1. Start the Frida server on your Android device:adb shell "/data/local/tmp/frida-server &"
  2. On your host machine, execute your script, replacing com.your.package with the target app’s package name and hook.js with your script’s filename:frida -U -f com.your.package -l hook.js --no-pause

    The --no-pause flag allows the app to start immediately, useful if the native function is called early in the app’s lifecycle (e.g., in JNI_OnLoad).

Advanced Considerations and Best Practices

  • ASLR (Address Space Layout Randomization): Frida automatically handles ASLR. When you use Module.findBaseAddress(libName), it gives you the current runtime base address of the library, so adding your static Ghidra offset (`0x12345`) will reliably point to the target function.
  • Register Analysis: Pay close attention to ARM/ARM64 calling conventions. For ARM32, `r0-r3` typically hold the first four arguments. For ARM64, `x0-x7` hold the first eight arguments. Larger arguments or floating-point values are handled differently. Refer to ABI specifications for precise details.
  • Memory Operations: Frida’s Memory.read*() and Memory.write*() functions are invaluable for inspecting or modifying buffer contents passed to native functions.
  • Iterative Process: The best results come from an iterative approach. Use Ghidra to find a potential area, hook it with Frida to observe runtime behavior and refine your understanding, then go back to Ghidra with new insights. For instance, if a hooked function’s arguments don’t make sense, trace its callers in Ghidra to understand how they’re prepared.
  • Anti-Frida/Anti-Debugging: Be aware that some applications implement checks to detect Frida or debuggers. These may involve checking for `frida-server` on the device, `ptrace` usage, or timing differences. Bypassing these requires more advanced Frida techniques or kernel-level modifications.

Conclusion

The combination of Ghidra’s powerful static analysis capabilities and Frida’s dynamic instrumentation framework provides an incredibly effective toolkit for tackling even heavily obfuscated native Android libraries. By methodically analyzing the binary structure, identifying potential function entry points through cross-references and string analysis, and then dynamically observing their behavior at runtime, security researchers can pinpoint crucial logic, bypass anti-analysis measures, and develop sophisticated hooks for penetration testing and vulnerability research. Mastering this workflow unlocks a deeper understanding of Android applications and their underlying native components.

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