Android App Penetration Testing & Frida Hooks

Frida Hooking ARM64: Troubleshooting Common Issues in Android Native Reverse Engineering

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction

Frida has revolutionized mobile application security analysis, offering unparalleled capabilities for dynamic instrumentation. When it comes to Android native reverse engineering, particularly on ARM64 architectures, Frida allows us to intercept, modify, and observe the execution of compiled C/C++ code. However, the path to successful ARM64 hooking is often fraught with subtle challenges, ranging from incorrect address identification to nuanced handling of calling conventions. This article delves into the most common pitfalls encountered when using Frida to hook native ARM64 functions on Android and provides practical troubleshooting strategies to overcome them.

Understanding these issues is crucial for anyone performing penetration testing, vulnerability research, or malware analysis on Android applications that heavily rely on native libraries.

Prerequisites and Setup

Before diving into troubleshooting, ensure you have the basic setup:

  • A rooted Android device or emulator (ARM64 architecture).
  • Frida server running on the device.
  • Frida tools (frida, frida-ps, frida-trace) installed on your host machine.
  • Basic familiarity with ARM64 assembly concepts and C/C++ calling conventions.
  • Tools like IDA Pro or Ghidra for static analysis.
# On your host machine: Pull Frida server for ARM64 to your device. curl -LO https://github.com/frida/frida/releases/download/16.1.4/frida-server-16.1.4-android-arm64 adb push frida-server-16.1.4-android-arm64 /data/local/tmp/frida-server adb shell "chmod 755 /data/local/tmp/frida-server" # On your device (via adb shell): adb shell "/data/local/tmp/frida-server &" # On your host machine: Test connection frida-ps -U

Common Issue 1: Target Function Not Found or Incorrect Address

Problem Description

One of the most frequent hurdles is simply failing to locate the target function or using an incorrect memory address, leading to errors like "Unable to resolve native address" or the hook never triggering.

Troubleshooting Steps

  1. Static Analysis (IDA Pro/Ghidra): Start by statically analyzing the target .so library. Look for exported functions or interesting internal functions. Note their offsets from the library’s base address.
  2. nm/readelf: For simpler cases, command-line tools can list exported symbols.
  3. # On your device (if nm/readelf is available) or after pulling the .so adb pull /data/app/~~.../com.example.app-XYZ/lib/arm64/libnative.so . nm -D libnative.so | grep "my_target_function" # Or use readelf readelf -s libnative.so | grep "my_target_function"
  4. Frida’s Module.findExportByName(): This is the easiest way for exported functions.
  5. Java.perform(function() { const libnative = Module.findBaseAddress('libnative.so'); if (libnative) { console.log('libnative.so loaded at: ' + libnative); const targetFunction = Module.findExportByName('libnative.so', 'my_target_function'); if (targetFunction) { console.log('Found my_target_function at: ' + targetFunction); // Attach Interceptor.attach(targetFunction, { onEnter: function(args) { console.log('my_target_function called!'); } }); } else { console.error('my_target_function not found!'); } } else { console.error('libnative.so not found!'); } });
  6. Manual Address Calculation: If a function isn’t exported, you’ll need its offset from static analysis.
  7. Java.perform(function() { const libnative = Module.findBaseAddress('libnative.so'); if (libnative) { console.log('libnative.so loaded at: ' + libnative); const functionOffset = new NativePointer('0x123456'); // Replace with actual offset from IDA/Ghidra const targetFunction = libnative.add(functionOffset); console.log('Calculated targetFunction at: ' + targetFunction); Interceptor.attach(targetFunction, { onEnter: function(args) { console.log('my_internal_function called!'); } }); } else { console.error('libnative.so not found!'); } });

Common Issue 2: Incorrect Argument Handling (ARM64 Specifics)

Problem Description

You’ve successfully attached a hook, but the arguments you read are garbage, or the application crashes when your hook attempts to process them. This often stems from a misunderstanding of ARM64 calling conventions and Frida’s data types.

Troubleshooting Steps

  1. ARM64 Calling Convention:
    The first eight arguments (x0x7) are passed in registers. Subsequent arguments are pushed onto the stack. The return value is typically in x0.
  2. Frida Data Types:
    Always use the correct Frida data type when reading or writing arguments:
    • args[0], args[1], … for arguments. These are NativePointer by default.
    • Use .toInt32(), .readCString(), .readPointer(), .readByteArray() as needed.
// Example: Hooking a function int encrypt(unsigned char* data, size_t dataLen, unsigned char* key, size_t keyLen); Java.perform(function() { const libnative = Module.findBaseAddress('libnative.so'); if (libnative) { const targetFunction = libnative.add(new NativePointer('0x123456')); // Assuming internal function Interceptor.attach(targetFunction, { onEnter: function(args) { console.log('encrypt called!'); console.log('  data (x0): ' + args[0] + ' -> ' + args[0].readByteArray(16)); // Read first 16 bytes console.log('  dataLen (x1): ' + args[1].toInt32()); console.log('  key (x2): ' + args[2] + ' -> ' + args[2].readByteArray(16)); console.log('  keyLen (x3): ' + args[3].toInt32()); // If more than 8 arguments, they are on the stack. // You would access them via this.context.sp.add(offset) } , onLeave: function(retval) { console.log('encrypt returned: ' + retval.toInt32()); } }); } });

Advanced Argument Handling

  • Structures/Objects: If an argument is a pointer to a struct, you’ll need to dereference it and read its members manually.
  • Larger Integers: For 64-bit integers (long long in C), .toUInt64() or .toSInt64() might be necessary. Frida’s NativePointer automatically handles 64-bit addresses, but for values, explicit conversion is key.

Common Issue 3: Hook Not Triggering / Process Crashing

Problem Description

Your script injects, but the expected output from your hook never appears, or the application crashes immediately upon launching with your script injected.

Troubleshooting Steps

  1. Timing of Hook Attachment:
    Native functions can be called very early in the application lifecycle, sometimes even before Java.perform() can execute. For early hooks (e.g., within JNI_OnLoad), consider attaching directly to the process or using techniques to ensure your script loads as early as possible.
  2. // To hook JNI_OnLoad, you can use: Interceptor.attach(Module.findExportByName(null, 'JNI_OnLoad'), { onEnter: function(args) { console.log('JNI_OnLoad called!'); // Your other hooks can be placed here to ensure they are active // when native libraries are initialized. } });
  3. Memory Permissions & Writes:
    While Frida generally handles memory protections for hooking, if you’re manually patching code or writing to specific memory regions, ensure you have appropriate permissions (Memory.patchCode(), Memory.protect()). Incorrect memory writes can lead to immediate crashes.
  4. Stack Corruption:
    If your onEnter or onLeave handlers perform complex register manipulation (e.g., changing this.context.sp or other argument registers), ensure you restore the stack and registers to a consistent state. An unbalanced stack will almost certainly lead to a crash.
  5. Exception Handling:
    Wrap your hook logic in try-catch blocks to gracefully handle unexpected errors within your JavaScript code, which might otherwise crash the Frida agent and the target application.
Interceptor.attach(targetFunction, { onEnter: function(args) { try { // Your potentially error-prone logic here console.log('Hook triggered!'); } catch (e) { console.error('Error in onEnter hook: ' + e); } }, onLeave: function(retval) { try { // Your potentially error-prone logic here } catch (e) { console.error('Error in onLeave hook: ' + e); } } });

Common Issue 4: Debugging Frida Scripts

Problem Description

Frida script errors can sometimes be cryptic, making it hard to pinpoint the exact cause of an issue within your complex JavaScript logic.

Troubleshooting Steps

  1. Extensive console.log():
    This is your best friend. Log values of arguments, return values, memory addresses, and intermediate results.
  2. Thread.backtrace():
    Use Thread.backtrace(this.context, Backtracer.ACCURATE) within your hook to get a stack trace, which can reveal where the function was called from and provide context.
  3. Interceptor.attach(targetFunction, { onEnter: function(args) { console.log('Called from:'); Thread.backtrace(this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress) .forEach(function(s) { console.log('  ' + s); }); } });
  4. frida-trace:
    For quick reconnaissance, frida-trace can trace function calls without writing a full script. It can help confirm if a function is being called as expected.
  5. frida-trace -U -i "libnative.so!my_target_function" com.example.app
  6. Inspect Registers:
    The this.context object in onEnter and onLeave handlers provides access to all ARM64 registers (e.g., this.context.x0, this.context.sp). Inspect them directly if you suspect register-related issues.

Conclusion

Frida is an incredibly powerful tool for native Android reverse engineering on ARM64. While the initial learning curve and troubleshooting can be challenging, a methodical approach to identifying target functions, understanding ARM64 calling conventions, ensuring proper hook timing, and robust debugging practices will significantly improve your success rate. By mastering these troubleshooting techniques, you can effectively bypass common obstacles and unlock deeper insights into the native behavior of Android applications.

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