Jailbreak Detector Detector: An Analysis of Jailbreak Detection Methods and the Tools Used to Evade Them
Why Do People Jailbreak?
Apple’s software distribution and security model relies on end users running software exclusively distributed by Apple, either via inclusion in the base operating system or via the App Store.
To run applications that are not available in the App Store or make modifications to the behavior of the operating system, a “jailbreak” is required—effectively, an exploit that allows the user to gain administrative access to the iOS device. After jailbreaking, users can install applications and tweaks via unofficial app stores.
Jailbroken devices are also excellent tools for security researchers. iOS kernel security research is significantly easier with root-level access to the device. Gal Beniamini from Google’s Project Zero says:
Apple does not provide a “developer-mode” iPhone, nor is there a mechanism to selectively bypass the security model. This means that in order to meaningfully explore the system, researchers are forced to subvert the device’s security model (i.e., by jailbreaking).
In short, people jailbreak their devices for many reasons, ranging from research to personal philosophy. Regardless of the user’s rationale, the presence of a jailbreak on a device means that the security model of the OS can no longer be adequately trusted or reasoned about by an application.
The History of Jailbreaking
The first iPhone was released in June 2007, and in August 2007, George Hotz became the first person to carrier-unlock the iPhone. A carrier-unlock is not the same as a jailbreak, but in this case, jailbreaking the device was a prerequisite. Hotz’s original exploit required a small hardware modification to the device, but software-only jailbreaks were released soon after.
Since then, Apple and jailbreak developers have been in a cat-and-mouse game, with Apple patching vulnerabilities while developers and researchers attempt to find new ones.
The jailbreak scene has shrunk significantly since the release of the original iPhone. As Apple hardens the security of its iOS devices, exploiting them becomes significantly harder. The value of an iOS exploit on the private market is easily several hundred thousand dollars, and can also exceed $1,000,000 under the right criteria (remote, persistent and zero-click), making a private sale a much more lucrative option than releasing it publicly.
Why Do We Care About Jailbreaking at Duo?
At Duo, we give administrators insight into the health of devices used to access corporate resources. In a BYOD context, it is important to be able to understand the security properties of the devices on your network.
Jailbreaking an iOS device does not, on its own, make it less secure. There are two main issues with the security of a jailbroken device:
- First, running untrusted (non-App-Store*) code on the device, especially outside of the sandbox, makes it harder to reason about the security properties of the device.
- The second, more concerning issue is that users of jailbroken devices frequently hold off on updating their devices, as jailbreak development usually lags behind official software releases.
Administrators may want to only allow up-to-date devices access to resources on their network, as software updates frequently patch security vulnerabilities. A jailbroken device can masquerade as an up-to-date device by misreporting its software version.
As a result, administrators cannot trust version information submitted by jailbroken devices, so it is important to be able to detect the jailbroken state.
* While, in general, we can expect that the App Store review process will prevent actively malicious applications from distribution on the App Store, this is not always the case. The XcodeGhost malware is an example of how malicious code was shipped as part of well-known and trusted applications on the App Store.
How Are Jailbreaks Usually Detected?
There exists only scattered information online about jailbreak detection methodology. This is partially because jailbreak detection is a sort of “special sauce.” Developers of mobile applications would rather keep their methodology private, and there are no real incentives to talking about it publicly.
I was able to learn about existing jailbreak detection methods from some online documentation and communities like r/jailbreak, but most of the useful information I learned in the course of this research came from reverse engineering popular anti-jailbreak-detection tools.
Most jailbreak detection methods fall into the following categories:
- File existence checks
- URI scheme registration checks
- Sandbox behavior checks
- Dynamic linker inspection
Most public jailbreak methods leave behind certain files on the filesystem. The clearest example is Cydia.
Cydia is an alternative app store commonly used to distribute tweaks (UI changes, extra gestures, etc.) and third-party applications to users of jailbroken devices. As a result, nearly every jailbroken device has a directory at
/Applications/Cydia.app. If this file exists on the filesystem, you can be sure your application is running on a jailbroken device.
There are also various binaries such as
sshd commonly found on jailbroken devices, as well as files intentionally left by jailbreak utilities to mark that a device has already been jailbroken, preventing the utility from running twice and possibly causing unintended harm.
iOS applications can register custom URI schemes. Duo uses this functionality so that clickable web links can open the Duo Mobile app, making the setup of Duo Mobile easy.
Cydia registered the
cydia:// URI scheme to allow direct links to apps available via Cydia. iOS allows applications to check which URI schemes are registered, so the presence of the
cydia:// URI scheme is frequently used to check if Cydia is installed and the device is jailbroken.
Unfortunately, some apps perform this detection by attempting to register the
cydia:// URI scheme for themselves, so checking if the scheme is registered may produce a false-positive on a non-jailbroken device.
Jailbreaks frequently patch the behavior of the iOS application sandbox. As an example, calls to
fork() are disallowed on a stock iOS device: an iOS app may not spawn a child process.
If you are able to successfully execute
fork(), your code is likely running on a jailbroken device.
Dynamic Linker Inspection
Dynamic linking is a way for executables to take advantage of code provided by other libraries without compiling and shipping that code in the executable. This helps different executables reuse code without including a copy of it. Dynamic linking allows for much smaller binaries with the same functionality - the alternative to this is “static linking,” where all code that an executable uses is shipped with the executable.
While we haven’t discussed them yet, anti-jailbreak-detection tools are frequently loaded as dynamic libraries. The iOS dynamic linker is called
dyld, and exposes the ability to inspect the libraries loaded into the currently-running process. As a result, we should be able to detect the presence of anti-jailbreak-detection tools by looking at the names and numbers of libraries loaded into the current process. If an anti-jailbreak-detection tool is running, we know the device is jailbroken.
How Do End Users Prevent Detection?
Many mobile applications will refuse to run if they detect that the device they are running on is jailbroken. In Duo’s case, we do not prevent use of the Duo Mobile app, but Duo administrators may prevent jailbroken devices from authenticating to protected applications.
For these reasons, users of jailbroken devices frequently install anti-jailbreak-detection tools that aim to hide the tampered status of the device. These tools modify operating system functionality such that the device acts as though it were in an untampered state. They are effectively a type of intentionally installed rootkit, though generally running in userland rather than in the iOS kernel.
The specific functions that are hooked and the methods used to hook them vary.
Objective-C Runtime Method Hooking
Objective-C dispatches method calls at runtime. Calling a method is akin to sending a message (ala Smalltalk). This stands counter to languages like C in which a function call might take the form of a jump to the called method’s location in memory.
Because method calls are dispatched at runtime, Objective-C also allows you to add or replace methods at runtime. This is sometimes referred to as “method swizzling,” and takes the form of a call to
fileExistsAtPath is an Objective-C method commonly used to check for the existence of jailbreak artifacts. Replacing the implementation of
fileExistsAtPath to always return false for a list of known jailbreak artifacts is a common strategy to defeat this jailbreak detection technique.
Editing the Linker Table
When a dynamically loaded library is used in an executable, its symbols must be bound: the executable has to figure out where the shared code actually lives in memory. On an iOS system using dyld, a call to
printf, for example, is actually a call to an address that lives in the
__stubs section. At this address is a single
jmp instruction to an address loaded from the
__la_symbol_ptr (lazy symbol pointers) or
__nl_symbol_ptr (non-lazy symbol pointers) section.
Lazy symbol pointers are resolved the first time they are called, and non-lazy symbol pointers are resolved before the program runs. You can read more about how the linker works on Mike Ash’s blog, but the important thing to understand is that the entry in the
__xx_symbol_ptr table will, after the symbol has been resolved, contain the proper address for the function being called.
A consequence of this design is that if you want to hook every call to
printf, you can do so by replacing a single entry in the
__la_symbol_ptr section. All calls to
printf from that point on will jump to your custom hook.
Anti-jailbreak-detection tools make use of this technique to hook functions that may be used to check for file existence or that may expose non-standard sandbox behavior.
This is an example of a hooked version of the
fopen function. As a reminder, the
fopen function will attempt to open a file (by path name), and either return a pointer to the open file handle or
null if it cannot open the file.
fopen returns non-null when called with a path to a known jailbreak artifact, you can be sure the device is jailbroken. The above hooked version checks the path of the file to be opened against a list of “forbidden” files. These are known jailbreak artifacts as well as files that are usually present on the system but can only be opened if the sandbox has been modified. The hooked
fopen will act as though those files do not exist or cannot be opened, and otherwise defer to the original fopen implementation.
lstat, etc. are hooked to prevent detection of files on the filesystem. Some other functions, such as
fork, as hooked to always return a constant value (for example: a hooked version of fork may return
-1, indicating that
fork is not allowed, which is consistent with the behavior of an untampered sandbox).
Patching the Linker
We mentioned that
dyld exposes functionality that allows clients to inspect what libraries have been loaded into the running process. Anti-jailbreak-detection tools are loaded into processes as shared libraries, and
dyld will expose this. To combat this, some anti-jailbreak-detection tools also hook exposed
dyld functionality to hide their presence.
A slightly more interesting way to detect the presence of a jailbreak using the dynamic linker makes use of
dlsym to try to determine the addresses of the original, unhooked functions.
dlsym should give you the correct address for a dynamically linked function, even if its entry in the linker symbol table has been overwritten.
Some anti-jailbreak-detection tools are aware of this, and will actually intercept calls to
dlsym and return pointers to the hooked functions. This is an interesting example of the cat-and-mouse game that has been played between app developers who wish to detect jailbroken devices and hobby developers who maintain anti-jailbreak-detection tools.
These are only some of the methods used to evade jailbreak detection. While they differ in nature, they all rely on various forms of indirection: functionality provided by the Objective-C runtime or by shared libraries can be overridden with ease and made to report “correct” answers, similar to a rootkit.
An ideal jailbreak detection method would rely on as little indirection as possible.
Can We Reliably Detect Jailbroken Devices?
We would like to look for artifacts of a jailbroken device (existence of certain files, sandbox behavior, etc,) while relying on as little shared functionality as possible. However, we need to rely on functionality exposed by the operating system to make these checks. In the usual case, to check if a file can be opened, we would call the
fopen syscall wrapper exposed as part of a shared library. As detailed in previous sections, functions in shared libraries might be replaced with tampered versions that prevent our checks from working.
As a refresher, a syscall is an interface to privileged functionality exposed to userspace code by the kernel. It may be dangerous to allow userspace code to directly read or write blocks on a hard drive, for example, so we instead use the
open syscall to say “hey kernel, can you please perform the privileged action of opening this file for me, and then give me a handle I can use to interact with it.” Functions like
fopen are just that—functions—but they wrap a special type of instruction used to jump into the kernel.
On the x86 architecture, under Linux, the
INT 0x80 instruction is the most well-known way to perform a syscall (with newer options available, like the x86-64
INT stands for “interrupt,” and the
INT instruction causes the CPU to jump to a special section of code called an interrupt handler, running in the context of the kernel. The end result is that userspace can trigger the execution of privileged code in a controlled manner, without being able to arbitrarily execute privileged code.
The iPhone uses the ARM processor architecture. ARM’s equivalent of
INT is the
SVC opcode (“Supervisor Call”), and the equivalent to
INT 0x80 on an ARM processor is
SVC 0x80. Functions like
fopen may do some sanity-checking and processing of arguments in user-space, but they will eventually use
SVC 0x80 to ask the kernel to perform the privileged action of providing access to a file.
The important takeaway here is that if we would like to avoid relying on shared wrapper functions that may be hooked, we can actually perform syscalls directly using the same opcodes the wrapper functions use. We can also inline these calls to avoid having a single call target for our custom syscall wrappers that might be overwritten. This lets us avoid the layers of indirection that come with jumping to functions exposed by shared libraries, shielding us from possible symbol table tampering.
Even though this approach solves some of our problems, there are drawbacks.
First, writing custom syscall wrappers can require maintenance, especially if there are new architectures you need to support. Additionally, the syscall interface may change over time, and the shared libraries provided by the operating system will keep up with those changes, whereas your custom implementation may not.
Second, while this approach makes it harder for end users to evade jailbreak detection, it doesn’t make it impossible. The flow of the data after the syscall—say, a boolean that indicates whether a jailbreak artifact exists—is still vulnerable to tampering. Additionally, a determined attacker could patch out the checks, or even possibly modify the kernel.
Approaches like this must be considered in the context of a threat model. It is impossible to guarantee that you will be able to detect a tampered device for the simple reason that you are restricted to running in userspace, whereas anti-jailbreak-detection utilities can run in a privileged context. With that said, the goal is not perfect security, but rather sufficient security such that the average end user of a jailbroken device—who is not a determined attacker—will not be able to evade detection.
Ultimately, the security of your application cannot rely on hiding the way it works. Proper server-side validation of client-submitted data, use of well-known cryptographic protocols, and use of hardware-backed cryptographic functionality available in many newer devices all go a long way to strengthening the security posture of your application without relying on obscurity.