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