Introduction to Android NDK Reversing with Frida
Android applications often leverage the Native Development Kit (NDK) to execute performance-critical code or protect intellectual property in native libraries (.so files). Reverse engineering these native components is a crucial skill for penetration testers and security researchers. While static analysis tools like Ghidra or IDA Pro provide invaluable insights, dynamic analysis with Frida offers unparalleled flexibility to inspect, modify, and intercept native code execution at runtime.
This article dives into advanced Frida techniques for Android NDK reversing, focusing on two critical aspects: hooking the JNI_OnLoad function and intercepting overloaded native functions. Mastering these techniques is fundamental for bypassing anti-tampering mechanisms, understanding obfuscated logic, and manipulating application behavior.
Understanding JNI_OnLoad: The Native Entry Point
Every Java Native Interface (JNI) native library can optionally define a function named JNI_OnLoad. This function, if present, is automatically called by the Java Virtual Machine (JVM) when the native library is loaded (e.g., via System.loadLibrary()). Its primary purpose is to perform initialization tasks, such as registering native methods, caching `jclass` references, or performing anti-tampering checks. For reverse engineers, JNI_OnLoad is a golden target because it often contains setup logic, decryption routines, or crucial anti-reversing initializations that reveal valuable information.
Example Native Code (Hypothetical)
Consider a simple native library (libnative.so) that performs some initialization in JNI_OnLoad and has a few native functions.
// native.cpp
#include
#include
#define LOG_TAG "NativeLib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) {
LOGD("Failed to get JNIEnv");
return JNI_ERR;
}
LOGD("JNI_OnLoad called! Initializing native library...");
// Perform some initialization, e.g., register native methods, check for root, etc.
// ...
return JNI_VERSION_1_6;
}
extern "C" JNIEXPORT jstring JNICALL Java_com_example_app_NativeUtils_getString(JNIEnv* env, jobject thiz) {
LOGD("getString called");
return env->NewStringUTF("Hello from Native NDK!");
}
extern "C" JNIEXPORT jint JNICALL Java_com_example_app_NativeUtils_calculateValue(JNIEnv* env, jobject thiz, jint a, jint b) {
LOGD("calculateValue(int, int) called: %d + %d", a, b);
return a + b;
}
extern "C" JNIEXPORT jdouble JNICALL Java_com_example_app_NativeUtils_calculateValue__D(JNIEnv* env, jobject thiz, jdouble x, jdouble y) {
LOGD("calculateValue(double, double) called: %.2f * %.2f", x, y);
return x * y;
}
Setting Up Your Frida Environment
Before proceeding, ensure you have:
- A rooted Android device or emulator.
- ADB (Android Debug Bridge) installed and configured.
- Frida-server running on the Android device.
- Frida-tools installed on your host machine (
pip install frida-tools).
To start frida-server on your device:
adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"
Hooking JNI_OnLoad with Frida
Since JNI_OnLoad is a standard export, hooking it is straightforward. We’ll use Module.findExportByName() to locate its address and Interceptor.attach() to hook it.
Frida Script for JNI_OnLoad
/* jni_onload_hook.js */
Java.perform(function() {
const moduleName = "libnative.so"; // Replace with your target library name
const jniOnLoadPtr = Module.findExportByName(moduleName, "JNI_OnLoad");
if (jniOnLoadPtr) {
console.log("[+] Found JNI_OnLoad at: " + jniOnLoadPtr);
Interceptor.attach(jniOnLoadPtr, {
onEnter: function(args) {
console.log("[*] JNI_OnLoad called!");
console.log(" JavaVM*: " + args[0]);
console.log(" void* reserved: " + args[1]);
// You can get the JNIEnv* from JavaVM* if needed:
// var javaVm = new JavaVM(args[0]);
// var jniEnv = Memory.readPointer(javaVm.getEnv());
// console.log(" JNIEnv*: " + jniEnv);
},
onLeave: function(retval) {
console.log("[*] JNI_OnLoad returned: " + retval);
// You can modify the return value if JNI_OnLoad performs checks
// retval.replace(ptr(JNI_VERSION_1_6));
}
});
console.log("[+] JNI_OnLoad hook deployed.");
} else {
console.log("[-] JNI_OnLoad not found in " + moduleName);
}
});
Executing the Frida Script
frida -U -f com.example.app -l jni_onload_hook.js --no-pause
This command attaches Frida to your target Android application (com.example.app), loads the script, and pauses the app until the script is fully loaded and hooks are deployed. When the app starts and loads libnative.so, you will see the `JNI_OnLoad` invocation logged in your console.
Hooking Overloaded Native Functions
In C++, functions can be overloaded, meaning multiple functions can share the same name but have different parameter lists. When a C++ compiler processes these, it generates unique
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 →