The purpose of “root detection” is to increase application security by ensuring that programs are run inside of a secure, tested and documented, environment.
Whilst “rooting” does offer users greater control, customizability and fine-tuning of device settings, it also allows for the circumvention of all operating system protections, allowing owners to modify sensitive and delicate files and settings.
One of the crucial concerns is that user software on a rooted device is run with higher privileges. These privileges are necessary to obtain the desired functionality, but also give applications the ability to cause damage to the device and access data ordinarily restricted for privacy or security reasons. Furthermore, the additional functionality offered by a rooted device encourages the installation of software from untrusted and un-regulated sources, increasing the likelihood of malicious software being installed and executed with excessive privileges.
As a result, critical applications will usually self terminate if their root detection algorithm suspects a rooted or emulated device. For the purpose of security auditing, this whitepaper will explore several techniques which may be used to bypass these protections. Detailed steps and commands will be provided so that those interested in recreating the test environment and learning from the example scenarios can do so.
- Frida & Frida-Server (This needs to match the architecture of the emulated device)
- An emulator such as Genymotion
- Visual Studio Code
Technique 1: Generic Frida Bypass Script
Frida is a dynamic binary instrumentation tool which allows us to modify the instructions of a program during runtime. Android applications are typically written in Java which is object oriented, so we will be looking to hook into specific classes and methods used by the application. We will often need to review the source code, or decompile the application in order to identify functions of interest. This can be done with tools such as “Jadx-gui” and “apktool”.
To keep things simple in the first example, we will use a generic anti-root script, published in the Frida CodeShare project. This script is designed to prevent the target application from discovering evidence which would confirm that a device has been rooted; such as the presence of the ‘SU’ binary or common root management apps.
As we will see later, this is not an exhaustive approach to bypassing root detection, and more advanced applications will have checks which are not accounted for in this particular script.
The example application is an intentionally vulnerable program written by ‘dineshshetty’ to aid security practitioners in learning mobile exploitation, and can be downloaded from the following github repository: https://github.com/dineshshetty/Android-InsecureBankv2. With the repo cloned, adb installed, and an android emulator running on the local machine, the app can be pushed to the device with “adb install <path to InsecureBankv2.apk>” or, if using Genymotion, by simply dragging and dropping the .apk file to the devices homescreen.
After configuring the InsecureBank application, push the appropriate frida-server matching the device's architecture to a suitable location on the device's file system, such as /data/local/tmp and run the server. (Commands to achieve this can be found in the appendix).
Logging into the application with credentials: “dinesh:Dinesh@123$” we can see that the app has flagged the device as “Rooted”.
Using apktool to decompile the app to smali code - (the assembly language of the Dalvik Virtual Machine):
apktool d -r <InsecureBankv2.apk>
We can see inside of the “PostLogin.smali” file, that the method ‘ShowRootStatus’ is invoked on line 273 and that this method, defined on line 422, also calls the following two methods:
- "doesSuperuserApkExist()" <-- Line 140
- "doesSUexist()" <-- Line 29
These methods can also be inspected independently, however, as their names suggest, these are checking for the presence of the ‘su’ binary in the system path, as well as the ‘Superuser.apk’ file in the ‘/system/app/’ directory.
Our bypass script, written by Daniele Linguaglossa and Simone Quatrini, can circumvent both of these detection methods.
With the “fridantiroot” script downloaded (available at: https://codeshare.frida.re/@dzonerzy/fridantiroot/) we will run the script in the context of the application during runtime, by using the application’s identifier (retrievable with “frida-ps -Uai”) and a path to the script:
frida -U -l <path-to-script.js> -f <application-identifier>
$ frida -U -l fridantiroot.js -f com.android.insecurebankv2
Upon logging into the app we can see that the root detection has been bypassed:
Technique 2: Method Re-implementation
This technique will be illustrated using two reverse engineering challenges.
Pushing the app to the device and launching it in its default state results in an immediate termination with the following output:
Rather than relying on generic bypass scripts, which are not application specific and will not evade all forms of detection, we can instead identify the individual methods responsible for performing the root detection and simply overwrite these with our own code during runtime.
Upon inspecting the first challenge in Jadx-gui, we can find the methods responsible for implementing root detection immediately inside the entry point to our program, which in Android applications is denoted by the OnCreate() method inside of the MainActivity file.
The if-statement on line 31 terminates the application if any one of the three methods: c.a(), c.b() or c.c() returns true. Thus, we can assume that the application utilises three root detection techniques; and, by inspecting each of these methods individually, we can see that the program is indeed checking for the presence of the ‘su’ binary, test-keys and various files in the /system directory.
In this case, the c.a() method is returning True and causing the app to flag the device as rooted. In our custom script we can use the Java.perform() method to attach a new function to the current thread, followed by the Java.use() method to obtain a handle to the class we wish to access. Finally, calling the implementation method on the newly created handle allows us to define a function which will be used in place of the existing method. In this example, the replacement function simply prints a message to the console and returns nothing which is the equivalent of returning false.
Loading this script into our application using frida, bypasses the protection, preventing the app from terminating:
Replicating this technique on the second challenge apk results in the following error:
For those familiar with object oriented programming (OOP), method overloading allows the programmer to define multiple methods with the same identifier. The compiler determines which method to use by the type and number of arguments passed to the method, which is known as the method signature. As shown in the error message displayed in the console, this application implements two methods with the name “a()”. One, which takes a single argument and another which takes two. The error provides a hint as to how this can be resolved in our script using the .overload() method with the correct signature on our function implementation.
Technique 3: Patching
If the application implements several advanced root detection techniques it may be preferable to decompile the apk to Smali, modify the code offline and then recompile, sign and run the application. The downside to this approach is that the application will not be signed by the original author and the user will be prompted to trust the new developer. It also requires some understanding of Smali code. Furthermore, if the application has robust integrity checks this technique may be more difficult, although in theory, even the code that performs the integrity check could be altered.
The application used to demonstrate this technique is called ‘RootBeer Sample’ and can be found on the play store or downloaded on apkpure from the following page: https://m.apkpure.com/rootbeer-sample/com.scottyab.rootbeer.sample. This application implements a variety of different checks and can not be bypassed using the generic frida script shown in technique 1. Also, due to the number of checks and the way they are implemented, this would be tedious to bypass using runtime method re-implementation:
(RootBeer output which performs 12 different root detection methods).
Decompiling the app with:
$ apktool d -r “RootBeer Sample_0.9_Apkpure.apk”
And opening the RootBeer.smali file under “/smali/com/scottyab/rootbeer/RootBeer.smali”, we can see that the first failed check for our device's configuration is for Test-Keys. Using CTRL+F to find methods related to this check reveals the ‘detectTestKeys()’ method on line 1156. The code appears to perform a check by invoking another method on line 1167 and storing the result in the variable v0. <-- If this variable is equal to zero the code jumps to condition 0. Let’s try modifying this code by inverting the if statement to see if this has any impact on the result of this specific check.
Line 1171 was modified from:
if-eqz v0, :cond_0
if-nez v0, :cond_0
Using the ‘apklab’ extension for Visual Studio Code allows the code to be rebuilt, signed and pushed to the device in a single click. The manual commands are also displayed in the terminal output: (Detailed instructions can be found in the appendix).
Re-running the app shows that the ‘TestKeys’ check has now passed.
CTRL+F search for ‘BusyBox’ reveals the ‘checkForBusyBoxBinary()’ on line 344 of the same source file.
Again, we can see that a check is performed, the result of which is stored in v0 and then returned to the caller. Let’s re-assign v0 with the hex value of 0x0 before it is returned so that it always returns false:
Recompiling, signing and running the app shows that the device now passes this check.
The checkForSuBinary() method can be found on line 889. Modifying the search string to something other than “su” should cause this check to pass, so long as the binary doesn’t exist. Changing this to a ‘j’ seemed to work in my case.
The 2nd su check can be circumvented by changing the if-statement on line 953 from if-eqz to if-nez.
Finally the native check can be negated by resetting the v0 variable by inserting the line:
const/4 v0, 0x0 <-- On line: 806
Any remaining failed checks should be bypassable, in a similar way, by resetting ‘result’ variables to 0 or by reversing if-statements which control conditional jumps. Saving all the changes and rebuilding, signing and installing the app one final time should cause all remaining checks to pass:
Although this technique can sometimes require an advanced understanding of Smali code, it does provide an attacker with full control over the application, including the ability to modify any of its functionality. As a result, it is extremely difficult to mitigate, as any client-side protections can simply be overwritten or removed, including application integrity checks.
This paper has covered several root detection bypass techniques which are all applicable at the time of writing. Whilst developers are finding novel and more advanced ways to identify rooted handsets, if the device has been rooted, the owner ultimately has full control over the device’s OS, giving them the ability to hide any evidence that applications may search for. This ultimately means that root detection mechanism can not claim with absolute certainty that a device is unrooted and therefore safe. At best they can identify devices which are “more likely” to be safe to run code on, and whilst it is better to include these detections than to omit them altogether, they should not be relied upon as a definitive assessment.
Applications should, in combination with root detection algorithms, implement defence in depth, storing any sensitive data server-side where possible and any local data in encrypted format. Particularly sensitive programs, such as mobile banking applications, should issue warnings to users, that whilst protective mechanisms are in place, they can not be 100% effective and that running the app on a rooted device is not recommended and is done at the users own risk.
To see the appendix instructions, see the link below