Pentest Logo

Research

Android Root Detection Bypass using Frida (Part 2 – RootBeer Sample)

Researchers:

Richard Mason

This is the second part in a blog series I am creating about bypassing various Android application’s root detection mechanisms. Please see part 1 here.

In this part we are going to be looking to bypass the RootBeer Sample application. For those of you that haven’t come across RootBeer before it is an open source root detection library for Android and the sample app is used to demonstrate it’s capabilities. The below screenshot is of the app running on my rooted/emulated Android device.

——————-

*Before I show how I have bypassed the root/emulator detection I should say that I’m not an expert at this and the way I have performed the bypass might not be the most efficient or “correct” way of doing so. At one point or another we have all been at the start of our learning journeys so please take this into consideration.

——————–

As before, the first things you’re gonna need some equipment and tools to perform the bypass:

  • Rooted Android device (I’m using an emulated device using Android Studio which has been rooted using rootAVD)
  • ADB
  • Frida
  • JADX
  • Anything you want to use to edit code

This bypass is performed pretty much the same way we bypassed OWASP’s Uncrackable 1 but used some other tools and techniques to trace the methods that are being called.

So as we can see from the screenshot I posted above the application, once run, detects our Android as rooted due to a few different reasons. Now we have observed the above behavior we can open the application within JADX just like we did the last time.

As we can see from the screenshot the device is failing on a check for “SU Binaries” so we are going to search for this within JAXD.

So we can see from JADX that there is a method that we can assume is checking for the SU Binary based on the name.

Using Frida-Trace we are gonna connect to the method and see in real time when it is called and the returned value from it.

				
					$.\frida-trace.exe -U -j "*rootbeer*!*checkForSuBinary*" "RootBeer Sample"
Instrumenting...
RootBeer.checkForSuBinary: Loaded handler at 
"[REDACTED]\\__handlers__\\com.scottyab.rootbeer.RootBeer\\checkForSuBinary.js"
Started tracing 1 function. Press Ctrl+C to stop.
           /* TID 0x250e */
  7288 ms  RootBeer.checkForSuBinary()
  7288 ms  <= true
				
			

The good thing about Frida-Trace is that it creates “handlers” that we can modify to hook into the method and change the value that is returned when it is called. From the above code snippet we can see that the value returned is “true” so to defeat that check we need to return “false”.

				
					/ *
 * Auto-generated by Frida. Please modify to match the signature of RootBeer.checkForSuBinary.
 *
 * For full API reference, see: https://frida.re/docs/javascript-api/
 */

{
  /**
   * Called synchronously when about to call RootBeer.checkForSuBinary.
   *
   * @this {object} - The Java class or instance.
   * @param {function} log - Call this function with a string to be presented to the user.
   * @param {array} args - Java method arguments.
   * @param {object} state - Object allowing you to keep state across function calls.
   */
  onEnter(log, args, state) {
    log(`RootBeer.checkForSuBinary(${args.map(JSON.stringify).join(', ')})`);
  },

  /**
   * Called synchronously when about to return from RootBeer.checkForSuBinary.
   *
   * See onEnter for details.
   *
   * @this {object} - The Java class or instance.
   * @param {function} log - Call this function with a string to be presented to the user.
   * @param {NativePointer} retval - Return value.
   * @param {object} state - Object allowing you to keep state across function calls.
   */
  onLeave(log, retval, state) {
    if (retval !== undefined) {
      log(`<= ${JSON.stringify(retval)}`);
      return false;
    }
  }
}
				
			

All we have done to defeat the check for the SU binary is add “return false;” to the “onLeave” function. Once we have saved the handler file and rerun the application with Frida-Trace running we have defeated the check.

So that’s the solution to validate and check that what we have done will defeat the check but we need to make it easier to defeat all the checks in one go. So we are going to do what we did in part 1 and create a script to use with Frida. So we know that “checkForSuBinary” function is being called and that we need to change the value returned we can use JADX to copy it as a Frida snippet.

Once we have placed the snippet within a Java function we can change the return value to false.

				
					Java.perform(function() {
  RootBeer["checkForSuBinary"].implementation = function () {
    console.log('checkForSuBinary is called');
    let ret = this.checkForSuBinary();
    console.log('checkForSuBinary ret value is ' + ret);
    return false;
  };
});
				
			

Obviously we need to validate what we have done and check that it works.

				
					$.\frida.exe -U -l RootBeer.js -f com.scottyab.rootbeer.sample
     ____
    / _  |   Frida 16.0.2 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Android Emulator 5554 (id=emulator-5554)
Spawned `com.scottyab.rootbeer.sample`. Resuming main thread!
[Android Emulator 5554::com.scottyab.rootbeer.sample ]-> checkForSuBinary is called
checkForSuBinary ret value is true
				
			

Success, we have defeated the check once again.

As we saw at the top from the original screenshot there are multiple checks that we need to defeat, so at this point it is essentially rinse and repeat. We need to check which functions are being called and which ones we need to defeat. Once done, we have come up with the below script.

				
					Java.perform(function() {
  let RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
  RootBeer["checkForMagiskBinary"].implementation = function () {
    console.log('checkForMagiskBinary is called');
    let ret = this.checkForMagiskBinary();
    console.log('checkForMagiskBinary ret value is ' + ret);
    return false;
  };
  RootBeer["checkForSuBinary"].implementation = function () {
    console.log('checkForSuBinary is called');
    let ret = this.checkForSuBinary();
    console.log('checkForSuBinary ret value is ' + ret);
    return false;
  };
  RootBeer["checkSuExists"].implementation = function () {
    console.log('checkSuExists is called');
    let ret = this.checkSuExists();
    console.log('checkSuExists ret value is ' + ret);
    return false;
  };
  RootBeer["checkForRWPaths"].implementation = function () {
    console.log('checkForRWPaths is called');
    let ret = this.checkForRWPaths();
    console.log('checkForRWPaths ret value is ' + ret);
    return false;
  };
  RootBeer["checkForNativeLibraryReadAccess"].implementation = function () {
    console.log('checkForNativeLibraryReadAccess is called');
    let ret = this.checkForNativeLibraryReadAccess();
    console.log('checkForNativeLibraryReadAccess ret value is ' + ret);
    return false;
  };
  RootBeer["canLoadNativeLibrary"].implementation = function () {
    console.log('canLoadNativeLibrary is called');
    let ret = this.canLoadNativeLibrary();
    console.log('canLoadNativeLibrary ret value is ' + ret);
    return false;
  };
});
				
			

And when we run it with Frida and run the application we have defeated RootBeer sample.

Hopefully you have found this run through somewhat helpful and has given you a basic understanding of using Frida-Trace and creating your own scripts to use with Frida. Like I said at the start of the write up, this might not be the most efficient or be the “correct” way to bypass the root detection, but I am figuring this out as I go along. If there is any help or tips that you can give me to get better that would be much appreciated.

Please see part 3 in this series by clicking here.

Originally published on Medium.

Looking for more than just a test provider?

Get in touch with our team and find out how our tailored services can provide you with the cybersecurity confidence you need.