Android App Penetration Testing & Frida Hooks

Frida Cheatsheet: Hooking Any Android Java Method Like a Pro for App Pentesting

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Frida for Android Java Hooking

Frida is an indispensable dynamic instrumentation toolkit for reverse engineers and penetration testers, especially when analyzing Android applications. It allows you to inject snippets of JavaScript or your own library into native apps on various platforms, enabling you to inspect and modify their runtime behavior. For Android app penetration testing, one of Frida’s most powerful features is its ability to hook into Java methods, allowing you to observe arguments, modify return values, or even completely replace method implementations.

This cheatsheet will guide you through the process of effectively hooking Android Java methods using Frida, from basic method interception to handling overloaded methods and dynamic class enumeration.

Prerequisites and Setup

Before diving into hooking, ensure you have a rooted Android device or an emulator with the Frida server running. You’ll also need the Frida client installed on your host machine.

  • On Android Device/Emulator: Download the appropriate Frida server for your device’s architecture (e.g., frida-server-16.x.x-android-arm64), push it to /data/local/tmp/, make it executable, and run it.
adb push frida-server /data/local/tmp/frida-server
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"
  • On Host Machine: Install Frida Python bindings.
pip install frida-tools

To attach to an application, you’ll typically use frida -U -f com.your.package.name -l script.js --no-pause. The -f flag spawns and attaches, -U specifies a USB device, and -l loads your JavaScript hook script.

The Basics: Hooking a Specific Known Java Method

The simplest scenario is hooking a Java method where you know the exact class and method name. This often involves methods like onCreate, onClick, or specific utility functions identified during static analysis.

Here’s a basic script to hook the onCreate method of an app’s MainActivity:

Java.perform(function() {
  // Target the specific class
  var MainActivity = Java.use('com.example.myapp.MainActivity');

  // Hook the onCreate method
  MainActivity.onCreate.implementation = function(savedInstanceState) {
    console.log('MainActivity.onCreate called!');
    // Call the original implementation
    this.onCreate(savedInstanceState);
  };
  console.log('Hooked MainActivity.onCreate!');
});

In this example:

  • Java.perform(function() { ... }); ensures our code runs within the Java VM’s context.
  • Java.use('com.example.myapp.MainActivity'); gets a wrapper for the target Java class.
  • .implementation = function(...) { ... }; replaces the original method’s code with our custom logic.
  • this.onCreate(savedInstanceState); is crucial to call the original method, ensuring the app functions correctly unless you intend to completely bypass it.

Handling Overloaded Java Methods

Java allows methods to have the same name but different parameter types (method overloading). When hooking overloaded methods, Frida requires you to specify the exact signature of the overload you want to target.

Consider a class with two doSomething methods:

public class MyService {
    public void doSomething(String data) { /* ... */ }
    public void doSomething(String data, int type) { /* ... */ }
}

To hook the specific overload, use .overload() with the full type signature:

Java.perform(function() {
  var MyService = Java.use('com.example.myapp.MyService');

  // Hook the first overload: doSomething(String)
  MyService.doSomething.overload('java.lang.String').implementation = function(data) {
    console.log('MyService.doSomething(String) called with:', data);
    this.doSomething(data);
  };

  // Hook the second overload: doSomething(String, int)
  MyService.doSomething.overload('java.lang.String', 'int').implementation = function(data, type) {
    console.log('MyService.doSomething(String, int) called with:', data, type);
    this.doSomething(data, type);
  };
  console.log('Hooked overloaded MyService.doSomething methods!');
});

Tip: If you’re unsure of the exact overload signature, you can often find it using decompilers like Jadx or Ghidra, or by dynamically enumerating methods (as shown next).

Enumerating Methods of a Class

Sometimes you need to inspect a class without knowing all its methods beforehand. Frida allows you to enumerate methods dynamically.

You can list all declared methods of a class using Frida’s internal representation:

Java.perform(function() {
  var TargetClass = Java.use('com.example.myapp.SomeUtilityClass');

  // Iterate over all own methods (declared directly in the class)
  console.log('Methods declared in SomeUtilityClass:');
  TargetClass.$ownMethods.forEach(function(methodName) {
    console.log('  - ' + methodName);
  });

  // To get all methods including inherited ones, use $methods
  // console.log('nAll methods (including inherited) in SomeUtilityClass:');
  // TargetClass.$methods.forEach(function(methodName) {
  //   console.log('  - ' + methodName);
  // });
});

This script will print a list of all method names. For more detailed information, including signatures for overloaded methods, you can iterate through the method’s `overloads` array:

Java.perform(function() {
  var TargetClass = Java.use('com.example.myapp.SomeUtilityClass');

  console.log('Detailed methods of SomeUtilityClass:');
  TargetClass.$ownMethods.forEach(function(methodName) {
    // Check if the method has overloads
    if (TargetClass[methodName].overloads && TargetClass[methodName].overloads.length > 0) {
      TargetClass[methodName].overloads.forEach(function(overload) {
        console.log(`  - ${methodName}(${overload.argumentTypes.map(arg => arg.className).join(', ')})`);
      });
    } else {
      // For methods without overloads, simply log the name
      console.log(`  - ${methodName}()`);
    }
  });
});

Dynamic Hooking: All Methods in a Class

A common scenario in penetration testing is wanting to hook *all* methods of a specific class to observe its full behavior or identify interesting entry points. This can be achieved by combining method enumeration with a looping hook strategy.

Java.perform(function() {
  var TargetClass = Java.use('com.example.myapp.SensitiveDataProcessor');

  console.log('Hooking all methods of SensitiveDataProcessor...');

  TargetClass.$ownMethods.forEach(function(methodName) {
    // Skip constructors or methods that aren't callable in the usual way
    if (methodName.indexOf('$') !== -1 || methodName === 'wait' || methodName === 'notify' || methodName === 'notifyAll') {
      return;
    }

    // Handle overloads
    if (TargetClass[methodName].overloads && TargetClass[methodName].overloads.length > 0) {
      TargetClass[methodName].overloads.forEach(function(overload) {
        overload.implementation = function() {
          var args = Array.prototype.slice.call(arguments);
          console.log(`[+] Called ${methodName}(${args.map(arg => JSON.stringify(arg)).join(', ')})`);
          var retval = this[methodName].apply(this, args);
          console.log(`[+] Returned from ${methodName}: ${JSON.stringify(retval)}`);
          return retval;
        };
      });
    } else {
      // For methods without overloads
      TargetClass[methodName].implementation = function() {
        var args = Array.prototype.slice.call(arguments);
        console.log(`[+] Called ${methodName}(${args.map(arg => JSON.stringify(arg)).join(', ')})`);
        var retval = this[methodName].apply(this, args);
        console.log(`[+] Returned from ${methodName}: ${JSON.stringify(retval)}`);
        return retval;
      };
    }
  });
  console.log('All methods of SensitiveDataProcessor hooked!');
});

This powerful script dynamically applies a generic hook to every method, logging both input arguments and return values. This is incredibly useful for black-box testing when you’re exploring an unknown API surface.

Modifying Arguments and Return Values

Beyond just observing, Frida allows you to manipulate the flow of data. You can alter method arguments before they reach the original implementation or modify the return value before it’s sent back to the caller.

Example: Changing a Boolean Return Value

Imagine an `isLoggedIn()` method. You can force it to always return true:

Java.perform(function() {
  var LoginManager = Java.use('com.example.myapp.LoginManager');

  LoginManager.isLoggedIn.implementation = function() {
    console.log('LoginManager.isLoggedIn called. Forcing true!');
    return true; // Always return true, bypassing original check
  };
});

Example: Modifying a String Argument

You can also change input parameters. For instance, if a method validates a string, you can inject your own value:

Java.perform(function() {
  var DataValidator = Java.use('com.example.myapp.DataValidator');

  DataValidator.validateInput.overload('java.lang.String').implementation = function(input) {
    console.log('Original input to validateInput:', input);
    var modifiedInput = 'always_valid_string_by_frida';
    console.log('Modified input to:', modifiedInput);
    return this.validateInput(modifiedInput); // Call original with modified input
  };
});

Hooking Constructors

Constructors in Java are special methods used to initialize objects. Frida provides a specific way to hook them using the `$init` property.

Java.perform(function() {
  var MyCustomObject = Java.use('com.example.myapp.MyCustomObject');

  MyCustomObject.$init.implementation = function() {
    console.log('MyCustomObject constructor called with arguments:', JSON.stringify(Array.from(arguments)));
    this.$init.apply(this, arguments);
  };
  console.log('Hooked MyCustomObject constructor!');
});

Practical Tips and Best Practices

  • Error Handling: Wrap your Frida scripts in try...catch blocks, especially when dealing with complex object manipulations, to prevent crashes.
  • console.log() vs send(): Use console.log() for simple output to your console. For sending structured data (objects, arrays) back to your host machine for further processing, use send().
  • Debugging: You can debug Frida scripts directly using a browser’s developer tools by attaching to the Frida process (e.g., using frida -U --debug-devtools -f com.your.package.name --no-pause).
  • Persistence: For hooks that need to be active throughout the app’s lifecycle, ensure your Frida server is running persistently and attach your script carefully. The --no-pause flag is often useful to prevent the app from pausing immediately after spawning.
  • Static Analysis First: Always perform static analysis (decompiling the APK) before dynamic analysis. This provides crucial context, class names, method signatures, and logic flows, making your Frida hooking efforts much more efficient and targeted.

By mastering these techniques, you’ll be well-equipped to perform in-depth analysis and manipulation of Android Java applications, uncovering vulnerabilities and understanding their inner workings like a true professional.

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