Android App Penetration Testing & Frida Hooks

Frida RPC Lab: Reverse Engineering Android API Calls & Object States in Real-Time

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Frida RPC for Android Penetration Testing

Frida is an invaluable dynamic instrumentation toolkit for security researchers and penetration testers. While basic hooking allows intercepting method calls and modifying arguments, Frida’s Remote Procedure Call (RPC) capabilities unlock a far more powerful dimension: direct, programmatic interaction with the target application’s runtime environment from your host machine. This enables real-time querying of object states, invoking arbitrary methods, and exfiltrating data, making it an indispensable tool for deep Android app analysis and exploitation.

This lab will guide you through setting up and utilizing Frida RPC to interact with an Android application. We’ll explore how to identify target APIs and objects, craft a Frida agent with RPC exports, and develop a Python client to control and query the application’s internal state remotely. The goal is to demonstrate how to dynamically interact with an app’s Java and native layers to extract information or manipulate its behavior without modifying the APK.

Prerequisites and Setup

Before diving into RPC, ensure you have Frida installed on your host machine and frida-server running on your Android device (rooted recommended for ease of use). You’ll need:

  • A rooted Android device or emulator with ADB access.
  • Frida tools installed on your host: pip install frida-tools.
  • frida-server pushed to your device and running:
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 &"

Identifying the Target Application and Process

First, identify the package name of the Android application you wish to analyze. For this example, let’s assume our target package is com.example.androidapp. You can list running processes with Frida:

frida-ps -Uai

This command lists all installed applications and their process IDs (PIDs) if they are running. Note the PID or package name for subsequent steps.

Building the Frida RPC Agent

The core of Frida RPC lies in defining functions in your JavaScript agent that can be called remotely by a client. These functions are exposed via rpc.exports. Let’s create a file named rpc_agent.js.

// rpc_agent.js

Java.perform(function() {
    // Helper function to get a Java class
    function getJavaClass(className) {
        try {
            return Java.use(className);
        } catch (e) {
            console.error("Error loading class " + className + ": " + e.message);
            return null;
        }
    }

    rpc.exports = {
        // Example 1: Read a static string field from a class
        getstaticstring: function(className, fieldName) {
            var targetClass = getJavaClass(className);
            if (targetClass && targetClass[fieldName] !== undefined) {
                return targetClass[fieldName].value;
            }
            return null;
        },

        // Example 2: Invoke a method on an existing instance and return its value
        // This assumes we have a way to get an instance, e.g., by hooking a constructor
        invokeinstancemethod: function(instancePtr, methodName, args) {
            try {
                var instance = Java.cast(ptr(instancePtr), Java.use('java.lang.Object'));
                var method = instance[methodName];
                if (method) {
                    // Prepare arguments for method invocation. Frida often handles basic types.
                    // For complex types, you might need to convert them from JSON-serializable to Java objects.
                    return method.apply(instance, args);
                }
            } catch (e) {
                console.error("Error invoking method: " + e.message);
            }
            return null;
        },

        // Example 3: Create a new Java object and return its reference (as a string pointer)
        createnewobject: function(className, constructorArgs) {
            var targetClass = getJavaClass(className);
            if (targetClass) {
                try {
                    var newObject = targetClass.$new.apply(targetClass, constructorArgs);
                    // Return the object's pointer as a string for the client to refer to
                    return newObject.toString(); // e.g., 'android.content.ContextWrapper@c72a59a'
                } catch (e) {
                    console.error("Error creating new object: " + e.message);
                }
            }
            return null;
        },

        // Example 4: Manipulate a private field of an existing instance
        setprivatefield: function(instancePtr, fieldName, newValue) {
            try {
                var instance = Java.cast(ptr(instancePtr), Java.use('java.lang.Object'));
                var field = instance.getClass().getDeclaredField(fieldName);
                field.setAccessible(true);
                field.set(instance, newValue);
                return true;
            } catch (e) {
                console.error("Error setting private field: " + e.message);
            }
            return false;
        }
    };
});

In this agent:

  • Java.perform() ensures our code runs within the context of the Java Virtual Machine.
  • rpc.exports is the crucial object that exposes our functions to the remote client.
  • getstaticstring demonstrates reading a static field directly.
  • invokeinstancemethod shows calling a method on a given instance. Obtaining the instance pointer (instancePtr) typically requires prior hooking of a constructor or a method that returns an instance.
  • createnewobject illustrates how to instantiate new Java objects, returning their string representation of the pointer, which can then be cast back in the agent for further interaction.
  • setprivatefield shows how to access and modify private fields using reflection, a common requirement in reverse engineering.

Developing the Python RPC Client

Now, let’s create a Python script, rpc_client.py, to connect to Frida, inject our agent, and call the exported RPC functions.

# rpc_client.py

import frida
import sys

def on_message(message, data):
    print(f"[on_message] {message} {data}")

def main(target_package):
    try:
        # Connect to the USB device or remote host
        device = frida.get_usb_device(timeout=5) # or frida.get_remote_device()
        
        # Attach to the specified package name
        session = device.attach(target_package)
        print(f"Attached to {target_package}")

        # Load the Frida agent script
        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.")

        # Access the exported RPC methods
        rpc_exports = script.exports

        # --- Example 1: Get a static string ---
        # Assume com.example.androidapp.Config has a static field 'API_KEY'
        api_key = rpc_exports.getstaticstring('com.example.androidapp.Config', 'API_KEY')
        print(f"[+] Static API Key: {api_key}")

        # --- Example 2: Create a new object ---
        # Assume com.example.androidapp.DataModel has a constructor DataModel(String data)
        new_data_model_ref = rpc_exports.createnewobject('com.example.androidapp.DataModel', ['SecretData123'])
        print(f"[+] New DataModel instance reference: {new_data_model_ref}")
        
        # Note: To invoke methods on this new_data_model_ref, you'd need another RPC export
        # that takes the string ref, casts it back to the specific Java class, and then calls a method.
        # E.g., in rpc_agent.js:
        // getdatamodeldata: function(instanceRef) {
        //     var instance = Java.cast(ptr(instanceRef.split('@')[1]), Java.use('com.example.androidapp.DataModel'));
        //     return instance.getData();
        // }

        # --- Example 3: Demonstrate setting a private field (requires an existing instance) ---
        # For this, we'd typically hook a constructor or method that returns an instance first.
        # Let's assume we previously hooked a constructor and got an instance pointer '0xcafebabe'
        # For a real scenario, you'd hook a constructor or a method that returns an instance
        # and use send() to pass its pointer to your client, then call another RPC method.
        # For demonstration, let's pretend we have an instance pointer for a 'UserSession' object.
        dummy_instance_ptr = '0x12345678' # Placeholder: In a real scenario, this comes from a hook.
        print(f"[!] Attempting to set private field on dummy instance {dummy_instance_ptr}")
        # success = rpc_exports.setprivatefield(dummy_instance_ptr, 'privateToken', 'NEW_TOKEN_FROM_FRIDA')
        # print(f"[+] Private field set: {success}")

        # Keep the script running if you need continuous interaction or hooks to fire
        sys.stdin.read() 

    except frida.core.RPCException as e:
        print(f"RPC Error: {e}")
    except Exception as e:
        print(f"General Error: {e}")
    finally:
        if 'session' in locals() and session:
            session.detach()
            print("Detached from process.")

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: python rpc_client.py <package_name>")
        sys.exit(1)
    main(sys.argv[1])

To run the client:

python3 rpc_client.py com.example.androidapp

In this Python client:

  • We connect to a Frida-enabled device.
  • Attach to the target application by its package name.
  • Load our rpc_agent.js script.
  • Access the exported functions via script.exports.
  • Demonstrate calling getstaticstring and createnewobject.
  • The setprivatefield example is commented out because it requires an actual instance pointer, which would typically be obtained by hooking a method (e.g., a constructor or a getter) that returns an instance of the class you want to modify, and then sending that instance’s pointer (instance.handle.toString()) back to the Python client via send().
  • sys.stdin.read() keeps the Python script alive, allowing the Frida agent to continue running in the background and potentially respond to callbacks or perform further RPC calls.

Real-World Scenarios and Advanced Techniques

Frida RPC excels in scenarios where simple hooking is insufficient:

  • Data Exfiltration: Instead of just logging data to console, you can use RPC to retrieve sensitive strings, arrays, or entire objects from the app’s memory and send them directly to your host for analysis.
  • State Manipulation: Call methods to change the application’s internal state, bypass checks, or trigger hidden functionalities. For instance, you could invoke a private method to simulate a successful login or bypass license checks.
  • Object Instantiation and Interaction: Create new instances of complex objects (e.g., cryptographic ciphers, network handlers) and interact with them in ways the app might not normally expose, for testing or exploitation.
  • Dynamic Patching: Beyond just hooking, RPC allows you to invoke methods that effectively

    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