In this blog post we will look at different ways in which developers can implment jailbreak detection in their applications and the methods to bypass them.
Jailbreak Detection Methods and Bypasses
Below are the different methods used to determine whether the current device is jailbroken or not.
Detection Based on Jailbreak Specific Files
When a device is jailbroken, many unix utilities (apt, su etc.), applications (Cydia.app) are written to the file system at specific locations. During runtime, an app can look if there are files existing at these paths to determine if the device is jailbroken or not. Below is a list of paths to files, applications which are written to the file system once a device is jailbroken.
/Applications/Cydia.app
/private/var/stash
/private/var/lib/apt
/private/var/tmp/cydia.log
/private/var/lib/cydia
/private/var/mobile/Library/SBSettings/Themes
/Library/MobileSubstrate/MobileSubstrate.dylib
/Library/MobileSubstrate/DynamicLibraries/Veency.plist
/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist
/System/Library/LaunchDaemons/com.ikey.bbot.plist
/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist
/var/cache/apt
/var/lib/apt
/var/lib/cydia
/var/log/syslog
/var/tmp/cydia.log
/bin/bash
/bin/sh
/usr/sbin/sshd
/usr/libexec/ssh-keysign
/usr/sbin/sshd
/usr/bin/sshd
/usr/libexec/sftp-server
/etc/ssh/sshd_config
/etc/apt
Note: This no way a complete list.
Let us look at a simple sample implementation in which the application looks for the presence of /Applications/Cydia.app
. The application will show an alert depending on the result.
Lets run this app on a jailbroke device to see if it works.
|
Let us analyze the application executable in hopper disassembler. Find the application executable location.
Connect to the device using cyber duck (or use sftp/scp to device from terminal).
Navigate to the application directory.
Pull the application binary to the host machine.
Open the application binary in Hopper disassembler.
Go with defaults.
Once hopper finishes disassembling the application binary into assembly code, we can look for procedures, strings in the application.
The pseudo code for the assembly code can be seen in hopper. The pseudo code is easily understandable (atleast in this case) and one can easily determine what it does.
Hopper also provides a control flow graph (CFG) view as well.
Bypassing FileSystem Based Checks By Patching
Once we understand the jailbreak detection implementation, next step would be to bypass it. When we inspect the assembly code/CFG, we can see that irrespective of the checks performed, the decision is determined by a single instruction as seen below.
The tbz arm instruction tests a bit and branches if zero to the specified label.
In our case the instruction is tbz w8, 0x0, loc_1000064ec
. This will check if the w8 register is zero and if zero the control will passed to the label loc_1000064ec
from where the code which shows alert for showing device not jailbroken starts.
Now lets patch this instruction to branch to the label loc_1000064ec
irrespective of the value of w8
register. Goto modify menu in hopper and select assemble instruction.
Replace the instruction with just a branch instruction to the desired label.
b loc_1000064ec
Control flow graph after patching.
Pseudo Code after patching.
Now, save the patched binary.
Put the patched binary in the application directory on the device replacing the original binary. This patched binary does not have code signing, so for it to run successfully, the AppSync Unified Tweak from Karens Repo must be installed from Cydia on the Jailbroken device.
Bypassing FileSystem Based Checks With Cydia Tweaks
There are several tweaks available in cydia which allows to bypass jailbreak detection. Below are some of the jailbreak detection bypass tweaks which works with different iOS versions.
Tweak Name | Cydia Repo | iOS Version Support |
---|---|---|
xCon | Available in Cydia Default Repo’s (ModMyi) | iOS 9 |
tsProtector 8+ | Available in Cydia Default Repo’s (BigBoss) | iOS 8 & 9 |
JailProtect | julioverne’s Repo | iOS 10 |
Shadow | Jolano’s Repo | iOS 8.0 - 12.1.2 |
Liberty Lite | Ryley’s Repo | iOS 11 - 12 |
UnSub | Nepeta Repo Mirror | iOS 9 - 12 |
———- | ———- | ——————– |
Lets install and test the Liberty Lite tweak to see whether it is able to bypass our simple jailbreak detection.
|
The Liberty Lite tweak successfully defeats the Cydia.app file system check.
Detection Based on cydia:// URI Scheme
Most of the popular jailbreaks installs the Cydia store application and registers the cydia:// uri scheme on the device. If an application is able to open a url starting with the cydia:// uri scheme, the device must be jailbroken.
Let us modify the jailbreak detection to include more file system path checks along with the cydia:// uri scheme check. The jbCheck1 method performs file system based checks and jbCheck2 does the cydia uri scheme check. in both the methods logging is enabled so that we can see the output of each check on the Xcode console during runtime.
Modify the code to run when the isJailbroken button in ui is clicked to invoke both the checks.
Install and run the application on the device and observe the xcode logs.
It can be seen that, both the checks were successfull and the app should show the alert stating that the device is jailbroken.
Now let us enable the Liberty Lite Tweak and see if it is able to bypass these checks.
The tweak is able to bypass the cydia:// uri scheme check and most of the file system based checks except for the /usr/bin/apt
check.
Bypassing Jailbreak Detection Using Frida
Lets hook the jailbreak detection methods jbCheck1 and jbCheck2 during run time and observe the values it return to the caller. We know the name of the view controller in which the jailbreak detection methods are implemented from the source code/reversing/frida scripts.
Name of view controller: ViewController
Name of Methods: jbCheck1 and jbCheck2
Below is the full javascript code which hooks both the aforementioned methods and displays the return values.
if (ObjC.available) {
try {
var className = "ViewController";
var funcName1 = "+ jbCheck1";
var funcName2 = "+ jbCheck2";
var hook1 = eval('ObjC.classes.' + className + '["' + funcName1 + '"]');
var hook2 = eval('ObjC.classes.' + className + '["' + funcName2 + '"]');
Interceptor.attach(hook1.implementation, {
onLeave: function(retval) {
console.log("[*] Class Name: " + className);
console.log("[*] Method Name: " + funcName1);
console.log("\t[-] Return Value: " + retval);
}
});
Interceptor.attach(hook2.implementation, {
onLeave: function(retval) {
console.log("[*] Class Name: " + className);
console.log("[*] Method Name: " + funcName2);
console.log("\t[-] Return Value: " + retval);
}
});
}
catch(err) {
console.log("[!] Exception2: " + err.message);
}
}
else {
console.log("Objective-C Runtime is not available!");
}
Let us inject the hook_methods.js code to our application using frida during runtime.
Return values when Liberty Lite tweak is not enabled for the application.
Earlier we observed that, Liberty Lite fails to mask detecting the /usr/bin/apt
file path and hence returning True for jbCheck1. This is confirmed when we inject the hook_methods.js into the application with Liberty Lite enabled.
Now let us modify the return values for both functions jbCheck1 and jbCheck2 so that they will return False in all cases. Below javascript code - method_override.js - does just this.
if (ObjC.available) {
try {
var className = "ViewController";
var funcName1 = "+ jbCheck1";
var funcName2 = "+ jbCheck2";
var hook1 = eval('ObjC.classes.' + className + '["' + funcName1 + '"]');
var hook2 = eval('ObjC.classes.' + className + '["' + funcName2 + '"]');
Interceptor.attach(hook1.implementation, {
onLeave: function(retval) {
console.log("[*] Class Name: " + className);
console.log("[*] Method Name: " + funcName1);
console.log("\t[*] Original Return Value: " + retval);
var newret = ptr("0x0");
retval.replace(newret);
console.log("\t[*] Fake Return Value: " + newret);
}
});
Interceptor.attach(hook2.implementation, {
onLeave: function(retval) {
console.log("[*] Class Name: " + className);
console.log("[*] Method Name: " + funcName2);
console.log("\t[*] Original Return Value: " + retval);
var newret = ptr("0x0");
retval.replace(newret);
console.log("\t[*] Fake Return Value: " + newret);
}
});
}
catch(err) {
console.log("[!] Exception2: " + err.message);
}
}
else {
console.log("Objective-C Runtime is not available!");
}
Now let us inject this into our application see the outcome.