Published by Security Testing and Assurance on 6 July
To get full coverage testing during mobile application reviews, a jailbreak is sometimes used to grant root access to a mobile device. However, often frameworks and libraries use jailbreak or root detection to prevent mobile application inspection, or modification. Several native detections can be bypassed using a combination of Frida (Ravnås, 2023) and Objection (Jacobs, 2023). However, a number of development libraries and frameworks provide their own means of jailbreak or root detection. This article investigates the Flutter framework (Google, n.d.) and the methods for bypassing its detections on iOS. CyberCX have also published the scripts used for this bypass for other mobile application security researchers to use in their workflow on our GitHub (https://github.com/CyberCX-STA/flutter-jailbreak-root-detection-bypass).
Introduction
Flutter is a mobile app development framework created by Google that allows building cross-platform apps for iOS, Android, and the web using a single codebase. With its growing popularity, we are reviewing more and more mobile apps built using Flutter.
One obstacle for mobile application penetration testing is the use of third-party libraries that include root and jailbreak detection checks. These libraries perform thorough checks to detect and restrict the functionalities of applications operating within privileged environments. This is done to protect the app from malicious activity that could compromise user data and the application’s functionality.
There are publicly available tools and research to get around these restrictions. However, these tools rely heavily on signatures and hooks into the application runtime to disable protection components. Some of these tools need updating (especially the signatures) or are often discontinued. The current trial and error approach consumes a substantial amount of time during mobile application reviews.
In this article, I will discuss the difficulties of working with precompiled applications where the source code is not readily available. We will explore reverse engineering techniques to identify the functions responsible for the anti-tampering and anti-root detection checks. Furthermore, I will attempt to bypass the checks with the use of a well known dynamic instrumentation framework, Frida (Ravnås, 2023).
This article has a specific focus on bypassing these checks on iOS applications built using the Flutter framework. However, it is worth noting that the techniques discussed here can be applied to any modern iOS application written in Swift.
Challenges with Current Tooling
While some common tools and scripts are available for iOS applications that work in specific cases, many of them use a local signature database to make changes during application runtime. These scripts are built on an app-by-app basis targeting specific memory addresses. Locations in memory vary from application to application due to the functionalities they offer, thus rendering them useless when using them against multiple applications.
How jailbreak detections work in Flutter
Root and jailbreak detection
Flutter has an extensive list of third-party libraries that could be utilised to perform these checks. Some more common ones are RootBear (Projo & yakupbaser, 2021) for Android and IOSSecuritySuite (Reguła, NikoXu, Pastuszak, Bahrenburg, & Melo, 2023) for iOS applications. Developers can also implement their own additional checks if needed.
We have focused on the “IOSSecuritySuite” library used specifically for iOS applications. We noticed that the checks performed by IOSSecuritySuite are quite extensive. The library checks if:
- A device is Jailbroken.
- A Debugger is attached.
- If the app is running in an emulator.
- If common reverse engineering tools are running on the device.
Code review
Since IOSSecuritySuite (Reguła, NikoXu, Pastuszak, Bahrenburg, & Melo, 2023) library is open source, a good place to start is to look at the source and to observe the execution flow during app launch. For this we used a custom application built using the Flutter framework. This test app makes use of the IOSSecuritySuite library and displays the outcome of jailbreak check results inside the application.
Based on the below screenshot of the Jailbreak check function, we can see that the function amIJailbroken() from IOSSecuritySuite library is called.
Figure 1 Flutter library (Trappers & Elshiekh, 2022) calling JB checks from IOSSecuritySuite library.
Upon obtaining the outcome (a Boolean true/false result), the app will present a message indicating whether the device is jailbroken or not.
Going into the Swift function that does these checks, we noticed another child function performChecks() (https://github.com/securing/IOSSecuritySuite/blob/master/IOSSecuritySuite/JailbreakChecker.swift#L38) (Reguła, NikoXu, Pastuszak, Bahrenburg, & Melo, 2023) that is responsible for a number of individual checks. If any of these checks return the value True, the overall result will be set to “True”, and the device will be flagged as Jailbroken.
Figure 2 (Reguła, NikoXu, Pastuszak, Bahrenburg, & Melo, 2023) Showing the extended checks performed by the Jailbreak Checker (https://github.com/securing/IOSSecuritySuite/blob/master/IOSSecuritySuite/JailbreakChecker.swift)
Exploring Further
Expanding any of these functions from the screenshot above will reveal the specific files or binary signatures the library is looking for in the file system.
For instance, the checkExistenceOfSuspiciousFiles() function searches for commonly found files on a jailbroken device. These files are typically absent in a clean iOS installation.
Figure 3 Showing the files checkExistenceOfSuspiciousFiles function looks for.
From our observation, the checks are quite extensive.
The following screenshot from Objection tool shows the libraries loaded by the application once launched. Objection is an extension for Frida that simplifies the process of performing common runtime security assessments and manipulations on Android and iOS applications. More details about Objection can be found on their Wiki https://github.com/sensepost/objection/wiki/. From the screenshot below, we can confirm that at the library is indeed loaded.
Figure 4 Showing the libraries loaded (including the jailbreak detection library) by an application once launched.
Challenges
While the source code for the Swift function was easier to follow, Flutter apps are compiled and packaged during the application deployment process. As a result, accessing these functions may prove challenging due to the representation of function names after the code is compiled.
With Objection, it is possible to hook into application classes and specific methods using Frida’s runtime instrumentation capabilities. This enables us to intercept method calls, modify their behaviour, log information, or perform various runtime manipulations.
However, upon closer examination of the app’s classes, there is no readily available jailbreak detection method in Frida when searching solely by the function name.
We might have better luck looking at the compiled code by using popular reverse engineering tools such as Hopper (Cryptic Apps, n.d.) or Ghidra (National Security Agency, 2023).
Modifying Assembly
Once installed, the Flutter app installation directory contains a Framework folder. The compiled libraries can be found inside this folder.
Figure 5 Showing the IOSSecuritySuite library present inside the application.
Figure 6 Showing the compiled IOSSecuritySuite library.
It is possible to open these files using tools such as Ghidra or Hopper as they contain the instructions for the mobile OS to run. For the following demonstration, the Hopper disassembler is used.
Since we had access to the source already, a good place to start will be looking for function names from the JailbreakChecker function inside the IOSSecuritySuite library. The function we are interested in is called amIJailbroken, and fortunately, we obtained several hits after conducting a search.
Looking at the screenshot below, we can see the function name, although slightly altered and using assembly instructions.
Figure 7 Showing the amIJailbroken() function instructions.
In summary, in Figure 7 we can observe the amIJailbroken function call in Step 1, validating our initial assumption. Step 2 serves as confirmation. In Step 3, the performChecks() function is invoked to execute a set of checks using the Branch with Link (BL) instruction (Arm, n.d.). The results are stored in Step 4 and passed to the parent function for further processing. If we can influence the instruction shown in Step 4, we should be able to control the resulting outcome. However, it is important to note that while the function names can still be identified from the instructions, they are obfuscated.
Implementing the Bypass
Bypassing IOSSecuritySuite checks
While it is possible to hook into each of these functions and return the test results as false, it would be much easier to change the overall outcome found in Line 26 of Figure 8.
Figure 8 Showing the amIJailbroken function in JailbreakChecker
(https://github.com/securing/IOSSecuritySuite/blob/master/IOSSecuritySuite/JailbreakChecker.swift)
This should set the Jailbroken status to our liking. Looking at the potential options, two methods could be used to get around these checks:
- Patching the compiled IOSSecuritySuite binary for the check to always return false and replacing it within the application.
- A Frida script that hooks into the specific Jailbreak check function and modifies the test results.
We will explore both methods in this section. To test and validate the patches, we developed a test Flutter app that utilises the IOSSecuritySuite library. Upon launching the app, it executes a series of checks and if the device is detected as Jailbroken, a corresponding status message will be displayed.
Figure 9 Mobile application showing device is detected as jailbroken.
Going through the binary route in Hopper disassembler, there is a feature that sets a specific region to NOP by navigating to Modify > NOP region. Once modified, the overall instruction sets will look like this.
Figure 10 Showing the NOP modification.
The modified binary can now be exported as a new file and easily substituted with the application binary. The application is then repacked and installed on iOS. After replacing the original binary with the modified one and restarting the application, the status message should display a value of ‘NO’.
Figure 11 Showing that jailbreak checks have been successfully bypassed.
The modified binary can be found on the GitHub page (https://github.com/CyberCX-STA/flutter-jailbreak-root-detection-bypass/raw/main/IOSSecuritySuite) and should work with any Flutter apps that makes use of IOSSecuritySuite. It has been tested with multiple applications without any issues. However, if this method does not yield the desired results, an alternative approach worth considering is using a Frida script.
Dynamic Instrumentation
Dynamic instrumentation tools such as Frida come in handy here. It lets testers hook and trace specific functions on the fly.
As explained in the Modifying Assembly section, it is indeed possible to retrieve the modified function names by examining the assembly code. However, a more efficient approach is available by using the runtime application instrument tool called Grapefruit (ChiChou, 2022).
Figure 13 Showing the Grapefruit interface.
Grapefruit takes advantage of Frida and presents a web interface to help with instrumenting mobile applications. It also lets users browse specific application function calls and explore the swift function names used by the apps during runtime.
Figure 14 Showing available functions from the Grapefruit interface.
Once specific methods are identified, Grapefruit can also generate a starter script that will notify us if the specific method is called by the application during runtime. Figure 15 shows an example script.
Figure 15 Showing a script hook to observe Jailbreak check call stack.
With that information handy, we could use the script to hook into the function and observe the Jailbreak check output.
Result
Here is the terminal output while running the script. The script shows the call stack along with the returned value from the function 0x1 (True).
Figure 16 Showing the terminal output of running the previous hooking script.
With that we are confident that the amIJailbroken() function is now reachable. We could go further and intercept the result using Frida’s interceptor function and change the detection result to FALSE.
Figure 17 Showing the Frida script used to achieve the bypass.
Figure 18 shows the script in action. Once we run the app with the Frida script, the script hooks into the target function and changes the outcome.
Figure 18 Showing the script changing the detection outcome.
Looking at the application, the detection checks are bypassed altogether.
Figure 19 Showing the mobile application with the checks successfully bypassed by the script.
Frida Scripts
The Frida scripts used to bypass Jailbreak and root detection checks can be found here in CyberCX GitHub repo.
https://github.com/CyberCX-STA/flutter-jailbreak-root-detection-bypass
What’s Next
Our primary focus in this article was on bypassing Jailbreak detection in general. Stay tuned for future articles where we will explore techniques for bypassing root detection in Android.
References
Arm. (n.d.). B and BL. Retrieved from ARM Developer Suite Assembler Guide: https://developer.arm.com/documentation/dui0068/b/CIHFDDAF
ChiChou. (2022, 10 13). Grapefruit: Runtime Application Instruments for iOS. Retrieved from GitHub: https://github.com/ChiChou/Grapefruit
Cryptic Apps. (n.d.). Hopper. Retrieved from https://www.hopperapp.com/
Google. (n.d.). Build apps for any screen. Retrieved from Flutter: https://flutter.dev/
Impact-I. (2022, 04 08). reFlutter. Retrieved from GitHub: https://github.com/ptswarm/reFlutter
Jacobs, L. (2023, 04 23). objection – Runtime Mobile Exploration. Retrieved from GitHub: https://github.com/sensepost/objection
National Security Agency. (2023, 06 16). Ghidra Releases. Retrieved from GitHub: https://github.com/NationalSecurityAgency/ghidra/releases
Projo, N., & yakupbaser. (2021, 05 03). root_check. Retrieved from Flutter Package: https://pub.dev/packages/root_check
Ravnås, O. (2023, 07 01). Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers. Retrieved from Frida: https://frida.re/
Reguła, W., NikoXu, Pastuszak, D., Bahrenburg, B., & Melo, I. (2023, 04 22). IOSSecuritySuite. Retrieved from GitHub: https://github.com/securing/IOSSecuritySuite
Trappers, J., & Elshiekh, M. (2022, 11 20). SwiftFlutterJailbreakDetectionPlugin.swift. Retrieved from GitHub: https://github.com/jeroentrappers/flutter_jailbreak_detection/blob/master/ios/Classes/SwiftFlutterJailbreakDetectionPlugin.swift
Author: Shofe Miraz – Security Consultant