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.
-
Download Frida Server: Visit the official Frida releases page. Find the latest release and download the
frida-serverbinary matching your device’s architecture (e.g.,frida-server-*-android-arm64for most modern Android devices). If you’re unsure, you can find your device’s architecture usingadb shell getprop ro.product.cpu.abi. -
Push to Device: Transfer the downloaded
frida-serverbinary 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 -
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 & -
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:
-
Locate the Shared Library:
adb shell find /data/app -name "libnative-lib.so" # Or similar path for your app's package -
Pull the Library:
adb pull /path/to/libnative-lib.so . # Pull it to your current directory -
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 functionExpected output will show the address and symbol type, e.g.,
000000000000xxxx T Java_com_example_app_NativeLib_stringFromJNIor000000000000yyyy 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)inonEnterto change arguments passed to the original function. - Return Value Modification: Use
retval.replace(newValue)inonLeaveto change what the calling function receives. - Calling Original: The
Interceptor.attachcallback gives youthis.callOriginal()to invoke the original function within your hook. - Memory Operations: Functions like
Memory.readByteArray(),Memory.writeUtf8String(), andNativePointer.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 →