Android App Penetration Testing & Frida Hooks

Frida & Stalker: Dynamic Code Tracing and Native Execution Flow Analysis on Android

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Dynamic Native Analysis on Android

Android applications often leverage native code (C/C++) for performance-critical operations, obfuscation, or to interact with underlying system libraries. While static analysis can reveal much, understanding the runtime behavior and execution flow of this native code is paramount for comprehensive security assessments and reverse engineering. Frida, a dynamic instrumentation toolkit, combined with its powerful Stalker engine, provides an unparalleled capability to achieve this, allowing deep introspection into native execution, function calls, and even instruction-level tracing.

This article delves into using Frida and Stalker to dynamically analyze native Android applications. We will explore how to set up your environment, instrument native functions, and then leverage Stalker to trace execution flow, providing insights into an application’s hidden logic.

Setting Up Your Android Native Analysis Environment

Before diving into the code, ensure your environment is correctly configured. You’ll need:

  • A rooted Android device or emulator (Frida Server requires root).
  • adb (Android Debug Bridge) installed and configured on your host machine.
  • Frida tools installed on your host: pip install frida-tools.
  • The appropriate Frida Server for your Android device’s architecture.

Step-by-Step Setup:

  1. Download Frida Server: Visit Frida Releases and download the frida-server-*-android-ARCH corresponding to your device’s architecture (e.g., arm64, x86_64).
  2. Push to Device: Push the downloaded server to your device and make it executable:
    adb push frida-server /data/local/tmp/
    adb shell "chmod 755 /data/local/tmp/frida-server"
  3. Start Frida Server: From an adb shell with root privileges, run the server:
    su
    /data/local/tmp/frida-server &

    Verify it’s running by executing frida-ps -U on your host machine. You should see a list of processes.

Intercepting Native Functions with Frida

Frida’s Module.findExportByName() and Interceptor.attach() are fundamental for hooking native functions. Let’s consider a simple example: hooking strcmp from libc.so.

Example: Hooking strcmp

Imagine an application uses strcmp to compare a user-supplied password against a hardcoded one. We can intercept this to log the comparison arguments.

Java.perform(function() {
    var libc = Module.findExportByName("libc.so", "strcmp");
    if (libc) {
        console.log("Found strcmp at: " + libc);
        Interceptor.attach(libc, {
            onEnter: function(args) {
                // Read the arguments as C strings
                var str1 = args[0].readCString();
                var str2 = args[1].readCString();
                console.log("[strcmp] Called with: '" + str1 + "' vs '" + str2 + "'");
            },
            onLeave: function(retval) {
                // Optionally log the return value
                console.log("[strcmp] Returned: " + retval);
            }
        });
    } else {
        console.log("strcmp not found in libc.so");
    }
});

To run this script against an application (e.g., com.example.app), use:

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

This script will print the arguments and return value of every strcmp call made by the target application.

Deep Dive: Dynamic Code Tracing with Frida Stalker

While Interceptor.attach() is excellent for function-level hooks, Stalker takes dynamic analysis to the next level by allowing instruction-level tracing of a thread’s execution. Stalker works by rewriting basic blocks on-the-fly and inserting trampolines to your JavaScript code, enabling you to observe and even modify the execution path.

How Stalker Works (Simplified)

When you attach Stalker to a thread, it instruments every basic block executed by that thread. For each original basic block, Stalker creates a ‘repurposed’ block that copies the original instructions and adds a call to a user-defined callback function. This callback gives you control right before or after the original block’s execution, providing a granular view of the CPU’s activity.

Tracing a Native Function with Stalker

Let’s use Stalker to trace the execution within a specific native function, for instance, a custom JNI function like Java_com_example_app_NativeLib_decrypt.

Java.perform(function() {
    var nativeLib = Module.findBaseAddress("libnativelib.so");
    if (nativeLib) {
        console.log("libnativelib.so base address: " + nativeLib);

        // Find the specific function, e.g., by symbol name or offset
        var targetFunction = Module.findExportByName("libnativelib.so", "Java_com_example_app_NativeLib_decrypt");
        if (!targetFunction) {
             // Fallback if not exported directly, requires knowledge of offset
            // targetFunction = nativeLib.add(0x1234); // Replace 0x1234 with actual offset
            console.log("Target function not found by export, trying offset if known...");
            return;
        }

        console.log("Tracing function at: " + targetFunction);

        Interceptor.attach(targetFunction, {
            onEnter: function(args) {
                console.log("n[Stalker] Entering Java_com_example_app_NativeLib_decrypt");

                // Stalker can be attached to the current thread onEnter
                this.tid = Process.getCurrentThreadId();
                console.log("Stalking thread: " + this.tid);

                Stalker.follow(this.tid, {
                    events: {
                        call: true,     // Log calls to other functions
                        ret: false,     // Don't log returns explicitly (can be noisy)
                        exec: true,     // Log execution of basic blocks
                        block: false    // Don't log basic block entries (covered by exec)
                    },
                    onReceive: function(events) {
                        // The 'events' buffer contains raw Stalker events
                        // You'll need to parse them. For simplicity, we'll log summary
                        // In a real scenario, you'd use Stalker.parse() here.
                        // Example: Stalker.parse(events).forEach(function(event) { console.log(JSON.stringify(event)); });
                        console.log("Stalker events received (length: " + events.byteLength + ")");
                    },
                    onCallSummary: function(summary) {
                        // summary: { 'callee_address': count, ... }
                        for (var address in summary) {
                            var symbol = DebugSymbol.fromAddress(ptr(address));
                            console.log("  [Call Summary] " + symbol + " called " + summary[address] + " times");
                        }
                    }
                    // onBlock: function(block) { console.log("Block: " + block.base); }, // More granular block tracing
                    // onEvent: function(event) { console.log(JSON.stringify(event)); }
                });
            },
            onLeave: function(retval) {
                console.log("[Stalker] Leaving Java_com_example_app_NativeLib_decrypt");
                // Stop stalking when the function returns
                Stalker.unfollow(this.tid);
            }
        });
    } else {
        console.log("libnativelib.so not loaded.");
    }
});

In this advanced example:

  • We first locate the target native library and the specific function within it.
  • Interceptor.attach() is used to hook the entry and exit points of our target function.
  • Inside onEnter, we obtain the current thread ID and instruct Stalker to follow it.
  • We configure Stalker to emit call and exec events.
  • onReceive is a low-level callback that gets raw event buffers. For practical logging, onCallSummary is often more convenient, providing a summary of functions called within the traced thread.
  • Crucially, Stalker.unfollow() is called in onLeave to stop tracing the thread once the function completes, preventing unnecessary overhead.

When running this script, you will observe detailed logs including which functions are called from within Java_com_example_app_NativeLib_decrypt and execution block details, offering a fine-grained understanding of its internal workings.

Practical Use Cases and Best Practices

Use Cases:

  • Obfuscation Bypass: Trace obfuscated native functions to understand their true logic.
  • Anti-Tampering Evasion: Identify and analyze anti-tampering checks implemented in native code.
  • Vulnerability Research: Pinpoint critical code paths and data manipulation in native libraries.
  • Reverse Engineering Algorithms: Understand cryptographic routines or custom algorithms implemented natively.

Best Practices:

  • Targeted Tracing: Use Stalker only on specific threads and for limited durations. Tracing too broadly can generate an overwhelming amount of data and significantly slow down the application.
  • Event Filtering: Carefully select the events you want Stalker to capture (call, ret, exec, block). Start with fewer events and add more as needed.
  • Data Processing: For large-scale tracing, consider buffering events in onReceive and processing them in batches or offloading to your host for analysis.
  • Error Handling: Always include checks for null values when searching for modules or functions.

Conclusion

Frida and its Stalker engine provide an incredibly powerful combination for dynamic analysis of native Android applications. From simple function hooks to instruction-level execution tracing, these tools empower security researchers and reverse engineers to peel back the layers of native code and gain deep insights into an application’s runtime behavior. Mastering Stalker opens doors to understanding even the most complex and obfuscated native implementations, making it an indispensable tool in the Android penetration testing arsenal.

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