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.