21 May 2024

Bypassing hardened Android applications

In this blog, Sanjay from NotSoSecure details how he successfully bypassed security measures on a well-protected Android app from the Play Store. Despite the app's defenses against execution on rooted devices, he managed to circumvent these checks to perform API and dynamic testing.

This is a tale of circumventing all checks to record the activity of an Android application on a rooted smartphone. It took us 16 hours to crack and in this blog we’ll tell you the story of how we did it.

  • The First 8 Hours: Root Detection Check in Native Java code
  • The Next 2 Hours: Emulator Check
  • The Next 1 Hour: Frida Running Check
  • The Next 2 Hours: Root Detection Check in React Native "index.android.bundle" file
  • The Final 3 Hours: SSL Pinning Bypass

The First 8 Hours: Root Detection Check in Native Java code

It all started with BlackScreen and a custom error message reading "Device Security" when the app was loaded on an emulator. The application was working fine on a non-rooted mobile device, however, only under the pretext that there was no proxy configured on the device. Keeping the following information in mind we proceeded to test the APK in our emulator.

When we installed the application from the Play Store on an emulator, it showed us the following screen:

Andriod1

To identify the function responsible for showing this error message, we usually use the string backtracking. To do that, first we had to extract the application APK from the device and decompile it using apktool.jar.

Andriod2

We searched for the "Device Security" string and found 2 occurrences of it:

  • "index.android.bundle" file
  • "strings.xml" file
Android 3

If the application is built in React Native, then the entire source code of the application is there in "index.android.bundle". To check whether the error message was picked up from that title value or not, we modified the error message from "Device Security" to "Device Security Changed with Custom Data".

Android 4

To test this, we had to perform a series of commands, including:

  • Rebuilding the source using apktool.jar and generate APK
  • Signing the newly generated APK
  • Uninstalling the application from device
  • Installing the newly-signed APK to device

This was a very time-consuming task and we had to repeat these steps multiple times. To overcome this, we used our tool created at NotSoSecure called android_application_analyzer where we can complete all the above steps with a single click.

Android 5

When we tried to install the newly created and signed APK to the emulator, it prompted an error message.

Android 6

Despite lengthy analysis of the source code, we had no luck finding out what the issue was. We tried reinstalling to see how it creates the files in the emulator.

We reinstalled the application from Play Store again, analysed the content in the application folder of the device and found "split_config" related information.

Android 7

We had seen similar information in the "AndroidMenifest.xml" of the app, so we removed that attribute and tried to install the application again, but it still was generating an error message.

Android 8

As an alternate approach, we manually replaced the modified signed APK file using "adb push" command.

Android 9

Our edited version of the application successfully loaded, but it still responded with the same error message.

Andriod 10

Now that we were able to load the app manually, we modified the content to see if our changes would reflect on the app in the "Device Security" Title field.

Android 11

We made the necessary changes and reinstalled the app.

Android 12

We were now able to see the modified content on the app, but we still had to bypass the root check.

Android 13

Using the backtrack process we started searching for the unique keywords “security_title” and the “id” value associated. Although we found a few instances, it was ultimately a dead end.

Android 14

We knew that the application was protected against being loaded on rooted Android devices with multiple root checks. To investigate further, we searched for random keywords like "Rooted" in the application source code and found a few occurrences.

Android 15

The next step was to convert the Smali code to .jar by using dex2jar and analyse the code in JD-GUI. However, during this process we got the following error:

Android 16

There is another tool to covert the Smali code called Enjarify. Enjarify is a tool for translating Dalvik bytecode to equivalent Java bytecode. This allows Java analysis tools to analyse Android applications. Enjarify will always produce a .jar file, even if there are errors in translation. Error pages will not be readable, but the remaining content can be easily traversed.

Android 17

Once we got the .jar file, we analysed the source code of "RootCheckUseCase" class. We identified RootBeer and custom implementation of root detection where it called "invoke" function, which essentially called "isDeviceRooted" function and within that it called all the other functions, such as:

  • checkRootMethod1
  • checkRootMethod2
  • checkRootMethod3
Android 18

Normally, to bypass this, we can write a Frida script and overwrite the definition of the "invoke" function. But this custom implementation accepts an object of class "Continuation" and returns an object. This presented another mystery for us to solve.

We tried to analyse the source code of the "isDeviceRooted" function which seemed to be the responsible function for root detection check, but Enjarify was not able to decompile the source code of that function. That put us in a tricky situation as we were not able to identify the object of that class and its possible return value which would be returned from this function.

Android 19

What does one do when coming up against regular checks? Take a step back and analyse. All the functions within "RootCheckUseCase" had the same function argument and returned the same value. Why not replace the code of the "invoke" function with another function such as "checkRootMethod2"?

Android 20

As the JD-GUI .jar file cannot be edited, we had to do this by modifying the Smali code.

  • Step 1: Copy the Smali code of "checkRootMethod2" function.
  • Step 2: Delete the existing Smali code of "invoke" function.
  • Step 3: Paste the Smali code of "checkRootMethod2" into "invoke" function.

Done correctly, the modified “invoke” function should execute the code written in "checkRootMethod2" function.

Step 1 : Copy the Smali code of "checkRootMethod2" function.

Android 21

Step 2: Delete the existing Smali code of "invoke" function.

Android 22

Step 3: Paste the Smali code of "checkRootMethod2" into "invoke" function.

Android 23

Once these steps were completed, we analysed the object of the class created with the "checkRootMethod2" method. It returned a Boolean condition.

Android 24

As per the code above, the function checks for existence of root files and folders. If these are detected, it returns with a True Boolean condition. In order to bypass this, we will have to force the return value to be false.

We accessed the corresponding Smali code and modified the condition to return false.

Android 25

We used the default process to recompile the application and generated a modified APK file where our condition for root detection was set to false. We removed the old APK from the device and pushed the modified one as shown below:

Android 26

The Next 2 Hours: Emulator Check

But even then, we faced the same issue. Sometimes a pen tester begins to wonder if their problem really can be solved. They despair. They almost give up. They lose hope of ever being able to solve their problem. Then they remember that solving such challenges is the joy of being a pen tester.

Android 27

We went back to the code in JD-GUI and spent time going through different files and spotted the class responsible for all the client-side checks. We found that the application had an emulator check and a Frida check as well.

Android 28

We noted the function responsible for "Emulator" detection check. This "Invoke" function did not have any parameter and returns a Boolean value. To bypass this, we created a Frida Script.

Android 29

The Next 1 Hour: Frida Running Check

But there was another problem: a Frida check was preventing our script from running. So we had to do yet another bypass on the Frida check function.

Android 30

We created a Frida script to bypass the "Emulator" and "Frida Running" check.

Android 31

Hoping to see some success, we hooked the Frida script and got the "Root Detection" error message, but this time clearly stating that the activity was triggering the Security Root Detection.

Android 32

The Next 2 Hours: Root Detection Check in React Native "index.android.bundle" file

Further analysis identified that the app had implemented the root detection check in the "index.android.bundle" file. To bypass this endpoint, we made the following changes in the "index.android.bundle" (see image below). Eventually, when this "JailMonkey" function is called, all the functions will return blank error messages and return false values, allowing us to bypass this check too.

Android 33

We installed the modified APK and hooked the Frida script on the application. Finally, we were able to load the application with all the following checks bypassed:

  • Root Detection check in Native Android
  • Emulator check in Native Android
  • Frida Running check in Native Android
  • Root Detection check in index.android.bundle
Android 34

The Final 3 Hours: SSL Pinning Bypass

But the story doesn't end there; we had only bypassed root detection but still had to capture the traffic. As stated previously, the application has a protection against network proxy. If we put in a proxy IP to redirect traffic to the proxy tool, we get a "No Network Connection" error. This obviously indicates that SSL Pinning is in place for the application.

Android 35

We used a well-renowned script written by Cubehouse that does most of the checks to bypass SSL Pinning. It works (almost) every time.

The script was able to detect some of the domains, but we were unable to do a complete SSL Pinning Bypass as we kept getting the same "No Network Connection" error message.

Android 36

Jumping back to JD-GUI, we identified the custom implementation of SSL Pinning in the application. As we can see there is a class named "XXXXCertificatePinner". The next thing is to identify the endpoint where the object for this class is created.

Android 37

We spotted the endpoint where the object of "XXXXCertificatePinner" was created and analysed the package "okhttp3.CertificatePinner".

Android 38

We identified a few methods which returned void and seemed responsible for SSL Pinning. The best way to verify our hypothesis was by deleting the entire code from all the methods highlighted below:

Android 39

We deleted from the code all the methods in Smali to simply return a blank void.

Android 40

Once again, we rebuilt the application. This time, FINALLY we were able to capture the traffic and perform the API testing further.

Android 41

This was one of the most interesting pen tests we have had the pleasure of working on. As always, our persistence and thinking outside the box paid off.

Penetration testing is an essential part of your cybersecurity programme. Implementing penetration testing as part of your information security strategy will allow you to assess the security your organisation’s principal assets and identify areas for improvement.

For more information on how penetration testing could benefit your security posture, get in touch below.