It refers to the technique of intercepting and modifying the normal behavior of an application. On mobile devices, this can be done using frameworks like Xposed or Frida, which allow security researchers to inject code at runtime into application processes. With hooking, you can monitor and manipulate function calls, data, control flow, and more, which is valuable for understanding the inner workings of an application and identifying potential vulnerabilities.
It is a feature of the Java language that allows you to examine and modify the structure of classes at run time. With reflection, it is possible to access information about classes, methods and attributes, as well as invoke methods dynamically, without knowing their implementations at compile time. This can be useful in pen tests to explore sensitive parts of the application code, examining classes and methods, accessing information that would not normally be available, or interacting with restricted functionality.
-> Understanding how Reflection enables dynamic code manipulation in Java provides a conceptual parallel to understanding the nature of dynamic manipulation performed by Frida in the context of hooking for Android applications.
When you run a script in Frida, it is injected into the application process being analyzed. However, it is crucial to run this script at the appropriate time, when the Java environment is available to be manipulated. This is where the use of Java.perform() comes in.
Java.perform(function() {
// your code...
});
Java.perform() is a fundamental function in Frida that encapsulates JavaScript code. It ensures that the injected code runs in the context of the Java Virtual Machine (VM) in the target application. This is crucial, as it allows manipulations, such as hooks and modifications of methods, classes and other elements, synchronized with the application's Java environment, ensuring the correct execution of operations and avoiding context or synchronization problems. Inside the Java.perform() code block, this is where you normally place Frida instructions to perform hooking, manipulate methods, access classes, among other operations to modify the behavior of the target application.
Java.use() is an essential method within the context of scripting in Frida to interact and manipulate Java classes during the execution of an application. This method is used to access specific Java classes within the target application process.
In Frida, when using java.use(), you can:
-> Access Java Classes
The Java.use() method allows access to Java classes, returning an object that represents the desired class. For example:
var MyClass = Java.use('com.example.MyClass');
-> Manipulate Methods and Properties
Once you have the reference to the Java class with java.use(), you can manipulate methods and properties of that class. This includes method overriding (implementation), method calling, property access, and more.
-> Access to Static Methods and Properties
Java.use() can also be used to access static methods and properties of a Java class, making it possible to directly call these methods or access properties.
-> Create Object Instances
Additionally, java.use() can be combined with $new() to create object instances of a specific Java class during Frida script execution.
The use of java.use() is essential to interact with the Java environment of an application during the execution of the Frida script. It allows you to dynamically manipulate, replace or access Java methods, properties and classes, enabling the modification of the application's behavior at run time.
In Frida scripts, implementation is used to override or intercept the behavior of a specific method in a target application. It is useful when you want to modify the operation of an existing method by inserting your own code to be executed in place of the original method.
-> A typical example is the replacement of an existing method in a target application:
Java.perform(function() {
var TargetClass = Java.use('com.example.TargetClass');
TargetClass.targetMethod.implementation = function() {
...
};
});
The implementation allows the insertion of custom logic to handle the execution of a specific method, providing flexibility in modifying the application's behavior.
overload() in Frida applies when you deal with methods or functions that have multiple versions with the same name but different signatures or parameters. The overload() function is used to explicitly select the correct version of the method you want to override.
Exemplo:
Java.perform(function() {
var clazz = Java.use('com.example.ClasseExemplo');
// Override the specific version of an overloaded method
clazz.exampleMethod.overload(arg1_type, arg2_type, ...).implementation = function(arg1, arg2, ...) {
...
};
});
In this example, clazz.exampleMethod is the overloaded method that has multiple versions with different signatures. overload() is used to explicitly select the correct version of the method with argument types arg1_type, arg2_type, etc. when it is called in the application. If you wanted to specify a version of the method that has no arguments, you would simply put overload() without arguments.
The combined use of java.use() and $new() in Frida allows for the dynamic creation of object instances of specific Java classes, such as Boolean and String, during script execution. This demonstrates how Frida enables direct interaction and dynamic manipulation of objects from these Java classes in the context of the running application.
-> Boolean
// The $new() function creates a new instance of the Boolean object with the Boolean value 'true'
var b1 = Java.use('java.lang.Boolean').$new("true");
// The $new() function creates a new instance of the Boolean object with the Boolean value 'false'
var b1 = Java.use('java.lang.Boolean').$new("false");
-> String
// The $new() function creates a new instance of the String object with the value "<string>"
var s1 = Java.use("java.lang.String").$new("<string>");
This is an example that allows dynamic manipulation of Boolean and String objects during script execution in Frida
'use strict'
if(Java.available){
// Java.perform(function(){
try{
Java.enumerateLoadedClasses({
onMatch: function(className){
console.log(className);
},
onComplete: function(){
console.log("[+] done!");
}
});
}catch(error){
console.log(error);
}
// });
}else{
console.log("[-] Java unavailable");
}
Sometimes, even when intercepting requests from an API that is being consumed by the application, certain webviews may not be displayed, because webviews are often implemented as separate components within the application, and the traffic from these components can be treated in a different way. different compared to requests made directly by application logic.
'use strict'
if(Java.available){
Java.perform(function(){
try{
var target_class = Java.use("android.webkit.WebView");
target_class.loadUrl.overload("java.lang.String").implementation = function (webview) {
console.log("[+] " + webview.toString());
};
}catch(error){
console.log("[-] Exception");
console.log(String(error.stack));
}
});
}else{
console.log("[-] Java unavailable");
}
-> Doc
https://frida.re/docs/javascript-api/#objc-registerclass
-> Code
package com.scottyab.rootbeer;
...
public class RootBeer {
...
public boolean isRooted() {
return detectRootManagementApps() || detectPotentiallyDangerousApps() || checkForBinary("su") || checkForDangerousProps() || checkForRWPaths() || detectTestKeys() || checkSuExists() || checkForRootNative() || checkForMagiskBinary();
}
...
-> Frida Script for Bypass direct with False Return
Java.perform(function() {
var clazz = Java.use('com.scottyab.rootbeer.RootBeer');
clazz.isRooted.overload().implementation = function() {
console.log("Bypass rootbeer");
return false;
}
});
-> Frida Script for Bypass with Boolean Object for Return false
Java.perform(function() {
var clazz = Java.use('com.scottyab.rootbeer.RootBeer');
clazz.isRooted.overload().implementation = function() {
console.log("Bypass rootbeer");
var b1 = Java.use('java.lang.Boolean').$new("false").booleanValue();
return b1;
}
});
If the code is obfuscated, it is important to look at the packages that are being imported into MainActivity, in order to find the code responsible for the protections.
-> Code - MainActivity
package com.example.app
import android.os.Bundle;
import c2.a;
import d.h;
/* loaded from: classes.dex */
public final class MainActivity extends h {
@Override // androidx.fragment.app.p, androidx.activity.ComponentActivity, w.f, android.app.Activity
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
if (new a(this).isRooted()) {
throw new y0.a();
}
}
}
-> Code - RootBeer
package c2;
...
public class a {
...
public boolean isRooted() {
return detectRootManagementApps() || detectPotentiallyDangerousApps() || checkForBinary("su") || checkForDangerousProps() || checkForRWPaths() || detectTestKeys() || checkSuExists() || checkForRootNative() || checkForMagiskBinary();
}
...
-> Frida Script for Bypass
Java.perform(function() {
var clazz = Java.use('c2.a');
clazz.isRooted.overload().implementation = function() {
console.log("Bypass rootbeer");
var b1 = Java.use('java.lang.Boolean').$new("false").booleanValue();
return b1;
}
});
-> Code - MainActivity
package com.example.app;
...
public final class MainActivity extends AppCompatActivity {
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
if (new EmulatorDetector().isEmulator()) {
throw new EmulatorDetectorException();
}
}
}
-> Code - EmulatorDetector
package com.example.app;
...
public final class EmulatorDetector {
...
public final boolean isEmulator() {
return checkBuildConfig() || checkEmulatorFiles();
}
-> Frida Script for Bypass
Java.perform(function() {
var clazz = Java.use('com.example.app.EmulatorDetector');
clazz.isEmulator.overload().implementation = function() {
console.log("Bypass Emulator");
return false;
}
});
-> is it possible to concatenate several frida scripts in the same file
Java.perform(function() {
var clazz = Java.use('com.scottyab.rootbeer.RootBeer');
clazz.isRooted.overload().implementation = function() {
console.log("Bypass rootbeer");
var b1 = Java.use('java.lang.Boolean').$new("false").booleanValue();
return b1;
}
});
Java.perform(function() {
var clazz = Java.use('com.example.app.EmulatorDetector');
clazz.isEmulator.overload().implementation = function() {
console.log("Bypass Emulator");
return false;
}
});
-> it is also possible to pass several scripts in different files to frida
frida -U -f com.example.app -l script1.js -l script2.js
-> Take the following code as an example:
https://codeshare.frida.re/@dzonerzy/fridantiroot/
-> Depending on the protections implemented by the application, if you are unable to bypass them, you may need to use StackTrace to debug errors and find the possible package and class responsible for the implemented protection. StackTrace reading must be done from bottom to top and depending on the protection. Remember that depending on the context it may be necessary to create a new frida script.
-> Example of using StackTrace in the script above:
function printStackTrace(){
var trace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
trace = trace.replace("java.lang.Exception\n", "Stack trace\n");
console.log(trace);
}
...
PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pname, flags) {
var shouldFakePackage = (RootPackages.indexOf(pname) > -1);
if (shouldFakePackage) {
send("Bypass root check for package: " + pname);
printStackTrace();
pname = "set.package.name.to.a.fake.one.so.we.can.bypass.it";
}
return this.getPackageInfo.overload('java.lang.String', 'int').call(this, pname, flags);
};
...
-> Using StackTrace to readapt scripts is highly efficient for cases where there is a frida script to bypass known protections such as RootBeer, for example, but there is obfuscation in the code.