Android App Penetration Testing & Frida Hooks

Building Custom Frida Stalker Scripts: A Guide to Advanced Android Hooking & Tracing

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Advanced Android Tracing with Frida Stalker

Frida is an indispensable toolkit for dynamic instrumentation, widely adopted in mobile application penetration testing and reverse engineering. While basic Frida hooks excel at intercepting function calls and modifying arguments or return values, they often fall short when comprehensive, instruction-level code tracing is required. This is where Frida Stalker comes into play.

Frida Stalker is a powerful API within the Frida ecosystem designed for precise, instruction-level code tracing and modification on a targeted thread. Instead of merely hooking function entry/exit points, Stalker enables you to observe and manipulate every executed instruction within a specified code path, offering unparalleled insights into application behavior, especially for obfuscated or JIT-compiled code.

Why Frida Stalker?

  • Instruction-level Visibility: Gain insights into the exact sequence of CPU instructions executed.
  • Dynamic Code Tracing: Trace code paths dynamically, even through complex branches and loops.
  • Bypass Anti-Tampering: Observe and potentially alter self-modifying or anti-analysis code in real-time.
  • Detailed Control Flow Analysis: Understand how data flows and functions are called internally within a method.

Setting Up Your Environment

Before diving into Stalker scripts, ensure you have the necessary environment configured:

  1. Rooted Android Device or Emulator: Stalker works best with root access for full control and less permission hassle.
  2. ADB (Android Debug Bridge): For pushing Frida server and interacting with the device.
  3. Frida CLI Tools: Install `frida-tools` via pip: `pip install frida-tools`.
  4. Frida Server: Download the appropriate `frida-server` for your device’s architecture (e.g., `frida-server-16.1.4-android-arm64`) from Frida’s GitHub releases. Push it to your device and run it.
# Push frida-server to device
adb push frida-server-16.1.4-android-arm64 /data/local/tmp/

# Set permissions and run
adb shell "chmod 755 /data/local/tmp/frida-server-16.1.4-android-arm64"
adb shell "/data/local/tmp/frida-server-16.1.4-android-arm64 &"

# Forward port for communication
adb forward tcp:27042 tcp:27042

Understanding Frida Stalker Core Concepts

Frida Stalker operates by redirecting a targeted thread’s execution to a temporary code buffer where a copy of the original instructions is placed. Before executing each instruction in this buffer, Stalker can inject custom code (a ‘probe’) to perform tracing, logging, or modification. This process is highly granular.

Key Stalker APIs:

  • Stalker.follow(threadId, callbacks): Starts tracing a specific thread.
  • Stalker.unfollow(): Stops tracing the current thread.
  • Stalker.exclude(range): Excludes a memory range from being traced.
  • Stalker.trust(range): Specifies a memory range to trust (e.g., system libraries not to be instrumented).

Stalker Callbacks (Event Handlers):

When you call Stalker.follow(), you provide an object containing callback functions that Stalker will invoke for various events:

  • onReceive(events): Called with an array of events that occurred.
  • onCallSummary(summary): Provides a summary of calls, useful for performance.
  • onCall(call): Invoked when a call instruction is encountered.
  • onRet(ret): Invoked when a return instruction is encountered.
  • onExec(exec): Invoked for every instruction executed.

Each event object (call, ret, exec) contains information like `address`, `target`, `depth`, etc.

Practical Example 1: Tracing All Instructions in a Function

Let’s trace all instructions within a specific Java method’s native implementation. We’ll assume we’ve identified the native function’s address (e.g., using `Module.findExportByName` or by dumping JNI exports).

Java.perform(function () {
    var targetClass = Java.use('com.example.app.SecretUtils');
    var targetMethod = targetClass.checkSignature.overload('java.lang.String');

    targetMethod.implementation = function (arg) {
        console.log("[*] Hooking SecretUtils.checkSignature(" + arg + ")");

        // Get the native address of the method (this is an example, actual finding may vary)
        // For simplicity, let's assume we know a native export or can find it via DebugSymbol
        // You might need to use `DebugSymbol.fromAddress` if you're targeting an arbitrary address
        // Or `Module.findExportByName` for direct exports.
        var targetNativeAddress = Module.findExportByName('libsecret.so', 'Java_com_example_app_SecretUtils_checkSignature');
        if (!targetNativeAddress) {
            console.error("[-] Could not find native function.");
            return this.checkSignature(arg);
        }

        console.log("[*] Starting Stalker on thread " + Process.getCurrentThreadId() + " for native address: " + targetNativeAddress);

        Stalker.follow(Process.getCurrentThreadId(), {
            events: {
                call: true,  // Trace call instructions
                ret: true,   // Trace return instructions
                exec: true   // Trace every instruction
            },
            onReceive: function (events) {
                var instructionList = Stalker.parse(events);
                instructionList.forEach(function (instruction) {
                    // Filter to relevant address range if needed
                    // For this example, we trace everything in the called path
                    console.log("  Trace: " + instruction.address + " " + instruction.mnemonic + " " + instruction.opStr);
                });
            }
        });

        // Call the original method, Stalker will trace its execution
        var result = this.checkSignature(arg);

        Stalker.unfollow();
        console.log("[*] Stalker stopped. Original method returned: " + result);
        return result;
    };
});

In this script, we hook a Java method, then start Stalker on the current thread. When the original method is called, Stalker will trace its native execution path. The `onReceive` callback processes batches of events, and we parse them to log each instruction’s address, mnemonic, and operands.

Practical Example 2: Filtering and Transforming Instructions

Sometimes you only care about specific types of instructions or want to modify them. Stalker’s `transform` callback allows you to rewrite instruction blocks on the fly.

Java.perform(function () {
    var targetClass = Java.use('com.example.app.Authenticator');
    var targetMethod = targetClass.verifyPin.overload('java.lang.String');

    targetMethod.implementation = function (pin) {
        console.log("[*] Hooking Authenticator.verifyPin(" + pin + ")");

        var targetNativeAddress = Module.findExportByName('libauth.so', 'Java_com_example_app_Authenticator_verifyPin');
        if (!targetNativeAddress) {
            console.error("[-] Could not find native function.");
            return this.verifyPin(pin);
        }

        Stalker.follow(Process.getCurrentThreadId(), {
            transform: function (iterator) {
                // This function is called for each basic block
                var instruction = iterator.next();
                while (instruction !== null) {
                    // Example: Log only specific instructions (e.g., branches)
                    if (instruction.mnemonic.startsWith('b') || instruction.mnemonic.startsWith('bl')) {
                        console.log("  [Branch] " + instruction.address + ": " + instruction.mnemonic + " " + instruction.opStr);
                    }

                    // Example: Nop out specific instructions (e.g., a security check)
                    // WARNING: Use with caution, can crash the app if not done carefully.
                    // For ARM64, 'nop' is '0xd503201f'.
                    // For ARM32, 'nop' is '0xe320f000'.
                    if (instruction.address.equals(ptr(targetNativeAddress).add(0x1337))) { // Replace 0x1337 with target offset
                        console.log("  [NOPing] Instruction at " + instruction.address);
                        iterator.putBytes(ptr('0xd503201f').readByteArray(4)); // ARM64 NOP
                    } else {
                        iterator.putAndAlign(instruction);
                    }
                    instruction = iterator.next();
                }
            }
        });

        var result = this.verifyPin(pin);
        Stalker.unfollow();
        console.log("[*] Stalker stopped. Original method returned: " + result);
        return result;
    };
});

The `transform` callback provides an `iterator` that allows you to examine and rewrite basic blocks. Here, we demonstrate logging branch instructions and conditionally ‘NOPing’ (No Operation) a specific instruction address. This is incredibly powerful for bypassing checks or understanding conditional logic.

Advanced Considerations

  • Performance: Stalker is highly granular and can introduce significant overhead. Use it judiciously and focus on critical code paths.
  • Anti-Frida: Some applications employ anti-Frida measures. Stalker itself can sometimes bypass basic checks by operating at a lower level, but advanced anti-tampering might detect its presence.
  • JIT-compiled Code: Stalker is particularly effective for JIT-compiled code (like Android’s ART runtime) where instruction addresses can change or are generated on the fly.
  • Thread Management: Always ensure you `unfollow()` a thread when done to release resources and avoid crashes.

Conclusion

Frida Stalker is a sophisticated and powerful tool for deep dives into application execution on Android. By providing instruction-level visibility and modification capabilities, it extends Frida’s utility beyond traditional API hooking, enabling expert-level reverse engineering, exploit development, and penetration testing. Mastering Stalker empowers you to dissect complex native code, bypass intricate protections, and gain an unparalleled understanding of how applications truly behave at the machine code level.

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