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-serverpushed 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.exportsis the crucial object that exposes our functions to the remote client.getstaticstringdemonstrates reading a static field directly.invokeinstancemethodshows 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.createnewobjectillustrates 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.setprivatefieldshows 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.jsscript. - Access the exported functions via
script.exports. - Demonstrate calling
getstaticstringandcreatenewobject. - The
setprivatefieldexample 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 viasend(). 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 →