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.

cydia_check

Lets run this app on a jailbroke device to see if it works.

install | run1

Let us analyze the application executable in hopper disassembler. Find the application executable location.

jbDetectPath

Connect to the device using cyber duck (or use sftp/scp to device from terminal).

cyberduck_con

Navigate to the application directory.

cyberduck_nav

Pull the application binary to the host machine.

cyberduck_pull

Open the application binary in Hopper disassembler.

hopper-bin

Go with defaults.

hopper-def

Once hopper finishes disassembling the application binary into assembly code, we can look for procedures, strings in the application.

jb-method

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.

pseudo

Hopper also provides a control flow graph (CFG) view as well.

cfg

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.

tbz

The tbz arm instruction tests a bit and branches if zero to the specified label.

tbz-arm

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.

stock_device_alert

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.

assemble

Replace the instruction with just a branch instruction to the desired label.

b loc_1000064ec

patch1 patch2

Control flow graph after patching.

patch-cfg

Pseudo Code after patching.

patch-pseudo

Now, save the patched binary.

new-bin

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.

liberty | liberty_bypass

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.

jbchecks

Modify the code to run when the isJailbroken button in ui is clicked to invoke both the checks.

jbBtnCode

Install and run the application on the device and observe the xcode logs.

xcodelog

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.

libertybeat

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!");
}

hook-methods-code

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.

hook-noliberty hook-noliberty-gif

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.

hook-liberty hook-liberty-gif

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!");
}

method-override-code

Now let us inject this into our application see the outcome.

frida-jbbypass