Skip to content

Jailbreak detection was changed through the years. This is a history overview and analysis of modern applications like Snapchat and Pokemon GO.

Notifications You must be signed in to change notification settings

vadim-a-yegorov/Jailbreak-detection-The-modern-way

Repository files navigation

Jailbreak detection: The modern way

Nov 20 2022 by Vadim Yegorov <vadim.a.yegorov@pm.me>, Security Researcher, Software Engineer.
Creative Commons Licence
Licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. Author should be mentioned when copying or redistributing this work.

Download PDF: 2022_C00C00R00CON_jailbreak.pdf

Introduction

  • iOS
    • Closed operating system
    • JailBreaks bypass iOS security to get (almost) full access
  • JailBreak detection
    • Used by banking applications and games
    • To make sure that the environment is “safe”…
    • …or to block cheats/cracks
  • Security researchers need to
    • Assess / reverse protected applications

Jailbreak detection

Debugging an iOS app

  • Without a JailBreak

    • With ptrace (lldb / frida) → app needs the get-task-allow entitlement

    • By injecting frida code → app needs to be repackaged

    • In both case, you need to resign the application

      but it has a lot of side effects:

      Different Team ID

      Files are modified

  • With a JailBreak

    • No entitlements are required
    • Frida is able to attach to any process

Jailbreak detection evasion

Jailbreak detection mechanisms are added to reverse engineering defense to make running the app on a jailbroken device more difficult. This blocks some of the tools and techniques reverse engineers like to use. Like most other types of defense, jailbreak detection is not very effective by itself, but scattering checks throughout the app's source code can improve the effectiveness of the overall anti-tampering scheme.

The enterprise security risks posed by jailbreaking compound in the face of tools that can help users easily evade common jailbreak detection methods. A user can download any app tweak like Spotify++ directly from the third-party app store Cydia. A lot of applications provide jailbreak detection, as do many media and financial service apps that want to limit content pirating and account compromise, respectively. Unfortunately, these jailbreak detections rely on a combination of relatively straightforward and evadable tests, such as:

  • Checking for files or directories common to jailbroken devices, such as Cydia
  • Checking for elevated directory permissions (i.e. more directories with “write” permission)
  • Checking to see if an app can successfully write files outside of its sandbox
  • Checking cydia:// protocol handler

The fundamental limitation with these and comparable detection tests is that as client-side tests they can be accessed, reverse-engineered, and evaded by attackers. In addition, the apps performing these jailbreak detection tests (e.g. the MDM app) must go through Apple’s app review process, limiting the scope of data they can collect to analyse a device’s jailbreak status. Let's take a quick look at them:


File based checks

Check for files and directories typically associated with jailbreaks, such as:

//suspicious system and app paths to check
private static var suspicousAppandSystemPaths: [String] {
    return [
        "/usr/sbin/frida-server",
        "/etc/apt/sources.list.d/electra.list",
        "/etc/apt/sources.list.d/sileo.sources",
        "/.bootstrapped_electra",
        "/usr/lib/libjailbreak.dylib",
        "/jb/lzma",
        "/.cydia_no_stash",
        "/.installed_unc0ver",
        "/jb/offsets.plist",
        "/usr/share/jailbreak/injectme.plist",
        "/etc/apt/undecimus/undecimus.list",
        "/var/lib/dpkg/info/mobilesubstrate.md5sums",
        "/Library/MobileSubstrate/MobileSubstrate.dylib",
        "/jb/jailbreakd.plist",
        "/jb/amfid_payload.dylib",
        "/jb/libjailbreak.dylib",
        "/usr/libexec/cydia/firmware.sh",
        "/var/lib/cydia",
        "/etc/apt",
        "/private/var/lib/apt",
        "/private/var/Users/",
        "/var/log/apt",
        "/Applications/Cydia.app",
        "/private/var/stash",
        "/private/var/lib/apt/",
        "/private/var/lib/cydia",
        "/private/var/cache/apt/",
        "/private/var/log/syslog",
        "/private/var/tmp/cydia.log",
        "/Applications/Icy.app",
        "/Applications/MxTube.app",
        "/Applications/RockApp.app",
        "/Applications/blackra1n.app",
        "/Applications/SBSettings.app",
        "/Applications/FakeCarrier.app",
        "/Applications/WinterBoard.app",
        "/Applications/IntelliScreen.app",
        "/private/var/mobile/Library/SBSettings/Themes",
        "/Library/MobileSubstrate/CydiaSubstrate.dylib",
        "/System/Library/LaunchDaemons/com.ikey.bbot.plist",
        "/Library/MobileSubstrate/DynamicLibraries/Veency.plist",
        "/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
        "/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
        "/Applications/Cydia.app",
        "/Applications/blackra1n.app",
        "/Applications/FakeCarrier.app",
        "/Applications/Icy.app",
        "/Applications/IntelliScreen.app",
        "/Applications/MxTube.app",
        "/Applications/RockApp.app",
        "/Applications/SBSettings.app",
        "/Applications/WinterBoard.app"
    ]
}

File based checks

Most often, these are checked using the

-(BOOL)fileExistsAtPath:(NSString*) path method in NSFileManager or FileManager.default.fileExists(atPath: path). However, there are also applications that use lower-level C functions like fopen(), stat(), or access().


Checking File Permissions

Another way to check for jailbreaking mechanisms is to try to write to a location that's outside the application's sandbox. You can do this by having the application attempt to create a file in, for example, the /private directory. If the file is created successfully, the device has been jailbroken.

This method consists of checking the permissions of specific files and directories on the system. More directories have write access on a jailbroken device than on a non compromised one. One example of this is the root partition, which originally has only read permission. If it is found it has read and write permissions it means the device is jailbroken. There are different ways of performing these checks such as using NSFileManager and C functions like statfs().

Swift:

do {
    let pathToFileInRestrictedDirectory = "/private/jailbreak.txt"
    try "This is a test.".write(toFile: pathToFileInRestrictedDirectory, atomically: true, encoding: String.Encoding.utf8)
    try FileManager.default.removeItem(atPath: pathToFileInRestrictedDirectory)
    // Device is jailbroken
} catch {
    // Device is not jailbroken
}

Objective-C:

NSError *error;
NSString *stringToBeWritten = @"This is a test.";
[stringToBeWritten writeToFile:@"/private/jailbreak.txt" atomically:YES
         encoding:NSUTF8StringEncoding error:&error];
if (error == nil) {
   // Device is jailbroken
   return YES;
} else {
   // Device is not jailbroken
   [[NSFileManager defaultManager] removeItemAtPath:@"/private/jailbreak.txt" error:nil];
}

Checking File Permissions

There are different ways of performing these checks such as using NSFileManager and C functions like statfs(), open(), utimes(), stat(), pathconf(), stat64(), fopen().


Checking Protocol Handlers

You can check protocol handlers by attempting to open a Cydia URL. The Cydia app store, which practically every jailbreaking tool installs by default, installs the cydia:// protocol handler.

Swift:

if let url = URL(string: "cydia://package/com.example.package"), UIApplication.shared.canOpenURL(url) {
    // Device is jailbroken
}

Objective-C:

if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package"]]){
    // Device is jailbroken
}

Why it's not enough

This simple checks can be accessed, reverse-engineered, and evaded by attackers.


Better ways of Jailbreak detection


Try to block/detect debuggers

  1. PT_DENY_ATTACH
ptrace(PT_DENY_ATTACH);
  1. Try to “kill” its own pid with the 0-signal
  2. Check if PTRACE is flagged
void try_kill() {
    const int pid = getpid();
    int ret = kill(pid, 0);
}

According to the man page of kill (man 2 kill), the signal 0 is used to check that the pid given in the first parameter really exists.

[…] A value of 0, however, will cause error checking to be performed (with no signal being sent). This can be used to check the validity of pid.

This kill operation is followed by PTRACE check:

inline bool ptrace_detect() {
  int32_t opt[4] = {
    CTL_KERN,
    KERN_PROC,
    KERN_PROC_PID,
    getpid(),
  };
  kinfo_proc info;
  sysctl(opt, 4, &info, sizeof(kinfo_proc), nullptr, 0);
  return info.kp_proc.p_flag & P_TRACED;
}

Check if the parent pid is launchd

getppid() == 1

Try to detect if the rootfs is writable

getfsstat64(), statvfs()

Try to load an invalid signature

fcntl(F_ADDSIGS)

Check signature state

csops(CS_OPS_MARKKILL)

Check signature directly

We can check the integrity of the signature of our binary. This check starts by opening the main app binary from the disk, seek till kSecCodeMagicEmbeddedSignature sequence 0xfade0cc0, read the entitlements and calculate the checksum.

The code signing segment contains a superblob structure, which itself contains other blobs of different types. The structure of both is defined in some more open-source Apple code:

/*
 * Structure of an embedded-signature SuperBlob
 */
typedef struct __BlobIndex {
	uint32_t type;					/* type of entry */
	uint32_t offset;				/* offset of entry */
} CS_BlobIndex;

typedef struct __SuperBlob {
	uint32_t magic;					/* magic number */
	uint32_t length;				/* total length of SuperBlob */
	uint32_t count;					/* number of index entries following */
	CS_BlobIndex index[];			/* (count) entries */
	/* followed by Blobs in no particular order as indicated by offsets in index */
} CS_SuperBlob;

The super blob just contains some magic, the length of the whole code signing section, a count of the blobs, and an array of indices to those blobs. The different identifying blob magics are defined in this file:

/*
 * Magic numbers used by Code Signing
 */
enum {
  kSecCodeMagicRequirement = 0xfade0c00,				/* single requirement */
  kSecCodeMagicRequirementSet = 0xfade0c01,			/* requirement set */
  kSecCodeMagicCodeDirectory = 0xfade0c02,			/* CodeDirectory */
  kSecCodeMagicEmbeddedSignature = 0xfade0cc0, 	/* single-architecture embedded signature */
  kSecCodeMagicDetachedSignature = 0xfade0cc1, 	/* detached multi-architecture signature */
  kSecCodeMagicEntitlement = 0xfade7171,				/* entitlement blob */
};

For example, the iOS ps binary has four blobs of content following the initial superblob:

CodeSign section

These are, respectively:

  1. The code directory blob (magic 0xfade0c02), which you can spot as it has the qualified binary name com.apple.ps in it.
  2. An empty and short requirement set (magic 0xfade0c01).
  3. The entitlements blob, which contains the xml plist (magic 0xfade7171).
  4. A blob wrapper, (magic 0xfade0b01).

Complicate the backtracing

Try to halt the app after a big timeout and do not exit directly from the jailbreak detection place.


API-based Detection

fork()

Process forking. Sandboxd does not deny applications the ability to use fork(), popen(), or any other C functions to create child processes on jailbroken devices. However, sandboxd explicitly denies process forking on non jailbroken devices. Therefore, by checking the returned pid on fork(), an application can tell if a device is compromised. If the fork is successful, the app can deduce that it is running on a jailbroken device.

system()

Calling the system() function with a NULL argument on a non-jailbroken device will return 0. Doing the same on a jailbroken device will return 1. This is because the function will check whether /bin/sh exists, and it only exists on jailbroken devices.


OpenSSH service detection

SSH loopback connections. Due to the very large portion of jailbroken devices that have OpenSSH installed, some applications attempt to make a connection to 127.0.0.1 on port 22. If the connection succeeds, it means OpenSSH is installed and running, which proves the device is jailbroken.

Check loopback for 22 (OpenSSH) and 44 (checkra1n) opened ports.


Check if some Jailbreak libraries are loaded in your process

This detection method starts with calling functions like _dyld_image_count() and _dyld_get_image_name() to see what dylibs are currently loaded. This method is very difficult to dynamically patch due to the fact that the patches themselves are part of dylibs.

/usr/lib/substitute-inserter.dylib for example

Can use dlopen / memory scanning / dyld internal structures etc.

private static func checkDYLD() -> Bool {
    let suspiciousLibraries = [
        "FridaGadget",
        "frida",
        "cynject",
        "libcycript"
    ]
    for libraryIndex in 0..<_dyld_image_count() {
        
        guard let loadedLibrary = String(validatingUTF8: _dyld_get_image_name(libraryIndex)) else { continue }
        for suspiciousLibrary in suspiciousLibraries {
            if loadedLibrary.lowercased().contains(suspiciousLibrary.lowercased()) {
                return true
            }
        }
    }
    return false
}

Check if your process is instrumented

  • Check code integrity

    • CRC, derive constants from the code, check API entries, etc.
  • Try to detect frida

private static func isFridaRunning() -> Bool {
    func swapBytesIfNeeded(port: in_port_t) -> in_port_t {
        let littleEndian = Int(OSHostByteOrder()) == OSLittleEndian
        return littleEndian ? _OSSwapInt16(port) : port
    }
    
    var serverAddress = sockaddr_in()
    serverAddress.sin_family = sa_family_t(AF_INET)
    serverAddress.sin_addr.s_addr = inet_addr("127.0.0.1")
    serverAddress.sin_port = swapBytesIfNeeded(port: in_port_t(27042))
    let sock = socket(AF_INET, SOCK_STREAM, 0)
    
    let result = withUnsafePointer(to: &serverAddress) {
        $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
            connect(sock, $0, socklen_t(MemoryLayout<sockaddr_in>.stride))
        }
    }
    if result != -1 {
        return true
    }
    return false
}

Summary

In general, the more complicated the jailbreak detection is, the more difficult it is to detect and bypass. The most common mistake when implementing jailbreak detection often lies in the implementation itself.

We often come across apps that have great jailbreak detection, but the implementation is in one function that returns true or false depending on whether the device is jailbroken. In these cases, we bypass jailbreak detection by using Cycript or a similar tool to invert the return value from the detection function.

In practice, the best jailbreak detection combining multiple techniques and integrating them into other functions so that they cannot easily be bypassed.


References

https://github.com/mehrankmlf/SecurityKit
https://www.synacktiv.com/sites/default/files/2021-10/2021_sthack_jailbreak.pdf
https://www.romainthomas.fr/post/21-07-pokemongo-anti-frida-jailbreak-bypass/
https://aeonlucid.com/Snapchat-detection-on-iOS/
https://github.com/AeonLucid/SnapHide
https://redmaple.tech/blogs/macho-files/

About

Jailbreak detection was changed through the years. This is a history overview and analysis of modern applications like Snapchat and Pokemon GO.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published