Frida Dynamic instrumentation to bypass Root detections
Hello guys and welcome back to another walkthrough. This time we’ll be bypassing Root detections on an android application through dynamic instrumentation with frida. Frida is a dynamic instrumentation toolkit that enables developers and security researchers to analyze and manipulate the behavior of applications at runtime. It is particularly useful for bypassing root checks in Android applications (APKs) during reverse engineering. By injecting JavaScript into a running app, Frida allows users to hook into various methods, modify their behavior, and effectively circumvent security mechanisms that detect whether a device is rooted. This capability makes Frida an invaluable tool for testing app security, performing vulnerability assessments, and understanding application behavior in a controlled environment. Its user-friendly interface and powerful scripting capabilities make it accessible for both novice and experienced users.
Before diving into the Frida instrumentation walkthrough for bypassing root checks in an APK, users should ensure they have a few prerequisites in place. First, ensure you have a compatible Android device or emulator with USB debugging enabled. Next, install the latest version of Frida on your machine, along with the Frida server on the target device. Users should also have a basic understanding of JavaScript, as scripting will be required for the instrumentation process. Additionally, make sure you have the necessary tools for decompiling APKs, such as JADX or Apktool, to analyze the app’s structure. Familiarity with a terminal or command line interface will also be beneficial for executing commands seamlessly.
There are too hooking that frida can perform namely. Native library hooking and hooking at the java layer. And this will make this writeup a two or three part series.
When working with Frida to instrument Android applications, two primary methods for hooking exist: Native library hooking and Java layer hooking. Each approach targets different components of an application and offers distinct implications and use cases.
Native library hooking involves intercepting and modifying calls to native code written in languages like C or C++, typically found in shared libraries (with .so extensions). This method hooks into native methods loaded as shared libraries, which can be part of the application itself or system libraries. Using Frida, you can hook native functions by specifying their memory addresses or function signatures, employing the `Interceptor` API or the `Module` class to identify the specific functions within the native library. Once hooked, you can modify arguments, change return values, or even replace the function’s implementation entirely. This approach is particularly useful for applications that perform critical operations in native code, such as cryptography or performance-sensitive tasks, and is often employed in reverse engineering to understand the underlying functionality of security-sensitive applications.
On the other hand, hooking at the Java layer focuses on intercepting calls within the Java code of Android applications, which runs on the Android Runtime (ART). This method allows you to hook into Java methods, including those from application classes, Android framework classes, or any libraries the app uses. Frida provides the `Java.use()` API, enabling you to replace or modify the behavior of Java methods directly. This process involves changing method arguments, altering return values, or introducing new behavior without needing to modify the app’s bytecode. Java layer hooking is ideal for high-level logic manipulation, making it easier to modify business logic, change UI behavior, or debug interactions within the app. It allows for rapid prototyping and testing due to its straightforward setup.
The key differences between these two methods lie in their level of operation, complexity, performance, and use cases. Native hooking operates at a lower level, directly interacting with native code and memory, which generally requires a deeper understanding of the native code structure, memory addresses, and calling conventions. In contrast, Java hooking works at a higher level and is often more accessible for those familiar with Java and the Android SDK. While native hooking can have performance implications due to overhead in dealing with native memory, Java hooking is typically more efficient for high-level operations. Ultimately, the choice between these two methods depends on the specific analysis or modification needs and the user’s familiarity with the underlying technologies.
The application we are going to be pentesting today can be downloaded and installed in an android device using the following link
After installing the APK and opening it normally we open and get 4 detections fired and the devices is considered as rooted
Our focus will be bypassing this detections while the application is in a running state. But to understand how the detection mechanisms are implemented first of all we need to decompile the Android applcation and get access to the Raw source code. The tool I’ll use is jadx-gui. JADX-GUI is a user-friendly graphical interface for the JADX decompiler, enabling easy decompilation and analysis of Android APK files into readable Java source code and resources. The tool can be downloaded using the following link
After loading and opening the APK we get the following source code
An not going to go into detailed about the structure of an APK or best practices when developing android application or How to pentest android application from a beginners perspective but it’s something you can look up since there are great resources about it. After a bit of reversing we see the functions that are called to perform the checks
We have about 13 functions that are being called from the native library and all the method returns Boolean values i.e. True or False. Meaning we can just hook in the methods where the detections happened, see the return values and modify accordingly
Time to begin coding
Being a red team and an application security engineer i deal with applications that might have over a thousand obfuscated methods in an application and hence when am beginning to perform dynamic i check which classes and functions were loaded into the application. Below is code snippet that checks the classes that were loaded in an android application
var callbacks = {
onMatch: function(args){
if(args.indexOf("frida") >= 0){
console.log(colors.cyan("[*] Class Loaded: " + args));
var method = Java.use(args);
var all__methods = method.class.getMethods();
for(var i = 0; i <= all__methods.length; i++){
//console.log(colors.blue("\t[**] Method assosiated: " + all__methods[i]));
}
}
},
onComplete: function(retval){
//do nothing
}
}
Java.perform(function(){
try{
Java.enumerateLoadedClasses(callbacks);
}catch(e){
console.log(colors.red("\t[-] An error occured: " + e))
}
});
This code snippet is written in JavaScript and is designed to be used with Frida, a dynamic instrumentation toolkit. Its primary purpose is to enumerate loaded classes in an Android application and log information about those classes, particularly focusing on those that include the string “frida” in their names.
The code begins by defining a callback object named `callbacks`, which contains two functions: `onMatch` and `onComplete`. The `onMatch` function is triggered for each loaded class that matches certain criteria. In this case, it checks if the class name contains the substring “frida”. If a match is found, the class name is logged in cyan color using `console.log`. The function then uses `Java.use(args)` to create a proxy for the matched class, allowing further interaction with it. It retrieves all methods of the matched class through `method.class.getMethods()`.
Within the `onMatch` function, there is a loop intended to iterate through all the methods associated with the matched class. Although the loop is present, it is commented out, indicating that the author might have intended to log the names of these methods for debugging or analysis purposes.
The `onComplete` function is defined but does nothing in this implementation. It serves as a placeholder that could be used for cleanup or final actions after the enumeration process is complete.
Finally, the code uses `Java.perform` to execute the class enumeration within the context of the Java Runtime Environment. It attempts to call `Java.enumerateLoadedClasses(callbacks)` to initiate the enumeration process, and any errors encountered during this process are caught and logged in red color, signaling an error occurred. This overall structure enables the user to gather insights into the classes loaded by the target application, particularly those related to “frida.”
To run frida with the script we use the below command
frida -U -l scripts/hook.js -f com.detectfridaexample
Knowing this classes were loaded, we could now being looking at the code as we did before
Next we need to hook into the four mechanisms that were detected by the application and modify there return values
I wrote the below code that will hook at the four functions
Java.perform(function(){
try{
var bypass1 = Java.use("com.detectfrida.util.RootDetectorHelper");
bypass1['isFoundWrongPathPermission'].overload().implementation = function(){
console.log(colors.yellow("\t[+] isFoundWrongPathPermission called"));
var ret = this.isFoundWrongPathPermission();
console.log(colors.green("\t[+] Return value for isFoundWrongPathPermission: " + JSON.stringify(ret) ));
return ret
}
}catch(e){
console.log(colors.red("\t[-] An error occured: " + e));
}
});
Java.perform(function(){
try{
var bypass1 = Java.use("com.detectfrida.util.RootDetectorHelper");
bypass1['isSuExists'].overload().implementation = function(){
console.log(colors.yellow("\t[+] isSuExists called"));
var ret = this.isSuExists();
console.log(colors.green("\t[+] Return value for isSuExists: " + JSON.stringify(ret) ));
return ret
}
}catch(e){
console.log(colors.red("\t[-] An error occured: " + e));
}
});
Java.perform(function(){
try{
var bypass1 = Java.use("com.detectfrida.util.RootDetectorHelper");
bypass1['isFoundSuBinary'].overload().implementation = function(){
console.log(colors.yellow("\t[+] isFoundSuBinary called"));
var ret = this.isFoundSuBinary();
console.log(colors.green("\t[+] Return value for isFoundSuBinary: " + JSON.stringify(ret) ));
return ret
}
}catch(e){
console.log(colors.red("\t[-] An error occured: " + e));
}
});
Java.perform(function(){
try{
var bypass1 = Java.use("com.detectfrida.util.RootDetectorHelper");
bypass1['isFoundDangerousProps'].overload().implementation = function(){
console.log(colors.yellow("\n\t[+] isFoundDangerousProps called"));
var ret = this.isFoundDangerousProps();
console.log(colors.green("\t[+] Return value for isFoundDangerousProps: " + JSON.stringify(ret) ));
return ret
}
}catch(e){
console.log(colors.red("\t[-] An error occured: " + e));
}
});
This code snippet utilizes Frida’s JavaScript API to bypass certain root detection methods in an Android application by hooking into the `RootDetectorHelper` class. The primary purpose is to intercept calls to various methods that are likely employed to identify whether the device is rooted, allowing the user to manipulate their behavior.
In the first section, the code hooks the `isFoundWrongPathPermission` method. When this method is called, it logs a message indicating the call and captures the return value, which is also logged before being returned unchanged. This allows for monitoring the method’s usage without altering its outcome.
The subsequent sections follow a similar pattern for the methods `isSuExists`, `isFoundSuBinary`, and `isFoundDangerousProps`. Each method is intercepted, and a log statement indicates when it is called. The return values are captured, logged, and then returned as-is, effectively allowing the bypass of any checks that these methods might perform without altering the application’s functionality.
Overall, the code demonstrates a systematic approach to overriding specific root detection methods in an Android app, providing visibility into their execution while maintaining their original return values. Each block of code is wrapped in a `try-catch` structure to handle any potential errors, ensuring that any issues encountered during the hooking process are logged appropriately.
Running the code we see that all the methods returned true
Meaning the bypass the checks we need to modify the values to false. I modified the code snippet to be as follows
Java.perform(function(){
try{
var bypass1 = Java.use("com.detectfrida.util.RootDetectorHelper");
bypass1['isFoundWrongPathPermission'].overload().implementation = function(){
console.log(colors.yellow("\t[+] isFoundWrongPathPermission called"));
var ret = this.isFoundWrongPathPermission();
console.log(colors.green("\t[+] Return value for isFoundWrongPathPermission: " + JSON.stringify(ret) ));
return false
}
}catch(e){
console.log(colors.red("\t[-] An error occured: " + e));
}
});
Java.perform(function(){
try{
var bypass1 = Java.use("com.detectfrida.util.RootDetectorHelper");
bypass1['isSuExists'].overload().implementation = function(){
console.log(colors.yellow("\t[+] isSuExists called"));
var ret = this.isSuExists();
console.log(colors.green("\t[+] Return value for isSuExists: " + JSON.stringify(ret) ));
return false
}
}catch(e){
console.log(colors.red("\t[-] An error occured: " + e));
}
});
Java.perform(function(){
try{
var bypass1 = Java.use("com.detectfrida.util.RootDetectorHelper");
bypass1['isFoundSuBinary'].overload().implementation = function(){
console.log(colors.yellow("\t[+] isFoundSuBinary called"));
var ret = this.isFoundSuBinary();
console.log(colors.green("\t[+] Return value for isFoundSuBinary: " + JSON.stringify(ret) ));
return false
}
}catch(e){
console.log(colors.red("\t[-] An error occured: " + e));
}
});
Java.perform(function(){
try{
var bypass1 = Java.use("com.detectfrida.util.RootDetectorHelper");
bypass1['isFoundDangerousProps'].overload().implementation = function(){
console.log(colors.yellow("\n\t[+] isFoundDangerousProps called"));
var ret = this.isFoundDangerousProps();
console.log(colors.green("\t[+] Return value for isFoundDangerousProps: " + JSON.stringify(ret) ));
return false
}
}catch(e){
console.log(colors.red("\t[-] An error occured: " + e));
}
});
After saving the file and running the application again. We can see from the below screenshot that we bypassed all the checks successfully
The application this the device is not rooted but in real sense the device is rooted
In conclusion, Frida’s dynamic instrumentation capabilities provide a powerful means to bypass root detection mechanisms in Android applications. By allowing users to hook into specific methods and manipulate their behavior at runtime, Frida enables security researchers and developers to analyze app security more effectively. This ability to intercept and log calls to root detection methods not only aids in understanding the app’s behavior but also facilitates testing and debugging in environments where root access is a concern. Overall, Frida serves as an invaluable tool in the arsenal of those looking to explore and enhance the security and functionality of Android applications.
In the second part we’ll be looking at hooking into the native code and bypassing the checks dynamically
If yopu liked the walkthough clap for me down below and follow me so that you don’t miss any upcoming walkthrough