Frida Tutorial 2

This is a summary of the post: https://11x256.github.io/Frida-hooking-android-part-2/ (Parts 2, 3 and 4)\ APKs and Source code: https://github.com/11x256/frida-android-examples

This part focuses on overloaded methods, calling app code from your Frida script, keeping references to live Java objects, and using Python as a controller for interactive instrumentation.

Part 2

Here you can see an example of how to hook 2 functions with the same name but different parameters.\ Also, you are going to learn how to call a function with your own parameters.\ And finally, there is an example of how to find an instance of a class and make it call a function.

// s2.js
console.log("Script loaded successfully");

Java.perform(function () {
  var MyActivity = Java.use("com.example.a11x256.frida_test.my_activity");
  var JString = Java.use("java.lang.String");

  var funInt = MyActivity.fun.overload("int", "int");
  funInt.implementation = function (x, y) {
    console.log("original call: fun(" + x + ", " + y + ")");
    return funInt.call(this, 2, 5);
  };

  var funString = MyActivity.fun.overload("java.lang.String");
  funString.implementation = function (x) {
    var myString = JString.$new("My TeSt String#####");
    console.log("Original arg: " + x);
    var ret = funString.call(this, myString);
    console.log("Return value: " + ret);
    return ret;
  };

  Java.choose("com.example.a11x256.frida_test.my_activity", {
    onMatch: function (instance) {
      console.log("Found instance: " + instance);
      console.log("Result of secret func: " + instance.secret());
    },
    onComplete: function () {},
  });
});

To create a String, this script first references java.lang.String and then creates a new object with $new(). That is the correct way to instantiate Java objects from Frida. In this specific case, passing a plain JavaScript string like funString.call(this, "hey there!") also works because Frida will coerce it to java.lang.String.

Python

# loader.py
import frida
import time

with open("s2.js", "r", encoding="utf-8") as f:
    jscode = f.read()

device = frida.get_usb_device(timeout=5)
pid = device.spawn(["com.example.a11x256.frida_test"])
device.resume(pid)
time.sleep(1)  # Without it Java.perform may run before ART is ready
session = device.attach(pid)
script = session.create_script(jscode)
script.load()
input()
python3 loader.py

Keeping instances after Java.choose

Java.choose() gives you a live wrapper during the callback. If you want to keep using that object later, retain it explicitly:

var cached = null;

Java.perform(function () {
  Java.choose("com.example.a11x256.frida_test.my_activity", {
    onMatch: function (instance) {
      cached = Java.retain(instance);
      console.log("Retained instance: " + cached);
      return "stop";
    },
    onComplete: function () {},
  });
});

This matters when you want to call the same instance later from rpc.exports, timers, or another hook without re-scanning the heap each time.

Classes loaded by custom ClassLoaders

On recent Android apps, especially apps using dynamic feature modules, plugin frameworks, or packed/encrypted code, Java.use() may fail with ClassNotFoundException even though the class exists. In that case enumerate the available class loaders and use the correct one through a dedicated Java.ClassFactory:

Java.perform(function () {
  var targetLoader = null;

  Java.enumerateClassLoaders({
    onMatch: function (loader) {
      try {
        if (loader.findClass("com.example.a11x256.frida_test.my_activity")) {
          targetLoader = loader;
          console.log("Found loader: " + loader);
        }
      } catch (e) {}
    },
    onComplete: function () {},
  });

  if (targetLoader !== null) {
    var factory = Java.ClassFactory.get(targetLoader);
    var MyActivity = factory.use("com.example.a11x256.frida_test.my_activity");

    factory.choose("com.example.a11x256.frida_test.my_activity", {
      onMatch: function (instance) {
        console.log("Instance from alternate loader: " + instance);
      },
      onComplete: function () {},
    });
  }
});

If you already know the app loads sensitive code late, this pattern is usually more reliable than retrying Java.use() in a loop.

Part 3

Python

Now you are going to see how to send commands to the hooked app via Python and use Frida RPC exports to call JavaScript functions:

# loader.py
import time
import frida


def on_message(message, payload):
    print(message)
    if payload is not None:
        print(payload)


with open("s3.js", "r", encoding="utf-8") as f:
    jscode = f.read()

device = frida.get_usb_device(timeout=5)
pid = device.spawn(["com.example.a11x256.frida_test"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
script = session.create_script(jscode)
script.on("message", on_message)
script.load()
api = script.exports_sync

while True:
    command = input(
        "Enter command:\n1: Exit\n2: Call secret function\n3: Hook Secret\nchoice: "
    ).strip()
    if command == "1":
        break
    if command == "2":
        api.callsecretfunction()
    elif command == "3":
        api.hooksecretfunction()

The command 1 will exit, the command 2 will find an instance of the class and call the private function secret(), and command 3 will hook the function secret() so it returns a different string.

If you call 2 first, you will get the real secret. If you call 3 and then 2, you will get the fake secret.

JS

console.log("Script loaded successfully");
var instancesArray = [];

function callSecretFun() {
  Java.perform(function () {
    if (instancesArray.length === 0) {
      Java.choose("com.example.a11x256.frida_test.my_activity", {
        onMatch: function (instance) {
          instancesArray.push(Java.retain(instance));
          console.log("Found instance: " + instance);
          console.log("Result of secret func: " + instance.secret());
          return "stop";
        },
        onComplete: function () {},
      });
    } else {
      console.log("Result of secret func: " + instancesArray[0].secret());
    }
  });
}

function hookSecret() {
  Java.perform(function () {
    var MyActivity = Java.use("com.example.a11x256.frida_test.my_activity");
    var JString = Java.use("java.lang.String");
    var secret = MyActivity.secret.overload();

    secret.implementation = function () {
      return JString.$new("TE ENGANNNNEEE");
    };
  });
}

rpc.exports = {
  callsecretfunction: callSecretFun,
  hooksecretfunction: hookSecret,
};

In Python, exported JavaScript methods are easier to call through script.exports_sync. For example, an export named enumerateModules becomes script.exports_sync.enumerate_modules().

Part 4

Here you will see how to make Python and JS interact using JSON objects. JS uses the send() function to send data to the Python client, and Python uses post() to send a JSON object back to the JS script. The JS will block the execution until it receives a response from Python.

Python

# loader.py
import base64
import time
import frida


def on_message(message, payload):
    print(message)
    if message.get("type") != "send":
        return

    encoded = message["payload"].split(":", 1)[1].strip()
    user, password = base64.b64decode(encoded).decode().split(":", 1)
    new_data = base64.b64encode(f"admin:{password}".encode()).decode()
    script.post({"type": "input", "payload": {"my_data": new_data}})
    print(f"Modified data sent for user {user}")


with open("s4.js", "r", encoding="utf-8") as f:
    jscode = f.read()

device = frida.get_usb_device(timeout=5)
pid = device.spawn(["com.example.a11x256.frida_test"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
script = session.create_script(jscode)
script.on("message", on_message)
script.load()
input()

JS

console.log("Script loaded successfully");

Java.perform(function () {
  var TextView = Java.use("android.widget.TextView");
  var setText = TextView.setText.overload("java.lang.CharSequence");

  setText.implementation = function (x) {
    var outgoing = x.toString();
    var incoming = outgoing;

    send("Candidate text: " + outgoing);
    recv("input", function (message) {
      incoming = message.payload.my_data;
    }).wait();

    console.log("Final string_to_recv: " + incoming);
    return setText.call(this, incoming);
  };
});

recv() handlers receive one message and must be registered again for the next one. Using .wait() blocks the current hooked thread until Python replies, so keep this pattern for cases where you really need an inline decision before the original method continues.

Modern Frida Notes

  • These examples still work as plain scripts loaded through frida, frida-python, or frida-trace.
  • If you migrate them to a Frida 17+ agent project built with frida-create/frida-compile, import the Java bridge explicitly with import Java from "frida-java-bridge".
  • Frida 17.1.4 bumped frida-java-bridge to 7.0.3 in internal Android agents, adding Android 16 support. If heap scans or Java hooks behave strangely on very recent Android versions, first verify that frida-tools, frida-python, and frida-server/gadget are on matching recent versions.
  • For anti-Frida, root detection, and SSL pinning bypasses, keep that content in the dedicated page:

../android-anti-instrumentation-and-ssl-pinning-bypass.md

There is a part 5 that is not explained here because it doesn't add anything substantially new. If you want to read it, it is here: https://11x256.github.io/Frida-hooking-android-part-5/

References