Android Software Reverse Engineering & Decompilation

From Zero to Hero: Frida Scripts for Dynamic Analysis of Android Native Libraries

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Dynamic Analysis with Frida

In the realm of Android application security, native libraries often present a formidable challenge. Unlike Java code, which can be easily decompiled and analyzed statically, native code (written in C/C++ and compiled into `.so` files) demands more sophisticated techniques for understanding its runtime behavior. This is where Frida, a dynamic instrumentation toolkit, shines. Frida allows you to inject scripts into running processes on Android, giving you unparalleled control to observe, modify, and even replace native functions on the fly. This article will guide you from setting up your environment to implementing advanced Frida hooks for dynamic analysis of Android native libraries, transforming you from a novice to a hero in native code reverse engineering.

Setting Up Your Frida Environment

Prerequisites

Before diving into the exciting world of Frida, ensure you have the following:

  • An Android device or emulator (rooted is highly recommended for full access).
  • Android Debug Bridge (ADB) installed and configured on your host machine.
  • Python 3 and `pip` for installing Frida tools.
  • Basic familiarity with C/C++ syntax and Android’s JNI (Java Native Interface).
  • A static analysis tool like Ghidra or IDA Pro (optional but highly valuable for finding unexported function addresses).

Installing Frida on Your Host Machine

Installing the Frida command-line tools is straightforward using pip:

pip install frida-tools

This command installs `frida`, `frida-ps`, `frida-trace`, and other utilities.

Preparing Your Android Device

Frida operates via a server running on the target device. You need to download the appropriate Frida server binary for your device’s architecture and push it:

  1. Determine your device’s architecture:

    adb shell getprop ro.product.cpu.abi

    Common architectures include `arm64-v8a`, `armeabi-v7a`, `x86_64`, or `x86`.

  2. Download the latest Frida server from the Frida releases page. Look for `frida-server-<VERSION>-android-<ARCH>.xz`. For instance, `frida-server-16.1.4-android-arm64.xz`.

  3. Extract the binary and push it to your device:

    # Example for arm64-v8a. Replace with your downloaded version and arch.xz -d frida-server-16.1.4-android-arm64.xzadb push frida-server-16.1.4-android-arm64 /data/local/tmp/frida-server
  4. Make the server executable and run it:

    adb shell "chmod 755 /data/local/tmp/frida-server"adb shell "/data/local/tmp/frida-server &"

    The `&` detaches the process, allowing your ADB shell to remain interactive.

Dissecting Android Native Libraries

Android applications often leverage native libraries for performance-critical tasks, platform interactions, or to obscure sensitive logic. These libraries are typically loaded using `System.loadLibrary()` or `System.load()` within Java code. When an app calls `System.loadLibrary(“mylib”)`, the Android runtime searches for `libmylib.so` in specific locations. On most apps, these are found in `/data/app/<PACKAGE_NAME>-<RANDOM_STRING>/lib/<ARCH>/`.

You can locate an app’s native libraries:

adb shell pm path com.example.app # Get package install pathadb shell "find /data/app/com.example.app-*/lib/ -name "*.so""

Basic Native Hooking with Frida

Identifying Exported Functions

Exported functions are those explicitly made available for external linking. You can identify them using static analysis tools (`readelf -Ws libnative-lib.so`) or Frida’s own `frida-trace` utility for common system calls:

frida-trace -i "open" -U com.android.chrome # Traces 'open' calls in Chrome

For custom libraries, static analysis is often required. The output will typically list symbols, some marked as `GLOBAL` and `DEFAULT`, indicating exported functions.

Your First Frida Hook: Intercepting an Exported Function

Let’s assume our target native library, `libnative-lib.so`, exports a simple function `sum_numbers(int a, int b)`. We’ll use `Interceptor.attach()` to hook it.

// your_script.jsInterceptor.attach(Module.findExportByName('libnative-lib.so', 'sum_numbers'), {    onEnter: function(args) {        console.log("[+] Entering sum_numbers");        console.log("    arg0 (a): " + args[0].toInt32());        console.log("    arg1 (b): " + args[1].toInt32());        this.a = args[0].toInt32(); // Store arguments for onLeave        this.b = args[1].toInt32();    },    onLeave: function(retval) {        console.log("    Result (a+b): " + retval.toInt32());        console.log("    Original sum check: " + (this.a + this.b));        // Optional: Modify the return value        // console.log("    Modifying return value to 999");        // retval.replace(ptr(999));        console.log("[-] Exiting sum_numbers");    }});

To run this script against an application (e.g., `com.example.app`):

frida -U -l your_script.js com.example.app

The `-U` flag targets a USB-connected device, `-l` loads your script, and `com.example.app` is the target package name. You’ll see output in your console whenever `sum_numbers` is called.

Advanced Native Hooking Techniques

Hooking Unexported Functions by Address

Many critical functions within native libraries are not exported. To hook these, you need to determine their memory address. This typically involves static analysis using tools like Ghidra or IDA Pro to find the function’s offset from the library’s base address. At runtime, Frida calculates the absolute address by adding this offset to the library’s dynamically loaded base address.

Let’s assume static analysis reveals a function `private_calc_hash(char* input, int length)` at offset `0x1234` within `libnative-lib.so`.

// advanced_hook.jsvar libName = 'libnative-lib.so';var privateCalcHashOffset = 0x1234; // Determined via Ghidra/IDA Provar baseAddress = Module.findBaseAddress(libName);if (baseAddress) {    var privateCalcHashPtr = baseAddress.add(privateCalcHashOffset);    console.log("[*] Found " + libName + " base at " + baseAddress);    console.log("[*] Hooking private_calc_hash at " + privateCalcHashPtr);    Interceptor.attach(privateCalcHashPtr, {        onEnter: function(args) {            console.log("[+] Entering private_calc_hash");            console.log("    Input: " + args[0].readCString());            console.log("    Length: " + args[1].toInt32());            this.input = args[0].readCString(); // Store for onLeave        },        onLeave: function(retval) {            // Assuming the hash result is returned as a pointer to a string            // Adjust based on the actual function's return type            console.log("    Hash result: " + retval.readCString());            // You could also modify the return value here, e.g., retval.replace(Memory.allocUtf8String("MY_FAKE_HASH"));            console.log("[-] Exiting private_calc_hash (Original input: " + this.input + ")");        }    });} else {    console.log("[-] Could not find base address for " + libName);}

This script finds the library’s base address, adds the static offset to get the runtime address of `private_calc_hash`, and then attaches an interceptor. Remember that `args[0]`, `args[1]`, etc., are pointers to the arguments on the stack/registers, so you need to use methods like `readCString()` or `toInt32()` to get their values.

Reading and Manipulating Memory

Frida provides powerful `Memory` and `NativePointer` methods to interact with memory:

  • `ptr(address)`: Converts an integer or string address to a `NativePointer`.
  • `nativePointer.readByteArray(size)`: Reads bytes from the given address.
  • `nativePointer.writeByteArray(bytes)`: Writes bytes to the given address.
  • `nativePointer.readCString()` / `nativePointer.writeUtf8String(string)`: For null-terminated strings.
  • `nativePointer.toInt32()`, `toUInt32()`, `toInt64()`, etc.: For different integer types.

Example: Modifying a string argument within a hook:

// Inside onEnter for a function taking a char* argumentvar originalStringPtr = args[0];var originalString = originalStringPtr.readCString();console.log("Original string: " + originalString);// Allocate new memory or ensure the existing buffer is large enoughvar newString = "HOOKED_DATA_BY_FRIDA";if (newString.length + 1 > originalString.length) { // +1 for null terminator    // Re-allocate if new string is longer, then write new pointer    var newAlloc = Memory.allocUtf8String(newString);    args[0].replace(newAlloc); // Replace the argument pointer} else {    // If new string fits, just overwrite in place    Memory.writeUtf8String(originalStringPtr, newString);}console.log("Modified string to: " + args[0].readCString());

Crafting Native Callbacks and Overloads

Frida allows you to completely replace native functions with your own JavaScript-defined logic, acting as a custom native callback. This is incredibly powerful for bypassing checks or injecting custom behavior.

// hijack_sum.jsvar libName = 'libnative-lib.so';var targetFunctionPtr = Module.findExportByName(libName, 'sum_numbers');if (targetFunctionPtr) {    // Define a custom function that matches the signature of the target native function    var MyCustomSum = new NativeCallback(function (a, b) {        console.log("[+] MyCustomSum: Original sum_numbers call intercepted!");        console.log("    Arguments received: a=" + a + ", b=" + b);        // Perform custom logic, e.g., bypass a check or return a fixed value        var result = a + b + 1337; // Always add 1337 to the sum        console.log("    Returning custom result: " + result);        return result;    }, 'int', ['int', 'int']); // Return type and argument types    // Replace the original function with our custom implementation    Interceptor.replace(targetFunctionPtr, MyCustomSum);    console.log("[*] Replaced sum_numbers in " + libName + " with custom implementation.");} else {    console.log("[-] Could not find sum_numbers in " + libName);}

Now, any call to `sum_numbers` will execute `MyCustomSum`, giving you full control over its behavior and return value.

Real-World Use Cases

  • Bypassing Anti-Tampering/Anti-Debugging Checks: Hooking native `ptrace`, `fork`, or custom integrity checks to disable them.
  • Reverse Engineering Proprietary Algorithms: Intercepting data entering and exiting encryption/obfuscation functions to understand their mechanics or extract keys.
  • Analyzing Network Protocols: When network logic is implemented natively, you can hook send/receive functions to inspect or modify data before it hits the wire.
  • Injecting Malicious Payloads: Modifying parameters to trigger hidden functionalities or exploit vulnerabilities.

Best Practices and Troubleshooting

  • Targeting Modules: Use `Process.findModuleByName()` or `Module.findBaseAddress()` to ensure your hooks are applied after the library is loaded.
  • Error Handling: Wrap your `Interceptor.attach` or `Interceptor.replace` calls in `try…catch` blocks to gracefully handle cases where a function or module might not be found.
  • Context Awareness: Always consider the `this` context within `onEnter` and `onLeave` for passing information. `this.context` provides access to CPU registers.
  • Performance: Extensive logging or complex JavaScript logic within hooks can slow down the target application. Be mindful of performance implications for critical functions.
  • Debugging: Frida scripts can be debugged by passing `–debug` flag to the `frida` command and attaching a Chrome DevTools instance.

Conclusion

Frida empowers security researchers and reverse engineers with an unparalleled capability to interact with Android native libraries at runtime. From basic function interception to advanced memory manipulation and function replacement, mastering Frida opens up a vast array of possibilities for dynamic analysis, exploit development, and security auditing. By combining static analysis with Frida’s dynamic insights, you can effectively demystify even the most complex native code, turning challenging reverse engineering tasks into manageable and insightful investigations. Continue experimenting, exploring, and you’ll undoubtedly become a hero in the world of Android native analysis!

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