Back

Bypassing Root detection in android flutter apps

Background

As part of the security research I was doing for an app, I had to run it in the emulator which is by default rooted. The app of course had root detection enabled. So before I could do anything such as inspecting the config files or the data diretory of the app in the android filesystem, I had to run the app in the emulator, but it was giving me the following error.

Rooted Device not allowed message

Before I proceed further I had to figure out how to bypass this message. Here goes the journey.

Figuring out the app

Before any inspections could be done, I had to figure out how this app is made!, it could be a java or kotlin app, reat native, xamarin or flutter. The obvious next step to figure out this is to decompile the apk and look inside the source code. Luckily the hard part is already done by smart minds and we just need to use it to decompile our app. The tool we are going to use is called apktool, which you can grab a copy here. I had it installed using the brew install apktool since I use a mac.

next up is to decompile the app

apktool d -s original.apk -o decoded

Looking through the options

  • d: This is the decode command of apktool, which is used to disassemble or decode an APK file to obtain its source code and resources.
  • -s: This is an option that stands for "no source code." When this option is used, apktool will decode the APK but skip decoding the source code. This is useful if you only need the resources and not the Java source code.
  • original.apk : This is the path to the Android APK file that you want to decode. In this case, the APK file is named "original.apk." Make sure that this APK file is in the current directory or provide the full path to it.
  • -o decoded This is an option that specifies the output directory where the decoded files will be placed. In this case, the decoded files will be saved in a directory named "decoded."

now that we have the apk decompiled, I quickly went through the decoded diretory to see if there is anything obvious. The AndroidManifest.xml file in the decoded directory gave some good indication that the app is built with flutter

<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme"/>
<meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background"/>

also I was able to find the lib/armeabi-v7a/libflutter.so file inside the decoded directory which is a strong indication that the app is indeed built with flutter.

Quick side note: The flutter apps are built with dart language, which is run with dart engine. flutter apks usually contain this engine because the android platforms can't directly run the dart code, usually the file libflutter.so contains the flutter+dart engine(s) and the app source code is usually inside libapp.so file, decompilation of those files could lead to more source code discoveries. It could also change in the future as flutter is ever changing so quickly.

Now that I have narrowed my operational surface to flutter, I did a quick google search around the root detection libraries in flutter. It came out with couple of libraries including flutter_jailbreak_detection library. Next up I scanned the decoded apk directory with the keywords like jailbreak, detection etc... and the original/META-INF/BNDLTOOL.SF file contained the following lines

Name: META-INF/flutter_jailbreak_detection_release.kotlin_module
SHA-256-Digest: djFOCps14EkrYAp1rBqRkg4CvxtCkJ3RoHt3d/2ZGY8=

which confirmed that the apk is using the flutter_jailbreak_detection library. Most of the time the exact libaries wont matter too much, because they all perform some if checks like if su binary is accessible/present in the os.

After going through the pub.dev page of the flutter jailbreak detetion library, it told me the following

Flutter jailbreak and root detection plugin.
It uses RootBeer on Android, and IOSSecuritySuite on iOS.

Now time for some more googling about bypassing rootbeer in android. I found a couple of articles about Frida, which is a tool for reverse engineering. Basically it could hook into a running application and hijack requests to system calls, network calls etc... and instrument the running app to do arbitrary stuff. Another use case is to set up a Man in the Middle proxy to decrypt network calls and do API tracing or SSL pinning. I also found a couple of articles dealing directly with frida and android.

Now coming back to our usecase, the research showed me that Frida is a useful tool to bypass the root detection.

Frida works as a two part tools

  1. Frida server: which should be present on the target device ie: the emulator
  2. Frida cli: Which connects to the server on the target and instruct the server to observe system calls or execute arbirary commands

Now before we can use frida, we could repack the decoded apk, sign it and install it in the emulator . We could also simply run the original.apk without repacking, but doing the extra will helps us further if we have to modify the source codes somehow, or peddle with the Androidmanifest.xml etc...

So to repack the decoded folder, we can use the same apktool

apktool b decoded -d -o repacked.apk
  • b : This is the build command of apktool, which is used to rebuild an APK after it has been decoded and modified.
  • decoded : This is the source directory or folder that contains the decoded (disassembled) APK files.
  • -d : This is an option that tells apktool to use the debug information from the original APK, if available, while rebuilding the APK. This can be helpful for maintaining debugging symbols or information in the APK.
  • -o repacked.apk : This is an option that specifies the output directory and filename for the rebuilt APK. In this case, the rebuilt APK will be named "repacked.apk" and will be placed in the current working directory. You can change the filename and location by providing a different path and filename.

Now we should have the repacked.apk with us. Next step is to zipalign.

zip alignining is a process that is done to ensure that the resources within an APK are aligned on 4-byte boundaries. The details of this process is not of important in the scope of this article but you can read it here.

To do the zip align Make sure your path contains the android sdk's build-tools directory. usually it's of the location $ANDROID_HOME/build-tools/xx.xx.xx where $ANDROID_HOME is the android sdk location. Inside of it you should find the zipalign executable. The same directory contains the apksigner which helps us to sign the apk

the command is as follows

zipalign -p -v 4 repacked.apk aligned.apk

The next step is to sign the aligned.apk file. to do this you have to generate a keystore and sign the apk with it. The detailed description on how to do this can be found all over internet. but I will list the commands below for reference.

# the following command generates the keystore and it will ask a password for the same, please remeber/store it somewhere
keytool -genkey -v -keystore my.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias app

Now use the keystore to sign the aligned apk

# It will ask for the keystore password that you gave in the earlier command
apksigner sign --ks-key-alias app --ks my.keystore --out signed.apk aligned.apk

Also you can verify the apk with the following command

apksigner verify --print-certs --verbose signed.apk

it should output something similar like below and a bit more.

Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v4 scheme (APK Signature Scheme v4): false

Another swiss knife to use in this case is called uber signer. Uber signer can do zip align, sign, and verify with just one command java -jar uber-apk-signer.jar --apks /path/to/apks_dir. Grab a copy from the releases here

Now we have the apk ready to be put into emulator, use adb to do this.

Like the build-tools directory, make sure your path contains the android sdk's platform-tools directory. usually it's of the location $ANDROID_HOME/platform-tools where $ANDROID_HOME is the android sdk location. Inside of it you should find the adb executable.

adb install -r signed.apk # the -r option is to reinstall if there is an existing installation

The next step is to send Frida to the emulator, ie: to install frida-server. Grab a copy of frida server for the android from their github releases here. There are many releases for various use cases. Usually you have to open the whole list to see the android one.

Frida server android release

uncompress the file

unxz frida-server.xz

and then send the resulting frida-server file to android emulator using the following commands

adb root # might be required
adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"

now from the host, you have to install the frida-cli tools using pip.

pip3 install frida-tools

If everything goes well until now, you can do a quick frida-ps -U from the host to check if its able to list out the processes from your android emulator.

frida-ps -U # should output something like below
  PID  Name
-----  ---------------------------------
12372  Hangouts                         
 3161  Photos                           
 3794  adbd                             
 3240  android.process.media            
  927  audioserver                      
  928  cameraserver

Now you can tell frida to load the root-detection bypass script and spawn the target app along with it.

frida -c dzonerzy/fridantiroot -U -f com.package.app # with this command you should be able to see the app starting without anti root message in the emulator
     ____
    / _  |   Frida 16.1.4 - 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.package.app`. Resuming main thread!                       
[Android Emulator 5554::com.package.app ]-> message: {'type': 'send', 'payload': 'Loaded 4358 classes!'} data: None
message: {'type': 'send', 'payload': 'loaded: -1'} data: None
message: {'type': 'send', 'payload': 'ProcessManager hook not loaded'} data: None
message: {'type': 'send', 'payload': 'KeyInfo hook not loaded'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.noshufou.android.su'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.noshufou.android.su.elite'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: eu.chainfire.supersu'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.koushikdutta.superuser'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.thirdparty.superuser'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.yellowes.su'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.topjohnwu.magisk'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.koushikdutta.rommanager'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.koushikdutta.rommanager.license'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.dimonvideo.luckypatcher'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.chelpus.lackypatch'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.ramdroid.appquarantine'} data: None
message: {'type': 'send', 'payload': 'Bypass root check for package: com.ramdroid.appquarantinepro'} data: None
message: {'type': 'send', 'payload': 'Bypass return value for binary: su'} data: None
...
...
...

The command options are follows

  • -c dzonerzy/fridantiroot: This option specifies the custom JavaScript file you want to run using Frida. In this case, it's dzonerzy/fridantiroot. which is our actual bypass script. There are more scripts avaialable online that you could try if it did not work in the first try.
  • -U : This option tells Frida to use a USB-connected Android device (in this case its the emulator) as the target.
  • -f com.package.app : This specifies the target application with the package name "com.package.app." The custom script (fridantiroot.js) will be injected into this application to perform dynamic analysis. You could figure out the package name from Androidmanifest.xml file or from the list of installed apps via adb.

Voila! the app is running without the root detection message

Running app after bypassing root detection