Android App Penetration Testing & Frida Hooks

Android NDK Reverse Engineering Lab: Deep Dive into Native Function Exploitation with Frida

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Android NDK and Native Code

The Android Native Development Kit (NDK) allows developers to implement parts of their applications using native-code languages such as C and C++. This is often done for performance-critical components, reusing existing native libraries, or for obfuscation and security-sensitive operations. While Java/Kotlin code is relatively straightforward to reverse engineer using decompilers, native code presents a greater challenge, requiring skills in assembly, debugging, and dynamic instrumentation.

This article will guide you through setting up a lab environment and using Frida, a powerful dynamic instrumentation toolkit, to intercept and manipulate native functions within Android applications. We’ll explore techniques to identify native functions and build Frida scripts to hook into them, examine arguments, and even alter return values, effectively bypassing native checks.

Setting Up Your Reverse Engineering Lab

Prerequisites

  • A rooted Android device or an emulator (e.g., Android Studio Emulator, Genymotion)
  • ADB (Android Debug Bridge) installed and configured on your host machine
  • Python 3 installed on your host machine
  • Frida client (pip install frida-tools)
  • Ghidra or IDA Pro (optional, for deeper static analysis)

Frida Server Setup on Device

First, download the appropriate Frida server binary for your Android device’s architecture (e.g., frida-server-*-android-arm64) from the official Frida releases page on GitHub. Push it to your device and make it executable:

adb push frida-server-*-android-arm64 /data/local/tmp/frida-serveradb shell chmod +x /data/local/tmp/frida-serveradb shell /data/local/tmp/frida-server &

Verify Frida server is running by executing frida-ps -U on your host. It should list processes from your device.

Identifying Native Functions for Hooking

Before we can hook a native function, we need to know its name and the library it resides in. Native libraries are typically .so files found within the application’s lib/ directory (e.g., /data/app/com.example.appname-XYZ/lib/arm64/libmylib.so).

Using nm for Symbol Listing

The nm utility can list symbols from object files. If the library is not stripped, this is the easiest way to find function names. You can extract the .so file from an APK or pull it directly from the device:

adb pull /data/app/com.example.appname-XYZ/lib/arm64/libmylib.so .nm -D libmylib.so | grep Java_

The -D flag shows dynamic symbols. We often look for functions prefixed with Java_, which are JNI (Java Native Interface) functions called directly from Java code.

Static Analysis with Disassemblers (Ghidra/IDA)

For stripped libraries, where `nm` yields little information, disassemblers like Ghidra or IDA Pro become indispensable. Load the .so file into these tools. They perform static analysis, identifying functions, their arguments, and control flow. You’ll need to understand ARM/ARM64 assembly to navigate the code and deduce the purpose of functions, even if they lack symbolic names.

Focus on JNI export functions like JNI_OnLoad, which is called when the native library is loaded, and other functions referenced by the Java side of the application.

Deep Dive into Frida Native Hooking

Frida allows us to dynamically attach to a running process and inject JavaScript code to manipulate its execution flow. For native functions, we primarily use Module.findExportByName or Module.findBaseAddress combined with Interceptor.attach.

Basic Native Function Hooking

Let’s consider a native library with a simple function that takes an integer and returns a boolean, say Java_com_example_app_NativeCheck_doCheck(JNIEnv* env, jobject thiz, jint value).

First, we locate the base address of the native library and then the offset of our target function.

// libnativecheck.cpp#include <jni.h>#include <string>#include <android/log.h>#define LOG_TAG "NativeCheck"#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)extern "C"JNIEXPORT jboolean JNICALLJava_com_example_app_NativeCheck_doCheck(JNIEnv* env, jobject /* this */, jint value) {    LOGI("NativeCheck: doCheck called with value: %d", value);    if (value == 42) {        return JNI_TRUE;    }    return JNI_FALSE;}

Now, let’s create a Frida script (hook_native.js) to hook this function:

console.log("Frida script loaded!");Interceptor.attach(Module.findExportByName("libnativecheck.so", "Java_com_example_app_NativeCheck_doCheck"), {    onEnter: function (args) {        // args[0] is JNIEnv*, args[1] is jobject (this)        // args[2] is the actual 'value' argument (jint)        this.value = args[2].toInt32(); // Read the integer argument        console.log("[+] Entered Java_com_example_app_NativeCheck_doCheck");        console.log("    Argument 'value': " + this.value);        // Modify argument if needed (e.g., args[2].replace(ptr(100)));    },    onLeave: function (retval) {        console.log("[+] Leaving Java_com_example_app_NativeCheck_doCheck");        console.log("    Original return value: " + retval.toInt32());        // Bypass: force return true (JNI_TRUE = 1)        retval.replace(ptr(1));        console.log("    Modified return value: " + retval.toInt32());    }});console.log("Hook installed for Java_com_example_app_NativeCheck_doCheck.");

Executing the Frida Script

Assuming your target application’s package name is com.example.app:

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

The -f flag spawns the process, --no-pause ensures it runs immediately, and -l loads our script. Once the doCheck function is called in the application, you’ll see the Frida logs in your terminal, demonstrating the argument inspection and return value modification.

Understanding Arguments and Return Values

When hooking native functions, understanding how arguments are passed and returned is crucial:

  • onEnter(args): The args array contains pointers to the function’s arguments.
  • For JNI functions, args[0] is typically JNIEnv* and args[1] is the jobject (the this reference). Subsequent indices correspond to the Java arguments.
  • To read primitive types (like jint, jboolean), use .toInt32(), .toBoolean(), etc., on the NativePointer.
  • For complex types (strings, arrays, objects), you’ll need to dereference pointers and interact with the JNIEnv functions. For example, to read a jstring, you’d call env->GetStringUTFChars. In Frida, this translates to something like new NativeFunction(args[0].readPointer().add(OFFSET_TO_GETSTRINGUTFCHARS), 'pointer', ['pointer', 'pointer', 'pointer'])(args[0], string_jobject, 0) (where OFFSET_TO_GETSTRINGUTFCHARS needs to be determined by analyzing JNIEnv struct).
  • onLeave(retval): The retval object allows you to inspect and modify the function’s return value. Use retval.replace(ptr(NEW_VALUE)) to change it.

Advanced Native Exploitation Concepts

Hooking Non-Exported Functions

Many critical native functions are not exported with easily identifiable names. In such cases, static analysis (Ghidra/IDA) to find the function’s address relative to the library’s base is essential. Once you have the offset, you can calculate the absolute address:

var baseAddr = Module.findBaseAddress('libmylib.so');var targetFunctionAddr = baseAddr.add(OFFSET_FROM_GHIDRA);Interceptor.attach(targetFunctionAddr, {    onEnter: function (args) {        console.log("Hooked non-exported function!");    },    onLeave: function (retval) {}});

Inline Hooking (Stalker API)

For more granular control, such as tracing code execution or modifying registers mid-function, Frida’s Stalker API can be used. Stalker allows you to follow threads, observe instructions, and even rewrite code blocks on the fly, offering advanced capabilities for runtime code manipulation and analysis.

Conclusion

Mastering Android NDK reverse engineering with Frida is a crucial skill for security researchers and penetration testers. By understanding how native libraries are structured, identifying key functions through static and dynamic analysis, and leveraging Frida’s powerful instrumentation capabilities, you can effectively bypass security controls, uncover vulnerabilities, and gain deeper insights into application behavior. This lab provides a foundation for more complex native exploitation scenarios, paving the way for advanced mobile security assessments.

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