Android App Penetration Testing & Frida Hooks

Advanced JNI Patches: Implementing Inline Hooking with Frida for Android Native Code

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Advanced JNI Hooking

Android applications frequently leverage the Java Native Interface (JNI) to execute performance-critical operations, access platform-specific features, or incorporate existing C/C++ libraries. While JNI provides a powerful bridge between Java and native code, it also introduces complexities for security analysis and reverse engineering. Traditional dynamic analysis techniques often rely on hooking exported native functions, but what happens when the target function is not exported, or when you need to intercept calls deep within the native execution flow? This is where advanced techniques like inline hooking with Frida become indispensable.

This article delves into implementing inline hooking using Frida to intercept JNI and other native calls within Android applications. We will explore how to identify target functions, calculate their runtime addresses, and use Frida’s powerful `Interceptor` API to patch code execution directly, even for unexported symbols.

Understanding JNI and Native Method Calls

JNI acts as a glue layer, allowing Java code to call native functions written in languages like C/C++ and vice-versa. When a Java method is declared `native`, it signals that its implementation resides in a shared library (e.g., `.so` file). The JNI function signature typically follows a specific naming convention: `Java_PackageName_ClassName_MethodName`. For instance, a Java method `com.example.app.NativeClass.nativeAdd(int a, int b)` would map to `Java_com_example_app_NativeClass_nativeAdd` in the native library.

Native functions receive at least two parameters: a `JNIEnv` pointer and a `jobject` (for non-static methods) or `jclass` (for static methods). The `JNIEnv` pointer is crucial as it provides access to the JNI function table, allowing native code to interact with the Java Virtual Machine (JVM)—e.g., creating new Java objects, throwing exceptions, or calling Java methods from native code.

Example JNI Native Method

#include <jni.h>#include <string.h>#include <android/log.h>#define LOG_TAG "NativeLib"#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)// This is an exported JNI functionJNIEXPORT jstring JNICALL Java_com_example_app_NativeLib_stringFromJNI(JNIEnv* env, jobject thiz) {    LOGD("stringFromJNI called!");    const char* hello = "Hello from C++";    return (*env)->NewStringUTF(env, hello);}// This is an internal, unexported native functionstatic int calculateSum(int a, int b) {    LOGD("calculateSum called with a=%d, b=%d", a, b);    return a + b;}// A JNI function that calls the internal functionJNIEXPORT jint JNICALL Java_com_example_app_NativeLib_performCalculation(JNIEnv* env, jobject thiz, jint x, jint y) {    LOGD("performCalculation called with x=%d, y=%d", x, y);    return calculateSum(x, y);}

The Challenge: Hooking Unexported Functions

While Frida excels at hooking exported functions using `Module.findExportByName()`, many critical operations in native libraries are performed by internal, unexported functions. These functions are often static or hidden from the dynamic linker’s export table to deter analysis. Inline hooking directly patches the target function’s prologue in memory, redirecting execution to our custom hook code, regardless of whether the function is exported.

Implementing Inline Hooking with Frida

To perform inline hooking, we need two key pieces of information:

  1. The base address of the native library in memory.
  2. The offset of the target function from the library’s base address.

1. Finding the Library Base Address

Frida’s `Module.findBaseAddress()` function can easily retrieve the runtime base address of a loaded library.

var moduleName = 'libnative-lib.so';var baseAddress = Module.findBaseAddress(moduleName);if (baseAddress) {    console.log('[+] ' + moduleName + ' loaded at: ' + baseAddress);} else {    console.error('[-] ' + moduleName + ' not found!');    return;}

2. Finding the Function Offset

This is the reverse engineering step. You’ll need tools like IDA Pro or Ghidra to analyze the native library (`.so` file) and determine the exact offset of your target function from the library’s base. For our `calculateSum` function from the example, after disassembling `libnative-lib.so`, you might find its entry point at, for instance, `0x1234` bytes from the start of the `.text` segment. This offset will vary depending on the compiler, architecture, and code. Let’s assume for demonstration purposes, `calculateSum` is at offset `0x1337` in `libnative-lib.so`.

3. Attaching the Interceptor

Once you have the base address and the function offset, you can calculate the absolute memory address of the target function and use `Interceptor.attach()`.

var moduleName = 'libnative-lib.so';var baseAddress = Module.findBaseAddress(moduleName);if (!baseAddress) {    console.error('[-] ' + moduleName + ' not found!');    return;}// Assume calculateSum is at offset 0x1337 within libnative-lib.so// (This offset must be determined via reverse engineering tools like IDA/Ghidra)var calculateSumOffset = 0x1337; // Example offsetvar targetFunctionAddress = baseAddress.add(calculateSumOffset);console.log('[+] Target function calculateSum at: ' + targetFunctionAddress);Interceptor.attach(targetFunctionAddress, {    onEnter: function(args) {        // args[0] and args[1] will typically be the first and second arguments        // In C/C++ ARM/ARM64 calling conventions, the first few arguments are passed in registers        // For calculateSum(int a, int b), a is args[0], b is args[1]        this.a = args[0].toInt32();        this.b = args[1].toInt32();        console.log('[*] Entering calculateSum(' + this.a + ', ' + this.b + ')');    },    onLeave: function(retval) {        // retval holds the return value of the function        console.log('[*] calculateSum returned: ' + retval.toInt32() + ' (Expected: ' + (this.a + this.b) + ')');    }});console.log('[+] Hooked calculateSum successfully!');

To run this script:

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

Advanced Considerations: Hooking JNIEnv Functions

The `JNIEnv` pointer is a double pointer to a structure that contains pointers to JNI functions (the `JNINativeInterface` table). Hooking these functions requires a slightly different approach, as their addresses are relative to the `JNIEnv` table structure, not directly within `libnative-lib.so`. You can still inline hook them by finding the specific function pointer within the `JNIEnv` table at runtime.

For instance, to hook `NewStringUTF`:

  1. Get an active `JNIEnv*` pointer (e.g., from an `onEnter` hook of any JNI method).
  2. Dereference `JNIEnv*` to get `JNINativeInterface**`.
  3. Dereference again to get `JNINativeInterface*`.
  4. Locate the `NewStringUTF` entry within this structure (its index is fixed by the JNI specification).
// This is more complex and typically done in a CModule or by parsing JNIEnv struct// For simplicity, Frida often provides higher-level APIs or you can hook known JNI functions from the libjvm.so// However, if you need to inline hook a *specific* JNIEnv method implementation,// you'd need the address of that specific method in memory.var moduleName = 'libart.so'; // Or libdalvik.so depending on Android versionvar baseAddress = Module.findBaseAddress(moduleName);if (!baseAddress) {    console.error('[-] ' + moduleName + ' not found!');    return;}// Example: Hooking 'NewStringUTF' from the JNI function table// The offset of NewStringUTF within the JNIEnv table can be found in Android's source code or by reversing libart.so// For demonstration, let's assume NewStringUTF is at a certain offset (e.g., 0x480 for ARM64) from the JNIEnv table base.// This is highly architecture and Android version dependent.var NewStringUTF_offset_in_JNIEnv_table = 0x480; // Placeholder value for ARM64 typicallyvar jni_env_ptr = NULL;Interceptor.attach(Module.findExportByName('libnative-lib.so', 'Java_com_example_app_NativeLib_stringFromJNI'), {    onEnter: function(args) {        // args[0] is JNIEnv*        jni_env_ptr = args[0];        // Dereference JNIEnv* to get JNINativeInterface**        var jni_native_interface_ptr = jni_env_ptr.readPointer();        // Calculate address of NewStringUTF function pointer        var newStringUTF_fn_ptr_addr = jni_native_interface_ptr.add(NewStringUTF_offset_in_JNIEnv_table);        // Get the actual function address        var newStringUTF_fn_addr = newStringUTF_fn_ptr_addr.readPointer();        console.log('[*] JNIEnv->NewStringUTF function address: ' + newStringUTF_fn_addr);        // Now, you can hook this specific NewStringUTF implementation if it hasn't been already        if (!this.hookedNewStringUTF) {            Interceptor.attach(newStringUTF_fn_addr, {                onEnter: function(args_jni_str) {                    // args_jni_str[0] is the JNIEnv* again                    // args_jni_str[1] is the const char*                    var c_string = args_jni_str[1].readCString();                    console.log('[**] NewStringUTF called with C string: ' + c_string);                },                onLeave: function(retval_jni_str) {                    // retval_jni_str is the jstring result                    console.log('[**] NewStringUTF returning jstring.');                }            });            this.hookedNewStringUTF = true;            console.log('[+] Hooked NewStringUTF successfully!');        }    }});

Conclusion

Inline hooking with Frida provides an unparalleled level of control over native code execution within Android applications. By understanding how to identify function offsets and leverage Frida’s `Interceptor` API, reverse engineers and penetration testers can bypass traditional symbol-based hooking limitations. This technique is crucial for analyzing obfuscated code, bypassing anti-tampering mechanisms, and gaining deep insights into the most critical parts of an application’s logic that reside in native libraries. Mastering inline hooking empowers you to truly patch and analyze the heart of Android’s native runtime.

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