Post

Frida

Frida

Frida is a dynamic instrumentation toolkit that allows you to inject scripts into running processes. It supports multiple platforms, including Android, iOS, Windows, macOS, and Linux. Frida is particularly popular in the mobile security community for reverse engineering and penetration testing.

How Frida Works

Frida works by injecting a small runtime into the target process. This runtime allows you to execute JavaScript or native code within the context of the target app. Frida uses a client-server architecture:

  • Frida Server: Runs on the target device (e.g., Android).
  • Frida Client: Runs on your machine and communicates with the server.

How Frida Injects into Android Apps

Frida can inject into Android apps in two ways:

  1. Runtime Injection:
    • Frida attaches to a running process on the device.
    • It injects the Frida gadget into the process, which allows you to execute scripts in the context of the app.
  2. Preloading (Gadget Mode):
    • You can embed the Frida gadget into the APK before running it.
    • When the app starts, the Frida gadget is loaded automatically, allowing you to hook into the app from the beginning.

Key Components of Frida

  • Frida Core: The core engine that handles instrumentation.
  • Frida Gadget: On Android, Frida injects a shared library (libfrida-agent.so) into the app’s process using ptrace (for non-root) or directly using frida-server (for rooted devices). The agent acts as a bridge between your computer and the app.
  • Frida CLI Tools: Command-line tools like frida, frida-ps, and frida-trace for interacting with the Frida server.
  • JavaScript API: The API used to write scripts that interact with the target app.

Example: Bypassing a Login Check

Before hooking:

1
2
3
public boolean isPasswordCorrect(String input) {
    return input.equals("SuperSecret123");
}

After hooking with Frida:

1
2
3
4
5
6
7
Java.perform(function() {
    var Login = Java.use("com.target.app.LoginActivity");
    Login.isPasswordCorrect.implementation = function(pass) {
        console.log("Original Password: " + pass);
        return true; // Bypass authentication
    };
});

Frida provides a rich JavaScript API to interact with Android apps. Here are the key components of the API:

A. Java API

Here’s a comprehensive list of important Frida Java APIs for Android/Java instrumentation:

  • Check if Java is available
1
2
3
4
5
if (Java.available) {
    Java.perform(() => {
        console.log("Java is available!");
    });
}
  • Working with Java Classes

Get a Java Class Reference

1
2
const Activity = Java.use("android.app.Activity");
const String = Java.use("java.lang.String");

List All Loaded Classes

1
2
3
4
5
6
7
8
Java.enumerateLoadedClasses({
    onMatch: function(className) {
        console.log(className);
    },
    onComplete: function() {
        console.log("Done!");
    }
});

Find a Class by Name (Wildcard Support)

1
Java.enumerateLoadedClassesSync().filter(c => c.includes("Activity"));
  • Hooking Java Methods

Replace a Method Implementation

1
2
3
4
5
const Activity = Java.use("android.app.Activity");
Activity.onCreate.implementation = function(bundle) {
    console.log("Activity.onCreate() hooked!");
    this.onCreate(bundle); // Call original
};

Hook Static Methods

1
2
3
4
5
const System = Java.use("java.lang.System");
System.currentTimeMillis.implementation = function() {
    console.log("System.currentTimeMillis() called!");
    return this.currentTimeMillis(); // Call original
};
  • Working with Java Objects

Create a New Java Object

1
2
3
const String = Java.use("java.lang.String");
const myString = String.$new("Hello Frida!");
console.log(myString.toString());

Call Methods on an Existing Object

1
2
3
// If you have an instance (e.g., from a hook)
const activity = Java.cast(this, Java.use("android.app.Activity"));
activity.finish(); // Call a method

Find Existing Instances of a Class

1
2
3
4
5
6
7
8
Java.choose("android.app.Activity", {
    onMatch: function(instance) {
        console.log("Found Activity instance:", instance);
    },
    onComplete: function() {
        console.log("Search complete!");
    }
});

Cast an Object to a Specific Class

1
2
const Activity = Java.use("android.app.Activity");
const activity = Java.cast(someObject, Activity);
  • Modifying & Bypassing Logic

Modify Field Values

1
2
const MyClass = Java.use("com.example.MyClass");
MyClass.secretField.value = "Hacked!";

Bypass a Method (Return Early)

1
2
3
4
const Security = Java.use("com.example.Security");
Security.checkPassword.implementation = function(pwd) {
    return true; // Always bypass
};

Replace a Method with a Custom Implementation

1
2
3
4
const Math = Java.use("java.lang.Math");
Math.random.implementation = function() {
    return 0.42; // Always return this value
};
  • Dynamic Class Loading & Injection

Load a Class Dynamically

1
2
const MyClass = Java.use("com.example.MyClass");
const dynamicClass = Java.classFactory.loader.loadClass("com.example.DynamicClass");

Register a New Java Class

1
2
3
4
5
6
7
8
9
10
const MyNewClass = Java.registerClass({
    name: "com.example.MyNewClass",
    methods: {
        myMethod: function() {
            return "Hello from Frida!";
        }
    }
});
const instance = MyNewClass.$new();
console.log(instance.myMethod());

In Frida’s Java API, $new is used to create a new instance of a Java class (i.e., call its constructor). It is the equivalent of the new keyword in Java but accessed via JavaScript.

Frida allows you to interact with Java classes and methods in the target app.

Accessing Java Classes:

1
2
3
4
Java.perform(function () {
    var StringClass = Java.use("java.lang.String");
    console.log(StringClass.$new("Hello, Frida!"));
});

Hooking Java Methods:

1
2
3
4
5
6
7
Java.perform(function () {
    var MainActivity = Java.use("com.example.app.MainActivity");
    MainActivity.login.implementation = function (username, password) {
        console.log("Login attempted with:", username, password);
        return this.login(username, password);
    };
});

Enumerating Classes and Methods:

1
2
3
4
5
6
7
8
9
10
Java.perform(function () {
    Java.enumerateLoadedClasses({
        onMatch: function (className) {
            console.log("Loaded class:", className);
        },
        onComplete: function () {
            console.log("Done enumerating classes.");
        }
    });
});

Creating Java Objects:

1
2
3
4
5
6
Java.perform(function () {
    var ArrayList = Java.use("java.util.ArrayList");
    var list = ArrayList.$new();
    list.add("Frida");
    console.log(list.toString());
});

B. Native API

Frida allows you to interact with native (C/C++) code in the app.

Finding Native Functions:

1
2
var strlen = Module.findExportByName("libc.so", "strlen");
console.log("strlen address:", strlen);

Hooking Native Functions:

1
2
3
4
5
6
7
8
Interceptor.attach(strlen, {
    onEnter: function (args) {
        console.log("strlen called with:", args[0].readCString());
    },
    onLeave: function (retval) {
        console.log("strlen returned:", retval);
    }
});

Reading and Writing Memory:

1
2
3
var address = ptr("0x12345678"); // Replace with actual address
console.log("Value at address:", address.readUtf8String());
address.writeUtf8String("New Value");

Enumerating Modules:

1
2
3
4
5
6
7
8
Process.enumerateModules({
    onMatch: function (module) {
        console.log("Module:", module.name, "Base:", module.base);
    },
    onComplete: function () {
        console.log("Done enumerating modules.");
    }
});

C. Inter-Process Communication (IPC)

Frida allows you to send and receive messages between your script and the Frida client.

  • Sending Messages:
    1
    
    send({ type: "status", message: "Script loaded!" });
    
  • Receiving Messages:
    1
    2
    3
    
    recv(function (message) {
      console.log("Received message:", message);
    });
    

D. Threading and Concurrency

Frida allows you to work with threads in the target app.

  • Enumerating Threads:
    1
    2
    3
    4
    5
    6
    7
    8
    
    Process.enumerateThreads({
      onMatch: function (thread) {
          console.log("Thread ID:", thread.id, "State:", thread.state);
      },
      onComplete: function () {
          console.log("Done enumerating threads.");
      }
    });
    
  • Manipulating Threads:
    1
    2
    
    var thread = Process.getCurrentThread();
    console.log("Current thread ID:", thread.id);
    

Here’s an example of hooking the strlen function in libc.so:

1
2
3
4
5
6
7
8
9
var strlen = Module.findExportByName("libc.so", "strlen");
Interceptor.attach(strlen, {
    onEnter: function (args) {
        console.log("strlen called with:", args[0].readCString());
    },
    onLeave: function (retval) {
        console.log("strlen returned:", retval);
    }
});

Installing Frida on Your Machine

1
pip install frida-tools

Verify the installation:

1
frida --version

Setting Up Frida on an Android Device

Download the Frida server binary for Android from the Frida releases page.

Choose the appropriate architecture (e.g., frida-server-16.0.8-android-arm64.xz for 64-bit ARM devices).

Check your device’s architecture:

1
adb shell getprop ro.product.cpu.abi

Extract the binary:

1
unxz frida-server-16.0.8-android-arm64.xz

Push the binary to your Android device:

1
adb push frida-server-16.0.8-android-arm64 /data/local/tmp/frida-server

Set the correct permissions and run the server:

1
2
3
4
5
6
7
8
adb shell
su
cd /data/local/tmp
chmod +x frida-server
./frida-server &

# OR
adb shell "su -c chmod 755 /data/local/tmp/frida-server"

Basic Usage of Frida

  • List running processes on the device:
    1
    
    frida-ps -U
    
  • Attach to a process using name (e.g., Chrome):
    1
    
    frida -U -n Chrome
    
  • Injecting JavaScript into an App Create a JavaScript file (script.js) with the following content:
1
console.log("Hello from Frida!");

Inject the script into the target app:

1
2
3
frida -U -n example -l script.js
# OR
frida -U -f com.example.app -l script.js

Hooking Theory

A class is a blueprint or template that defines the properties and behavior of an object. A method is a self-contained block of code that performs a specific task within a class.

Frida allows you to intercept and modify method behavior at runtime in Android applications.

There are two primary approaches:

1. Hooking Java Methods

You can hook Java methods using Frida’s JavaScript API to:

  • Modify method parameters
  • Print return values
  • Change method implementation completely

Frida works by injecting JavaScript into the target process. The main object for Android hooks is:

1
2
3
4
Java.perform(function() {
    // your code here
});

Frida’s Java.perform() function is a key part of how Frida injects JavaScript into Java processes on Android.

When you run Frida, it attaches to the target Java process using ptrace (on Linux/Android) (on non-rooted) or similar mechanisms. Frida injects a small native library called “frida-gadget” into the target process. This gadget acts as a bridge between native code and JavaScript. The gadget initializes a JavaScript runtime (using V8 or Duktape engine) within the target process. When you call Java.perform(), Frida:

  • Ensures the code runs on the correct thread (Android’s UI thread)
  • Sets up the Java VM bridge that allows JavaScript to interact with Java

Java.perform provides access to the Java Virtual Machine through the Java object, allowing you to:

  • Look up classes (Java.use())
  • Implement Java interfaces
  • Replace method implementations
  • Create new Java objects
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(function() {
    var targetClass = Java.use("com.package.ClassName");
    
    targetClass.methodName.implementation = function(args) {
        // Log method entry
        console.log("Method called with arguments: " + args);
        
        // Call original method using 'this'
        var result = this.methodName(args);
        
        // Modify or log return value
        console.log("Method returned: " + result);
        
        return result;
    };
});

2. Hooking Native Functions

For methods defined in native code, Frida provides specialized techniques to:

  • Intercept native Android methods
  • Use Interceptor.attach() to run custom code when methods are invoked

Hook a native function (e.g., strlen):

1
2
3
4
5
6
7
8
9
var strlen = Module.findExportByName("libc.so", "strlen");
Interceptor.attach(strlen, {
    onEnter: function (args) {
        console.log("strlen called with:", args[0].readCString());
    },
    onLeave: function (retval) {
        console.log("strlen returned:", retval);
    }
});

NOTE Java.perform() is a crucial method in Frida’s Java API for Android instrumentation that ensures code execution within the Java Virtual Machine (VM) context.

Manipulating Function Arguments and Return Values

1
2
3
4
5
6
Java.perform(function () {
    var MathClass = Java.use("java.lang.Math");
    MathClass.random.implementation = function () {
        return 0.5; // Always return 0.5
    };
});

Tampering with App Logic

Modify app logic (e.g., bypass a login check):

1
2
3
4
5
6
Java.perform(function () {
    var LoginManager = Java.use("com.example.app.LoginManager");
    LoginManager.isLoggedIn.implementation = function () {
        return true; // Always return true
    };
});

Writing Frida Scripts for Automation

1
2
3
4
5
6
7
Java.perform(function () {
    var Button = Java.use("android.widget.Button");
    Button.performClick.implementation = function () {
        console.log("Button clicked!");
        return this.performClick();
    };
});

By referring to the API docs:

1
2
3
4
5
6
7
Java.perform(() => {
  const Activity = Java.use('android.app.Activity');
  Activity.onResume.implementation = function () {
    send('onResume() got called! Let\'s call the original implementation');
    this.onResume();
  };
});

Java.perform(fn): ensure that the current thread is attached to the VM and call fn. (This isn’t necessary in callbacks from Java.) Will defer calling fn if the app’s class loader is not available yet. Use Java.performNow() if access to the app’s classes is not needed.

1
2
frida -U -f de.fgerbig.spacepeng
# After this we are now in frida terminal

We can paste our script in Frida terminal or load it using -l script.js

android.app.Activity android.app.Activity is a fundamental class in Android application development that serves as the entry point for user interaction with an app. This is also called as “root class of all activities”. It manages the lifecycle of a single screen in an application. It supports different states through callback methods like onCreate(), onStart(), and onResume().

Activity Lifecycle Activities transition through multiple states:

  • Created using onCreate()
  • Started with onStart()
  • Resumed with onResume()
  • Paused with onPause()
  • Stopped with onStop()
  • Destroyed with onDestroy()

Activity Lifecycle

In the following code, we can hook onResume and onPause methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Java.perform(function() {
    // Use Java.use to get a handle on the Activity class
    var Activity = Java.use("android.app.Activity");
    
    // Hook the onResume method
    Activity.onResume.implementation = function() {
        console.log("[*] onResume method called");
        
        // Call the original method
        this.onResume();
    };

    // Hook the onPause method
    Activity.onPause.implementation = function() {
        // Log when onPause is called
        console.log("[*] Activity onPause() method called");
        
        // Call the original onPause method
        this.onPause();
    };
});

Java.use() is a Frida method that allows dynamic interaction with Java classes in Android applications.

Syntax:

1
2
var TargetClass = Java.use("full.package.ClassName");
// In our case android.app.Activity android.app is package name and Activity is class name.

In Frida, this is a context-specific keyword that refers to the current method or class instance being hooked. In Java, this is a reference keyword that refers to the current object instance within a class.

To print all classes in the android.app package, you can use the following Frida script:

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function() {
    Java.enumerateLoadedClasses({
        onMatch: function(className) {
            // Filter for android.app package
            if (className.startsWith("android.app.")) {
                console.log("[*] Loaded Class: " + className);
            }
        },
        onComplete: function() {
            console.log("[*] Class enumeration complete");
        }
    });
});

Challenge - Dicer App

https://f-droid.org/repo/org.secuso.privacyfriendlydicer_10.apk

First find out how many Activities you have. You can do that manually or use drozer for it.

1
2
3
4
5
dz> run app.activity.info -a org.secuso.privacyfriendlydicer
Attempting to run shell module
Package: org.secuso.privacyfriendlydicer
  org.secuso.privacyfriendlydicer.ui.SplashActivity
    Permission: null

Let’s see the Launch Intent of the App:

1
2
3
4
5
6
7
8
9
10
11
dz> run app.package.launchintent org.secuso.privacyfriendlydicer
Attempting to run shell module
Launch Intent:
  Action: android.intent.action.MAIN
  Component: {org.secuso.privacyfriendlydicer/org.secuso.privacyfriendlydicer.ui.SplashActivity}
  Data: null
  Categories: 
     - android.intent.category.LAUNCHER
  Flags: [ACTIVITY_NEW_TASK]
  Mime Type: null
  Extras: null

The SplashActivity is the Activity which runs when we launch app and it has "ROLL THE DICE" button. So let’s analyze it.

Let’s reverse the app in jadx-gui:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* loaded from: classes.dex */  
public class SplashActivity extends AppCompatActivity {  
    @Override // androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity  
    protected void onCreate(Bundle bundle) {  
        Intent intent;  
        super.onCreate(bundle);  
        getBaseContext();  
        if (getSharedPreferences("firstShow", 0).getBoolean("isFirstRun", true)) {  
            intent = new Intent(this, (Class<?>) TutorialActivity.class);  
        } else {  
            intent = new Intent(this, (Class<?>) MainActivity.class);  
        }  
        startActivity(intent);  
        finish();  
    }  
}

We can see it starts TutorialActivity or MainActivity depending on the choice.

Let’s see MainActivity:

1
2
3
4
    public void rollDice() {
        applySettings();
        this.dicerViewModel.rollDice();
    }

This method rollDice() looks nice. Let’s look into it. It is defined in org.secuso.privacyfriendlydicer.dicer.Dicer class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.secuso.privacyfriendlydicer.dicer;  
  
import java.security.SecureRandom;  
  
/* loaded from: classes.dex */  
public class Dicer {  
    private static final SecureRandom random = new SecureRandom();  
  
    public int[] rollDice(int i, int i2) {  
        int[] iArr = new int[i];  
        for (int i3 = 0; i3 < i; i3++) {  
            iArr[i3] = random.nextInt(i2) + 1;  
        }  
        return iArr;  
    }  
}

On clicking on Dicer class and “copy as Frida snippet”:

1
2
3
4
5
6
7
let Dicer = Java.use("org.secuso.privacyfriendlydicer.dicer.Dicer");
Dicer["rollDice"].implementation = function (i, i2) {
    console.log(`Dicer.rollDice is called: i=${i}, i2=${i2}`);
    let result = this["rollDice"](i, i2);
    console.log(`Dicer.rollDice result=${result}`);
    return result;
};

Let’s do some modification to it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function () {
    let Dicer = Java.use("org.secuso.privacyfriendlydicer.dicer.Dicer");
    Dicer["rollDice"].implementation = function (i, i2) {
        console.log(`Dicer.rollDice is called: Number of Dice=${i}, Number of faces=${i2}`);
        let result = this["rollDice"](i, i2);
        console.log(`Dicer.rollDice result=${result}`);
        // return result;
        let result2 = [1, 2, 3, 4, 5]; // Override the result with the desired array
        console.log(`Dicer.rollDice returning overridden result=${JSON.stringify(result2)}`); // Use JSON.stringify to log the array
        return result2;

		// Create a Java int array
        // const modified_arr = Java.array('int', [1, 2, 3, 4, 5]);
        // Return the modified array
        // return modified_arr;
    };
});

Function Overloading

Function overloading is a feature in object-oriented programming where multiple functions can share the same name but have different parameter lists.

Java Example of Method Overloading:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Calculator {
    // Method 1: Add two integers
    public int add(int a, int b) {
        return a + b;
    }

    // Method 2: Add three integers
    public int add(int a, int b, int c) {
        return a + b + c;
    }
	public static void main(String[] args) {
        Calculator calc = new Calculator();
        
        // Different method calls
        System.out.println(calc.add(5, 10));         // Calls first method
        System.out.println(calc.add(5, 10, 15));     // Calls second method
    }
}

In Frida you can overload class using .overload(params...):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java.perform(() => {
    const targetClass = Java.use('YourClassName');
    
    // Overload with specific parameter types
    targetClass.methodName.overload('java.lang.String', 'int').implementation = function(str, num) {
        console.log('Hooked method with String and int');
        return this.methodName(str, num);
    };

    // Another overload with different parameters
    targetClass.methodName.overload('int', 'int').implementation = function(a, b) {
        console.log('Hooked method with two ints');
        return this.methodName(a, b);
    };
});

Let’s hook the nextInt method of the java.security.SecureRandom class and make it return 0 in Dice app.

If the number of dice becomes same as number of faces then print 0 on all dices.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Java.perform(function () {
    // Import required classes
    const SecureRandom = Java.use("java.security.SecureRandom");
    const Dicer = Java.use("org.secuso.privacyfriendlydicer.dicer.Dicer");

    // Store the original implementation of nextInt
    const originalNextInt = SecureRandom.nextInt.overload("int").implementation;

    // Hook the rollDice method
    Dicer.rollDice.implementation = function (dices, faces) {
        console.log(`Dicer.rollDice called: dices=${dices}, faces=${faces}`);

        // Check if the number of dices equals the number of faces
        if (dices === faces) {
            // Hook the nextInt method to return 0 when dices == faces
            SecureRandom.nextInt.overload("int").implementation = function (bound) {
                console.log(`SecureRandom.nextInt called with bound: ${bound}`);
                return 0; // Return 0 to make the dice roll result 1 (0 + 1)
            };
        } else {
            // Restore the original nextInt behavior when dices != faces
            SecureRandom.nextInt.overload("int").implementation = originalNextInt;
        }

        // Call the original rollDice method
        const result = this.rollDice(dices, faces);

        // Log the result
        console.log(`Dicer.rollDice result: ${result}`);
        return result;
    };

    console.log("Hooks applied successfully!");
});

Now, next task is to print 1,2,3,4,5,6 on dice.

1
2
3
4
5
6
7
8
9
10
11
12
Java.perform(() => {
    let Dicer = Java.use("org.secuso.privacyfriendlydicer.dicer.Dicer");
    Dicer["rollDice"].implementation = function (dices, faces) {
        console.log(`Dicer.rollDice is called: dices=${dices}, faces=${faces}`);
        let result = this["rollDice"](dices, faces);
        console.log(`Dicer.rollDice result=${result}`);
        // return result;
        let newArray = Java.array('int',[1,2,3,4,5,6]);
        // let newArray = Java.array('int', new Array(6).fill(0));
        return newArray;
    };
});

Root check bypass:

1
2
3
4
5
6
7
8
9
10
Java.perform(() => {
    let RootUtil = Java.use("com.apphacking.rootcheck.RootUtil");
    RootUtil["isDeviceRooted"].implementation = function () {
        console.log(`RootUtil.isDeviceRooted is called`);
        let result = this["isDeviceRooted"]();
        console.log(`RootUtil.isDeviceRooted result=${result}`);
        // return result;
        return false;
    };
});

Actively Calling a Method

Actively calling a method in an Android app using Frida involves invoking a method directly from your Frida script, rather than just hooking or intercepting it. This is useful when you want to trigger specific functionality in the app, manipulate its state, or test how it behaves under certain conditions.

1. Steps to Call a Java Method

  1. Identify the Class and Method:
    • Use tools like jadx or apktool to decompile the APK and find the class and method you want to call.
    • Example: You want to call the login method in the com.example.app.MainActivity class.
  2. Use Java.use() to Get a Reference to the Class:
    • Use Java.use() to get a reference to the class.
  3. Call the Method:
    • Use the reference to call the method directly.

Suppose the app has a login method in the MainActivity class:

1
2
3
4
5
public class MainActivity extends AppCompatActivity {
    public void login(String username, String password) {
        // Logic for login
    }
}

Here’s how you can call this method using Frida:

1
2
3
4
5
6
7
8
Java.perform(function () {
    // Get a reference to the MainActivity class
    var MainActivity = Java.use("com.example.app.MainActivity");

    // Call the login method
    MainActivity.login("user123", "password123");
    console.log("Login method called!");
});

Handling Static Methods

If the method is static, you can call it directly without creating an instance of the class.

1
2
3
4
Java.perform(function () {
    var Utils = Java.use("com.example.app.Utils");
    Utils.doSomethingStatic("Hello, Frida!");
});

Calling Methods on an Existing Instance

If you need to call a method on an existing instance of a class (e.g., an activity that is already running), you can use Java.choose() to find the instance.

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function () {
    Java.choose("com.example.app.MainActivity", {
        onMatch: function (instance) {
            console.log("Found MainActivity instance:", instance);
            instance.login("user12c3", "password123");
        },
        onComplete: function () {
            console.log("Done searching for instances.");
        }
    });
});

2. Steps to Call a Native Function

If the app uses native code (e.g., C/C++), you can also call native functions using Frida.

  • Find the Function Address: Use Module.findExportByName() or Module.getExportByName() to find the address of the native function.
  • Create a NativeFunction Object: Use new NativeFunction() to create a callable function.
  • Call the Function: Call the function with the required arguments.

Suppose the app has a native function int add(int a, int b) in libnative-lib.so.

Here’s how you can call it using Frida:

1
2
3
4
5
6
7
8
9
// Find the address of the native function
var addAddress = Module.findExportByName("libnative-lib.so", "add");

// Define the function signature
var add = new NativeFunction(addAddress, 'int', ['int', 'int']);

// Call the function
var result = add(5, 10);
console.log("Result of add(5, 10):", result);

Java Refresher

Let’s refresh Java.

Class: A blueprint for creating objects. It defines the properties (fields) and behaviors (methods) that the objects will have.

1
2
3
public class Car {
    // Fields and methods go here
}

Object: An instance of a class. For example, Car myCar = new Car(); creates an object of the Car class.

Access Modifiers

Access modifiers control the visibility of classes, fields, and methods:

  • public: Accessible from any other class.
  • private: Accessible only within the same class.
  • protected: Accessible within the same package and subclasses.
  • Default (no modifier): Accessible only within the same package.

In Frida, you can only interact with public fields and methods directly. For private or protected members, you may need to use reflection or other techniques.

Static vs. Non-Static

  • Static: Belongs to the class itself, not to any specific instance. You can access static members using the class name.
1
2
3
4
public static int count = 0; // Static field
public static void printCount() { // Static method
    System.out.println(count);
}

Example usage: Car.count or Car.printCount().

  • Non-Static (Instance Members): Belongs to an instance of the class. You need an object to access them.
1
2
3
4
public int speed; // Non-static field
public void accelerate() { // Non-static method
    speed += 10;
}

Example usage: Car myCar = new Car(); myCar.speed = 100; myCar.accelerate();

In Frida:

  • For static members, use Java.use() to get the class reference and call the method or access the field directly.
  • For non-static members, you need an instance of the class (e.g., using Java.choose() or Java.cast()).

Fields and Methods

  • Field: A variable in a class (also called a member variable).
    1
    
    public int speed; // Field
    
  • Method: A function in a class.
    1
    2
    3
    
    public void accelerate() { // Method
      speed += 10;
    }
    

In Frida:

  • Use Java.use() to access fields and methods.
  • Use .value to get or set the value of a field.
  • Use .implementation to hook a method.

Constructors

A special method used to initialize objects.

1
2
3
public Car(int initialSpeed) { // Constructor
    speed = initialSpeed;
}

In Frida, you can call constructors using $new. A constructor is a special method used to initialize objects. The constructor name must match the class name.

How These Concepts Apply to Frida

Accessing Public Fields

If a class has a public field:

1
2
3
public class Car {
    public int speed;
}

You can access it in Frida like this:

1
2
3
4
5
6
Java.perform(function () {
    var Car = Java.use("com.example.Car");
    var myCar = Car.$new(); // Create a new instance
    myCar.speed.value = 100; // Set the field value
    console.log("Speed:", myCar.speed.value); // Get the field value
});

Accessing Static Fields

If a class has a static field:

1
2
3
public class Car {
    public static int count;
}

You can access it in Frida like this:

1
2
3
4
5
Java.perform(function () {
    var Car = Java.use("com.example.Car");
    Car.count.value = 5; // Set the static field value
    console.log("Count:", Car.count.value); // Get the static field value
});

Calling Public Methods

If a class has a public method:

1
2
3
4
5
public class Car {
    public void accelerate() {
        speed += 10;
    }
}

You can call it in Frida like this:

1
2
3
4
5
Java.perform(function () {
    var Car = Java.use("com.example.Car");
    var myCar = Car.$new(); // Create a new instance
    myCar.accelerate(); // Call the method
});

Calling Static Methods

If a class has a static method:

1
2
3
4
5
public class Car {
    public static void printCount() {
        System.out.println(count);
    }
}

You can call it in Frida like this:

1
2
3
4
Java.perform(function () {
    var Car = Java.use("com.example.Car");
    Car.printCount(); // Call the static method
});

Accessing Private Fields and Methods

Private fields and methods are not directly accessible in Frida. However, you can use reflection to bypass this restriction.

Example: Accessing a private field:

1
2
3
public class Car {
    private int speed;
}

In Frida:

1
2
3
4
5
6
7
8
9
10
Java.perform(function () {
    var Car = Java.use("com.example.Car");
    var myCar = Car.$new(); // Create a new instance

    // Use reflection to access the private field
    var speedField = Car.class.getDeclaredField("speed");
    speedField.setAccessible(true);
    speedField.setInt(myCar, 100); // Set the private field value
    console.log("Speed:", speedField.getInt(myCar)); // Get the private field value
});

Working with Constructors

If a class has a constructor:

1
2
3
4
5
public class Car {
    public Car(int initialSpeed) {
        speed = initialSpeed;
    }
}

You can call it in Frida like this:

1
2
3
4
5
Java.perform(function () {
    var Car = Java.use("com.example.Car");
    var myCar = Car.$new(50); // Call the constructor with an argument
    console.log("Speed:", myCar.speed.value);
});

For demonstration we will use fridaFunc.apk.

We will try to call nextLevel method.

1
2
3
4
5
6
7
8
9
10
11
package com.apphacking.fridafunc;
//....
public class MainActivity extends AppCompatActivity {
//...
    public static void nextLevel() {  
        level++;  
        System.out.println("Current Level is = " + level);  
        System.out.println("Current HighScore is = " + highScore);  
    }
//...
}

Steps to Call a Static Method Using Frida

  • Identify the Class and Method:
    • The class is com.apphacking.fridafunc.MainActivity.
    • The method is nextLevel(), which is public and static.
  • Use Java.use() to Get a Reference to the Class:
    • Use Java.use("com.apphacking.fridafunc.MainActivity") to get a reference to the MainActivity class.
  • Call the Method:
    • Since nextLevel is a static method, you can call it directly using the class reference.
1
2
3
4
5
6
7
8
Java.perform(function () {
    // Get a reference to the MainActivity class
    var MainActivity = Java.use("com.apphacking.fridafunc.MainActivity");

    // Call the nextLevel method
    MainActivity.nextLevel();
    console.log("nextLevel() called!");
});

Since nextLevel is a static method, you don’t need an instance of MainActivity to call it.

To clear adb logs, use adb logcat -c. This will clear non-rooted buffers (main, system, etc.). To clear all buffers (including radio, kernel, etc.) use adb logcat -b all -c.

To print logs of com.apphacking.fridafunc in pidcat:

1
pidcat com.apphacking.fridafunc

Now, let’s try to hook Class Method like increaseLive in Player class. This is not the same as hooking static class method.

The increaseLive method in the Player class is a non-static (instance) method, which means it belongs to an instance of the Player class. Unlike the nextLevel method in the previous example, you cannot call increaseLive directly using the class reference because it requires an instance of the Player class.

Steps to Call a Non-Static Method Using Frida

  • Find an Instance of the Class:
    • You need an existing instance of the Player class to call increaseLive.
    • In your app, the Player instance is created in the MainActivity class:
      1
      2
      3
      4
      5
      6
      7
      8
      
      public class MainActivity extends AppCompatActivity {
      //...
        @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity  
        protected void onCreate(Bundle savedInstanceState) {
      this.player = new Player();
      //...
        }
      }
      
  • Access the Instance:
    • Use Java.choose() to find the existing instance of the Player class in memory.
    • Alternatively, if you have access to the MainActivity instance, you can retrieve the player field from it.
  • Call the Method:
    • Once you have the instance, call the increaseLive method.

Here’s how you can call the increaseLive method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java.perform(function () {
    // Get a reference to the Player class
    var Player = Java.use("com.apphacking.fridafunc.Player");

    // Find an existing instance of the Player class
    Java.choose("com.apphacking.fridafunc.Player", {
        onMatch: function (instance) {
            console.log("Found Player instance:", instance);

            // Call the increaseLive method
            instance.increaseLive();
            console.log("increaseLive() called!");
        },
        onComplete: function () {
            console.log("Done searching for Player instances.");
        }
    });
});

Alternative: Accessing Player from MainActivity

If you have access to the MainActivity instance, you can retrieve the player field and call increaseLive on it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java.perform(function () {
    // Get a reference to the MainActivity class
    var MainActivity = Java.use("com.apphacking.fridafunc.MainActivity");

    // Find an existing instance of MainActivity
    Java.choose("com.apphacking.fridafunc.MainActivity", {
        onMatch: function (instance) {
            console.log("Found MainActivity instance:", instance);

            // Access the player field
            var player = instance.player.value;
            console.log("Player instance:", player);

            // Call the increaseLive method
            player.increaseLive();
            console.log("increaseLive() called!");
        },
        onComplete: function () {
            console.log("Done searching for MainActivity instances.");
        }
    });
});

But this will find the instance in the memory and then update it. To create a new instance in memory, you need to use $new(). $new() is used to create a new instance of a class in memory. This is particularly useful when you want to create a fresh object and interact with it, rather than relying on existing instances.

When to Use $new()

  • Use $new() when you want to create a new object of a class.
  • This is useful when:
    • No instances of the class exist in memory.
    • You want to create a separate instance for testing or manipulation.
    • You want to bypass existing logic and use your own instance.

How to Use $new()

  • Get a Reference to the Class:
    • Use Java.use() to get a reference to the class.
  • Create a New Instance:
    • Use $new() to create a new instance of the class.
    • If the class has a constructor, pass the required arguments to $new().
  • Interact with the Instance:
    • Call methods or access fields on the newly created instance.

Here’s how you can create a new Player instance and call its methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function () {
    // Get a reference to the Player class
    var Player = Java.use("com.apphacking.fridafunc.Player");

    // Create a new instance of Player
    var newPlayer = Player.$new();
    console.log("New Player instance created:", newPlayer);

    // Call the increaseLive method on the new instance
    newPlayer.increaseLive();
    console.log("increaseLive() called on the new Player instance!");

    // Access and modify fields
    // console.log("Initial lives:", newPlayer.lives.value);
    // newPlayer.lives.value = 10;
    // console.log("Updated lives:", newPlayer.lives.value);
});

To verify that a new instance of the Player class has been created, we can check the logcat output. The constructor of the Player class contains a System.out.println statement that prints: "A new player object has been created!"

When you create a new instance of the Player class using $new() in your Frida script, this message will appear in the logcat logs, confirming that the constructor was successfully called and a new object was instantiated.

Summary

  • Use $new() to create new instances of a class.
  • Use Java.choose() to find existing instances in memory.
  • Combine these techniques to interact with both existing and new objects in the app.

Frida does not allow direct assignment to fields in this way. Instead, you must use .value to access or modify the field.

If you try to increase lives by passing the script as -l, you will most likely fail. But if you copy paste the code you will succeed.

The issue is likely related to timing. When you pass the script using -l script.js, Frida injects the script immediately when attaching to the process. If the Player instance hasn’t been created yet, the script won’t find it, and nothing will happen.

When you paste the code into the Frida console, the app has already started, and the Player instance exists in memory, so it works.

The Player instance is created during the app’s runtime (e.g., in MainActivity.onCreate()), so if the script runs too early, it won’t find the instance.

Hook MainActivity.onCreate() to Wait for Initialization

You can hook the onCreate() method of MainActivity to ensure the script runs only after the app has initialized.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Java.perform(function () {
    var MainActivity = Java.use("com.apphacking.fridafunc.MainActivity");

    // Hook onCreate to wait for app initialization
    MainActivity.onCreate.implementation = function (savedInstanceState) {
        // Call the original onCreate method
        this.onCreate(savedInstanceState);

        // Now search for Player instances
        var Player = Java.use("com.apphacking.fridafunc.Player");
        Java.choose("com.apphacking.fridafunc.Player", {
            onMatch: function (instance) {
                console.log("Found Player instance:", instance);
                instance.increaseLive();
                console.log("increaseLive() called!");
                instance.lives.value = 100;
                console.log("lives =", instance.lives.value);
            },
            onComplete: function () {
                console.log("Done searching for Player instances.");
            }
        });
    };
});

Let’s hack SpacePeng Game.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Frida Script to Print Loaded Classes
setTimeout(function () {
    Java.perform(function () {
        // Specify the package name of your app
        var targetPackage = "de.fgerbig.spacepeng";

        // Callback object for enumerateLoadedClasses
        var callbacks = {
            onMatch: function (className) {
                // Filter classes by the target package
                if (className.startsWith(targetPackage)) {
                    console.log(className);
                }
            },
            onComplete: function () {
                console.log("Enumeration complete!");
            }
        };

        // Enumerate all loaded classes
        Java.enumerateLoadedClasses(callbacks);
    });
}, 3000); // Delay execution by 3 second to ensure Java runtime is ready
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Frida Script to Print Methods in a Class

Java.perform(function () {
    // Specify the fully qualified class name
    var className = "de.fgerbig.spacepeng.SpacePeng";

    try {
        // Get a reference to the class
        var targetClass = Java.use(className);

        // Enumerate all methods in the class
        var methods = targetClass.class.getDeclaredMethods();

        // Print each method's name
        console.log("Methods in class " + className + ":");
        methods.forEach(function (method) {
            console.log(method.toString());
        });
    } catch (e) {
        console.error("Error: " + e.message);
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Increase Lives

Java.perform(() => {
    // Get reference to Player class
    let Player = Java.use("de.fgerbig.spacepeng.components.Player");

    // Function to modify player lives
    function modifyPlayerLives() {
        Java.choose("de.fgerbig.spacepeng.components.Player", {
            onMatch: function (instance) {
                console.log("Found Player instance @ ", instance);
                console.log(`Initial lives: ${instance.lives.value}`);
                instance.lives.value = 100; // Set lives to 100
                console.log(`Final lives: ${instance.lives.value}`);
            },
            onComplete: function () {
                console.log("Done searching for Player instances.");
            }
        });
    }

    // Delay execution to ensure the Player object is created
    setTimeout(() => {
        console.log("Waiting for game to start...");
        modifyPlayerLives();
    }, 5000); // Adjust the delay (in milliseconds) as needed
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Continuously scan Player object and set lives to 100

Java.perform(() => {
    // Get reference to the Player class
    let Player = Java.use("de.fgerbig.spacepeng.components.Player");

    // Function to scan for Player instances
    function scanForPlayer() {
        console.log("Scanning for Player instance...");

        // Use Java.choose to find Player instances
        Java.choose("de.fgerbig.spacepeng.components.Player", {
            onMatch: function (instance) {
                console.log("Found Player instance @ ", instance);
                console.log(`Initial lives: ${instance.lives.value}`);
                instance.lives.value = 100; // Set lives to 100
                console.log(`Final lives: ${instance.lives.value}`);
            },
            onComplete: function () {
                console.log("Scan complete. No Player instance found yet. Retrying...");
                // Retry after a short delay
                setTimeout(scanForPlayer, 1000); // Retry every 1 second
            }
        });
    }

    // Start scanning
    scanForPlayer();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Scan for Player object, increase live to 100 and then stop scanning!

Java.perform(() => {
    // Get reference to the Player class
    let Player = Java.use("de.fgerbig.spacepeng.components.Player");

    // Flag to control scanning
    let playerFound = false;

    // Function to scan for Player instances
    function scanForPlayer() {
        if (playerFound) {
            console.log("Player already found. Stopping scan.");
            return; // Stop scanning if the Player object has already been found
        }

        console.log("Scanning for Player instance...");

        // Use Java.choose to find Player instances
        Java.choose("de.fgerbig.spacepeng.components.Player", {
            onMatch: function (instance) {
                console.log("Found Player instance @ ", instance);
                console.log(`Initial lives: ${instance.lives.value}`);
                instance.lives.value = 100; // Set lives to 100
                console.log(`Final lives: ${instance.lives.value}`);
                playerFound = true; // Set the flag to true to stop further scans
            },
            onComplete: function () {
                if (!playerFound) {
                    console.log("Player instance not found yet. Retrying...");
                    // Retry after a short delay
                    setTimeout(scanForPlayer, 1000); // Retry every 1 second
                } else {
                    console.log("Player instance found and modified. Stopping scan.");
                }
            }
        });
    }

    // Start scanning
    scanForPlayer();
});

Increase High Score

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Increase HighScore

Java.perform(() => {
    // Get reference to the Player class
    let Profile = Java.use("de.fgerbig.spacepeng.services.Profile");

    // Flag to control scanning
    let profileFound = false;

    // Function to scan for Player instances
    function scanForProfile() {
        if (profileFound) {
            console.log("Profile already found. Stopping scan.");
            return; // Stop scanning if the Profile object has already been found
        }

        console.log("Scanning for Profile instance...");

        // Use Java.choose to find Profile instances
        Java.choose("de.fgerbig.spacepeng.services.Profile", {
            onMatch: function (instance) {
                console.log("Found Profile instance => ", instance);
                let profileHighScore = instance.getHighScore();
                console.log("Profile HighScore: ", profileHighScore);
                console.log("Setting HighScore => 999999");
                instance.setHighScore(999999);
                console.log("Profile New HighScore: ", instance.getHighScore());
                profileFound = true; // Set the flag to true to stop further scans
            },
            onComplete: function () {
                if (!profileFound) {
                    console.log("Profile instance not found yet. Retrying...");
                    // Retry after a short delay
                    setTimeout(scanForProfile, 1000); // Retry every 1 second
                } else {
                    console.log("Profile instance found and modified. Stopping scan.");
                }
            }
        });
    }

    // Start scanning
    scanForProfile();
});

We can also change the HighScore using different way, like changing the return value of getHighScore() method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Java.perform(function () {

    let profileFound = false;
    function scanForProfile() {
        if (profileFound) {
            console.log("Profile already found. Stopping scanning.");
            return;
        }
        console.log("Scanning for Profile instances...");


        Java.choose("de.fgerbig.spacepeng.services.Profile", {
            onMatch: function (instance) {
                console.log("Found Profile instance =>", instance);
                instance.getHighScore.implementation = function () {
                    return 8080;
                }
                profileFound = true;

            },
            onComplete: function () {
                // console.log("Scanning Profile instance completed!");
                if (!profileFound) {
                    console.log("Profile instance not found. Retrying...");
                    setTimeout(scanForProfile, 1000);
                } else {
                    console.log("Profile instance found and modified. Stopping scan.");
                }
            }
        });
    }
    scanForProfile();
});

Let’s increase current Score in Game.

It is defined as a field in Player class.

We all know we cannot access an instance field without an instance. Let’s search for Player class instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Java.perform(() => {
    // Get reference to the Player class
    let Player = Java.use("de.fgerbig.spacepeng.components.Player");

    // Flag to control scanning
    let playerFound = false;

    // Function to scan for Player instances
    function scanForPlayer() {
        if (playerFound) {
            console.log("Player already found. Stopping scan.");
            return; // Stop scanning if the Player object has already been found
        }

        console.log("Scanning for Player instance...");

        // Use Java.choose to find Player instances
        Java.choose("de.fgerbig.spacepeng.components.Player", {
            onMatch: function (instance) {
                console.log("Found Player instance @ ", instance);
                console.log(`Initial Score: ${instance.score.value}`);
                instance.score.value = 99999;
                console.log(`Modified Score: ${instance.score.value}`);
                playerFound = true; // Set the flag to true to stop further scans
            },
            onComplete: function () {
                if (!playerFound) {
                    console.log("Player instance not found yet. Retrying...");
                    // Retry after a short delay
                    setTimeout(scanForPlayer, 1000); // Retry every 1 second
                } else {
                    console.log("Player instance found and modified. Stopping scan.");
                }
            }
        });
    }

    // Start scanning
    scanForPlayer();
});

Instance as a Parameter

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.apphacking.fridainstance;  
  
/* loaded from: classes2.dex */  
public class Boss {  
    Item itemDrop;  
    int hitpoints = 10;  
    int power = 9000;  
  
    Boss(Item item) {  
        System.out.println("A new boss has been created!");  
        this.itemDrop = item;  
    }  
}

First we need to create a Boss object.

1
Boss boss = new Boss();
1
2
3
4
5
6
7
8
9
10
11
12
Java.perform(() => {
    // Boss Reference
    let bossClassReference = Java.use('com.apphacking.fridainstance.Boss');
    // Boss boss = new Boss(item);
    // Get Item class reference
    let itemClassReference = Java.use('com.apphacking.fridainstance.Item');
    // Item item = new Item(int itemPower);
    let itemPower = 1234;
    let itemNewInstance = itemClassReference.$new(itemPower);

    let bossNewInstance = bossClassReference.$new(itemNewInstance);
});

Existing Instance as a Parameter

1

Create Multiple Player Shots

We need to use the existing World object and pass it to Entity

1
 public static Entity createBackground(World world, String name);

Constructor Hooking

A constructor is a special method in a class that is called when a new instance of the class is created.

Hooking a constructor means intercepting the constructor call and executing custom code before or after the original constructor logic.

Steps to Hook a Constructor

  • Get a Reference to the Class:
    • Use Java.use() to get a reference to the class whose constructor you want to hook.
  • Hook the Constructor:
    • Use .$init.implementation to override the constructor.
  • Add Custom Logic:
    • Execute custom code before or after calling the original constructor.
  • Call the Original Constructor (Optional):
    • Use this.$init() to call the original constructor if needed.

Suppose you have the following Java class:

1
2
3
4
5
6
7
8
public class Player {
    int lives;

    public Player() {
        this.lives = 5;
        System.out.println("Player created with lives: " + this.lives);
    }
}

Here’s how you can hook the constructor using Frida:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function () {
    // Get a reference to the Player class
    var Player = Java.use("com.apphacking.fridafunc.Player");

    // Hook the constructor
    Player.$init.implementation = function () {
        // Custom logic before the original constructor
        console.log("Player constructor called!");

        // Call the original constructor
        this.$init();

        // Custom logic after the original constructor
        this.lives.value = 10; // Modify the lives field
        console.log("Player created with lives:", this.lives.value);
    };
});

If the constructor takes arguments, you can hook it and manipulate the arguments.

1
2
3
4
5
6
7
8
public class Player {
    int lives;

    public Player(int initialLives) {
        this.lives = initialLives;
        System.out.println("Player created with lives: " + this.lives);
    }
}

Here’s how to hook this constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java.perform(function () {
    var Player = Java.use("com.apphacking.fridafunc.Player");

    // Hook the constructor with arguments
    Player.$init.overload("int").implementation = function (initialLives) {
        // Custom logic before the original constructor
        console.log("Player constructor called with lives:", initialLives);

        // Modify the argument
        var newLives = initialLives + 5;

        // Call the original constructor with the modified argument
        this.$init(newLives);

        // Custom logic after the original constructor
        console.log("Player created with lives:", this.lives.value);
    };
});

In fridaInst.apk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java.perform(() => {
    // Get a reference to the Item class
    let Item = Java.use("com.apphacking.fridainstance.Item");

    // Hook the constructor with an int argument
    Item.$init.overload('int').implementation = function (itemPower) {
        // Log the original itemPower value
        console.log("Original itemPower => ", itemPower);

        // Call the original constructor with a modified value
        this.$init(1337);

        // Log the modified itemPower
        console.log("itemPower changed to:", this.itemPower.value);

        // Access and log the name field
        console.log("Item name:", this.name.value);
    };
});

Advanced: Accessing Private Fields

If the name field were private, you would need to use reflection to access it. Here’s how:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(() => {
    let Item = Java.use("com.apphacking.fridainstance.Item");

    Item.$init.overload('int').implementation = function (itemPower) {
        // Call the original constructor
        this.$init(1337);

        // Use reflection to access the private name field
        var nameField = Item.class.getDeclaredField("name");
        nameField.setAccessible(true);
        var nameValue = nameField.get(this);

        // Log the name field
        console.log("Item name:", nameValue);
    };
});

Manipulating UI Thread

Manipulating the UI Thread in Android using Frida involves interacting with the main thread to update UI components or execute code that affects the user interface. Since Android’s UI toolkit is not thread-safe, all UI updates must be performed on the main thread (also called the UI thread). Frida provides tools to schedule code execution on the main thread, ensuring that UI manipulations are safe and effective.

Understanding the UI Thread

The UI thread is the main thread in an Android app where all UI updates and event handling occur. If you try to update the UI from a background thread, the app will crash with a CalledFromWrongThreadException. Frida provides Java.scheduleOnMainThread() to execute code on the UI thread safely.

Using Java.scheduleOnMainThread()

The Java.scheduleOnMainThread() function allows you to schedule a block of code to run on the main thread. This is essential for manipulating UI components.

1
2
3
Java.scheduleOnMainThread(function () {
    // Code to run on the main thread
});

Example: Updating a TextView

Suppose the app has a TextView with the ID textViewLives in the MainActivity class. Here’s how you can update its text using Frida:

Java Code (App):

1
2
3
4
5
6
7
8
9
10
public class MainActivity extends AppCompatActivity {
    TextView txtViewLive;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        txtViewLive = findViewById(R.id.textViewLives);
    }
}

Frida Script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Java.perform(function () {
    // Get a reference to the MainActivity class
    var MainActivity = Java.use("com.apphacking.fridainstance.MainActivity");

    // Schedule code to run on the main thread
    Java.scheduleOnMainThread(function () {
        // Find the MainActivity instance
        Java.choose("com.apphacking.fridainstance.MainActivity", {
            onMatch: function (instance) {
                console.log("Found MainActivity instance:", instance);

                // Update the TextView text
                instance.txtViewLive.setText("Lives: 100");
                console.log("TextView updated!");
            },
            onComplete: function () {
                console.log("Done searching for MainActivity instances.");
            }
        });
    });
});

We will try to create a new Alien object in fridaFunc.apk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// new Alien(320);
package com.apphacking.fridafunc;  
  
import android.view.View;  
import android.widget.ImageView;  
import androidx.constraintlayout.widget.ConstraintSet;  
  
/* loaded from: classes2.dex */  
public class Alien {  
    Alien(int margin) {  
        System.out.println("A new Alien object has been created");  
        MainActivity.alien = new ImageView(MainActivity.mainContext);  
        MainActivity.alien.setBackgroundResource(C0459R.drawable.alien);  
        MainActivity.alien.setId(View.generateViewId());  
        MainActivity.mainLayout.addView(MainActivity.alien, 0);  
        ConstraintSet set = new ConstraintSet();  
        set.clone(MainActivity.mainLayout);  
        set.connect(MainActivity.alien.getId(), 3, MainActivity.mainLayout.getId(), 3, margin);  
        set.connect(MainActivity.alien.getId(), 1, MainActivity.mainLayout.getId(), 1);  
        set.connect(MainActivity.alien.getId(), 2, MainActivity.mainLayout.getId(), 2);  
        set.applyTo(MainActivity.mainLayout);  
    }  
}

If we use:

1
2
3
4
5
6
7
8
// Alien alien = new Alien(123);

Java.perform(() => {
    // Get Alien Class Reference
    let Alien = Java.use("com.apphacking.fridafunc.Alien");
    // Get Alien Class Instance
    let AlienInstance = Alien.$new(444);
});

We will get error

Error: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Steps to Inject a Toast

  1. Identify the Target: Determine where you want to inject the Toast (e.g., in a specific method or activity).

  2. Use Frida to Hook the Method: Hook the target method and inject the Toast code.

  3. Display the Toast: Use the Toast.makeText() method to create and show the Toast.

Here’s a Frida script that injects a Toast message into the onCreate method of MainActivity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Java.perform(function () {
    // Import required classes
    const MainActivity = Java.use("com.apphacking.fridafunc.MainActivity"); // Replace with the correct class name
    const Toast = Java.use("android.widget.Toast");
    const Context = Java.use("android.content.Context");

    // Hook the onCreate method
    MainActivity.onCreate.implementation = function (savedInstanceState) {
        console.log("MainActivity.onCreate() called");

        // Call the original onCreate method
        this.onCreate(savedInstanceState);

        // Create and show a Toast message
        const toastMessage = "Hello from Frida!";
        const toast = Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT.value);
        toast.show();

        console.log("Toast injected successfully!");
    };
});

NDK

You can refer my blog on Android NDK

The Android Native Development Kit (NDK) is a toolset that allows you to implement parts of your Android app using native-code languages such as C and C++. This is particularly useful for performance-critical applications, reusing existing C/C++ libraries, or accessing low-level system features.

Before diving into Android NDK development, ensure you have the following:

  • Basic knowledge of Android development (Java/Kotlin).
  • Familiarity with C/C++ programming.
  • Android Studio installed (latest version recommended).
  • NDK and CMake installed (via SDK Manager in Android Studio).

Step 1: Set Up Your Environment

  1. Install NDK and CMake:
    • Open Android Studio.
    • Go to Tools > SDK Manager > SDK Tools.
    • Check NDK (Native Development Kit) and CMake.
    • Click Apply and wait for the installation to complete.
  2. Verify Installation:
    • Open a terminal and run:
      1
      
      ndk-build --version
      

Step 2: Create a New Project with NDK Support

  • Open Android Studio and create a new project.
  • Select Native C++ as the template.
  • Configure your project (name, package name, etc.).
  • Set the Minimum API level (API 21 or higher is recommended for NDK development).
  • Click Finish.

Step 3: Understand the Project Structure

  • app/src/main/cpp/: Contains your native C/C++ code.
  • CMakeLists.txt: Configuration file for building native libraries.
  • app/build.gradle: Links the native library to your Android app.

Step 4: Write Your First Native Code

  • Open the native-lib.cpp file in the cpp folder.
  • Replace the existing code with a simple function:
1
2
3
4
5
6
7
8
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(JNIEnv* env, jobject /* this */) {
    std::string hello = "Hello from C++!";
    return env->NewStringUTF(hello.c_str());
}

This function returns a string to your Java/Kotlin code.

Explanation:

  • JNIEXPORT and JNICALL are macros for JNI functions.
  • Java_com_example_myapp_MainActivity_stringFromJNI is the function name, following the JNI naming convention: Java_ + package_name + class_name + method_name.

Step 5: Link Native Code to Java/Kotlin

  • Open MainActivity.java or MainActivity.kt.
  • Add the following code to load the native library and call the native method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    public native String stringFromJNI();
}
  • System.loadLibrary("native-lib") loads the native library.
  • stringFromJNI() is the native method declared in the C++ file.

Step 6: Build and Run the App

  • Click Run in Android Studio.
  • The app should display "Hello from C++!" on the screen.

Step 7: Understand CMake

The CMakeLists.txt file is used to build your native code. Here’s a basic example:

cmake_minimum_required(VERSION 3.4.1)

add_library(
    native-lib
    SHARED
    src/main/cpp/native-lib.cpp
)

find_library(
    log-lib
    log
)

target_link_libraries(
    native-lib
    ${log-lib}
)
  • add_library: Defines the native library.
  • find_library: Finds system libraries (e.g., log for logging).
  • target_link_libraries: Links the native library to system libraries.

NDK Hooking

Frida can hook into native C/C++ functions, making it a powerful tool for analyzing and modifying native code behavior.

Step 1: Identify the Target Native Function

  1. Decompile the APK using tools like Jadx or APKTool.
  2. Locate the native library (.so file) and the function you want to hook.
  3. Use nm or readelf to list symbols in the .so file:
1
nm -D libnative-lib.so

Look for the function name (e.g., Java_com_example_myapp_MainActivity_stringFromJNI).

Step 2: Write a Frida Script to Hook the Native Function

  1. Create a JavaScript file (e.g., hook.js) with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Hook a native function
function hookNativeFunction() {
    // Replace with the actual function name and module
    const functionName = "Java_com_example_myapp_MainActivity_stringFromJNI";
    const moduleName = "libnative-lib.so";

    // Get the base address of the module
    const moduleBase = Module.findBaseAddress(moduleName);
    if (moduleBase) {
        console.log(`[+] Found module ${moduleName} at ${moduleBase}`);

        // Find the function address
        const functionAddress = Module.findExportByName(moduleName, functionName);
        if (functionAddress) {
            console.log(`[+] Found function ${functionName} at ${functionAddress}`);

            // Intercept the function
            Interceptor.attach(functionAddress, {
                onEnter: function (args) {
                    console.log(`[+] ${functionName} called`);
                    // Log arguments (if any)
                    for (let i = 0; i < args.length; i++) {
                        console.log(`  arg[${i}]: ${args[i]}`);
                    }
                },
                onLeave: function (retval) {
                    console.log(`[+] ${functionName} returned: ${retval}`);
                    // Modify the return value (if needed)
                    retval.replace(0x42); // Example: Replace return value with 0x42
                }
            });
        } else {
            console.log(`[-] Function ${functionName} not found`);
        }
    } else {
        console.log(`[-] Module ${moduleName} not found`);
    }
}

// Main entry point
Java.perform(function () {
    hookNativeFunction();
});
  1. Explanation:
    • Module.findBaseAddress: Finds the base address of the native library.
    • Module.findExportByName: Finds the address of the target function.
    • Interceptor.attach: Hooks the function and allows you to inspect/modify its behavior.

Attach Frida to the target app:

1
frida -U -n com.example.myapp -l hook.js

Interceptor.attach(target, callbacks[, data]): intercept calls to function at target. This is a NativePointer specifying the address of the function you would like to intercept calls to. Note that on 32-bit ARM this address must have its least significant bit set to 0 for ARM functions, and 1 for Thumb functions. Frida takes care of this detail for you if you get the address from a Frida API (for example Module.getExportByName()).

The callbacks argument is an object containing one or more of:

  • onEnter(args): callback function given one argument args that can be used to read or write arguments as an array of NativePointer objects. {: #interceptor-onenter}
  • onLeave(retval): callback function given one argument retval that is a NativePointer-derived object containing the raw return value. You may call retval.replace(1337) to replace the return value with the integer 1337, or retval.replace(ptr("0x1234")) to replace with a pointer. Note that this object is recycled across onLeave calls, so do not store and use it outside your callback. Make a deep copy if you need to store the contained value, e.g.: ptr(retval.toString()).

SYNTAX:

1
2
3
4
5
6
7
8
9
10
Interceptor.attach(Module.getExportByName('libc.so', 'read'), {
  onEnter(args) {
    this.fileDescriptor = args[0].toInt32();
  },
  onLeave(retval) {
    if (retval.toInt32() > 0) {
      /* do something with this.fileDescriptor */
    }
  }
});

Module.getExportByName(moduleName|null, exportName): returns the absolute address of the export named exportName in moduleName. If the module isn’t known you may pass null instead of its name, but this can be a costly search and should be avoided.

In our ndkFrida.apk app we can directly hook the return value of NDK function.

1
2
3
4
5
6
7
8
9
Java.perform(() => {
    let MainActivity = Java.use("com.apphacking.ndkfrida.MainActivity");
    MainActivity["decryptString"].implementation = function (str, i) {
        console.log(`MainActivity.decryptString is called: str=${str}, i=${i}`);
        let result = this["decryptString"](str, i);
        console.log(`MainActivity.decryptString result=${result}`);
        return result;
    };
});

But if the function is not called in Java e.g. (strcpy) and used in C/C++ then we need to use Interceptor.attach().

Enumerate JNI/NDK Functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function enumerateJNIFunctions() {
    // Iterate over all loaded modules
    Process.enumerateModules({
        onMatch: function (module) {
            console.log(`\n[+] Module: ${module.name} (Base: ${module.base})`);

            // Enumerate all exports in the module
            Module.enumerateExports(module.name, {
                onMatch: function (export) {
                    // Filter for JNI functions (prefixed with "Java_")
                    if (export.name.startsWith("Java_")) {
                        console.log(`  [*] JNI Function: ${export.name} at ${export.address}`);
                    }
                },
                onComplete: function () {
                    console.log(`  [*] Finished enumerating exports for ${module.name}`);
                }
            });
        },
        onComplete: function () {
            console.log("\n[+] Finished enumerating all modules");
        }
    });
}

// Main entry point
Java.perform(function () {
    enumerateJNIFunctions();
});

In Frida, the Module API provides methods to enumerate different aspects of a loaded module, such as imports, exports, and symbols.

  • Enumerate Exports
    • Lists all functions that a module exports (e.g., JNI functions in a shared library).
    • These are functions that other modules or the Android system can use.
1
2
3
4
5
6
7
8
9
Module.enumerateExports("libnative-lib.so", {
    onMatch: function(exp) {
        console.log("[+] Exported function: " + exp.name + " at " + exp.address);
    },
    onComplete: function() {
        console.log("[*] Finished enumerating exports.");
    }
});

  • Enumerate Imports
    • Lists all functions that a module imports from another library.
    • These are dependencies the module requires to function.
1
2
3
4
5
6
7
8
Module.enumerateImports("libnative-lib.so", {
    onMatch: function(imp) {
        console.log("[+] Imported function: " + imp.name + " from " + imp.module);
    },
    onComplete: function() {
        console.log("[*] Finished enumerating imports.");
    }
});
  • Enumerate Symbols
    • Lists all symbols in the module, including exports, statically linked functions, and global variables.
1
2
3
4
5
6
7
8
Module.enumerateSymbols("libnative-lib.so", {
    onMatch: function(sym) {
        console.log("[+] Symbol: " + sym.name + " at " + sym.address);
    },
    onComplete: function() {
        console.log("[*] Finished enumerating symbols.");
    }
});

The Java.cast function in Frida is used to cast an object to a specific Java class when dealing with Java objects. This is helpful when you have a generic JavaObject reference and need to use methods or properties from a specific class.

SYNTAX:

1
2
var myClass = Java.use("com.example.MyClass");
var castedObject = Java.cast(myObject, myClass);

When working with Frida, you may encounter JNI functions that return a jstring (Java string). Since jstring is a pointer in native code, you need to convert it into a readable format.

Java.cast(handle, klass): create a JavaScript wrapper given the existing instance at handle of given class klass as returned from Java.use().

1
2
const Activity = Java.use('android.app.Activity');
const activity = Java.cast(ptr('0x1234'), Activity);

Frida Cheatsheet

1. Print all the classes available in a target application

1
2
3
4
5
6
7
8
9
10
11
12
Java.perform(function () {
    Java.enumerateLoadedClasses({
        onMatch: function (className) {
            if (className.startsWith("com.example.app")) {
                console.log(className);
            }
        },
        onComplete: function () {
            console.log("Done enumerating classes!");
        }
    });
});

2. Print the methods of a specific class

1
2
3
4
5
6
7
8
9
Java.perform(function () {
    var className = "com.example.app.TargetClass"; // Replace with your target class name
    var targetClass = Java.use(className);

    var methods = targetClass.class.getDeclaredMethods();
    methods.forEach(function (method) {
        console.log(method.toString());
    });
});

4. Print the fields of a specific class

1
2
3
4
5
6
7
8
9
Java.perform(function () {
    var className = "com.example.app.TargetClass"; // Replace with your target class name
    var targetClass = Java.use(className);

    var fields = targetClass.class.getDeclaredFields();
    fields.forEach(function (field) {
        console.log(field.toString());
    });
});

5. Access variables (fields) of all types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(function () {
    var className = "com.example.app.TargetClass"; // Replace with your target class name
    var targetClass = Java.use(className);

    var fields = targetClass.class.getDeclaredFields();
    fields.forEach(function (field) {
        field.setAccessible(true); // Make the field accessible
        var fieldName = field.getName();
        var fieldType = field.getType().getName();
        var fieldValue = targetClass[fieldName].value; // Access the field value

        console.log("Field Name: " + fieldName);
        console.log("Field Type: " + fieldType);
        console.log("Field Value: " + fieldValue);
    });
});

6. Start an activity in an Android application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Java.perform(function () {
    // Import required Java classes
    var Intent = Java.use("android.content.Intent");
    var MainActivity = Java.use("com.example.app.MainActivity"); // Replace with the actual MainActivity class
    var LoginActivity = Java.use("com.example.app.LoginActivity"); // Replace with the actual LoginActivity class

    // Get the current activity instance
    var currentActivity = Java.use("android.app.ActivityThread").currentActivity().get();

    // Create a new intent to start LoginActivity
    var intent = Intent.$new(currentActivity, LoginActivity.class);

    // Set the FLAG_ACTIVITY_NEW_TASK flag to start the activity in a new task
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK.value);

    // Start the LoginActivity
    currentActivity.startActivity(intent);

    console.log("Started LoginActivity from MainActivity!");
});
This post is licensed under CC BY 4.0 by the author.