Android App Penetration Testing & Frida Hooks

Advanced Frida: Techniques for Runtime Argument & Return Value Modification in Android Apps

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Runtime Manipulation with Frida

Frida, a dynamic instrumentation toolkit, is an indispensable asset for security researchers and penetration testers. While basic hooking allows observing method calls, its true power lies in the ability to actively manipulate an application’s behavior at runtime. This article delves into advanced Frida techniques for modifying method arguments and return values in Android applications, enabling profound control over app logic during live execution.

Understanding how to inject and modify data flows is crucial for bypassing security controls, altering application states, or even enabling hidden features. We will explore practical scenarios, focusing on Java-based Android methods, using Frida’s powerful JavaScript API.

Prerequisites

Before we begin, ensure you have the following setup:

  • A rooted Android device or an emulator (e.g., Android Studio AVD, Genymotion).
  • ADB (Android Debug Bridge) installed and configured on your host machine.
  • Frida command-line tools (frida-tools) installed on your host:pip install frida-tools
  • Frida server running on your Android device/emulator:
# Push frida-server to device (adjust version/arch as needed)adb push frida-server-<version>-android-<arch> /data/local/tmp/# Grant execute permissionsadb shell "chmod 755 /data/local/tmp/frida-server-<version>-android-<arch>"# Run frida-server in the backgroundadb shell "/data/local/tmp/frida-server-<version>-android-<arch> &"# Set up ADB port forwarding for Frida (optional but good practice)adb forward tcp:27042 tcp:27042adb forward tcp:27043 tcp:27043

Hooking Methods for Inspection

The foundation of argument and return value modification begins with successful method hooking. We typically use Java.perform to ensure our script runs within the context of the Java VM and Java.use to get a handle on target classes and methods.

Java.perform(function () {    // Get a reference to the target class    var targetClass = Java.use("com.example.app.SomeClass");    // Hook a specific method    targetClass.someMethod.implementation = function (arg1, arg2) {        // Log original arguments        console.log("someMethod called with:");        console.log("  arg1: " + arg1);        console.log("  arg2: " + arg2);        // Call the original method (important!)        var retval = this.someMethod(arg1, arg2);        // Log original return value        console.log("  Return value: " + retval);        return retval;    };});

To execute this script against an app (e.g., com.example.app), save it as hook.js and run:

frida -U -f com.example.app -l hook.js --no-pause

Modifying Method Arguments

The real power comes when we manipulate the arguments passed into a method. When you redefine a method’s implementation, the parameters passed to your JavaScript function are the actual arguments of the method. You can directly reassign these, or provide new values when you manually invoke the original method via this.methodName(...).

Example 1: Modifying Primitive/String Arguments

Consider an application that checks a user’s license key against a hardcoded value or a server. We can modify this key to always be valid.

Target Java Method (Hypothetical):

public class LicenseManager {    public boolean checkLicense(String key, int userId) {        // ... complex license validation logic ...        return key.equals("VALID_SECRET_KEY") && userId > 100;    }}

Frida Script (modify_args.js):

Java.perform(function () {    var LicenseManager = Java.use("com.example.app.LicenseManager");    // Using overload to target a specific method signature if multiple exist    LicenseManager.checkLicense.overload("java.lang.String", "int").implementation = function (licenseKey, userId) {        console.log("[+] Original checkLicense call:");        console.log("    License Key: " + licenseKey);        console.log("    User ID: " + userId);        // Modify arguments        var newLicenseKey = "VALID_SECRET_KEY"; // The key we want to inject        var newUserId = 9999;                   // A valid user ID        console.log("[*] Modifying arguments to:");        console.log("    New License Key: " + newLicenseKey);        console.log("    New User ID: " + newUserId);        // Call the original method with the modified arguments        var originalResult = this.checkLicense(newLicenseKey, newUserId);        console.log("    Original method result (with modified args): " + originalResult);        return originalResult; // Return the result from the original method with modified args    };});

In this example, we intercept checkLicense, log the original arguments, then craft new arguments and pass them to the underlying original method using this.checkLicense(newLicenseKey, newUserId). The application will proceed with our injected values.

Example 2: Modifying Object Arguments

Sometimes, arguments are complex Java objects. You might need to create new instances of these objects or modify their internal state.

Target Java Method (Hypothetical):

public class NetworkClient {    public String makeRequest(RequestData data) {        // ... network request logic using data.url and data.headers ...        return "Response for " + data.getUrl();    }}public class RequestData {    private String url;    private Map<String, String> headers;    public RequestData(String url, Map<String, String> headers) {        this.url = url;        this.headers = headers;    }    public String getUrl() { return url; }    public Map<String, String> getHeaders() { return headers; }}

Frida Script (modify_object_args.js):

Java.perform(function () {    var NetworkClient = Java.use("com.example.app.NetworkClient");    var RequestData = Java.use("com.example.app.RequestData");    var HashMap = Java.use("java.util.HashMap");    NetworkClient.makeRequest.implementation = function (requestData) {        console.log("[+] Original makeRequest call:");        console.log("    Original URL: " + requestData.getUrl());        // Create a new HashMap for headers        var newHeaders = HashMap.$new();        newHeaders.put("X-Frida-Modified", "true");        newHeaders.put("Authorization", "Bearer FAKE_TOKEN");        // Create a new RequestData object with a different URL and new headers        var newRequestData = RequestData.$new("https://api.example.com/frida/data", newHeaders);        console.log("[*] Modifying RequestData object:");        console.log("    New URL: " + newRequestData.getUrl());        console.log("    New Headers: " + newRequestData.getHeaders());        // Call the original method with the new object        var result = this.makeRequest(newRequestData);        console.log("    Original method result (with modified RequestData): " + result);        return result;    };});

Here, we use $new() to instantiate new Java objects (HashMap and RequestData) and populate them with our desired values before passing the new RequestData object to the original method.

Modifying Method Return Values

Equally powerful is the ability to change the value a method returns, regardless of its original computation. This is particularly useful for bypassing license checks, permission checks, or altering success/failure indicators.

Example 3: Forcing a Boolean Return Value

Let’s revisit the license check, but this time, we want to ensure it always returns true.

Target Java Method (Hypothetical):

public class FeatureManager {    public boolean isFeatureEnabled(String featureName) {        // ... complex logic to check feature status ...        return false; // Assume it returns false for "premium" feature    }}

Frida Script (modify_return.js):

Java.perform(function () {    var FeatureManager = Java.use("com.example.app.FeatureManager");    FeatureManager.isFeatureEnabled.implementation = function (featureName) {        console.log("[+] isFeatureEnabled called for: " + featureName);        // Call the original method to see its original return        var originalResult = this.isFeatureEnabled(featureName);        console.log("    Original return value: " + originalResult);        // Force the return value to true        var modifiedResult = true;        console.log("[*] Forcing return value to: " + modifiedResult);        return modifiedResult;    };});

In this script, after logging the original return, we simply return true directly from our Frida hook, effectively short-circuiting the application’s logic to always enable the feature.

Example 4: Modifying an Object Return Value

If a method returns an object, you can instantiate and return a completely different object or modify the properties of the original object before returning it.

Target Java Method (Hypothetical):

public class UserProfileManager {    public UserProfile getUserProfile(String userId) {        // ... retrieves user profile from server or local storage ...        return new UserProfile("Default", "Guest"); // Returns a basic profile by default    }}public class UserProfile {    public String name;    public String role;    public UserProfile(String name, String role) {        this.name = name;        this.role = role;    }}

Frida Script (modify_object_return.js):

Java.perform(function () {    var UserProfileManager = Java.use("com.example.app.UserProfileManager");    var UserProfile = Java.use("com.example.app.UserProfile");    UserProfileManager.getUserProfile.implementation = function (userId) {        console.log("[+] getUserProfile called for User ID: " + userId);        // Call original method        var originalProfile = this.getUserProfile(userId);        console.log("    Original Profile: Name=" + originalProfile.name + ", Role=" + originalProfile.role);        // Create a new, elevated UserProfile object        var modifiedProfile = UserProfile.$new("Admin", "Administrator");        console.log("[!] Modifying return value to:");        console.log("    New Profile: Name=" + modifiedProfile.name + ", Role=" + modifiedProfile.role);        return modifiedProfile;    };});

Here, we create an entirely new UserProfile object with elevated privileges (e.g., “Admin”, “Administrator”) and return it, completely overriding the app’s default profile retrieval mechanism.

Advanced Considerations

  • Overloaded Methods:

    Use .overload("arg1_type", "arg2_type", ...) to target the specific method signature you intend to hook, as shown in Example 1.

  • Type Casting:

    When working with Java objects, especially those returned from methods that might be `java.lang.Object` or generic types, you might need to cast them explicitly using Java.cast(object, Java.use("com.example.app.TargetType")) to access specific fields or methods.

  • Object Instantiation:

    Always use Java.use("com.example.app.MyObject").$new(...) to instantiate new Java objects within your Frida script.

  • Exception Handling:

    Be mindful of potential exceptions in your script. A crashing Frida script can crash the target application. Use try...catch blocks for robustness, especially when dealing with complex object manipulations.

Conclusion

Frida’s capabilities for runtime argument and return value modification provide unparalleled control over Android application execution. By mastering these techniques, you can effectively bypass security checks, manipulate data flows, and gain deeper insights into an application’s internal workings during penetration tests or security research. Remember to always use these powerful tools responsibly and ethically, with proper authorization.

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