iOS: Defeating Swift jailbreak detection

Abstract.

Today we are looking at a simple Swift program that uses a jailbreak detection function. To increase the fun and get some practical code at the end of the day, I searched for a jailbreak detection function for Swift online. The foundation code can be found here. The main goal is, to bypass the Swift jailbreak detection function.

Code.

By taking a look at the following Swift function, you can see that it performs several checks for files and paths that usually exist on a jailbroken device. Furthermore, it will try to call a Cydia package URL as well as try to create a file in the /private/ directory.

import Foundation
import UIKit

func isJailbroken() -> Bool {
		
		guard let cydiaUrlScheme = NSURL(string: "cydia://package/com.example.package") else { return false }
		if UIApplication.shared.canOpenURL(cydiaUrlScheme as URL) {
            print("[Jailbreak detection]:\tCydia URL scheme.")
			return true
		}
		
		#if arch(i386) || arch(x86_64)
			// This is a Simulator not an idevice
            print("[Jailbreak detection]:\tSimulator detected.")
			return true
		#endif
		
		let fileManager = FileManager.default
		if fileManager.fileExists(atPath: "/Applications/Cydia.app") ||
			fileManager.fileExists(atPath: "/Library/MobileSubstrate/MobileSubstrate.dylib") ||
			fileManager.fileExists(atPath: "/bin/bash") ||
			fileManager.fileExists(atPath: "/usr/sbin/sshd") ||
			fileManager.fileExists(atPath: "/etc/apt") ||
			fileManager.fileExists(atPath: "/usr/bin/ssh") ||
			fileManager.fileExists(atPath: "/private/var/lib/apt") {
            print("[Jailbreak detection]:\tUncommon file exists.")
			return true
		}
		
		if canOpen(path: "/Applications/Cydia.app") ||
			canOpen(path: "/Library/MobileSubstrate/MobileSubstrate.dylib") ||
			canOpen(path: "/bin/bash") ||
			canOpen(path: "/usr/sbin/sshd") ||
			canOpen(path: "/etc/apt") ||
			canOpen(path: "/usr/bin/ssh") {
            print("[Jailbreak detection]:\tCan open uncommon path.")
			return true
		}
		
		let path = "/private/" + NSUUID().uuidString
		do {
			try "anyString".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
			try fileManager.removeItem(atPath: path)
            print("[Jailbreak detection]:\tCreate file in /private/.")
			return true
		} catch {
			return false
		}
	}
	
func canOpen(path: String) -> Bool {
		let file = fopen(path, "r")
		guard file != nil else { return false }
		fclose(file)
		return true
	}

Every check returns the boolean value true (0x1), if the execution was successful. If none of the checks resulted in a jailbreak detection the value false (0x0) will be returned.

The following screenshot shows the corresponding Swift ViewController, to manually trigger the isJailbroken() function:

Assumptions.

When I compiled the program, I were pretty sure that I will also discover the name of the function isJailbroken() somewhere inside of Hopper, guess what? I was wrong :). As we have learned earlier (see Blogpost #0 and #1), we most likely will find the string values that exceed the size of 15 bytes in the Str section of Hopper, which will make it easy to discover the code that represents the isJailbroken() function. But what if some fancy developer was into some obfuscation, encoding all string values with whatever technique? So it would be nice to find a way, to be still able to discover the function of interest.

Runtime analysis.

Let’s assume, that we have not been able to discover the code responsible for jailbreak detection with Hopper Disassembler. What could we do next? How about analyzing the iOS Swift app during the runtime? Well we could do that, but how? Which tools can be used? The first tools that came to my mind were the following:

  • GDB
  • FRIDA

GDB a very popular debugger, can be used to attach to a running process or even starting the binary and then analyzing and manipulating the app during runtime.

Problem:

  • no-stable version available for iOS > 10.x (as long as I know)

Frida, a very popular tool that can be used for many purposes, but one of the main functionalities (that I use) is tracing and hooking objective-c methods during runtime and manipulating the return values.

Problem:

  • Swift is not supported out, of the box, the only project I discovered so far is here but I don’t know the capabilities

As we can see, GDB and Frida don’t fit our needs. Well there is another tool … lldb, which comes with every XCode installation. Let’s see if we can use lldb to analyze and bypass the jailbreak detection! lldb is very similar to GDB, I found the tool to be super intuitive! Many app’s that you can download from the iOS app store, also suffer from protecting against debugging.

Bypass.

Of course, we will not cover all functionalities of lldb (not that I know all of them), but I will show you how to find the function of interest and how to manipulate the return value!

First fire up the app with XCode which will automatically attach lldb to the process:

Next we hit the Pause program execution button:

This will give us a lldb shell, here we can get information on the available functions by entering the command help. Next we are going to find our Swift isJailbroken() function by using the breakpoint utility with the regex option.

(lldb) breakpoint set -r "Jail"

This will create more breakpoints than necessary but we will be able to also see our jailbreak detection function which is super helpful (Yes I know, what if the fancy developer also obfuscated the Swift function name? We assume that this is not the case…). The following screenshot shows all created breakpoints:

Oh, breakpoint 1.10 looks interesting, so we just discovered the isJailbroken() Swift function:

1.10: where = Swift challenge two`Swift_challenge_two.isJailbroken() -> Swift.Bool + 36 at JailBreak.swift:8, address = 0x00000001044f8a3c, resolved, hit count = 1 

We could also create the breakpoint with the following command, but I wanted to show the power of the regex „-r“ parameter, because most of the time in a blackbox assessment we must guess the function names:

(lldb) breakpoint set -F isJailbroken 

So we hit the Jailbreak detection button in the UI on the test device and wait until a breakpoint is triggered. We can now examine the content of the arm64 registers, which will give us a lot of information where we are and where we are coming from 🙂

(lldb) register read
General Purpose Registers:
x0 = 0x000000010550c560
x1 = 0x000000010550c560
x2 = 0x000000010550c560
x3 = 0x00000001c0103ba0
x4 = 0x00000001c0103ba0
x5 = 0x00000001c0103ba0
x6 = 0x0000000000000000
x7 = 0x0000000000000403
x8 = 0x0000000000000000
x9 = 0x0000000000000000
x10 = 0x0000000000000006
x11 = 0x0000000000000000
x12 = 0x0000000105825410
x13 = 0x000005a1044fd495 (0x00000001044fd495) (void *)0x01b5404c38000001
x14 = 0x0000000000000000
x15 = 0x00526401005264c0
x16 = 0x00000001044fd490 (void *)0x000001a1044fd5e1
x17 = 0x000000018cc6ce10 UIKit'-[UIViewController(UIKitManual) retain]
x18 = 0x0000000000000000
x19 = 0x00000001c0103ba0
x20 = 0x00000001055098d0
x21 = 0x00000001044fa23a "buttonJailbreakDetection:"
x22 = 0x00000001055098d0
x23 = 0x00000001055098d0
x24 = 0x00000001c001d280
x25 = 0x0000000000000000
x26 = 0x000000018d9b453e "objectAtIndex:"
x27 = 0x0000000000000001
x28 = 0x00000001c4052660
fp = 0x000000016b90cff0
lr = 0x00000001044f6fec Swift challenge two'Swift_challenge_two.ViewController.buttonJailbreakDetection(__C.UIButton) -> () + 44 at ViewController.swift:20
sp = 0x000000016b90cb50
pc = 0x00000001044f8a3c Swift challenge two'Swift_challenge_two.isJailbroken() -> Swift.Bool + 36 at JailBreak.swift:8
cpsr = 0x00000000

The pc register holds the address value of the actual function and the lr register holds the address of where code execution should resume after a called function returns. Now we step through the code flow, and every step we examine the content of the registers, this will give us a brief understanding of what is going on:

(lldb) step     
(lldb) register read
General Purpose Registers:
        x0 = 0x0000000000000001
        x1 = 0x00000001c00c9680
        x2 = 0x0000000000000008
        x3 = 0x0000000182b2506c  libsystem_malloc.dylib`nano_free_definite_size
        x4 = 0x0000000104ba05d0  libswiftCore.dylib`protocol witness table for Swift._Stdout : Swift.TextOutputStream in Swift
        x5 = 0x000000016b90c920
        x6 = 0x0000000000000001
        x7 = 0x0000000000000000
        x8 = 0x0000000000000001
        x9 = 0x0000000000000000
       x10 = 0x00008cfd4b9132f9
       x11 = 0xbaddc0dedeadbead
       x12 = 0x000000000000000b
       x13 = 0xfffffffe3fdd47bf
       x14 = 0x0000000000000002
       x15 = 0x05a03ece641e795a
       x16 = 0x0000000182c778cc  libsystem_platform.dylib`OSAtomicEnqueue$VARIANT$mp
       x17 = 0x0000000104b51624  libswiftCore.dylib`-[_SwiftNativeNSStringBase release]
       x18 = 0x0000000000000000
       x19 = 0x00000001c0103ba0
       x20 = 0x00000001055098d0
       x21 = 0x00000001044fa23a  "buttonJailbreakDetection:"
       x22 = 0x00000001055098d0
       x23 = 0x00000001055098d0
       x24 = 0x00000001c001d2f0
       x25 = 0x0000000000000000
       x26 = 0x000000018d9b453e  "objectAtIndex:"
       x27 = 0x0000000000000001
       x28 = 0x00000001c4052660
        fp = 0x000000016b90d090
        lr = 0x00000001044f6fec  Swift challenge two'Swift_challenge_two.ViewController.buttonJailbreakDetection(__C.UIButton) -> () + 44 at ViewController.swift:20
        sp = 0x000000016b90d000
        pc = 0x00000001044f6fec  Swift challenge two'Swift_challenge_two.ViewController.buttonJailbreakDetection(__C.UIButton) -> () + 44 at ViewController.swift:20
      cpsr = 0x60000000

Suddenly the previous caller appears in the pc register. Furthermore, x0 holds a value that looks pretty much like a boolean true value 0x0000000000000001. Let’s change the value in register x0, from true (0x1) to false (0x0), with another lldb command:

(lldb) register write x0 0x0000000000000000     
(lldb) continue

The following screenshot shows the result of the jailbreak detection, on a jailbroken iOS 11.4 device:

Conclusion.

As you can see, we were able to successfully bypass the jailbreak detection. A pretty easy task, once we discovered the responsible function. Possible countermeasures you’ll discover in the wild are obfuscation and frequent calling of the isJailbroken function, which would make patching super annoying.

Tip of the day.

If you want to attach to a running app, with lldb and XCode you start the app on the iOS device, then goto XCode and perform the following steps:

  • Make sure that the connected device is set as target
  • Select Debug -> Attach to Process -> App