Android App Penetration Testing & Frida Hooks

Building a Frida RPC Toolkit: Customizing Android App Interaction Scripts

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Frida RPC for Android App Penetration Testing

Frida is a dynamic instrumentation toolkit that allows injecting custom scripts into processes. While its basic hooking capabilities are powerful for observing runtime behavior, Frida’s Remote Procedure Call (RPC) feature elevates dynamic analysis to an entirely new level. RPC enables seamless communication between your Python (or other language) client script and the injected JavaScript agent, allowing you to invoke JavaScript functions from your client and receive results. This capability is invaluable for building custom toolkits to interact with Android applications dynamically, bypass client-side controls, and exfiltrate sensitive data.

This article will guide you through building a practical Frida RPC toolkit. We’ll focus on creating a JavaScript agent that exposes several functions for interacting with an Android application, and a Python client to leverage these functions for common penetration testing scenarios like data exfiltration and runtime modification.

Prerequisites and Setup

Before diving into RPC, ensure you have the following setup:

  • Android Device/Emulator: A rooted Android device or an emulator (e.g., AVD, Genymotion) running Frida-server.
  • Frida-server: Download the appropriate frida-server binary for your device’s architecture from Frida’s GitHub releases. Push it to the device and run it.
  • Frida-tools: Install the Python client tools:
    pip install frida-tools

  • ADB (Android Debug Bridge): For interacting with your Android device.
  • Python Environment: A working Python 3 environment.

To start frida-server on your device:

adb push /path/to/frida-server /data/local/tmp/frida-serveradb shell"chmod 755 /data/local/tmp/frida-server"adb shell"/data/local/tmp/frida-server &"

Understanding Frida RPC: The Basics

Frida RPC works by exposing specific JavaScript functions from your injected script to the Python client. In your JavaScript code, you define an Rpc.exports object, which serves as a dictionary of functions accessible from the outside. The Python client then loads this script, and these exported functions become directly callable as methods on the script object.

This allows for a clean separation of concerns: your JavaScript agent handles the low-level instrumentation and interaction with the target application’s runtime, while your Python client orchestrates the attack logic, data processing, and user interaction.

Building the Frida RPC Agent (JavaScript)

Let’s create a Frida JavaScript agent (e.g., rpc_agent.js) that exposes functions for common tasks:

  • dumpField(className, fieldName, instancePtr): To dump the value of an instance field.
  • callStaticMethod(className, methodName, argTypes, args): To invoke a static method.
  • interceptMethodAndReturn(className, methodName, returnValue): To hook a method and force it to return a specific value.

// rpc_agent.jsJava.perform(function () {    // Helper to get an instance from a pointer (or just use a fully qualified class name for static contexts)    function getInstance(className, instancePtr) {        if (instancePtr && instancePtr !== '0x0') {            return new Java.Class(className).wrap(ptr(instancePtr));        }        return Java.use(className); // For static access or new instance creation    }    Rpc.exports = {        // Dumps the value of a specific field from an object instance        dumpField: function (className, fieldName, instancePtr) {            try {                var targetClass = getInstance(className, instancePtr);                var targetInstance = instancePtr && instancePtr !== '0x0' ? targetClass : null;                // If instancePtr is null/0x0, we assume it's a static field if not found on instance                var value = null;                if (targetInstance) {                    value = targetInstance[fieldName].value;                } else {                    // Try static field                    value = targetClass[fieldName].value;                }                return JSON.stringify({ status: 'success', fieldName: fieldName, value: String(value) });            } catch (e) {                return JSON.stringify({ status: 'error', message: e.message });            }        },        // Calls a static method with specified arguments        callStaticMethod: function (className, methodName, argTypes, args) {            try {                var targetClass = Java.use(className);                var methodSignature = methodName + '(' + argTypes.join(',') + ')';                var method = targetClass[methodSignature];                var result = method.apply(targetClass, args);                return JSON.stringify({ status: 'success', method: methodName, result: String(result) });            } catch (e) {                return JSON.stringify({ status: 'error', message: e.message });            }        },        // Intercepts a method and forces it to return a specified value        interceptMethodAndReturn: function (className, methodName, returnVal) {            try {                var targetClass = Java.use(className);                var originalMethod = targetClass[methodName];                var returnType = typeof returnVal === 'boolean' ? 'boolean' : typeof returnVal === 'string' ? 'java.lang.String' : 'int'; // Simplified type inference                originalMethod.implementation = function () {                    console.log("[Frida] Intercepted " + className + "." + methodName + ", returning custom value: " + returnVal);                    if (returnType === 'boolean') return Boolean(returnVal);                    if (returnType === 'java.lang.String') return Java.use('java.lang.String').$new(returnVal);                    return returnVal;                };                return JSON.stringify({ status: 'success', message: 'Method ' + methodName + ' intercepted to return ' + returnVal });            } catch (e) {                return JSON.stringify({ status: 'error', message: e.message });            }        }    };});

Building the Python RPC Client

Now, let’s create a Python script (e.g., rpc_client.py) to interact with our Frida agent. This script will attach to a target Android application, load the rpc_agent.js, and then call the exposed functions.

# rpc_client.pyimport fridaimport sysimport jsondef on_message(message, data):    if message['type'] == 'send':        print("[+] {0}".format(message['payload']))    elif message['type'] == 'error':        print("[!] {0}".format(message['stack']))def main():    if len(sys.argv) != 2:        print("Usage: python rpc_client.py ")        sys.exit(1)    package_name = sys.argv[1]    try:        device = frida.get_usb_device(timeout=10)        pid = device.spawn([package_name])        print(f"[+] Spawning {package_name} with PID: {pid}")        session = device.attach(pid)        device.resume(pid)        print(f"[+] Attached to {package_name} (PID: {pid})")        with open("rpc_agent.js", 'r') as f:            script_code = f.read()        script = session.create_script(script_code)        script.on('message', on_message)        script.load()        print("[+] Frida agent loaded. RPC exports available.")        # --- Example 1: Call a static method ---        print("n--- Calling Static Method ---")        result_json = script.exports.call_static_method(            "com.example.myapp.SomeUtilityClass",            "generateSecretToken",            [],            []        )        result = json.loads(result_json)        if result['status'] == 'success':            print(f"[+] generateSecretToken result: {result['result']}")        else:            print(f"[!] Error calling generateSecretToken: {result['message']}")        # --- Example 2: Intercept a method to bypass a check ---        print("n--- Intercepting a Method ---")        result_json = script.exports.intercept_method_and_return(            "com.example.myapp.AuthManager",            "isPremiumUser",            True        )        result = json.loads(result_json)        if result['status'] == 'success':            print(f"[+] {result['message']}")            print("[!] Now 'isPremiumUser()' will always return true.")        else:            print(f"[!] Error intercepting method: {result['message']}")        # --- Example 3: Dump a field from an instance (requires knowing instance pointer, or attach during runtime) ---        # This example assumes you might have obtained an instance pointer from another hook        # For demonstration, let's assume '0x12345678' is a valid pointer we found earlier        print("n--- Dumping Field from Instance (Example with dummy pointer) ---")        dummy_instance_ptr = '0x0' # In a real scenario, this would be a real pointer        result_json = script.exports.dump_field(            "com.example.myapp.UserSession",            "_authToken",            dummy_instance_ptr        )        result = json.loads(result_json)        if result['status'] == 'success':            print(f"[+] Field '_authToken' value: {result['value']}")        else:            print(f"[!] Error dumping field: {result['message']}")        # Keep the script running for a while, or prompt user to exit        input("nPress Enter to detach from the process...")        session.detach()        print("[+] Detached from process.")    except frida.core.RPCException as e:        print(f"[!] RPC Error: {e}")        sys.exit(1)    except Exception as e:        print(f"[!] An error occurred: {e}")        sys.exit(1)if __name__ == '__main__':    main()

Executing the RPC Toolkit

To run this toolkit, ensure your rpc_agent.js and rpc_client.py files are in the same directory. Replace com.example.myapp.SomeUtilityClass, AuthManager, and UserSession with actual class names from your target Android application.

Execute the Python client from your terminal:

python rpc_client.py com.your.target.package

You’ll see output showing the Frida agent attaching, the RPC calls being made, and their results. The interceptMethodAndReturn function effectively demonstrates how you can dynamically alter application logic, for instance, bypassing a premium user check. The dumpField and callStaticMethod functions illustrate powerful data extraction and direct method invocation capabilities, crucial for exfiltrating sensitive information or triggering hidden functionalities.

Advanced Data Exfiltration with RPC

The examples above return simple stringified results. For more complex data structures, your JavaScript agent can convert Java objects to JSON strings using JSON.stringify() before returning them. The Python client can then parse these strings back into Python dictionaries or objects using json.loads(). This allows for exfiltrating entire object graphs, including nested properties and array contents.

Consider a scenario where an object holds an entire session state. Your RPC function could enumerate its fields, recursively dump their values, and return a comprehensive JSON representation, providing deep insights into the application’s internal workings.

Conclusion

Frida’s RPC capabilities transform dynamic analysis from mere observation to active interaction. By building custom RPC toolkits, penetration testers and security researchers can programmatically control Android applications, bypass client-side security mechanisms, and extract critical runtime data with unprecedented flexibility. This approach significantly speeds up the analysis process and uncovers vulnerabilities that might be missed with static analysis or basic hooking techniques. Embrace Frida RPC to unlock the full potential of your Android app penetration testing engagements.

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