Frida Function Interception
Today, we’re going to use Frida to intercept function calls, return values, and bypass checks. We’re going to start with the basics.
We have 3 challenges, and we’re going to start with the 1st example function.
Let’s use frida-trace to trace function calls in the running process.
1
| frida-trace -U -j 'io.hextree.*!*' FridaTarget
|
Clicking the 1st example function resulted in:
1
2
3
4
5
6
7
8
9
10
| 1381 ms InterceptionFragment$2.onClick(
<instance: android.view.View>,
<className: com.google.android.material.button.MaterialButton>
)
1382 ms └─ InterceptionFragment.function_to_intercept(
"example argument"
)
1382 ms └─ Returned: "EXAMPLE ARGUMENT"
|
As we can see, it’s returning "EXAMPLE ARGUMENT" as its return value. What if we want to manipulate that?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Intercept the return value of function_to_intercept
Java.perform(() => {
// Get the target class and store it in our variable
var InterceptionFragment = Java.use("io.hextree.fridatarget.ui.InterceptionFragment");
// Hook the function via .implementation
// .implementation allows us to replace the original code of a specific function
InterceptionFragment.function_to_intercept.implementation = function(argument) {
// Run the original function first
this.function_to_intercept(argument);
// Return our custom value instead of the original
return "SOMETHING DIFFERENT";
};
});
|
Clicking the first function now returns “SOMETHING DIFFERENT” instead of the original value.
Bypassing Checks
Clicking the 2nd function that checks the license, we can see that it returns “License is not valid.”
1
2
3
4
5
6
7
8
9
10
11
12
| 1391 ms InterceptionFragment$3.onClick(
<instance: android.view.View>,
<className: com.google.android.material.button.MaterialButton>
)
1392 ms └─ InterceptionFragment.license_check()
└─ LicenseManager.isLicenseValid()
└─ Returned: false
1393 ms └─ InterceptionFragment$1.$init(
<instance: io.hextree.fridatarget.ui.InterceptionFragment>
)
|
isLicenseValid() is the source of the false return value. Let’s hook it directly and force it to return true.
1
2
3
4
5
6
7
8
9
10
11
12
13
| Java.perform(() => {
// Get the target class and store it in our variable
var LicenseManager = Java.use("io.hextree.fridatarget.LicenseManager");
// Hook isLicenseValid and force it to always return true
LicenseManager.isLicenseValid.implementation = function() {
return true;
};
});
|
This bypasses the license check and reveals the 2nd flag.
Bypassing a Timestamp Check
Clicking the 3rd function gives us this trace:
1
2
3
4
5
6
7
8
9
10
11
12
| 993 ms InterceptionFragment$4.onClick(
<instance: android.view.View>,
<className: com.google.android.material.button.MaterialButton>
)
994 ms └─ InterceptionFragment.license_check_2()
996 ms └─ LicenseManager.isLicenseStillValid(
<instance: android.content.Context,
$className: io.hextree.fridatarget.MainActivity>,
"1779349231"
)
|
Decompiling the APK in JADX reveals the actual logic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public static void isLicenseStillValid(Context context, long unixTimestamp) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
if (unixTimestamp > 1672531261) {
Log.d("LicenseManager", "The license expired on 01.01.2023.");
builder.setMessage("License expired.");
} else {
builder.setMessage(FlagCryptor.decode("VUtHe3ZhZ3JlcHJjZ3ZhdC1ndnpyLXZmLXJuZmx9"));
}
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
|
The function shows the flag only if unixTimestamp is less than or equal to 1672531261. Since the app is passing a timestamp of 1779349231 which is greater, it hits the expired branch. Since we can intercept the function, we can force a timestamp of 0 which will always pass the check.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| Java.perform(() => {
// Get the target class and store it in our variable
var LicenseManager = Java.use("io.hextree.fridatarget.LicenseManager");
// Hook isLicenseStillValid and force a timestamp that passes the check
LicenseManager.isLicenseStillValid.implementation = function(context, unixTimestamp) {
// Pass 0 as the timestamp, which is always less than 1672531261
this.isLicenseStillValid(context, 0);
};
});
|
Passing 0 as the timestamp satisfies the condition and reveals the 3rd flag.