Introduction to Frida Gadget for Android Native Hooking
Frida is an indispensable toolkit for dynamic instrumentation, allowing security researchers and developers to inject custom scripts into running processes. While frida-server is the standard approach for remote instrumentation, it can be detected. For stealthier operations, especially when targeting native Android libraries or bypassing anti-tampering measures, Frida Gadget becomes the tool of choice. Frida Gadget is a shared library (.so) that you can embed directly into an application or inject dynamically, allowing for powerful local instrumentation without a visible frida-server instance.
This article dives deep into using Frida Gadget for intercepting native functions on Android. We’ll cover various injection techniques, writing effective Frida scripts, and best practices for advanced app penetration testing scenarios.
Understanding Frida Gadget vs. Frida Server
Before proceeding, let’s clarify the key difference:
- Frida Server: A standalone daemon running on the target device, listening for connections from a Frida client. It’s robust but detectable, as its process is usually visible.
- Frida Gadget: A shared library (e.g.,
frida-gadget.so) that loads into a target process. It operates within the process’s own memory space, making it harder to detect externally. Gadget can be configured to wait for a connection or run a script immediately.
Frida Gadget is particularly useful when:
frida-serveris blocked or detected by the target application.- You need to hook very early in the application’s lifecycle, even before Java code execution.
- You want to distribute a pre-instrumented application for specific testing scenarios.
Prerequisites
To follow this guide, you will need:
- An Android device or emulator (rooted is preferred for most methods).
- ADB (Android Debug Bridge) installed and configured.
- Frida command-line tools installed on your host machine (
pip install frida-tools). - Knowledge of basic Android application structure (APKs,
smali). - Basic understanding of C/C++ and native libraries on Android.
- Tools for APK decompilation/recompilation (e.g.,
apktool).
Method 1: Dynamic Injection using LD_PRELOAD
This method involves injecting Frida Gadget by manipulating the LD_PRELOAD environment variable. When an application starts, the dynamic linker loads libraries specified in LD_PRELOAD before any other shared libraries. This allows Frida Gadget to initialize early.
Step-by-step Injection:
- Obtain Frida Gadget: Download the correct
frida-gadget.sofor your target device’s architecture (e.g.,arm64,arm,x86) from the official Frida releases page. Rename it for simplicity, e.g., togadget.so. - Push to Device: Push the gadget library to a world-readable location on your Android device. A common location is
/data/local/tmp/.adb push /path/to/gadget.so /data/local/tmp/ - Set LD_PRELOAD and Launch App: Use
adb shellto set theLD_PRELOADenvironment variable and then launch the target application. This typically requires root privileges.adb shellsuLD_PRELOAD=/data/local/tmp/gadget.so am start -n com.example.targetapp/.MainActivityReplacecom.example.targetapp/.MainActivitywith the actual package and activity name of your target application.
Once the app launches, Frida Gadget will be loaded. By default, it will look for a script named frida-gadget.config or listen for a connection on port 27042. You can then attach your Frida client:
frida -H 127.0.0.1:27042 -f com.example.targetapp -l my_script.js --no-pause
Or, if you prefer to embed the script, create a frida-gadget.config file:
{ "interaction": { "type": "script", "path": "/data/local/tmp/my_script.js" }}
Push this config to /data/local/tmp/frida-gadget.config alongside gadget.so. This makes the gadget execute your script immediately upon loading.
Method 2: Embedding Frida Gadget by Patching the APK
For persistent and more robust injection, embedding Frida Gadget directly into the APK is often preferred. This involves decompiling the APK, adding the gadget library, and modifying the application’s code to load it.
Step-by-step APK Patching:
- Decompile the APK: Use
apktoolto decompile the target APK.apktool d target.apk -o target_app_patched - Place Gadget Library: Copy the appropriate
frida-gadget.so(e.g., forarm64-v8a) into the application’s native libraries directory. If your app has multiple architectures, place it in all relevantlib/abi/folders.cp /path/to/gadget.so target_app_patched/lib/arm64-v8a/libfrida-gadget.so - Modify Smali Code to Load Gadget: Identify a suitable place in the application’s lifecycle to load the gadget. The application’s main Application class (if it exists) or the entry point of the main activity (
onCreatemethod) are good candidates. Look for the main application class inAndroidManifest.xmlorsmali/com/example/targetapp/ApplicationClass.smali.Add a static initializer to load your gadget. Find the.method static constructor <clinit>()Vor the `onCreate` method of your main application class (or any suitable early-executing method). Insert the followingsmalicode:.method static constructor <clinit>()V .locals 0 invoke-static {"frida-gadget"}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V return-void.end methodThis ensureslibfrida-gadget.sois loaded when the class is initialized. - Recompile and Sign: Recompile the modified APK and sign it with a debug key.
apktool b target_app_patched -o target_patched.apkjava -jar sign.jar target_patched.apk(Useapksignerfrom Android SDK Build-tools or a similar tool). - Install and Test: Uninstall the original app and install the patched version.
adb uninstall com.example.targetappadb install target_patched.apk
Now, when the application starts, libfrida-gadget.so will be loaded. You can configure it with a frida-gadget.config file placed in the app’s internal storage (e.g., /data/data/com.example.targetapp/files/frida-gadget.config) or by listening for connections.
Writing Frida Scripts for Native Hooking
Once Frida Gadget is injected, you can write powerful JavaScripts to intercept native functions. The key is to correctly identify the target library and function signature.
Example: Hooking open from libc.so
Let’s say we want to monitor file access. The open system call is often exposed via libc.so.
Java.perform(function() { var libc = Module.findBaseAddress('libc.so'); if (libc) { console.log('[+] libc.so base address: ' + libc); var open_ptr = Module.findExportByName('libc.so', 'open'); if (open_ptr) { console.log('[+] Found open at: ' + open_ptr); Interceptor.attach(open_ptr, { onEnter: function(args) { // arg[0] is the path (char*) var path = args[0].readCString(); // arg[1] is flags (int) var flags = args[1].toInt32(); console.log('[-] open() called with path: "' + path + '", flags: ' + flags); }, onLeave: function(retval) { console.log('[+] open() returned: ' + retval); } }); } else { console.error('[-] Could not find open in libc.so'); } } else { console.error('[-] Could not find libc.so'); }});
Example: Hooking a Function in an Application-Specific Library
Consider an application that uses a custom native library, libnative-lib.so, with a function named Java_com_example_app_NativeLib_doSomethingNative.
Java.perform(function() { var targetLib = Module.findBaseAddress('libnative-lib.so'); if (targetLib) { console.log('[+] libnative-lib.so base address: ' + targetLib); var targetFunction = targetLib.add(0x1234); // Replace 0x1234 with the actual offset // Or, if it's an exported symbol: // var targetFunction = Module.findExportByName('libnative-lib.so', 'Java_com_example_app_NativeLib_doSomethingNative'); if (targetFunction) { console.log('[+] Found target function at: ' + targetFunction); Interceptor.attach(targetFunction, { onEnter: function(args) { console.log('[-] Hooked function called!'); // Example: Read a string argument (assuming it's the second arg, a JNI string) // var env = args[0]; // JNIEnv* // var obj = args[1]; // JNI jobject (this) // var jstring_arg = args[2]; // The jstring argument // var java_string = Java.vm.get === 'android' ? Java.api.stringFromJni(env, jstring_arg) : null; // if (java_string) console.log(' Argument: ' + java_string); }, onLeave: function(retval) { console.log('[+] Hooked function returning. Original return value: ' + retval); // Modify return value if needed // retval.replace(ptr('0x1')); } }); } else { console.error('[-] Could not find target function in libnative-lib.so'); } } else { console.error('[-] Could not find libnative-lib.so'); }});
Important Considerations:
- Offsets: If the function isn’t exported, you’ll need its offset from the library’s base address. You can find this using disassemblers like IDA Pro or Ghidra.
- Calling Conventions: Pay close attention to the ABI (Application Binary Interface) for ARM/ARM64. Arguments are passed in registers (e.g.,
x0-x7on ARM64) and then on the stack. Frida’sInterceptor.attachhandles basic argument parsing, but for complex structures or JNI functions, you’ll need to manually interpretargs. - JNI Functions: When hooking JNI functions (those starting with
Java_), remember the first two arguments are typicallyJNIEnv*andjobject(thethispointer for instance methods orjclassfor static methods).
Conclusion
Frida Gadget offers a powerful and stealthy alternative to frida-server for dynamic instrumentation on Android, especially for native code analysis and bypassing detection mechanisms. By understanding how to inject the gadget via LD_PRELOAD or by embedding it directly into an APK, and by mastering Frida’s native hooking APIs, you gain unparalleled control over application execution. This capability is crucial for advanced penetration testing, reverse engineering, and security research on Android platforms.
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 →