iOS jailbreak detection clusters into three vectors. Knowing each one and the matching bypass lets you survive on a JB device through any app that runs JB checks. This post is a working playbook.
The three vectors, VulnLabAppiOS’s implementation
1. File-existence checks. VulnLabAppiOS’s checkJailbreakFilesystem:
Highlight 1 is if FileManager.default.fileExists(atPath: path) { return true } - path existence check routed through NSFileManager; Frida hooks -[NSFileManager fileExistsAtPath:] to always return false, bypassing the entire path list in one hook.
Highlight 2 is if UIApplication.shared.canOpenURL(url) { return true } - URL scheme reachability check routed through UIApplication; Frida hooks -[UIApplication canOpenURL:] to return false for any jailbreak scheme, silencing this check independently.
2. URL scheme checks. VulnLabAppiOS’s checkJailbreakURLSchemes:
1
2
3
4
5
6
7
8
private func checkJailbreakURLSchemes() -> Bool {
let schemes = ["cydia://", "sileo://", "zbra://", "filza://"]
for scheme in schemes {
if let url = URL(string: scheme),
UIApplication.shared.canOpenURL(url) { return true }
}
return false
}
3. dyld image scan. VulnLabAppiOS’s checkJailbreakDyldImages:
1
2
3
4
5
6
7
8
9
10
private func checkJailbreakDyldImages() -> Bool {
let count = _dyld_image_count()
for i in 0..<count {
if let name = _dyld_get_image_name(i) {
let s = String(cString: name)
if s.contains("MobileSubstrate") || s.contains("TweakInject") { return true }
}
}
return false
}
Plus more obscure libc-level checks seen in the wild:
stat/access/fopenon the same paths (bypassable at a lower layer).fork()returning a valid PID on jailbroken devices.- Reading
/etc/passwdand noting non-default entries.
The bypass, hooking the underlying OS functions
VulnLabAppiOS’s check methods are declared private in Swift — no @objc thunk, unreachable by ObjC selector. Hook the OS functions they call instead. This also works on real-world apps where JB-check selectors are stripped or obfuscated.
For NSFileManager:
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
const NSFM = ObjC.classes.NSFileManager;
const blockedPaths = [
'/Applications/Cydia.app',
'/Applications/Sileo.app',
'/Applications/Zebra.app',
'/Applications/Filza.app',
'/private/var/lib/apt/',
'/private/var/lib/cydia/',
'/usr/bin/ssh',
'/bin/bash',
'/etc/apt/',
'/var/lib/undecimus/',
'/usr/share/jailbreak/'
];
const origExists = NSFM['- fileExistsAtPath:'].implementation;
Interceptor.attach(origExists, {
onEnter: function (args) {
const path = new ObjC.Object(args[2]).toString();
for (const b of blockedPaths) {
if (path.indexOf(b) === 0) {
this.block = true;
break;
}
}
},
onLeave: function (retval) {
if (this.block) retval.replace(0); // false
}
});
For UIApplication canOpenURL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const UIApp = ObjC.classes.UIApplication;
const blockedSchemes = ['cydia', 'sileo', 'zbra', 'filza', 'undecimus'];
Interceptor.attach(UIApp['- canOpenURL:'].implementation, {
onEnter: function (args) {
const url = new ObjC.Object(args[2]).toString();
for (const s of blockedSchemes) {
if (url.indexOf(s + '://') === 0) {
this.block = true;
break;
}
}
},
onLeave: function (retval) {
if (this.block) retval.replace(0);
}
});
After these two hooks, VulnLabAppiOS’s JB checks return “clean”. Apps that bypass NSFileManager and call stat/access/fopen directly at the libc layer need an additional hook per symbol — find the export in the specific system library (libsystem_kernel.dylib) and attach the same path-filter logic.
The fork() and getppid() checks
Some apps check fork():
1
if (fork() >= 0) return YES;
fork returns -1 on a non-JB device (the sandbox forbids it). On JB devices the sandbox is loosened and fork returns a valid PID. Bypass:
1
2
3
4
5
6
7
const kernLib = Process.findModuleByName('libsystem_kernel.dylib');
const fork = kernLib && kernLib.findExportByName('fork');
if (fork) Interceptor.attach(fork, {
onLeave: function (retval) {
retval.replace(-1); // pretend fork failed
}
});
Or check getppid():
1
if (getppid() != 1) return YES;
The expected parent PID is 1 (launchd). On JB devices it might be different. Bypass:
1
2
3
4
5
6
7
const kernLib = Process.findModuleByName('libsystem_kernel.dylib');
const getppid = kernLib && kernLib.findExportByName('getppid');
if (getppid) Interceptor.attach(getppid, {
onLeave: function (retval) {
retval.replace(1);
}
});
The dyld and module-name checks
Apps that check the loaded module list for JB-specific tweaks:
1
2
3
4
5
uint32_t count = _dyld_image_count();
for (uint32_t i = 0; i < count; i++) {
const char *name = _dyld_get_image_name(i);
if (strstr(name, "MobileSubstrate") || strstr(name, "TweakInject")) return YES;
}
Bypass:
1
2
3
4
5
6
7
8
9
10
const dyldLib = Process.findModuleByName('libdyld.dylib');
const dyld = dyldLib && dyldLib.findExportByName('_dyld_get_image_name');
if (dyld) Interceptor.attach(dyld, {
onLeave: function (retval) {
const name = retval.readCString();
if (name && /Substrate|TweakInject|frida|libtweakinject/.test(name)) {
retval.replace(Memory.allocUtf8String('libSystem.B.dylib'));
}
}
});
This rewrites the returned module name to a benign name. The detection sees only legitimate libraries.
The combined drop-in script
The hooks above stitch into a single file. Save as ios-jb-bypass.js:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// frida -U -f com.vulnlab.iosapp --no-pause -l ios-jb-bypass.js
setTimeout(function () {
const blockedPaths = [
'/Applications/Cydia.app', '/Applications/Sileo.app',
'/Applications/Zebra.app', '/Applications/Filza.app',
'/private/var/lib/apt/', '/private/var/lib/cydia/',
'/usr/bin/ssh', '/usr/libexec/sshd', '/bin/bash',
'/etc/apt/', '/var/lib/undecimus/', '/usr/share/jailbreak/'
];
const blockedSchemes = ['cydia', 'sileo', 'zbra', 'filza', 'undecimus'];
// NSFileManager.fileExistsAtPath
const NSFM = ObjC.classes.NSFileManager;
Interceptor.attach(NSFM['- fileExistsAtPath:'].implementation, {
onEnter: function (args) {
const p = new ObjC.Object(args[2]).toString();
for (const b of blockedPaths) if (p.indexOf(b) === 0) { this.block = true; break; }
},
onLeave: function (retval) { if (this.block) retval.replace(0); }
});
// UIApplication.canOpenURL
const UIApp = ObjC.classes.UIApplication;
Interceptor.attach(UIApp['- canOpenURL:'].implementation, {
onEnter: function (args) {
const u = new ObjC.Object(args[2]).toString();
for (const s of blockedSchemes) if (u.indexOf(s + '://') === 0) { this.block = true; break; }
},
onLeave: function (retval) { if (this.block) retval.replace(0); }
});
// fork / getppid spoof
const kernLib = Process.findModuleByName('libsystem_kernel.dylib');
if (kernLib) {
const fork = kernLib.findExportByName('fork');
if (fork) Interceptor.attach(fork, { onLeave: function (retval) { retval.replace(-1); } });
const getppid = kernLib.findExportByName('getppid');
if (getppid) Interceptor.attach(getppid, { onLeave: function (retval) { retval.replace(1); } });
}
// dyld module name rewrite
const dyldLib = Process.findModuleByName('libdyld.dylib');
if (dyldLib) {
const dyld = dyldLib.findExportByName('_dyld_get_image_name');
if (dyld) Interceptor.attach(dyld, {
onLeave: function (retval) {
const name = retval.readCString();
if (name && /Substrate|TweakInject|frida|libtweakinject/.test(name)) {
retval.replace(Memory.allocUtf8String('libSystem.B.dylib'));
}
}
});
}
console.log('[*] jailbreak bypass hooks armed');
}, 2000);
Two ObjC hooks cover file-existence checks and URL scheme checks. The fork/getppid/dyld hooks cover the lower-layer signals. Most JB detection ceases firing after this.
The remaining 5-10% of apps use behavioral / native-side checks (binary checksums, periodic re-scans, attestation-based detection) that need per-app analysis on top of this baseline.
The integrity-check variant
Modern banking apps add checksum-based checks on the app’s own binary or on critical libraries:
1
2
3
4
// Compute SHA-256 of __TEXT segment
uint8_t hash[32];
sha256_compute_segment(get_text_segment(), hash);
if (memcmp(hash, EXPECTED_HASH, 32) != 0) return YES; // tampered
These do not depend on file-existence or syscalls. They are bypassed by:
- Patching the comparison logic out of the binary (find the
memcmpcall, replace with always-equal). - Restoring the original bytes before each verification window.
Significantly more work than the standard JB-detection bypass. Worth recognizing when you see it (“the app keeps crashing after I attach Frida, even though my JB-detection bypass is active”).
Closing
iOS jailbreak detection is the iOS analogue of Android root detection. The vectors are well-known. The drop-in script above plus the methodology of hooking-then-watching-trace covers the bulk of apps. For apps with binary-integrity checks, the bypass moves to inline patching.
Happy Hacking !!

