Contents

Bypassing jailbreak detection mechanisms

/assets/post/Jailbreak-Detection-Bypass/Banner.png

In this post, I will talk about the challenges that this week with the mechanisms of Jailbreak Detection and how to bypass it.

In much important software such as banking software, mechanisms such as Jailbreak Detection or debugging are incorporated to prevent software implementation.

The use of these types of mechanisms can have various reasons, such as preventing software lock bypassing by using debugging and dumping sensitive software information, etc. For example, if you can bypass the lock screen of the software by debugging the software, you can access the information stored in it.

In the scenario that I have been dealing with all this time, I have to bypass the jailbreak detection mechanisms of an IOS software so that we can do various things such as traffic capture etc.

Dynamic analysis

First, I captured a list of all function calls using frida-trace

1
2
3
4
# Get a list of app IDs
frida-ps -Ua
# Capture all calls
frida-trace -i '*'  -U -f com.app.example > calls.txt

At the very beginning of the list, I came across an item that caught my attention

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
IOSSecuritySuite.RuntimeHookChecker
IOSSecuritySuite.ReverseEngineeringToolsChecker
IOSSecuritySuite.ProxyChecker
IOSSecuritySuite.MSHookFunctionChecker
IOSSecuritySuite.JailbreakChecker
IOSSecuritySuite.IntegrityChecker
IOSSecuritySuite.IOSSecuritySuite
IOSSecuritySuite.SymbolFound
IOSSecuritySuite.FishHookChecker
IOSSecuritySuite.FileChecker
IOSSecuritySuite.EmulatorChecker
IOSSecuritySuite.DebuggerChecker

With a little search, I found out that the IOSSecuritySuite library was used, as it is written in the description of this project, it is a library to prevent anti-tampering on the IOS platform.

This library has different parts that are explained in the table below each of them

ClassDescription
DebuggerCheckerThis class has methods to check the status of the software, which determines whether the software is in debugging mode or not
EmulatorCheckerThis class has methods to check the execution of the software in the Emulator environment
JailbreakCheckerThis class has methods to check read and write access to paths that only root has access to
ReverseEngineeringToolsCheckerThis class has methods to check for the presence of reverse engineering tools

Using this script I collected all the classes related to IOSSecuritySuite:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (ObjC.available)
{
    for (var className in ObjC.classes)
    {
        if (ObjC.classes.hasOwnProperty(className) && className.includes("IOSSecuritySuite"))
        {
            console.log(className);
        }
    }
}
else
{
    console.log("Objective-C Runtime is not available!");
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
frida -l ./find-classes.js -U -f com.app.example

# output
IOSSecuritySuite.RuntimeHookChecker
IOSSecuritySuite.ReverseEngineeringToolsChecker
IOSSecuritySuite.ProxyChecker
IOSSecuritySuite.MSHookFunctionChecker
IOSSecuritySuite.JailbreakChecker
IOSSecuritySuite.IntegrityChecker
IOSSecuritySuite.IOSSecuritySuite
IOSSecuritySuiteXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXFishHookXXXX
IOSSecuritySuite.SymbolFound
IOSSecuritySuite.FishHookChecker
IOSSecuritySuite.FileChecker
IOSSecuritySuite.EmulatorChecker
IOSSecuritySuite.DebuggerChecker

At first, I looked for scripts ready to bypass IOSSecuritySuite, one of these scripts was Darkprince-Jailbreak-Detection-Bypass, but this script only bypasses the JailbreakChecker class.

I decided to hook IOSSecuritySuite methods so that the inputs and outputs of each can be manipulated. For example, if the amIDbugged method returns true, I can manually change it to false, for example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Interceptor.attach(Module.findExportByName("IOSSecuritySuite", "amIDebugged"), {
  onEnter: function(args) {
    // Print out the function name and arguments
    console.log("amIDebugged has been called with arguments:");
    console.log("arg0: " + args[0] + " (context)");

    // Print out the call stack
    console.log("amIDebugged called from:\n" +
      Thread.backtrace(this.context, Backtracer.ACCURATE)
      .map(DebugSymbol.fromAddress).join("\n") + "\n");
  },
  onLeave: function(retval) {
    // Print out the return value
    console.log("amIDebugged returned: " + retval);
    console.log("Set results to False");
    // Set the return value to 0x0 (False)
    retval.replace(0x0);
  }
});

There is a repository with the topic “Jailbreak/Root Detection Bypass in Flutter” which I followed, but then I faced a serious challenge. I could not find almost any of the functions like amIDebugge with its original name, even the names with formats like $s16IOSSecuritySuiteAAC13amIJailbrokenSbyFZ did not exist. They probably created an obfuscation in the code so that we could not easily find the functions.

The names of some functions were in the following format: IOSSecuritySuiteXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXFishHookXXXX

But I did not find all the functions :)

Static analysis

Next, I decided to decompile the project, the ipa files consist of different parts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
+ example.ipa
    |___Payload
        |___example.app
            |___en.lproj
            |___Frameworks # Libraries used
            |   |___libswiftCore.dylib
            |   |___...
            |____CodeSignature
            |___Info.plist
            |___example # The original binary

I decided to look for IOSSecuritySuite classes to patch any spots that used these classes.

I started the search from Constant values like strings, for example, in the DebuggerChecker class there is a string with this value "Error occurred when calling sysctl(). The debugger check may not be reliable" This can be a good thread to find the DebuggerChecker class be

Original source code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static func amIDebugged() -> Bool {
    var kinfo = kinfo_proc()
    var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
    var size = MemoryLayout<kinfo_proc>.stride
    let sysctlRet = sysctl(&mib, UInt32(mib.count), &kinfo, &size, nil, 0)
    
    if sysctlRet != 0 {
      print("Error occured when calling sysctl(). The debugger check may not be reliable")
    }
    
    return (kinfo.kp_proc.p_flag & P_TRACED) != 0
  }

The same part is decompiled:

/assets/post/Jailbreak-Detection-Bypass/DebuggerChecker-amIDugged0.png/assets/post/Jailbreak-Detection-Bypass/DebuggerChecker-amIDugged1.png

Other classes can be found in the same way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
internal class EmulatorChecker {
  static func amIRunInEmulator() -> Bool {
    return checkCompile() || checkRuntime()
  }

  private static func checkRuntime() -> Bool {
    return ProcessInfo().environment["SIMULATOR_DEVICE_NAME"] != nil
  }

  private static func checkCompile() -> Bool {
  #if targetEnvironment(simulator)
      return true
  #else
      return false
  #endif
  }
}

/assets/post/Jailbreak-Detection-Bypass/EmulatorChecker.png

JailbreakChecker class

/assets/post/Jailbreak-Detection-Bypass/JailbreakChecker.png

ReverseEngineeringToolsChecker class

/assets/post/Jailbreak-Detection-Bypass/ReverseEngineeringToolsChecker.png

Almost all classes can be found in this way, and we can debug it to check the correctness of this search.

Debugging

There are different ways to debug iOS software:

We need Debugserver to start the debug environment.

debugserver is a console app that acts as server for remote gdb or lldb debugging. It is installed when a device is marked for development. It can be found in /Developer/usr/bin/debugserver. This is also the process invoked by Xcode to debug applications on the device.

Setup Debugserver

1
2
3
hdiutil attach /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/<VERSION>/DeveloperDiskImage.dmg

cp /Volumes/DeveloperDiskImage/usr/bin/debugserver ./

If you do not have access to xcode, you can download DeveloperDiskImage from this repository:

1
debugserver [<options>] host:<port> [<prog-name> <arg1> <arg2> ...]

options can be as follows:

OptionEffect
-a processAttach debugserver to process. The process can be a pid or executable name.
-d integerAssign the waitfor-duration.
-f ??
-gTurn on debugging.
-i integerAssign the waitfor-interval.
-l filenameLog to file. Set filename to stdout to log to standard output.
-tUse task ID instead of process ID.
-vVerbose.
-w ??
-x method
–launch=methodHow to launch the program. Can be one of:
- auto: Auto-detect the best launch method to use.
- fork: Launch program using fork(2) and exec(3).
- posix: Launch program using posix_spawn(2).
- backboard: Launch program via BackBoard Services.
The backboard option is only available in the closed-source version included in Xcode.
–lockdownObtain parameters from lockdown (?)

The vanilla debugserver lacks the task_for_pid() entitlement. For building and debugging your own apps on a properly provisioned device, this is not a problem; assuming your project and device are properly configured with your active iOS Developer Program, debugserver should have no trouble attaching to an app built and sent down to the device by Xcode. However, debugserver cannot attach to any other processes, including other apps from the App Store, due to lack of entitlement to allow task_for_pid(). An entitlement must be inserted into the binary to allow this. Note: The /Developer directory is actually a mounted read-only ramdisk. You cannot add any entitlements to the copy of debugserver installed there; it must be extracted to another directory and used from there.

Save the following xml as entitlements.xml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
      <key>com.apple.backboardd.debugapplications</key>
      <true/>
      <key>com.apple.backboardd.launchapplications</key>
      <true/>
      <key>com.apple.diagnosticd.diagnostic</key>
      <true/>
      <key>com.apple.frontboard.debugapplications</key>
      <true/>
      <key>com.apple.frontboard.launchapplications</key>
      <true/>
      <key>com.apple.security.network.client</key>
      <true/>
      <key>com.apple.security.network.server</key>
      <true/>
      <key>com.apple.springboard.debugapplications</key>
      <true/>
      <key>com.apple.system-task-ports</key>
      <true/>
      <key>get-task-allow</key>
      <true/>
      <key>platform-application</key>
      <true/>
      <key>run-unsigned-code</key>
      <true/>
      <key>task_for_pid-allow</key>
      <true/>
  </dict>
</plist>

Apply the entitlement with ldid:

1
ldid -Sentitlements.xml debugserver

ldid is a tool made by saurik for modifying a binary’s entitlements easily. ldid also generates SHA1 and SHA256 hashes for the binary signature, so the iPhone kernel executes the binary. The package name in Cydia is “Link Identity Editor”.

ldid -e dumps the binary’s entitlements.

ldid -Sent.xml sets the binary’s entitlements, where ent.xml is the path to an entitlements file.

ldid -S pseudo-signs a binary with no entitlements.

Attaching to a process

On the device, type:

1
2
3
4
5
6
/usr/bin/debugserver 0.0.0.0:1234 -a "Files"

debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-1200.2.12
 for arm64.
Attaching to process Files...
Listening to port 1234 for a connection from 0.0.0.0...

To run an application in debug mode:

1
/usr/bin/debugserver -x backboard 0.0.0.0:4321 /private/var/containers/Bundle/Application/ID/example.app/example

This will launch the app and wait for remote troubleshooting.

Debugging through USB instead of WiFi

After going through these steps, I found that debugging via WIFI is slow, an alternative solution is to debug via USB, for this you can use libimobiledevice.

Unfortunately, libimobiledevice is not compiled for Windows. Download the compiled version for Windows from this fork

iproxy - A proxy that binds local TCP ports to be forwarded to the specified ports on a usbmux device.

EXAMPLES

  • iproxy 2222:44
    • Bind local TCP port 2222 and forward to port 44 of the first device connected via USB.

SSH proxying:

1
2
3
iproxy.exe 2222:22

ssh root@localhost -p 2222

debugserver proxying:

1
2
3
iproxy.exe 1234:4321

/usr/bin/debugserver -x backboard 0.0.0.0:4321 /private/var/containers/Bundle/Application/ID/example.app/example

Configuring the debugger

To connect IDA to debugserver, follow the steps below:

Debugger->Select debugger…​ and select Remote iOS Debugger:

/assets/post/Jailbreak-Detection-Bypass/SelectDebugger.png

Now go to Debugger>Process options...>​ and ensure the following fields are set:

  • Hostname: localhost
  • Port: 1234

/assets/post/Jailbreak-Detection-Bypass/ProcessOptions.png

And Debugger>Attach to process...>

/assets/post/Jailbreak-Detection-Bypass/AttachToProcess.png

And finally it connects:

/assets/post/Jailbreak-Detection-Bypass/DebuggerAttached.png

Next, I checked the classes I had found.

Patching

After checking the points where IOSSecuritySuite classes are used, I came to the conclusion that it is enough to patch the points where IOSSecuritySuite functions are used.

For example, see the image below:

/assets/post/Jailbreak-Detection-Bypass/FunctionCall.png

The sub_10303D914 function is basically one of the methods of IOSSecuritySuite whose output specifies the jailbreak status.

If we go to sub_10303D914:

/assets/post/Jailbreak-Detection-Bypass/FunctionCall2.png

It is clear that this is the JailbreakChecker class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
internal class JailbreakChecker {
...

  private static func checkFork() -> CheckResult {
      let pointerToFork = UnsafeMutableRawPointer(bitPattern: -2)
      let forkPtr = dlsym(pointerToFork, "fork")
      typealias ForkType = @convention(c) () -> pid_t
      let fork = unsafeBitCast(forkPtr, to: ForkType.self)
      let forkResult = fork()
      
      if forkResult >= 0 {
        if forkResult > 0 {
          kill(forkResult, SIGTERM)
        }
        return (false, "Fork was able to create a new process (sandbox violation)")
      }
      
      return (true, "")
  }

  private static func checkSuspiciousObjCClasses() -> CheckResult {
    if let shadowRulesetClass = objc_getClass("ShadowRuleset") as? NSObject.Type {
      let selector = Selector(("internalDictionary"))
      if class_getInstanceMethod(shadowRulesetClass, selector) != nil {
        return (false, "Shadow anti-anti-jailbreak detector detected :-)")
      }
    }
    return (true, "")
  }

...
}

We need to change code BL sub_10303D914 to NOP to prevent W0 register value from changing.

1
2
3
4
MOV             X20, X21
BL              sub_10303D914 # to NOP
MOV             X19, X2
TBZ             W0, #0, loc_103021650

Unfortunately, IDA does not support arm patching and you will be faced with the following message:

1
Sorry, this processor module doesn't support the assembler.

That’s why I used Keypatch.

Multi-architecture assembler for IDA Pro. Powered by Keystone Engine.

First, you need to add the Keypatch plugin to IDA

Then we click on the desired instruction and press Ctrl + Alt + K keys

We change the value of the Assembly field to NOP

/assets/post/Jailbreak-Detection-Bypass/Patch.png

After patching the instructions will change like this:

1
2
3
4
5
MOV             X20, X21
NOP                     ; Keypatch modified this from:
                        ;   BL sub_10303D914
MOV             X19, X2
TBZ             W0, #0, loc_103021650

After patching all parts, go to Edit -> Patch Program -> Patches to input file and save the file.

Bypassing IOS Code Signatures

After patching the binary, IOS prevents the application from running because the application signature is invalid.

First, I tried to disable the signature verification function through sysctl and changing the value of proc_enforce to 0, but it seems that this method no longer works.

1
2
sysctl -w security.mac.proc_enforce=0
sysctl -w security.mac.vnode_enforce=0

/assets/post/Jailbreak-Detection-Bypass/Sysctl.png

Next, I got acquainted with AppSync Unified

AppSync Unified is a tweak that allows users to freely install ad-hoc signed, fakesigned, or unsigned IPA app packages on their iOS devices that iOS would otherwise consider invalid.

Follow the steps below to install AppSync:

  • Add cydia.akemi.ai to Cydia sources
  • Search for the AppSync Unified package and then install it

Using the ldid tool, we register a fakesign for our binary:

1
ldid -S example

Now we can run the program and that’s it

Links