Android App Penetration Testing & Frida Hooks

Frida Hooking Android Native Functions: A Practical How-To Guide for Beginners

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: Unveiling Android’s Native Core with Frida

Android applications, especially those prioritizing performance or strong security, frequently leverage native code written in C/C++ compiled into shared libraries (.so files). These native functions often handle critical operations like cryptography, obfuscation, or performance-intensive tasks. For security researchers, penetration testers, and reverse engineers, understanding and manipulating these native layers is paramount. This is where Frida, a dynamic instrumentation toolkit, shines.

Frida allows you to inject scripts into running processes on various platforms, including Android. By hooking native functions, you can monitor their calls, inspect arguments, modify return values, and even entirely replace their implementations. This guide will walk beginners through setting up Frida and practically intercepting Android native functions.

Prerequisites: Gearing Up for Native Hooking

Hardware & Software Essentials

  • An Android device or emulator: Preferably rooted, as it simplifies Frida server deployment and grants necessary permissions.
  • ADB (Android Debug Bridge): For communicating with your Android device.
  • Python 3: Required for installing and running Frida tools on your workstation.
  • Frida tools: The command-line utilities for interacting with Frida server.
  • Basic understanding of C/C++: While not strictly necessary for simple hooks, it helps in understanding native function signatures.

Setting Up Your Frida Environment

Before we can start hooking, we need to get Frida up and running on both your host machine and the target Android device.

1. Install Frida Tools on Your Workstation

Open your terminal or command prompt and use pip to install the Frida tools:

pip install frida-tools

2. Deploy Frida Server to Your Android Device

Frida server is the daemon that runs on the Android device and executes your Frida scripts. You need to download the correct version for your device’s architecture.

  1. Download Frida Server: Visit the official Frida releases page. Find the latest release and download the frida-server binary matching your device’s architecture (e.g., frida-server-*-android-arm64 for most modern Android devices). If you’re unsure, you can find your device’s architecture using adb shell getprop ro.product.cpu.abi.

  2. Push to Device: Transfer the downloaded frida-server binary to your device using ADB. We’ll push it to /data/local/tmp/ as it’s typically writable.

    adb push /path/to/your/frida-server /data/local/tmp/frida-server
  3. Set Permissions and Run: Connect to your device via ADB shell, set executable permissions, and then run the server in the background.

    adb shellchmod 755 /data/local/tmp/frida-server/data/local/tmp/frida-server &
  4. Verify Installation: On your workstation, run frida-ps -U. If you see a list of processes running on your device, Frida server is working correctly.

    frida-ps -U

Identifying Native Functions for Hooking

To hook a native function, you first need to know its name and the library it resides in. For beginners, we’ll focus on functions that are explicitly exported from a shared library.

Example Scenario: A Sample Native Library

Let’s imagine an Android application uses a native library named libnative-lib.so which contains a JNI function and a custom C++ function:

#include <jni.h>#include <string>#include <android/log.h>#define LOG_TAG "NativeLib"#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)extern "C" JNIEXPORT jstring JNICALLJava_com_example_app_NativeLib_stringFromJNI(JNIEnv *env, jobject /* this */) {    std::string hello = "Hello from C++";    LOGD("stringFromJNI called");    return env->NewStringUTF(hello.c_str());}extern "C" int secret_calc(int a, int b) {    LOGD("secret_calc called with %d, %d", a, b);    return a + b * 2;}

To find the exported functions like Java_com_example_app_NativeLib_stringFromJNI or secret_calc, you’d typically extract the .so file from the APK or the installed app directory on the device. Then, use tools like nm or readelf:

  1. Locate the Shared Library:

    adb shell find /data/app -name "libnative-lib.so" # Or similar path for your app's package
  2. Pull the Library:

    adb pull /path/to/libnative-lib.so . # Pull it to your current directory
  3. Inspect Exports with nm:

    nm -D libnative-lib.so | grep Java_com_example_app_NativeLib_stringFromJNI # To find the JNI functionnm -D libnative-lib.so | grep secret_calc # To find our custom function

    Expected output will show the address and symbol type, e.g., 000000000000xxxx T Java_com_example_app_NativeLib_stringFromJNI or 000000000000yyyy T secret_calc, where ‘T’ indicates a text (code) symbol.

The Art of Native Hooking with Frida

Frida’s core for native hooking revolves around two key APIs: Module.findExportByName() to locate the function’s address and Interceptor.attach() to apply the hook.

Hooking a JNI Function: Java_com_example_app_NativeLib_stringFromJNI

Let’s create a Frida script (e.g., jni_hook.js) to intercept our sample JNI function.

Java.perform(function () {    var targetLib = "libnative-lib.so";    var targetFunction = "Java_com_example_app_NativeLib_stringFromJNI";    // Find the base address of the module    var lib_base = Module.findBaseAddress(targetLib);    if (lib_base) {        console.log("[+] Base address of " + targetLib + ": " + lib_base);        // Find the specific function by its exported name        var function_ptr = Module.findExportByName(targetLib, targetFunction);        if (function_ptr) {            console.log("[+] Found " + targetFunction + " at " + function_ptr);            // Attach an interceptor to the function pointer            Interceptor.attach(function_ptr, {                onEnter: function (args) {                    console.log("----------------------------------------");                    console.log("[*] " + targetFunction + " called!");                    // The first two arguments for JNI functions are JNIEnv* and jobject (this)                    console.log("    arg[0] (JNIEnv*): " + args[0]);                    console.log("    arg[1] (jobject this): " + args[1]);                },                onLeave: function (retval) {                    console.log("[*] " + targetFunction + " returned: " + retval);                    // If it's a jstring, you'd typically read its content using JNIEnv methods                    // For simplicity, we just show the raw return value here.                    console.log("----------------------------------------");                }            });            console.log("[+] Hooked " + targetFunction + " successfully!");        } else {            console.log("[-] Could not find " + targetFunction + " in " + targetLib);        }    } else {        console.log("[-] Could not find " + targetLib);    }});

To run this script against an app (replace com.example.app with your target app’s package name):

frida -U -f com.example.app --no-pause -l jni_hook.js

Frida will inject your script, spawn the app, and pause it. When you resume the app (e.g., %resume in the Frida console, or simply letting it run if not paused), any calls to stringFromJNI will be logged.

Hooking a Non-JNI Exported Function: secret_calc

Now let’s hook our custom secret_calc function, which takes two integers and returns an integer. We’ll also demonstrate how to read integer arguments.

Java.perform(function () {    var targetLib = "libnative-lib.so";    var targetFunction = "secret_calc";    var function_ptr = Module.findExportByName(targetLib, targetFunction);    if (function_ptr) {        console.log("[+] Found " + targetFunction + " at " + function_ptr);        Interceptor.attach(function_ptr, {            onEnter: function (args) {                console.log("----------------------------------------");                console.log("[*] " + targetFunction + " called!");                // Store arguments for onLeave, convert NativePointer to integer                this.arg1 = args[0].toInt32();                 this.arg2 = args[1].toInt32();                 console.log("    arg[0] (a): " + this.arg1);                console.log("    arg[1] (b): " + this.arg2);            },            onLeave: function (retval) {                console.log("[*] Original return value: " + retval.toInt32());                // Optional: Modify the return value                // retval.replace(ptr(999));                // console.log("[*] Modified return value to: " + retval.toInt32());                console.log("----------------------------------------");            }        });        console.log("[+] Hooked " + targetFunction + " successfully!");    } else {        console.log("[-] Could not find " + targetFunction + " in " + targetLib);    }});

Run this script similarly:

frida -U -f com.example.app --no-pause -l secret_calc_hook.js

When the app calls secret_calc(10, 5), your Frida console will show something like:

[*] secret_calc called!    arg[0] (a): 10    arg[1] (b): 5[*] Original return value: 20

Beyond Basic Hooks: Further Exploration

This guide covered the basics, but Frida offers much more for native hooking:

  • Argument Manipulation: Use args[i].replace(newValue) in onEnter to change arguments passed to the original function.
  • Return Value Modification: Use retval.replace(newValue) in onLeave to change what the calling function receives.
  • Calling Original: The Interceptor.attach callback gives you this.callOriginal() to invoke the original function within your hook.
  • Memory Operations: Functions like Memory.readByteArray(), Memory.writeUtf8String(), and NativePointer.readCString() are crucial for inspecting and manipulating complex data structures (strings, buffers, etc.) passed as arguments or returned values.
  • Calling Native Functions: Use new NativeFunction(ptr, 'returnType', ['argType1', 'argType2', ...]) to create callable wrappers for native functions you want to invoke from your script.

Conclusion

Frida is an incredibly powerful tool for dissecting and interacting with native code on Android. By following this guide, you’ve learned how to set up your environment, identify native functions within shared libraries, and implement basic hooks to intercept function calls and inspect arguments and return values. This fundamental knowledge opens the door to advanced reverse engineering, security analysis, and exploit development for Android applications. Continue experimenting with different native libraries and Frida’s extensive API to unlock its full potential.

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