I recently saw this post on Hacker News claiming that “Volkswagen started blocking GrapheneOS users”. After taking a look at French banking apps’ jailbreak detection here, here and here, I thought it would be fun to see exactly how Volkswagen’s app handles “compromised” environments on iOS, and if we can easily bypass it.
I installed the app on my test device (running a rootless palera1n jailbreak), and right on cue, it crashed immediately on launch.
To figure out what was triggering the crash, I grabbed a decrypted copy of the IPA using TrollDecryptor and fired up lldb to catch the exception. Hitting the process right as it died gave me this:
[ TiD: 1 ]-------------------------------------------------------------------------------------------[regs]
X0: 0x0000000000000000 X8: 0x000000000000000E X16: 0x000000019EE519EC X24: 0x0000000000000000
X1: 0xE600000000000000 X9: 0x0000000000000003 X17: 0x000000006B800000 X25: 0x0000000000000000
X2: 0x000000016B895BB8 X10: 0x0000000104D88608 X18: 0x0000000000000000 X26: 0xFFFFFFFFFFFFFFFF
X3: 0x00000001085A4021 X11: 0x00000000000001E8 X19: 0x00000001085A4876 X27: 0x00000001085A4218
X4: 0x00000000000001F8 X12: 0x00000001085A4875 X20: 0x0000000107A66708 X28: 0x0000000104D88734
X5: 0x00000002818CBC40 X13: 0x0000000000000000 X21: 0x00000001085A4020 FP: 0x000000016B895B60
X6: 0x656E6974756F725F X14: 0x0000000000000000 X22: 0x0000000000000000 LR: 0x0000000104D85D2C
X7: 0x0000000000000001 X15: 0x0000000000650000 X23: 0x0000656361727470 SP: 0x000000016B895AD0
PC: 0x0000000104D88730 N z c v a i f
------------------------------------------------------------------------------------------------------[code]
@ /private/var/containers/Bundle/Application/4B0F7986-7746-497B-86E9-2151D1DC4069/Volkswagen.app/Volkswagen:
-> 0x104d88730 (0x100820730): 20 00 20 d4 brk #0x1
0x104d88734 (0x100820734): 50 ff ff ff
0x104d88738 (0x100820738): b8 01 00 00 udf #0x1b8
0x104d8873c (0x10082073c): d0 00 00 00 udf #0xd0
0x104d88740 (0x100820740): e8 01 00 00 udf #0x1e8
0x104d88744 (0x100820744): 14 02 00 00 udf #0x214
0x104d88748 (0x100820748): 10 00 00 00 udf #0x10
0x104d8874c (0x10082074c): c4 01 00 00 udf #0x1c4
------------------------------------------------------------------------------------------------------------
Process 10907 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x104d88730)
frame #0: 0x0000000104d88730 Volkswagen`___lldb_unnamed_symbol_10082030c + 1060
The brk #0x1 instruction is a software breakpoint that generates an EXC_BREAKPOINT exception. In Swift, this is the standard way the compiler implements fatal errors. The next instruction is a udf #0x1b8 (undefined instruction), meaning we can’t easily skip over this by just shifting the program counter forward.
Something interesting caught my eye in the registers: X23 contains 0x0000656361727470, which is the string ptrace in little-endian. On iOS, ptrace (specifically the PT_DENY_ATTACH flag) is the classic system call used by apps to detect and prevent debuggers from attaching. So, the app knows we’re here.
Let’s check where this instruction lives in memory:
(lldbinit) image lookup -a 0x104d88730
Address: Volkswagen[0x0000000100820730] (Volkswagen.__TEXT.__text + 8505136)
Summary: Volkswagen`___lldb_unnamed_symbol_10082030c + 1060
Initial recon
I threw the binary into IDA Pro and jumped to 0x100820730. Taking a look at the disassembly graph, it was immediately clear the function was obfuscated:

Click to expand the decompiled sub_10082030C function
__int64 __fastcall sub_10082030C(__int64 a1, __int64 a2, _QWORD *a3, unsigned __int8 *a4, __int64 a5, __int64 a6)
{
// variables
if ( a5 < 1 )
{
v9 = 0;
LOBYTE(v15) = 0;
v11 = -1;
goto LABEL_4;
}
result = 0;
v9 = 0;
v10 = &a4[a5];
v11 = -1;
v12 = 0;
v13 = 0;
v14 = 0;
v15 = 0;
LABEL_14:
v25 = a4;
while ( 2 )
{
v26 = *a4++;
v27 = v26 & 0xF;
switch ( (unsigned __int64)v26 >> 4 )
{
case 0uLL:
goto LABEL_4;
case 1uLL:
goto LABEL_69;
case 2uLL:
goto LABEL_38;
case 3uLL:
goto LABEL_75;
case 4uLL:
v48 = v13;
v49 = v12;
v50 = v9;
v51 = v6;
v39 = a2;
v40 = a4;
v54 = result;
v52 = a3;
v53 = a6;
goto LABEL_79;
case 5uLL:
goto LABEL_16;
case 6uLL:
goto LABEL_71;
case 7uLL:
case 8uLL:
goto LABEL_58;
case 9uLL:
case 0xBuLL:
goto LABEL_55;
case 0xAuLL:
goto LABEL_62;
case 0xCuLL:
goto LABEL_51;
case 0xDuLL:
if ( v27 != 1 )
{
if ( !v27 )
goto LABEL_66;
return 0;
}
if ( (v14 & 1) != 0 )
return 0;
v25 = a4;
break;
default:
return 0;
}
while ( 1 )
{
if ( v25 >= v10 )
goto LABEL_4;
a4 = v25 + 1;
v28 = *v25;
v27 = v28 & 0xF;
v29 = v28 >> 4;
if ( v29 != 13 )
break;
++v25;
if ( v27 != 1 )
{
if ( v27 )
return result;
v14 = 0;
do
LABEL_66:
v37 = (char)*a4++;
while ( v37 < 0 );
LABEL_13:
if ( a4 >= v10 )
goto LABEL_4;
goto LABEL_14;
}
}
if ( v29 > 5 )
{
if ( v29 > 9 )
{
if ( v29 == 10 )
{
LABEL_62:
if ( (v12 & 1) != 0 )
goto LABEL_4;
do
v36 = (char)*a4++;
while ( v36 < 0 );
goto LABEL_64;
}
if ( v29 != 11 )
{
if ( v29 != 12 )
return result;
LABEL_51:
if ( (v13 & 1) == 0 )
{
do
v33 = (char)*a4++;
while ( v33 < 0 );
do
v34 = (char)*a4++;
while ( v34 < 0 );
v14 = 0;
v13 = 0;
goto LABEL_13;
}
goto LABEL_4;
}
}
else
{
if ( (unsigned __int64)(v29 - 7) < 2 )
{
v14 = 0;
do
LABEL_58:
v35 = (char)*a4++;
while ( v35 < 0 );
goto LABEL_13;
}
if ( v29 == 6 )
{
v14 = 0;
LABEL_71:
v38 = v25[1];
a4 = v25 + 2;
if ( (v38 & 0x40) != 0 )
v9 = v38 & 0x7F | 0xFFFFFFFFFFFFFF80LL;
else
v9 = v38 & 0x7F;
goto LABEL_13;
}
}
LABEL_55:
if ( (v12 & 1) != 0 )
goto LABEL_4;
LABEL_64:
v14 = 0;
v13 = 0;
v12 = 0;
goto LABEL_13;
}
if ( v29 > 2 )
{
if ( v29 == 3 )
{
v14 = 0;
LABEL_75:
if ( !v27 )
{
v11 = 0;
goto LABEL_13;
}
LABEL_93:
__break(1u);
JUMPOUT(0x100820734LL);
}
if ( v29 == 4 )
{
v48 = v13;
v49 = v12;
v50 = v9;
v51 = v6;
v39 = a2;
v40 = v25 + 1;
v54 = result;
v52 = a3;
v53 = a6;
v14 = 0;
LABEL_79:
v41 = String.init(cString:)(v25 + 2);
if ( v41 == a1 && v42 == v39 )
{
v43 = a1;
swift_bridgeObjectRelease(v42);
goto LABEL_83;
}
v47 = v15;
v44 = v42;
v43 = a1;
v45 = _stringCompareWithSmolCheck(_:_:expecting:)(v41, v42, a1, v39, 0);
swift_bridgeObjectRelease(v44);
if ( (v45 & 1) != 0 )
{
LABEL_83:
v15 = 1;
v14 = 1;
v13 = 1;
v12 = 1;
a3 = v52;
a6 = v53;
result = v54;
a1 = v43;
if ( !*v40 )
goto LABEL_88;
}
else
{
a3 = v52;
a6 = v53;
result = v54;
a1 = v43;
v15 = v47;
v13 = v48;
v12 = v49;
if ( !*v40 )
{
LABEL_88:
a4 = v25 + 2;
a2 = v39;
v9 = v50;
v6 = v51;
goto LABEL_13;
}
}
v25 = v40 - 1;
do
{
v46 = v25[2];
++v25;
}
while ( v46 );
goto LABEL_88;
}
v14 = 0;
LABEL_16:
v25 = a4;
if ( a4 >= v10 )
goto LABEL_4;
continue;
}
break;
}
switch ( v29 )
{
case 0LL:
LABEL_4:
result = 0;
if ( (v15 & 1) == 0 )
return result;
if ( (v11 & 0x8000000000000000LL) != 0 )
return result;
result = 0;
v18 = *(_QWORD *)(a6 + 16);
v16 = a6 + 16;
v17 = v18;
if ( !v18 || v17 < v11 )
return result;
if ( !v11 )
{
__break(1u);
LABEL_92:
__break(1u);
goto LABEL_93;
}
v19 = a1;
v20 = a2;
v21 = a3;
v22 = (__int64 *)(v16 + 16 * v11);
v23 = *v22;
v24 = v22[1];
v55 = 0;
swift_bridgeObjectRetain(v24);
LOBYTE(v20) = sub_10082076C(v19, v20, v23, v24, &v55);
swift_bridgeObjectRelease(v24);
if ( (v20 & 1) != 0 && v55 )
{
*v21 = v55 + v9;
return 1;
}
return 0;
case 1LL:
v14 = 0;
LABEL_69:
v11 = v27;
goto LABEL_13;
case 2LL:
v14 = 0;
LABEL_38:
v11 = 0;
v30 = 0;
do
{
v32 = *a4;
if ( v30 <= 63 )
{
if ( v30 >= -64 )
{
v31 = (unsigned __int64)(v32 & 0x7F) >> -(char)v30;
if ( v30 == -64 )
v31 = 0;
if ( v30 >= 0 )
v31 = (unsigned __int64)(v32 & 0x7F) << v30;
}
else
{
v31 = 0;
}
v11 |= v31;
v30 += 7;
}
++a4;
}
while ( (char)v32 < 0 );
if ( (v11 & 0x8000000000000000LL) == 0 )
goto LABEL_13;
goto LABEL_92;
}
return result;
}
Running Chernobog (an IDA plugin for detecting code obfuscation techniques) confirmed my suspicions:
Detected obfuscations: 0x40062
- Bogus control flow
- Instruction substitution
- Split basic blocks
- Indirect call obfuscation (Hikari)
Even with the control flow flattened out by Hikari, looking through the broader symbol table gave the game away. I found a bunch of heavily structured detection modules:
CodeSignatureDetection
DebuggerDetection
DeviceBindingDetection
EnvironmentDetection
FileDetection
FishHookDetection
IntegrityDetection
JailbreakDetection
MSHookFunctionDetection
ModesDetection
NetworkSecurityDetection
ProxyDetection
ReverseEngineeringDetection
RuntimeHookDetection
SimulatorDetection
I did a quick string search for jailbreak and found a pretty unique artifact: "Shadow anti-anti-jailbreak detector detected :-)".
Whenever you find a unique string like this, the easiest shortcut is to throw it into grep.app. I got two hits:
- securing/IOSSecuritySuite
- FuturraGroup/SecurityKit (which is a fork/expansion of the first)
Looking at the feature list of SecurityKit, the class and method names perfectly match the symbols I recovered from the binary. Volkswagen is using SecurityKit off the shelf. Having access to the exact open-source repository of the library they are using makes reversing it much more straightforward.
Let’s break down how this integration tries to keep us out, layer by layer.
Layer 1: The anti-debug logic
First, let’s look at the function handling ptrace and hooking detection:Click to expand the decompiled
sub_10081D704 function__int64 sub_10081D704()
{
// variables
v0 = _dyld_image_count();
if ( !v0 )
{
LABEL_132:
v59 = dlsym((void *)0xFFFFFFFFFFFFFFFELL, "ptrace");
result = ((__int64 (__fastcall *)(__int64, _QWORD, _QWORD, _QWORD))v59)(31, 0, 0, 0);
if ( (_DWORD)result )
{
v60 = sub_100005E84(&unk_1034FE420, &unk_1028055C8);
v61 = swift_allocObject(v60, 64, 7);
*(_OWORD *)(v61 + 16) = xmmword_1027AF220;
*(_QWORD *)(v61 + 56) = &type metadata for String;
*(_QWORD *)(v61 + 32) = 0xD000000000000056LL;
*(_QWORD *)(v61 + 40) = 0x8000000102A868C0LL;
print(_:separator:terminator:)();
return swift_bridgeObjectRelease(v61);
}
return result;
}
v1 = v0;
v2 = 0;
v66 = v0;
LABEL_5:
image_header = _dyld_get_image_header(v2);
if ( !image_header )
goto LABEL_4;
v4 = (unsigned __int64)image_header;
image_vmaddr_slide = _dyld_get_image_vmaddr_slide(v2);
v71 = 0;
result = type metadata accessor for SymbolFound(0);
v7 = (int *)(v4 + 32);
if ( v4 < 0xFFFFFFFFFFFFFFE0LL )
{
v8 = *(_DWORD *)(v4 + 16);
if ( !v8 )
goto LABEL_4;
v68 = image_vmaddr_slide;
v67 = v4;
result = swift_retain(&_swiftEmptyArrayStorage);
v69 = 0;
v9 = 0;
v10 = result;
while ( 1 )
{
v11 = *v7;
if ( *v7 <= -2147483614 )
{
if ( v11 == -2147483624 || v11 == -2147483617 )
{
LABEL_24:
v14 = String.init(cString:)((char *)v7 + (unsigned int)v7[2]);
v16 = v15;
result = swift_isUniquelyReferenced_nonNull_native(v10);
if ( (result & 1) == 0 )
{
result = sub_10081B234(0, *(_QWORD *)(v10 + 16) + 1LL, 1, v10);
v10 = result;
}
v18 = *(_QWORD *)(v10 + 16);
v17 = *(_QWORD *)(v10 + 24);
if ( v18 >= v17 >> 1 )
{
result = sub_10081B234(v17 > 1, v18 + 1, 1, v10);
v10 = result;
}
*(_QWORD *)(v10 + 16) = v18 + 1;
v19 = v10 + 16 * v18;
*(_QWORD *)(v19 + 32) = v14;
*(_QWORD *)(v19 + 40) = v16;
goto LABEL_11;
}
if ( v11 == -2147483614 )
LABEL_10:
v69 = v7;
}
else if ( v11 > 24 )
{
if ( v11 == 34 )
goto LABEL_10;
if ( v11 == 25 )
{
if ( String.init(cString:)(v7 + 2) == 0x44454B4E494C5F5FLL && v20 == 0xEA00000000005449LL )
{
result = swift_bridgeObjectRelease(0xEA00000000005449LL);
LABEL_35:
v9 = v7;
goto LABEL_11;
}
v21 = v20;
v22 = _stringCompareWithSmolCheck(_:_:expecting:)();
result = swift_bridgeObjectRelease(v21);
if ( (v22 & 1) != 0 )
goto LABEL_35;
}
}
else if ( v11 == -2147483613 || v11 == 12 )
{
goto LABEL_24;
}
LABEL_11:
v7 = (int *)((char *)v7 + (unsigned int)v7[1]);
if ( !--v8 )
{
if ( !v9 )
goto LABEL_3;
v23 = v69;
if ( !v69 )
goto LABEL_3;
v24 = *((_QWORD *)v9 + 3);
if ( v24 < 0 )
goto LABEL_136;
v25 = __OFADD__(v68, v24);
v26 = v68 + v24;
if ( v25 )
goto LABEL_137;
v27 = *((_QWORD *)v9 + 5);
if ( v27 < 0 )
goto LABEL_138;
v28 = v26 - v27;
if ( __OFSUB__(v26, v27) )
goto LABEL_139;
if ( v28 < 0 )
goto LABEL_140;
v29 = (unsigned int)v69[9];
if ( !(_DWORD)v29 )
goto LABEL_126;
v30 = (unsigned __int8 *)(v28 + (unsigned int)v69[8]);
if ( !v30 )
goto LABEL_126;
v31 = 0;
v32 = 0;
v33 = 0;
v34 = &v30[v29];
v35 = -1;
while ( 2 )
{
v36 = v30;
while ( 2 )
{
v30 = v36 + 1;
v37 = *v36;
v38 = v37 >> 4;
if ( v37 >> 4 <= 4 )
{
if ( v37 >> 4 <= 2 )
{
if ( !v38 )
goto LABEL_47;
if ( v38 == 1 )
goto LABEL_88;
if ( v38 != 2 )
goto LABEL_126;
LABEL_90:
v35 = 0;
v43 = 0;
do
{
v45 = *v30;
if ( v43 <= 63 )
{
if ( v43 >= -64 )
{
v44 = (unsigned __int64)(v45 & 0x7F) >> -(char)v43;
if ( v43 == -64 )
v44 = 0;
if ( v43 >= 0 )
v44 = (unsigned __int64)(v45 & 0x7F) << v43;
}
else
{
v44 = 0;
}
v35 |= v44;
v43 += 7;
}
++v30;
}
while ( (char)v45 < 0 );
if ( (v35 & 0x8000000000000000LL) != 0 )
goto LABEL_141;
goto LABEL_47;
}
if ( v38 != 3 )
{
if ( v38 != 4 )
goto LABEL_126;
v63 = v33;
v64 = v32;
v65 = v31;
LABEL_104:
if ( String.init(cString:)(v36 + 2) == 0x656361727470LL && v46 == 0xE600000000000000LL )
{
result = swift_bridgeObjectRelease(0xE600000000000000LL);
goto LABEL_108;
}
v47 = v46;
v62 = _stringCompareWithSmolCheck(_:_:expecting:)();
result = swift_bridgeObjectRelease(v47);
if ( (v62 & 1) != 0 )
{
LABEL_108:
v32 = 1;
v33 = 1;
v31 = v65;
if ( !*v30 )
goto LABEL_113;
}
else
{
v33 = v63;
v32 = v64;
v31 = v65;
if ( !*v30 )
{
LABEL_113:
v30 = v36 + 2;
goto LABEL_47;
}
}
v36 = v30 - 1;
do
{
v48 = v36[2];
++v36;
}
while ( v48 );
goto LABEL_113;
}
++v36;
if ( (v37 & 0xF) != 0 )
goto LABEL_66;
goto LABEL_53;
}
if ( v37 >> 4 > 8 )
{
if ( v38 != 9 )
goto LABEL_126;
if ( v33 & 1 | (v30 >= v34) )
goto LABEL_114;
while ( 1 )
{
v39 = v30 + 1;
v37 = *v30;
v40 = v37 >> 4;
if ( v37 >> 4 != 9 )
break;
++v30;
if ( v39 >= v34 )
goto LABEL_114;
}
if ( v37 >> 4 > 4 )
{
if ( v40 - 7 >= 2 )
{
if ( v40 != 5 )
{
if ( v40 != 6 )
goto LABEL_126;
v33 = 0;
v36 = v30++;
goto LABEL_78;
}
LABEL_46:
v33 = 0;
++v30;
goto LABEL_47;
}
v33 = 0;
++v30;
do
LABEL_81:
v42 = (char)*v30++;
while ( v42 < 0 );
goto LABEL_47;
}
if ( v37 >> 4 <= 1 )
{
if ( !v40 )
goto LABEL_46;
if ( v40 != 1 )
goto LABEL_126;
v33 = 0;
++v30;
LABEL_88:
v35 = v37 & 0xF;
goto LABEL_47;
}
if ( v40 == 2 )
{
v33 = 0;
++v30;
goto LABEL_90;
}
if ( v40 != 3 )
{
v65 = v31;
v63 = 0;
v64 = v32;
v36 = v30++;
goto LABEL_104;
}
v33 = 0;
v36 = v30 + 1;
if ( (v37 & 0xF) != 0 )
{
LABEL_66:
v35 = v37 | 0xF0;
v30 = v36;
goto LABEL_47;
}
LABEL_53:
v35 = 0;
if ( v36 >= v34 )
{
if ( (v32 & 1) == 0 )
goto LABEL_126;
v35 = 0;
v49 = *(_QWORD *)(v10 + 16);
if ( !v49 )
goto LABEL_126;
goto LABEL_117;
}
continue;
}
break;
}
if ( v38 - 7 < 2 )
goto LABEL_81;
if ( v38 != 5 )
{
if ( v38 != 6 )
goto LABEL_126;
LABEL_78:
v41 = *v30;
v30 = v36 + 2;
if ( (v41 & 0x40) != 0 )
v31 = v41 & 0x7F | 0xFFFFFFFFFFFFFF80LL;
else
v31 = v41 & 0x7F;
}
LABEL_47:
if ( v30 < v34 )
continue;
break;
}
LABEL_114:
if ( (v32 & 1) == 0 )
goto LABEL_126;
if ( (v35 & 0x8000000000000000LL) != 0 )
goto LABEL_126;
v49 = *(_QWORD *)(v10 + 16);
if ( !v49 )
goto LABEL_126;
LABEL_117:
if ( v49 < v35 )
goto LABEL_126;
if ( v35 - 1 >= v49 )
goto LABEL_142;
v50 = v10 + 16 * (v35 - 1);
v51 = *(_QWORD *)(v50 + 32);
v52 = *(_QWORD *)(v50 + 40);
v70 = 0;
swift_bridgeObjectRetain(v52);
LOBYTE(v51) = sub_10082076C(0x656361727470LL, 0xE600000000000000LL, v51, v52, &v70);
swift_bridgeObjectRelease(v52);
if ( (v51 & 1) != 0 )
{
v53 = v70;
if ( v70 )
{
swift_bridgeObjectRelease(v10);
v54 = v53 + v31;
v71 = v53 + v31;
v1 = v66;
v55 = v67;
v56 = v68;
goto LABEL_130;
}
}
v23 = v69;
LABEL_126:
if ( !v23[5] || (v57 = v28 + (unsigned int)v23[4]) == 0 )
{
LABEL_3:
swift_bridgeObjectRelease(v10);
v1 = v66;
goto LABEL_4;
}
v58 = sub_10082030C(0x656361727470LL, 0xE600000000000000LL, &v71, v57);
swift_bridgeObjectRelease(v10);
v1 = v66;
v55 = v67;
v56 = v68;
if ( (v58 & 1) != 0 )
{
v54 = v71;
LABEL_130:
if ( v54 )
{
type metadata accessor for FishHook(0);
sub_10081FD94(0x656361727470LL, 0xE600000000000000LL, v55, v56, v54, &v70);
}
}
LABEL_4:
if ( ++v2 == v1 )
goto LABEL_132;
goto LABEL_5;
}
}
}
__break(1u);
LABEL_136:
__break(1u);
LABEL_137:
__break(1u);
LABEL_138:
__break(1u);
LABEL_139:
__break(1u);
LABEL_140:
__break(1u);
LABEL_141:
__break(1u);
LABEL_142:
__break(1u);
return result;
}
Matching this up with SecurityKit’s open-source repository, this corresponds to FishHookDetection.denyFishHook("ptrace") bundled with a standard ptrace(PT_DENY_ATTACH, 0, 0, 0) call.
The goal here isn’t just to call ptrace, it’s to verify that ptrace hasn’t been hooked by something like Substrate or Fishhook. Here is what the routine does:
- Walks the
Mach-Oload commands: For every loaded image, it findsLC_SYMTAB,LC_DYSYMTAB, and the__LINKEDITsegment - Parses lazy bind info: It actually implements a mini bytecode interpreter to walk through the lazy binding opcodes (like
BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM) looking for the string"ptrace" - Resolves the real address: If it finds it, it walks the compressed export trie to find the true address of
ptracedirectly fromlibsystem_kernel.dylib - Force-restores the pointer: It uses
vm_protectto make the indirect symbol table writable, then overwrites the hooked pointer with the real one - Finally, it calls
ptrace(31)
Full implementation is here.
Layer 2: The jailbreak detection logic
Now let’s look at the main jailbreak detection orchestrator, sub_100824E9C:Click to expand the
sub_100824E9C function__int64 sub_100824E9C()
{
// variables
v0 = (char *)&v49
- ((*(_QWORD *)(*(_QWORD *)(sub_100005E84(&unk_1034FEAA0, &unk_102805870) - 8) + 64LL) + 15LL)
& 0xFFFFFFFFFFFFFFF0LL);
v1 = type metadata accessor for URL(0);
v2 = *(_QWORD *)(v1 - 8);
v3 = (char *)&v49 - ((*(_QWORD *)(v2 + 64) + 15LL) & 0xFFFFFFFFFFFFFFF0LL);
v53 = (unsigned __int64)"ction";
v67 = 0;
v68 = 0xE000000000000000LL;
v50 = 0x8000000102A86CE0LL;
v56 = (unsigned __int64)"tor detected :-)";
v52 = 0x8000000102A86D80LL;
v54 = (unsigned __int64)"the fork detect.";
v55 = 0x8000000102A86D40LL;
v4 = &_swiftEmptyArrayStorage;
v5 = swift_retain(&_swiftEmptyArrayStorage);
v6 = 0;
v59 = 1;
v51 = xmmword_1027AF220;
v58 = v2;
v60 = v0;
do
{
v8 = 0;
v9 = byte_1034FDAE8[v6 + 40];
v10 = (void *)0xE000000000000000LL;
if ( v9 <= 3 )
{
if ( byte_1034FDAE8[v6 + 40] > 1u )
{
if ( v9 == 2 )
{
v61 = 2;
v11 = sub_100824238(v5);
}
else
{
if ( v9 != 3 )
goto LABEL_3;
v61 = 3;
v11 = sub_1008245FC(v5);
}
}
else
{
if ( !byte_1034FDAE8[v6 + 40] )
{
v61 = 0;
v57 = v4;
v17 = &aCydia[8];
v18 = 5;
v62 = v6;
while ( 1 )
{
v20 = *((_QWORD *)v17 - 1);
v19 = *(_QWORD *)v17;
swift_bridgeObjectRetain(*(_QWORD *)v17);
URL.init(string:)(v20, v19);
if ( (*(unsigned int (__fastcall **)(char *, __int64, __int64))(v2 + 48))(v0, 1, v1) == 1 )
{
swift_bridgeObjectRelease(v19);
sub_1002479C0(v0, &unk_1034FEAA0, &unk_102805870);
}
else
{
(*(void (__fastcall **)(char *, char *, __int64))(v2 + 32))(v3, v0, v1);
v21 = v1;
v22 = objc_retainAutoreleasedReturnValue(objc_msgSend((id)swift_getInitializedObjCClass(&OBJC_CLASS___UIApplication), "sharedApplication"));
URL._bridgeToObjectiveC()(v23);
v25 = v24;
v26 = v3;
v27 = (unsigned __int8)objc_msgSend(v22, "canOpenURL:", v24);
objc_release(v22);
objc_release(v25);
if ( (v27 & 1) != 0 )
{
*(_QWORD *)&v66[0] = 0;
*((_QWORD *)&v66[0] + 1) = 0xE000000000000000LL;
_StringGuts.grow(_:)(22);
swift_bridgeObjectRelease(*((_QWORD *)&v66[0] + 1));
*(_QWORD *)&v66[0] = v20;
*((_QWORD *)&v66[0] + 1) = v19;
v47._countAndFlagsBits = 0xD000000000000014LL;
v47._object = (void *)(v54 | 0x8000000000000000LL);
String.append(_:)(v47);
v10 = *((void **)&v66[0] + 1);
v8 = *(_QWORD *)&v66[0];
(*(void (__fastcall **)(char *, __int64))(v2 + 8))(v26, v21);
swift_arrayDestroy(aCydia, 5, &type metadata for String);
v1 = v21;
v0 = v60;
v3 = v26;
v4 = v57;
v6 = v62;
goto LABEL_31;
}
(*(void (__fastcall **)(char *, __int64))(v2 + 8))(v26, v21);
swift_bridgeObjectRelease(v19);
v1 = v21;
v0 = v60;
v3 = v26;
v6 = v62;
}
v17 += 16;
if ( !--v18 )
{
swift_arrayDestroy(aCydia, 5, &type metadata for String);
v8 = 0;
v10 = (void *)0xE000000000000000LL;
v4 = v57;
goto LABEL_3;
}
}
}
v61 = 1;
v11 = sub_100823E38(v5);
}
goto LABEL_30;
}
if ( byte_1034FDAE8[v6 + 40] <= 5u )
{
if ( v9 == 4 )
{
v61 = 4;
v28 = objc_retainAutoreleasedReturnValue(objc_msgSend((id)swift_getInitializedObjCClass(&OBJC_CLASS___NSProcessInfo), "processInfo"));
v29 = objc_retainAutoreleasedReturnValue(objc_msgSend(v28, "environment"));
objc_release(v28);
v30 = static Dictionary._unconditionallyBridgeFromObjectiveC(_:)(
v29,
&type metadata for String,
&type metadata for String,
&protocol witness table for String);
objc_release(v29);
if ( *(_QWORD *)(v30 + 16) )
{
sub_1003E0028(0xD000000000000015LL, v56 | 0x8000000000000000LL);
v32 = v31;
swift_bridgeObjectRelease(v30);
if ( (v32 & 1) != 0 )
{
v33 = sub_100005E84(&unk_1034FE420, &unk_1028055C8);
v34 = swift_allocObject(v33, 64, 7);
*(_OWORD *)(v34 + 16) = v51;
*(_QWORD *)(v34 + 56) = &type metadata for String;
v35 = v52;
*(_QWORD *)(v34 + 32) = 0xD000000000000040LL;
*(_QWORD *)(v34 + 40) = v35;
print(_:separator:terminator:)();
swift_bridgeObjectRelease(v34);
LABEL_47:
v8 = 0;
v10 = (void *)0xE000000000000000LL;
LABEL_40:
v2 = v58;
goto LABEL_3;
}
}
else
{
swift_bridgeObjectRelease(v30);
}
v41 = (__int64 (*)(void))dlsym((void *)0xFFFFFFFFFFFFFFFELL, "fork");
v42 = v41();
if ( (v42 & 0x80000000) == 0 )
{
if ( v42 )
kill(v42, 15);
v10 = (void *)v55;
v8 = 0xD000000000000039LL;
LABEL_31:
swift_bridgeObjectRetain(v10);
if ( (swift_isUniquelyReferenced_nonNull_native(v4) & 1) == 0 )
v4 = (_QWORD *)sub_10081B6B8(0, v4[2] + 1LL, 1, v4);
v37 = v4[2];
v36 = v4[3];
if ( v37 >= v36 >> 1 )
v4 = (_QWORD *)sub_10081B6B8(v36 > 1, v37 + 1, 1, v4);
v4[2] = v37 + 1;
v38 = (char *)&v4[3 * v37];
v38[32] = v61;
*((_QWORD *)v38 + 5) = v8;
*((_QWORD *)v38 + 6) = v10;
v39 = v67 & 0xFFFFFFFFFFFFLL;
if ( (v68 & 0x2000000000000000LL) != 0 )
v39 = HIBYTE(v68) & 0xF;
if ( v39 )
{
v40._countAndFlagsBits = 8236;
v40._object = (void *)0xE200000000000000LL;
String.append(_:)(v40);
}
v59 = 0;
goto LABEL_40;
}
goto LABEL_47;
}
if ( v9 != 5 )
goto LABEL_3;
v61 = 5;
v11 = sub_100824A14(v5);
goto LABEL_30;
}
if ( v9 == 6 )
{
v61 = 6;
v11 = sub_100824C7C(v5);
LABEL_30:
v8 = v12;
v10 = v13;
if ( (v11 & 1) == 0 )
goto LABEL_31;
v59 &= v11;
goto LABEL_3;
}
if ( v9 != 9 )
goto LABEL_3;
v61 = 9;
v14 = objc_retainAutoreleasedReturnValue(objc_getClass("ShadowRuleset"));
if ( v14 )
{
v15 = v14;
_bridgeAnyObjectToAny(_:)(&v64);
v16 = v15;
v2 = v58;
swift_unknownObjectRelease(v16);
}
else
{
v64 = 0u;
v65 = 0u;
}
v66[0] = v64;
v66[1] = v65;
if ( *((_QWORD *)&v65 + 1) )
{
v43 = sub_100005E84(&unk_1034FEAA8, &unk_102805880);
if ( (swift_dynamicCast(&v63, v66, (char *)&type metadata for Any + 8, v43, 6) & 1) != 0 )
{
v44 = v63;
v45 = (const char *)Selector.init(_:)(0xD000000000000012LL, v53 | 0x8000000000000000LL);
ObjCClassFromMetadata = (objc_class *)swift_getObjCClassFromMetadata(v44);
if ( class_getInstanceMethod(ObjCClassFromMetadata, v45) )
{
v8 = 0xD000000000000030LL;
v10 = (void *)v50;
goto LABEL_31;
}
v8 = 0;
goto LABEL_40;
}
v8 = 0;
}
else
{
sub_1002479C0(v66, &unk_1034FE378, "B2>");
v8 = 0;
}
LABEL_3:
++v6;
v7._countAndFlagsBits = v8;
v7._object = v10;
String.append(_:)(v7);
v5 = swift_bridgeObjectRelease(v10);
}
while ( v6 != 8 );
return v59 & 1;
}
This function is called by 3 tiny functions, all of which invert the result:
Click to expand the 3 tiny functions
bool sub_100????()
{
char v0; // w19
__int64 v1; // x3
v0 = sub_100824E9C();
swift_bridgeObjectRelease(v1);
return (v0 & 1) == 0;
}
We can interpret sub_100824E9C() == true to mean “device looks clean”, and false to mean “at least one check tripped.”
Let’s look at the dispatch table of 8 checks (byte_1034FDAE8+40):
__data:00000001034FDAE8 byte_1034FDAE8 DCB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
__data:00000001034FDAF9 DCB 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0x10
__data:00000001034FDB09 DCB 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 9, 0, 0
__data:00000001034FDB1A DCB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
__data:00000001034FDB2B DCB 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0xA, 0, 0, 0
__data:00000001034FDB3C DCB 0, 0, 0, 0
The active dispatch list is: [0, 1, 2, 3, 4, 5, 6, 9].
Each check returns true (passed) or false (tripped). The instant any check returns false, the code jumps to a shared handler that appends a log record to a tracking array detailing which checks failed, setting the final status to failed.
Here is the mapping of check indices to their corresponding functions and techniques:
| Tag | Function | Technique |
|---|---|---|
| 0 | inline | Cydia/package-manager URL scheme probe |
| 1 | sub_100823E38 | Known jailbreak binary paths (read access) |
| 2 | sub_100824238 | Restricted-path write probe |
| 3 | sub_1008245FC | Write file to / (root write test) |
| 4 | inline | Env var check + fork() test |
| 5 | sub_100824A14 | System-path symlink-farm check |
| 6 | sub_100824C7C | Loaded-dylib blocklist scan |
| 9 | inline | ShadowRuleset ObjC class probe |
Case 0: Cydia URL scheme probe
Builds candidate URL objects from a static scheme array and calls UIApplication.canOpenURL: on each. If any resolve to true, the check fails. This check relies on the app’s Info.plist containing the queried schemes under LSApplicationQueriesSchemes.
According to the iOS Security Suite README:
For jailbreak detection to work correctly, you need to update your main Info.plist.
<key>LSApplicationQueriesSchemes</key>
<array>
<string>cydia</string>
<string>undecimus</string>
<string>sileo</string>
<string>zbra</string>
<string>filza</string>
</array>
Looking at the extracted Info.plist:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>comgooglemaps</string>
<string>cydia</string>
<string>undecimus</string>
<string>sileo</string>
</array>
Here is the source code for this check in SecurityKit:
@MainActor private static func detectURLSchemes() -> DetectResult {
let urlSchemes = [
"cydia://",
"undecimus://",
"sileo://",
"zbra://",
"filza://"
]
return canOpenUrlFromList(urlSchemes: urlSchemes)
}
Case 1: Known-binary existence check
Click to expand the sub_100823E38 function
__int64 sub_100823E38()
{
// variables
v33 = -1;
v0 = sub_100005E84(&unk_1034FE1B8, &unk_102805890);
inited = swift_initStaticObject(v0, &unk_1034FD238);
v31 = inited;
InitializedObjCClass = (void *)swift_getInitializedObjCClass(&OBJC_CLASS___NSProcessInfo);
swift_retain(inited);
v3 = objc_retainAutoreleasedReturnValue(objc_msgSend(InitializedObjCClass, "processInfo"));
v4 = objc_retainAutoreleasedReturnValue(objc_msgSend(v3, "environment"));
objc_release(v3);
v5 = static Dictionary._unconditionallyBridgeFromObjectiveC(_:)(
v4,
&type metadata for String,
&type metadata for String,
&protocol witness table for String);
objc_release(v4);
if ( !*(_QWORD *)(v5 + 16)
|| (swift_bridgeObjectRetain(v5),
sub_1003E0028(0xD000000000000015LL, 0x8000000102A86D20LL),
v7 = v6,
swift_bridgeObjectRelease(v5),
(v7 & 1) == 0) )
{
swift_bridgeObjectRelease(v5);
goto LABEL_20;
}
swift_bridgeObjectRelease(v5);
for ( i = *(_QWORD *)(inited + 16); i; i = *(_QWORD *)(v31 + 16) )
{
v10 = 0;
v11 = (void **)(inited + 40);
while ( v10 < *(_QWORD *)(inited + 16) )
{
v13 = (__int64)*(v11 - 1);
v12 = *v11;
v14 = (void *)swift_getInitializedObjCClass(&OBJC_CLASS___NSFileManager);
swift_bridgeObjectRetain(v12);
v15 = objc_retainAutoreleasedReturnValue(objc_msgSend(v14, "defaultManager"));
v16 = String._bridgeToObjectiveC()();
v17 = (unsigned int)objc_msgSend(v15, "fileExistsAtPath:", v16);
objc_release(v15);
objc_release(v16);
if ( v17 )
goto LABEL_15;
memset(&v32, 0, sizeof(v32));
v18 = String._bridgeToObjectiveC()();
v19 = (const char *)objc_msgSend(v18, "fileSystemRepresentation");
v20 = objc_autorelease(v18);
if ( !stat(v19, &v32) )
{
swift_bridgeObjectRelease(inited);
_StringGuts.grow(_:)(26);
swift_bridgeObjectRelease(0xE000000000000000LL);
v28._countAndFlagsBits = v13;
v28._object = v12;
String.append(_:)(v28);
swift_bridgeObjectRelease(v12);
v26 = 0;
return v26 & 1;
}
v21 = sub_10081F938(v13, v12, 0);
if ( v22 )
{
v29 = v21;
swift_bridgeObjectRelease(v12);
swift_bridgeObjectRelease(inited);
v26 = v29;
return v26 & 1;
}
v23 = String._bridgeToObjectiveC()();
v24 = (const char *)objc_msgSend(v23, "fileSystemRepresentation");
v25 = objc_autorelease(v23);
if ( !access(v24, 4) )
{
LABEL_15:
swift_bridgeObjectRelease(inited);
*(_QWORD *)&v32.st_dev = 0;
v32.st_ino = 0xE000000000000000LL;
_StringGuts.grow(_:)(26);
swift_bridgeObjectRelease(v32.st_ino);
*(_QWORD *)&v32.st_dev = 0xD000000000000018LL;
v32.st_ino = 0x8000000102A86A80LL;
v27._countAndFlagsBits = v13;
v27._object = v12;
String.append(_:)(v27);
swift_bridgeObjectRelease(v12);
v26 = 0;
return v26 & 1;
}
++v10;
swift_bridgeObjectRelease(v12);
v11 += 2;
if ( i == v10 )
goto LABEL_14;
}
__break(1u);
LABEL_20:
swift_once(&qword_1034FD0B0, sub_100823B44);
v9 = swift_bridgeObjectRetain(qword_1034FE9E0);
sub_10081BD18(v9);
inited = v31;
}
LABEL_14:
swift_bridgeObjectRelease(inited);
v26 = 1;
return v26 & 1;
}
First checks ProcessInfo.environment for suspicious entries, then loops through the path list testing via stat() and access(path, R_OK). This often catches jailbroken environments containing basic shell utilities.
Here is the matching source code in SecurityKit:
internal class EnvironmentDetection {
typealias DetectResult = (passed: Bool, errorMessage: String)
/// Environment variables commonly set by jailbreak tools and injection frameworks
private static let suspiciousVariables = [
"DYLD_INSERT_LIBRARIES",
"DYLD_LIBRARY_PATH",
"DYLD_FRAMEWORK_PATH",
"DYLD_PRINT_TO_FILE",
"_MSSafeMode",
"INJECT_DYLIB",
"SUBSTRATE_INSERT_LIBRARIES",
"SIMULATED_JAILBREAK"
]
/**
Checks the process environment for known suspicious variables used by injection tools.
- Returns: A tuple with the detection result and error message if a suspicious variable was found.
*/
static func detectSuspiciousEnvironment() -> DetectResult {
for envVar in suspiciousVariables {
if let value = ProcessInfo.processInfo.environment[envVar] {
return (false, "Suspicious environment variable set: \(envVar)=\(value)")
}
}
return (true, "")
}
/**
Convenience method that returns a simple Bool indicating whether any suspicious environment variables are set.
- Returns: Bool indicating if suspicious environment variables were detected (true) or not (false)
*/
static func hasSuspiciousEnvironment() -> Bool {
return !detectSuspiciousEnvironment().passed
}
}
Note: SIMULATED_JAILBREAK looks like an internal QA testing flag, not a real OS path.
Case 2: Restricted-path write probe
Click to expand the sub_100824238 function
__int64 sub_100824238()
{
// variables
v0 = sub_100005E84(&unk_1034FE1B8, &unk_102805890);
inited = swift_initStaticObject(v0, &unk_1034FD740);
v28 = inited;
InitializedObjCClass = (void *)swift_getInitializedObjCClass(&OBJC_CLASS___NSProcessInfo);
swift_retain(inited);
v3 = objc_retainAutoreleasedReturnValue(objc_msgSend(InitializedObjCClass, "processInfo"));
v4 = objc_retainAutoreleasedReturnValue(objc_msgSend(v3, "environment"));
objc_release(v3);
v5 = static Dictionary._unconditionallyBridgeFromObjectiveC(_:)(
v4,
&type metadata for String,
&type metadata for String,
&protocol witness table for String);
objc_release(v4);
if ( !*(_QWORD *)(v5 + 16)
|| (swift_bridgeObjectRetain(v5),
sub_1003E0028(0xD000000000000015LL, 0x8000000102A86D20LL),
v7 = v6,
swift_bridgeObjectRelease(v5),
(v7 & 1) == 0) )
{
swift_bridgeObjectRelease(v5);
goto LABEL_20;
}
swift_bridgeObjectRelease(v5);
for ( i = *(_QWORD *)(inited + 16); i; i = *(_QWORD *)(v28 + 16) )
{
v10 = 0;
v11 = (void **)(inited + 40);
while ( v10 < *(_QWORD *)(inited + 16) )
{
v13 = (__int64)*(v11 - 1);
v12 = *v11;
v14 = (void *)swift_getInitializedObjCClass(&OBJC_CLASS___NSFileManager);
swift_bridgeObjectRetain(v12);
v15 = objc_retainAutoreleasedReturnValue(objc_msgSend(v14, "defaultManager"));
v16 = String._bridgeToObjectiveC()();
v17 = (unsigned int)objc_msgSend(v15, "isReadableFileAtPath:", v16);
objc_release(v15);
objc_release(v16);
if ( v17 )
{
swift_bridgeObjectRelease(inited);
_StringGuts.grow(_:)(33);
swift_bridgeObjectRelease(0xE000000000000000LL);
goto LABEL_17;
}
v18 = String.utf8CString.getter(v13, v12);
v19 = String.utf8CString.getter(11122, 0xE200000000000000LL);
v20 = fopen((const char *)(v18 + 32), (const char *)(v19 + 32));
swift_release(v18);
swift_release(v19);
if ( v20 )
{
fclose(v20);
_StringGuts.grow(_:)(26);
swift_bridgeObjectRelease(0xE000000000000000LL);
v25._countAndFlagsBits = v13;
v25._object = v12;
String.append(_:)(v25);
swift_bridgeObjectRelease(v12);
v26 = inited;
goto LABEL_18;
}
v21 = String._bridgeToObjectiveC()();
v22 = (const char *)objc_msgSend(v21, "fileSystemRepresentation");
v23 = objc_autorelease(v21);
if ( !access(v22, 2) )
{
swift_bridgeObjectRelease(inited);
_StringGuts.grow(_:)(26);
swift_bridgeObjectRelease(0xE000000000000000LL);
LABEL_17:
v27._countAndFlagsBits = v13;
v27._object = v12;
String.append(_:)(v27);
v26 = (__int64)v12;
LABEL_18:
swift_bridgeObjectRelease(v26);
return 0;
}
++v10;
swift_bridgeObjectRelease(v12);
v11 += 2;
if ( i == v10 )
goto LABEL_13;
}
__break(1u);
LABEL_20:
swift_once(&qword_1034FD0B0, sub_100823B44);
v9 = swift_bridgeObjectRetain(qword_1034FE9E0);
sub_10081BD18(v9);
inited = v28;
}
LABEL_13:
swift_bridgeObjectRelease(inited);
return 1;
}
Iterates over paths to check if they are writable. On stock iOS, directories outside the app container are read-only; root filesystem remounts common in older jailbreaks make these writable.
Source code from SecurityKit:
static func detectExistenceOfSuspiciousFilesViaFOpen(path: String, mode: FileMode) -> DetectResult? {
// the 'a' or 'w' modes, create the file if it does not exist.
let mode: String = FileMode.writable == mode ? "r+" : "r"
if let filePointer: UnsafeMutablePointer<FILE> = fopen(path, mode) {
fclose(filePointer)
return (false, "Suspicious file exists: \(path)")
} else {
return nil
}
}
Case 3: Root-directory write test
Click to expand the sub_1008245FC function
__int64 sub_1008245FC()
{
// variables
v45 = -1;
v0 = type metadata accessor for UUID(0);
v32 = *(_QWORD *)(v0 - 8);
v33 = v0;
v1 = (char *)&v31 - ((*(_QWORD *)(v32 + 64) + 15LL) & 0xFFFFFFFFFFFFFFF0LL);
v2 = type metadata accessor for String.Encoding(0);
v3 = *(_QWORD *)(v2 - 8);
v4 = (__int64 *)((char *)&v31 - ((*(_QWORD *)(v3 + 64) + 15LL) & 0xFFFFFFFFFFFFFFF0LL));
static String.Encoding.utf8.getter(v4);
v5 = sub_10081FA34(47, 0xE100000000000000LL, v4);
v34 = *(void (__fastcall **)(_QWORD *, __int64))(v3 + 8);
v35 = v2;
v34(v4, v2);
if ( v5 != 2 && (v5 & 1) == 0 )
goto LABEL_11;
static String.Encoding.utf8.getter(v4);
sub_10081F330(&v41, 47, 0xE100000000000000LL, v4);
v34(v4, v35);
if ( v42 )
{
v6 = v44;
v39[0] = v41;
v39[1] = v42;
sub_1008255A4(v39);
v40 = v43;
sub_1008255A4(&v40);
if ( (v6 & 1) == 0 )
goto LABEL_11;
}
v7 = sub_10081FAF0(47, 0xE100000000000000LL);
if ( v7 == 2 || (v7 & 1) != 0 )
{
v8 = 4;
for ( i = &unk_1034FD7F0; ; i += 2 )
{
v11 = *(i - 1);
v10 = (void *)*i;
v12 = swift_bridgeObjectRetain(*i);
v13 = UUID.init()(v12);
v14 = UUID.uuidString.getter(v13);
v16 = v15;
(*(void (__fastcall **)(char *, __int64))(v32 + 8))(v1, v33);
v37 = v11;
v38 = (unsigned __int64)v10;
swift_bridgeObjectRetain(v10);
v17._countAndFlagsBits = v14;
v17._object = v16;
String.append(_:)(v17);
swift_bridgeObjectRelease(v16);
v18 = v37;
v19 = v38;
v36[1] = v38;
v37 = 0x7974697275636553LL;
v38 = 0xEB0000000074694BLL;
v36[0] = v18;
v20 = static String.Encoding.utf8.getter(v4);
v21 = sub_100819470(v20);
StringProtocol.write<A>(toFile:atomically:encoding:)(
v36,
1,
v4,
&type metadata for String,
&type metadata for String,
v21,
v21);
v34(v4, v35);
v22 = objc_retainAutoreleasedReturnValue(objc_msgSend((id)swift_getInitializedObjCClass(&OBJC_CLASS___NSFileManager), "defaultManager"));
v23 = String._bridgeToObjectiveC()();
swift_bridgeObjectRelease(v19);
v37 = 0;
v24 = (unsigned int)objc_msgSend(v22, "removeItemAtPath:error:", v23, &v37);
objc_release(v22);
objc_release(v23);
v25 = v37;
if ( v24 )
break;
v26 = objc_retain((id)v37);
swift_bridgeObjectRelease(v10);
v27 = _convertNSErrorToError(_:)(v25);
objc_release(v26);
swift_willThrow();
swift_errorRelease(v27);
if ( !--v8 )
{
swift_arrayDestroy(&unk_1034FD7E8, 4, &type metadata for String);
return 1;
}
}
v29 = objc_retain((id)v37);
swift_arrayDestroy(&unk_1034FD7E8, 4, &type metadata for String);
v37 = 0;
v38 = 0xE000000000000000LL;
_StringGuts.grow(_:)(28);
swift_bridgeObjectRelease(v38);
v37 = 0xD00000000000001ALL;
v38 = 0x8000000102A86E10LL;
v30._countAndFlagsBits = v11;
v30._object = v10;
String.append(_:)(v30);
swift_bridgeObjectRelease(v10);
return 0;
}
else
{
LABEL_11:
swift_arrayDestroy(&unk_1034FD7E8, 4, &type metadata for String);
return 0;
}
}
Generates a random UUID filename and attempts to write to /. On a standard sandbox, this write fails. If it succeeds, the app detects a sandbox escape.
private static func detectRestrictedDirectoriesWriteable() -> DetectResult {
let paths = [
"/",
"/root/",
"/private/",
"/jb/"
]
if FileDetection.detectRestrictedPathIsReadonlyViaStatvfs(path: "/") == false {
return (false, "Restricted path '/' is not Read-Only")
} else if FileDetection.detectRestrictedPathIsReadonlyViaStatfs(path: "/") == false {
return (false, "Restricted path '/' is not Read-Only")
} else if FileDetection.detectRestrictedPathIsReadonlyViaGetfsstat(name: "/") == false {
return (false, "Restricted path '/' is not Read-Only")
}
// If library won't be able to write to any restricted directory the return(false, ...) is never reached
// because of catch{} statement
for path in paths {
do {
let pathWithSomeRandom = path + UUID().uuidString
try "SecurityKit".write(
toFile: pathWithSomeRandom,
atomically: true,
encoding: String.Encoding.utf8
)
// clean if succesfully written
try FileManager.default.removeItem(atPath: pathWithSomeRandom)
return (false, "Wrote to restricted path: \(path)")
} catch {}
}
return (true, "")
}
Case 4: Env var + fork() test
This check attempts to execute a fork() process:
fork_fn = dlsym(RTLD_NEXT, "fork");
pid = fork_fn();
if (pid >= 0) { // fork() succeeded
if (pid != 0) kill(pid, SIGTERM); // kill the child
}
On vanilla iOS, the application sandbox blocks fork() (returning -1 with EINVAL). If fork() successfully creates a child process, the environment is compromised.
private static func detectFork() -> DetectResult {
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, "")
}
Case 5: Symlink-farm check
Click to expand the sub_100824A14 function
__int64 sub_100824A14()
{
void *InitializedObjCClass; // x19
__int64 v1; // x26
_UNKNOWN **i; // x27
id v3; // x20
__int64 v4; // x22
void *v5; // x21
__int64 v6; // x22
id v7; // x23
NSString v8; // x24
id v9; // x20
id v10; // x25
__int64 v11; // x24
unsigned __int64 v12; // x1
unsigned __int64 v13; // x23
id v14; // x0
__int64 v15; // x8
Swift::String v17; // x0
Swift::String v18; // x0
Swift::String v19; // x0
Swift::String v20; // x0
id v21[4]; // [xsp+0h] [xbp-70h] BYREF
v21[3] = (id)-1LL;
InitializedObjCClass = (void *)swift_getInitializedObjCClass(&OBJC_CLASS___NSFileManager);
v1 = 8;
for ( i = &off_1034FD858; ; i += 2 )
{
v6 = (__int64)*(i - 1);
v5 = *i;
swift_bridgeObjectRetain(*i);
v7 = objc_retainAutoreleasedReturnValue(objc_msgSend(InitializedObjCClass, "defaultManager"));
v8 = String._bridgeToObjectiveC()();
v21[0] = 0;
v9 = objc_retainAutoreleasedReturnValue(objc_msgSend(v7, "destinationOfSymbolicLinkAtPath:error:", v8, v21));
objc_release(v7);
objc_release(v8);
v10 = v21[0];
if ( v9 )
break;
v3 = objc_retain(v21[0]);
swift_bridgeObjectRelease(v5);
v4 = _convertNSErrorToError(_:)(v10);
objc_release(v3);
swift_willThrow();
swift_errorRelease(v4);
LABEL_3:
if ( !--v1 )
{
swift_arrayDestroy(&unk_1034FD850, 8, &type metadata for String);
return 1;
}
}
v11 = static String._unconditionallyBridgeFromObjectiveC(_:)(v9);
v13 = v12;
v14 = objc_retain(v10);
objc_release(v9);
v15 = HIBYTE(v13) & 0xF;
if ( (v13 & 0x2000000000000000LL) == 0 )
v15 = v11 & 0xFFFFFFFFFFFFLL;
if ( !v15 )
{
swift_bridgeObjectRelease(v13);
swift_bridgeObjectRelease(v5);
goto LABEL_3;
}
swift_arrayDestroy(&unk_1034FD850, 8, &type metadata for String);
v21[0] = 0;
v21[1] = (id)0xE000000000000000LL;
_StringGuts.grow(_:)(52);
v17._object = (void *)0x8000000102A86E60LL;
v17._countAndFlagsBits = 0xD000000000000025LL;
String.append(_:)(v17);
v18._countAndFlagsBits = v6;
v18._object = v5;
String.append(_:)(v18);
swift_bridgeObjectRelease(v5);
v19._countAndFlagsBits = 0x2073746E696F7020LL;
v19._object = (void *)0xEB00000000206F74LL;
String.append(_:)(v19);
v20._countAndFlagsBits = v11;
v20._object = (void *)v13;
String.append(_:)(v20);
swift_bridgeObjectRelease(v13);
return 0;
}
Verifies that system folders are not symbolic links pointing to the writable /var partition (a standard practice used by storage-limited jailbreaks to redirect system files).
private static func detectSymbolicLinks() -> DetectResult {
let paths = [
"/var/lib/undecimus/apt", // unc0ver
"/Applications",
"/Library/Ringtones",
"/Library/Wallpaper",
"/usr/arm-apple-darwin9",
"/usr/include",
"/usr/libexec",
"/usr/share"
]
for path in paths {
do {
let result = try FileManager.default.destinationOfSymbolicLink(atPath: path)
if !result.isEmpty {
return (false, "Non standard symbolic link detected: \(path) points to \(result)")
}
} catch {}
}
return (true, "")
}
Case 6: Loaded-image (dylib) blocklist scan
Click to expand the sub_100824C7C function
__int64 sub_100824C7C()
{
// variables
v0 = sub_100005E84(&unk_1034FE1B8, &unk_102805890);
inited = swift_initStaticObject(v0, &unk_1034FD8D8);
v2 = swift_retain(inited);
v3 = sub_100825C84(v2);
swift_release(inited);
swift_arrayDestroy(inited + 32, 31, &type metadata for String);
v20 = _dyld_image_count();
if ( v20 )
{
v4 = 0;
while ( 1 )
{
result = (__int64)_dyld_get_image_name(v4);
if ( !result )
break;
++v4;
v6 = String.init(cString:)(result);
v8 = v7;
v9 = 1LL << *(_BYTE *)(v3 + 32);
if ( v9 < 64 )
v10 = ~(-1LL << v9);
else
v10 = -1;
v11 = v10 & *(_QWORD *)(v3 + 56);
v12 = (unsigned __int64)(v9 + 63) >> 6;
result = swift_bridgeObjectRetain(v3);
v13 = 0;
while ( v11 )
{
v14 = v13;
LABEL_15:
v15 = (__int64 *)(*(_QWORD *)(v3 + 48) + ((v14 << 10) | (16 * __clz(__rbit64(v11)))));
v17 = *v15;
v16 = v15[1];
v11 &= v11 - 1;
v22 = v6;
v23 = (unsigned __int64)v8;
v21[0] = v17;
v21[1] = v16;
v18 = sub_100819470(result);
result = StringProtocol.localizedCaseInsensitiveContains<A>(_:)(
v21,
&type metadata for String,
&type metadata for String,
v18,
v18);
if ( (result & 1) != 0 )
{
swift_bridgeObjectRelease(v3);
v22 = 0;
v23 = 0xE000000000000000LL;
_StringGuts.grow(_:)(29);
swift_bridgeObjectRelease(v23);
v22 = 0xD00000000000001BLL;
v23 = 0x8000000102A86E90LL;
v19._countAndFlagsBits = v6;
v19._object = v8;
String.append(_:)(v19);
swift_release(v3);
swift_bridgeObjectRelease(v8);
return 0;
}
}
while ( 1 )
{
v14 = v13 + 1;
if ( __OFADD__(v13, 1) )
{
__break(1u);
goto LABEL_20;
}
if ( v14 >= v12 )
break;
v11 = *(_QWORD *)(v3 + 56 + 8 * v14);
++v13;
if ( v11 )
{
v13 = v14;
goto LABEL_15;
}
}
swift_release(v3);
swift_bridgeObjectRelease(v8);
if ( v4 == v20 )
goto LABEL_17;
}
LABEL_20:
__break(1u);
}
else
{
LABEL_17:
swift_bridgeObjectRelease(v3);
return 1;
}
return result;
}
Walks the array of loaded Mach-O images using _dyld_get_image_name() and compares them against a blocklist of known dynamic injection libraries.
private static func detectDYLD() -> DetectResult {
let suspiciousLibraries: Set<String> = [
"systemhook.dylib", // Dopamine - hide jailbreak detection https://github.com/opa334/Dopamine/blob/dc1a1a3486bb5d74b8f2ea6ada782acdc2f34d0a/Application/Dopamine/Jailbreak/DOEnvironmentManager.m#L498
"SubstrateLoader.dylib",
"SSLKillSwitch2.dylib",
"SSLKillSwitch.dylib",
"MobileSubstrate.dylib",
"TweakInject.dylib",
"CydiaSubstrate",
"cynject",
"CustomWidgetIcons",
"PreferenceLoader",
"RocketBootstrap",
"WeeLoader",
"/.file", // HideJB (2.1.1) changes full paths of the suspicious libraries to "/.file"
"libhooker",
"SubstrateInserter",
"SubstrateBootstrap",
"ABypass",
"FlyJB",
"Substitute",
"Cephei",
"Electra",
"AppSyncUnified-FrontBoard.dylib",
"Shadow",
"FridaGadget",
"frida",
"libcycript",
"roothideinit.dylib",
"ellekit",
"libellekit",
"palera1nHelper",
"bootstrapd"
]
for index in 0..<_dyld_image_count() {
let imageName = String(cString: _dyld_get_image_name(index))
// The fastest case insensitive contains detect.
for library in suspiciousLibraries where imageName.localizedCaseInsensitiveContains(library) {
return (false, "Suspicious library loaded: \(imageName)")
}
}
return (true, "")
}
Case 9: ShadowRuleset reflection check
if let cls = NSClassFromString("ShadowRuleset") {
// checks class_getInstanceMethod(cls, someSelector)
}
Shadow is itself a known anti-jailbreak-detection tweak (it patches apps to hide jailbreak indicators). This check looks for the presence of classes registered by this tweak.
private static func detectSuspiciousObjCClasses() -> DetectResult {
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, "")
}
A quick way to bypass all those checks is just to make sub_100824E9C return true.
Layer 3: The secondary security gate
The application also invokes a secondary check routine inside sub_100296CBC:Click to expand the
sub_100296CBC functionvoid __fastcall sub_100296CBC(void *a1, void *a2)
{
// variables
v3 = v2;
v6 = *(_QWORD *)log.unsafeMutableAddressor();
v33[0] = v6;
v7 = swift_allocObject(&unk_103188688, 24, 7);
*(_QWORD *)(v7 + 16) = a2;
v8 = type metadata accessor for Logger(0);
swift_retain(v6);
v9 = objc_retain(a2);
sub_1008F57B0(
sub_10029728C,
v7,
0,
0xD00000000000001ELL,
0x8000000102A4F3D0LL,
0xD00000000000001FLL,
0x8000000102A4F430LL,
22,
v8,
&off_103187B00);
swift_release(v6);
swift_release(v7);
sub_10027A23C(0, &qword_1034A6FC8, &classRef_NSProcessInfo);
if ( (static NSProcessInfo.isTest.getter() & 1) == 0 )
{
type metadata accessor for SecurityKit(0);
sub_100828D24();
if ( (sub_100828BFC() & 1) != 0 || (sub_100828D20() & 1) != 0 || sub_100828CD4() )
{
_assertionFailure(_:_:file:line:flags:)(
"Fatal error",
11,
2,
0xD000000000000011LL,
0x8000000102A4F470LL,
"Volkswagen/SecurityCheck.swift",
30,
2,
14,
0);
__break(1u);
}
else
{
v10 = objc_opt_self(&OBJC_CLASS___UIWindowScene);
v11 = swift_dynamicCastObjCClass(a1, v10);
if ( v11 )
{
v12 = v11;
v13 = objc_allocWithZone((Class)&OBJC_CLASS___UIWindow);
v14 = objc_retain(a1);
v15 = objc_msgSend(v13, "initWithWindowScene:", v12);
v16 = (void *)v3[1];
v3[1] = v15;
v17 = objc_retain(objc_retain(v15));
objc_release(v16);
objc_msgSend(v17, "makeKeyAndVisible");
v18 = v3[3];
v19 = *(void (__fastcall **)(_QWORD *__return_ptr, __int64))(*(_QWORD *)v18 + 112LL);
v20 = swift_retain(v18);
v19(v33, v20);
swift_release(v18);
if ( v34 )
{
sub_100009280(v33);
sub_100258C10(v17);
_s15CarKeyKitUIBase20LoadingViewResourcesVwxx_0(v33);
}
else
{
sub_1002479C0(v33, &unk_1034AA1C8, &unk_1027B2DA8);
}
v21 = v3[2];
v22 = *(void (__fastcall **)(_QWORD *__return_ptr, __int64))(*(_QWORD *)v21 + 112LL);
v23 = swift_retain(v21);
v22(v33, v23);
swift_release(v21);
if ( v34 )
{
sub_100009280(v33);
sub_100329AD0();
_s15CarKeyKitUIBase20LoadingViewResourcesVwxx_0(v33);
}
else
{
sub_1002479C0(v33, &unk_1034AA1D0, &unk_1027B2DB0);
}
v24 = objc_retainAutoreleasedReturnValue(objc_msgSend(v9, "URLContexts"));
v25 = sub_10027A23C(0, &unk_1034AA198, &classRef_UIOpenURLContext);
v26 = sub_100296C74(&unk_1034AA1A0, &unk_1034AA198, &classRef_UIOpenURLContext);
v27 = static Set._unconditionallyBridgeFromObjectiveC(_:)(v24, v25, v26);
objc_release(v24);
sub_100296094(v27);
swift_bridgeObjectRelease(v27);
v28 = objc_retainAutoreleasedReturnValue(objc_msgSend(v9, "userActivities"));
v29 = sub_10027A23C(0, &unk_1034AA180, &classRef_NSUserActivity);
v30 = sub_100296C74(&unk_1034AA188, &unk_1034AA180, &classRef_NSUserActivity);
v31 = static Set._unconditionallyBridgeFromObjectiveC(_:)(v28, v29, v30);
objc_release(v28);
sub_100296554(v31);
swift_bridgeObjectRelease(v31);
v32 = v3[5];
swift_retain(v32);
sub_1003DD568(v15);
objc_release(v14);
swift_release(v32);
objc_release(v17);
objc_release(v17);
}
}
}
}
This routine performs three independent checks:
1: Environment variables
__int64 sub_100828BFC()
{
id v0; // x19
id v1; // x21
__int64 v2; // x20
char v3; // w1
char v4; // w19
v0 = objc_retainAutoreleasedReturnValue(objc_msgSend((id)swift_getInitializedObjCClass(&OBJC_CLASS___NSProcessInfo), "processInfo"));
v1 = objc_retainAutoreleasedReturnValue(objc_msgSend(v0, "environment"));
objc_release(v0);
v2 = static Dictionary._unconditionallyBridgeFromObjectiveC(_:)(
v1,
&type metadata for String,
&type metadata for String,
&protocol witness table for String);
objc_release(v1);
if ( *(_QWORD *)(v2 + 16) )
{
swift_bridgeObjectRetain(v2);
sub_1003E0028(0xD000000000000015LL, 0x8000000102A86D20LL);
v4 = v3;
swift_bridgeObjectRelease(v2);
}
else
{
v4 = 0;
}
swift_bridgeObjectRelease(v2);
return v4 & 1;
}
__int64 __fastcall sub_1003E0028(__int64 a1, __int64 a2)
{
__int64 v2; // x20
Swift::Int v5; // x0
_QWORD v7[9]; // [xsp+8h] [xbp-68h] BYREF
Hasher.init(_seed:)(v7, *(_QWORD *)(v2 + 40));
String.hash(into:)(v7, a1, a2);
v5 = Hasher._finalize()();
return sub_100298C14(a1, a2, v5);
}
Checks the environment state for dynamic library injection flags.
It might be linked with the previous detectSuspiciousEnvironment() function from the SecurityKit source.
2: Debugger check via sysctl
__int64 sub_100828D20()
{
return sub_10081D5A4();
}
__int64 sub_10081D5A4()
{
// variables
v9 = -1;
bzero(v7, 0x288u);
v0 = sub_100005E84(&unk_1034FE428, &unk_1028055D0);
v1 = swift_allocObject(v0, 48, 7);
*(_OWORD *)(v1 + 16) = xmmword_1027B7670;
*(_QWORD *)(v1 + 32) = 0xE00000001LL;
*(_DWORD *)(v1 + 40) = 1;
*(_DWORD *)(v1 + 44) = getpid();
v6 = 648;
v2 = sysctl((int *)(v1 + 32), 4u, v7, &v6, 0, 0);
swift_bridgeObjectRelease(v1);
if ( v2 )
{
v3 = sub_100005E84(&unk_1034FE420, &unk_1028055C8);
v4 = swift_allocObject(v3, 64, 7);
*(_OWORD *)(v4 + 16) = xmmword_1027AF220;
*(_QWORD *)(v4 + 56) = &type metadata for String;
*(_QWORD *)(v4 + 32) = 0xD000000000000058LL;
*(_QWORD *)(v4 + 40) = 0x8000000102A86920LL;
print(_:separator:terminator:)();
swift_bridgeObjectRelease(v4);
}
return (v8 >> 11) & 1;
}
Queries process information structures via sysctl to verify if the process has P_TRACED set, identifying active debugger attachments.
static func isDebugged() -> 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("SecurityKit: Error occured when calling sysctl(). The debugger check may not be reliable")
}
return (kinfo.kp_proc.p_flag & P_TRACED) != 0
}
3: Sub-check dispatch
Click to expand the sub_100828CD4 function
bool sub_100828CD4()
{
char v0; // w19
__int64 v1; // x1
v0 = sub_100826E3C();
swift_bridgeObjectRelease(v1);
return (v0 & 1) == 0;
}
__int64 sub_100826E3C()
{
// variables
v26 = -1;
v0 = &_swiftEmptyArrayStorage;
isUniquelyReferenced_nonNull_native = swift_retain(&_swiftEmptyArrayStorage);
v2 = 0;
v3 = 0xE000000000000000LL;
v4 = 1;
do
{
v5 = byte_1034FDE90[v2 + 40];
if ( v5 <= 6 )
{
if ( v5 == 1 )
{
v6 = sub_100826640(isUniquelyReferenced_nonNull_native);
}
else
{
if ( v5 != 6 )
goto LABEL_3;
v6 = sub_100826998(isUniquelyReferenced_nonNull_native);
}
goto LABEL_16;
}
if ( v5 == 7 )
{
v6 = sub_100826BB8(isUniquelyReferenced_nonNull_native);
LABEL_16:
v17 = v6;
v16 = v7;
v18 = v8;
isUniquelyReferenced_nonNull_native = swift_bridgeObjectRelease(v3);
v3 = v18;
if ( (v17 & 1) != 0 )
{
v4 &= v17;
goto LABEL_3;
}
goto LABEL_17;
}
if ( v5 != 8 )
goto LABEL_3;
bzero(v24, 0x288u);
v9 = sub_100005E84(&unk_1034FE428, &unk_1028055D0);
v10 = swift_allocObject(v9, 48, 7);
*(_OWORD *)(v10 + 16) = xmmword_1027B7670;
*(_QWORD *)(v10 + 32) = 0xE00000001LL;
*(_DWORD *)(v10 + 40) = 1;
*(_DWORD *)(v10 + 44) = getpid();
v23 = 648;
v11 = sysctl((int *)(v10 + 32), 4u, v24, &v23, 0, 0);
swift_bridgeObjectRelease(v10);
if ( v11 )
{
v12 = sub_100005E84(&unk_1034FE420, &unk_1028055C8);
v13 = swift_allocObject(v12, 64, 7);
*(_OWORD *)(v13 + 16) = xmmword_1027AF220;
*(_QWORD *)(v13 + 56) = &type metadata for String;
*(_QWORD *)(v13 + 32) = 0xD000000000000051LL;
*(_QWORD *)(v13 + 40) = 0x8000000102A87030LL;
print(_:separator:terminator:)();
swift_bridgeObjectRelease(v13);
}
v14 = v25;
v15 = (v25 & 0x40) == 0;
isUniquelyReferenced_nonNull_native = swift_bridgeObjectRelease(v3);
v3 = 0x8000000102A87090LL;
v16 = 0xD000000000000016LL;
if ( (v14 & 0x40) == 0 )
{
v4 &= v15;
v3 = 0xE000000000000000LL;
goto LABEL_3;
}
LABEL_17:
swift_bridgeObjectRetain(v3);
isUniquelyReferenced_nonNull_native = swift_isUniquelyReferenced_nonNull_native(v0);
if ( (isUniquelyReferenced_nonNull_native & 1) == 0 )
{
isUniquelyReferenced_nonNull_native = sub_10081B6B8(0, v0[2] + 1LL, 1, v0);
v0 = (_QWORD *)isUniquelyReferenced_nonNull_native;
}
v20 = v0[2];
v19 = v0[3];
if ( v20 >= v19 >> 1 )
{
isUniquelyReferenced_nonNull_native = sub_10081B6B8(v19 > 1, v20 + 1, 1, v0);
v0 = (_QWORD *)isUniquelyReferenced_nonNull_native;
}
v4 = 0;
v0[2] = v20 + 1;
v21 = (char *)&v0[3 * v20];
v21[32] = v5;
*((_QWORD *)v21 + 5) = v16;
*((_QWORD *)v21 + 6) = v3;
LABEL_3:
++v2;
}
while ( v2 != 4 );
swift_bridgeObjectRelease(v3);
return v4 & 1;
}
sub_100828CD4 is a wrapper around sub_100826E3C that inverts the boolean. sub_100826E3C iterates exactly 4 times over a type dispatch table at byte_1034FDE90. It runs v4 = 1 as an “all clean” flag; any single sub-check that fires sets v4 = 0.
__data:00000001034FDE90 byte_1034FDE90 DCB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
__data:00000001034FDEA1 DCB 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 8, 0
__data:00000001034FDEB2 DCB 0, 0, 0, 0, 0, 0, 1, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0
__data:00000001034FDEC3 DCB 0, 0, 0, 0, 0
The dispatch table is: [4, 8, 1, 6, 7, 8]
Type 1: Filesystem Path Check
Click to expand the
sub_100826640 function__int64 sub_100826640()
{
// variables
v0 = (void **)&ptr_aUsrLibAbsubloa;
v29 = -1;
v1 = 9;
while ( 1 )
{
v3 = (__int64)*(v0 - 1);
v2 = *v0;
InitializedObjCClass = (void *)swift_getInitializedObjCClass(&OBJC_CLASS___NSFileManager);
swift_bridgeObjectRetain(v2);
v5 = objc_retainAutoreleasedReturnValue(objc_msgSend(InitializedObjCClass, "defaultManager"));
v6 = String._bridgeToObjectiveC()();
v7 = (unsigned __int8)objc_msgSend(v5, "fileExistsAtPath:", v6);
objc_release(v5);
objc_release(v6);
if ( (v7 & 1) != 0 )
break;
swift_bridgeObjectRelease(v2);
v0 += 2;
if ( !--v1 )
{
swift_arrayDestroy(&unk_1034FDCF0, 9, &type metadata for String);
v8 = objc_retainAutoreleasedReturnValue(objc_msgSend((id)swift_getInitializedObjCClass(&OBJC_CLASS___NSFileManager), "defaultManager"));
v9 = String._bridgeToObjectiveC()();
v27 = 0;
v10 = objc_retainAutoreleasedReturnValue(objc_msgSend(v8, "contentsOfDirectoryAtPath:error:", v9, &v27));
objc_release(v8);
objc_release(v9);
v11 = v27;
if ( v10 )
{
v12 = static Array._unconditionallyBridgeFromObjectiveC(_:)(v10, &type metadata for String);
v13 = objc_retain(v11);
objc_release(v10);
v14 = *(_QWORD *)(v12 + 16);
if ( v14 )
{
v15 = 0;
v16 = (_QWORD *)(v12 + 40);
while ( 1 )
{
if ( v15 >= *(_QWORD *)(v12 + 16) )
__break(1u);
v18 = *(v16 - 1);
v17 = (void *)*v16;
swift_bridgeObjectRetain(*v16);
v19._countAndFlagsBits = 0x2D6164697266LL;
v19._object = (void *)0xE600000000000000LL;
if ( String.hasPrefix(_:)(v19) )
break;
v20._countAndFlagsBits = 0x6F7463656A6E696CLL;
v20._object = (void *)0xE900000000000072LL;
if ( String.hasPrefix(_:)(v20) )
break;
++v15;
swift_bridgeObjectRelease(v17);
v16 += 2;
if ( v14 == v15 )
goto LABEL_11;
}
swift_bridgeObjectRelease(v12);
v27 = 0;
v28 = 0xE000000000000000LL;
_StringGuts.grow(_:)(36);
swift_bridgeObjectRelease(v28);
v27 = (id)0xD000000000000022LL;
v28 = 0x8000000102A870D0LL;
v26._countAndFlagsBits = v18;
v26._object = v17;
String.append(_:)(v26);
v22 = v17;
goto LABEL_16;
}
LABEL_11:
swift_bridgeObjectRelease(v12);
}
else
{
v23 = objc_retain(v27);
v24 = _convertNSErrorToError(_:)(v11);
objc_release(v23);
swift_willThrow();
swift_errorRelease(v24);
}
return 1;
}
}
swift_arrayDestroy(&unk_1034FDCF0, 9, &type metadata for String);
_StringGuts.grow(_:)(25);
swift_bridgeObjectRelease(0xE000000000000000LL);
v27 = (id)0xD000000000000017LL;
v28 = 0x8000000102A870B0LL;
v21._countAndFlagsBits = v3;
v21._object = v2;
String.append(_:)(v21);
v22 = v2;
LABEL_16:
swift_bridgeObjectRelease(v22);
return 0;
}
sub_100826640 iterates 9 hardcoded jailbreak paths via NSFileManager.fileExistsAtPath:. After the path loop, it also scans a directory listing checking for filenames that hasPrefix("frida-") or hasPrefix("linjector"), hunting for Frida’s helper processes and injection tool artifacts.
Returns 0 if any path/prefix matched, 1 if clean.
private static func detectExistenceOfSuspiciousFiles() -> DetectResult {
let paths = [
"/usr/sbin/frida-server",
"/usr/local/bin/frida-server",
"/usr/lib/frida/frida-agent.dylib",
"/usr/bin/frida-trace",
"/usr/local/bin/cycript",
"/usr/lib/libcycript.dylib",
"/usr/local/bin/r2",
"/usr/local/bin/radare2",
"/usr/local/bin/objection"
]
for path in paths where FileManager.default.fileExists(atPath: path) {
return (false, "Suspicious file found: \(path)")
}
if let tmpContents = try? FileManager.default.contentsOfDirectory(atPath: "/tmp") {
for file in tmpContents where file.hasPrefix("frida-") || file.hasPrefix("linjector") {
return (false, "Suspicious named pipe found: /tmp/\(file)")
}
}
return (true, "")
}
Type 6: Loaded Dylib Scan
Click to expand the sub_100826998 function
__int64 sub_100826998()
{
// variables
v0 = sub_100005E84(&unk_1034FE1B8, &unk_102805890);
inited = swift_initStaticObject(v0, &unk_1034FDD88);
v2 = swift_retain(inited);
v3 = sub_100825C84(v2);
swift_release(inited);
swift_arrayDestroy(inited + 32, 10, &type metadata for String);
v20 = _dyld_image_count();
if ( v20 )
{
v4 = 0;
while ( 1 )
{
result = (__int64)_dyld_get_image_name(v4);
if ( !result )
break;
++v4;
v6 = String.init(cString:)(result);
v8 = v7;
v9 = 1LL << *(_BYTE *)(v3 + 32);
if ( v9 < 64 )
v10 = ~(-1LL << v9);
else
v10 = -1;
v11 = v10 & *(_QWORD *)(v3 + 56);
v12 = (unsigned __int64)(v9 + 63) >> 6;
result = swift_bridgeObjectRetain(v3);
v13 = 0;
while ( v11 )
{
v14 = v13;
LABEL_15:
v15 = (__int64 *)(*(_QWORD *)(v3 + 48) + ((v14 << 10) | (16 * __clz(__rbit64(v11)))));
v17 = *v15;
v16 = v15[1];
v11 &= v11 - 1;
v22 = v6;
v23 = (unsigned __int64)v8;
v21[0] = v17;
v21[1] = v16;
v18 = sub_100819470(result);
result = StringProtocol.localizedCaseInsensitiveContains<A>(_:)(
v21,
&type metadata for String,
&type metadata for String,
v18,
v18);
if ( (result & 1) != 0 )
{
swift_bridgeObjectRelease(v3);
v22 = 0;
v23 = 0xE000000000000000LL;
_StringGuts.grow(_:)(29);
swift_bridgeObjectRelease(v23);
v22 = 0xD00000000000001BLL;
v23 = 0x8000000102A86E90LL;
v19._countAndFlagsBits = v6;
v19._object = v8;
String.append(_:)(v19);
swift_release(v3);
swift_bridgeObjectRelease(v8);
return 0;
}
}
while ( 1 )
{
v14 = v13 + 1;
if ( __OFADD__(v13, 1) )
{
__break(1u);
goto LABEL_20;
}
if ( v14 >= v12 )
break;
v11 = *(_QWORD *)(v3 + 56 + 8 * v14);
++v13;
if ( v11 )
{
v13 = v14;
goto LABEL_15;
}
}
swift_release(v3);
swift_bridgeObjectRelease(v8);
if ( v4 == v20 )
goto LABEL_17;
}
LABEL_20:
__break(1u);
}
else
{
LABEL_17:
swift_bridgeObjectRelease(v3);
return 1;
}
return result;
}
sub_100826998 calls _dyld_image_count() then loops _dyld_get_image_name(i) for every loaded image. Each image path is compared using localizedCaseInsensitiveContains against a pre-built set of known injection library name substrings.
Returns 0 if any loaded image matches a known injection lib, 1 if clean.
private static func detectDYLD() -> DetectResult {
let suspiciousLibraries: Set<String> = [
"FridaGadget",
"frida",
"cynject",
"libcycript",
"SSLKillSwitch",
"SSLKillSwitch2",
"RevealServer",
"r2frida",
"Objection",
"objection_agent"
]
for index in 0..<_dyld_image_count() {
let imageName = String(cString: _dyld_get_image_name(index))
// The fastest case insensitive contains detect.
for library in suspiciousLibraries where imageName.localizedCaseInsensitiveContains(library) {
return (false, "Suspicious library loaded: \(imageName)")
}
}
return (true, "")
}
Type 7: Localhost Port Scan
Click to expand the sub_100826BB8 function
__int64 sub_100826BB8()
{
// variables
v6 = -1;
*(_WORD *)&v5.sa_len = 512;
*(_QWORD *)&v5.sa_data[6] = 0;
*(_DWORD *)&v5.sa_data[2] = inet_addr("127.0.0.1");
*(_WORD *)v5.sa_data = bswap32(0x69A2u) >> 16;
v0 = socket(2, 1, 0);
if ( connect(v0, &v5, 0x10u) != -1 )
goto LABEL_5;
close(v0);
*(_QWORD *)&v5.sa_len = 512;
*(_QWORD *)&v5.sa_data[6] = 0;
*(_DWORD *)&v5.sa_data[2] = inet_addr("127.0.0.1");
*(_WORD *)v5.sa_data = bswap32(0x115Cu) >> 16;
v0 = socket(2, 1, 0);
if ( connect(v0, &v5, 0x10u) != -1 )
goto LABEL_5;
close(v0);
*(_QWORD *)&v5.sa_len = 512;
*(_QWORD *)&v5.sa_data[6] = 0;
*(_DWORD *)&v5.sa_data[2] = inet_addr("127.0.0.1");
*(_WORD *)v5.sa_data = bswap32(0x16u) >> 16;
v0 = socket(2, 1, 0);
if ( connect(v0, &v5, 0x10u) != -1 )
goto LABEL_5;
close(v0);
*(_QWORD *)&v5.sa_len = 512;
*(_QWORD *)&v5.sa_data[6] = 0;
*(_DWORD *)&v5.sa_data[2] = inet_addr("127.0.0.1");
*(_WORD *)v5.sa_data = bswap32(0x2Cu) >> 16;
v0 = socket(2, 1, 0);
if ( connect(v0, &v5, 0x10u) == -1 )
{
close(v0);
return 1;
}
else
{
LABEL_5:
close(v0);
*(_QWORD *)&v5.sa_len = 0x2074726F50LL;
*(_QWORD *)&v5.sa_data[6] = 0xE500000000000000LL;
v1._countAndFlagsBits = dispatch thunk of CustomStringConvertible.description.getter(
&type metadata for Int,
&protocol witness table for Int);
object = v1._object;
String.append(_:)(v1);
swift_bridgeObjectRelease(object);
v3._countAndFlagsBits = 0x6E65706F20736920LL;
v3._object = (void *)0xE800000000000000LL;
String.append(_:)(v3);
return 0;
}
}
sub_100826BB8 attempts socket() + connect() to 127.0.0.1 on four ports in sequence. The port encoding uses bswap32(x) >> 16 which is just a compiler idiom for htons(x):
| Encoded value | Decoded port | What it detects |
|---|---|---|
0x69A2 | 27042 | Frida server |
0x115C | 4444 | Needle |
0x16 | 22 | SSH |
0x2C | 44 | checkra1n |
If a connect() succeeds, it means that that daemon is running, so device is jailbroken. Returns 0 if any port is open, 1 if all are closed.
private static func canOpenLocalConnection(port: Int) -> 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(port))
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))
}
}
defer {
close(sock)
}
if result != -1 {
return true // Port is opened
}
return false
}
private static func detectOpenedPorts() -> DetectResult {
let ports = [
27042, // default Frida
4444, // default Needle
22, // OpenSSH
44 // checkra1n
]
for port in ports where canOpenLocalConnection(port: port) {
return (false, "Port \(port) is open")
}
return (true, "")
}
Type 8: Inlined sysctl check
An inlined version of the sysctl(KERN_PROC_PID) check inside the loop, checking a specific flag byte from the kinfo_proc buffer. This doubles up on the debugger check without calling sub_10081D5A4 externally.
Summary of detection layers
Taking these all together, the app implements three parallel detection layers sourced from SecurityKit:
Layer 1: FishHook anti-hooking (sub_10081D704)
| Check | What It Does |
|---|---|
| Bind info walk | Parses lazy + non-lazy binding opcodes looking for symbol "ptrace" |
| Export trie resolve | Walks the compressed export trie in __LINKEDIT to find ptrace’s real address |
| Pointer overwrite | Uses vm_protect to make the section writable, then restores any hooked pointer back to the original |
ptrace(31) | After unhooking, calls PT_DENY_ATTACH to kill the process if a debugger is still attached |
Layer 2: Three-check security gate (sub_100828BFC / sub_100828D20 / sub_100828CD4)
| Gate | Technique | What It Detects |
|---|---|---|
sub_100828BFC | DYLD_INSERT_LIBRARIES env var | Frida / Substrate / Cynject injection |
sub_100828D20 | sysctl(KERN_PROC_PID): P_TRACED flag | Debugger attached (lldb, Frida tracer) |
sub_100828CD4 | Type dispatch table: 4 sub-checks | Jailbreak artifacts (files, dylibs, ports, debugger) |
Layer 3: Case dispatch table (sub_100824E9C)
| Case | Technique | What It Detects |
|---|---|---|
| 0 | canOpenURL: | Cydia / Sileo / Zebra / Filza URL schemes |
| 1 | access() on known paths | Jailbreak binary presence on disk |
| 2 | Write to restricted paths | Writable /private/ directories |
| 3 | Write + delete in / | Writable root filesystem |
| 4 | Environment + fork() | Jailbreak env vars + fork() succeeding |
| 5 | stat() on symlink paths | Jailbreak symlink farms (e.g. /Applications/Cydia.app) |
| 6 | _dyld_get_image_name() blocklist | Loaded jailbreak dylibs (expanded list including Substitute, tweak injectors) |
| 9 | NSClassFromString(@"ShadowRuleset") | Shadow anti-detection tweak |
The bypass
The immediate crash we ran into (brk #0x1 at sub_10082030C + 1060) originates in the custom Fishhook parser parsing the lazy bind opcodes of our injected dylib (e.g., Substitute or ElleKit helper). It trips on an unexpected opcode structure before any actual security gates are reached.
Instead of trying to clean up every single point of failure manually, I built a Theos tweak using Cydia Substrate to intercept the key orchestrator functions. Since these checks are consolidated into specific functions returning boolean flags, we can hook them and force them to return clean results.
Here is the tweak code (Tweak.x):
#import <substrate.h>
#import <mach-o/dyld.h>
#import <string.h>
#import <Foundation/Foundation.h>
#define TARGET_MODULE "Volkswagen"
#define IDA_BASE 0x100000000
// sysctl P_TRACED check : always return 0 (no debugger)
#define ADDR_SYSCTL_DEBUG_CHECK 0x10081D5A4
// ptrace(PT_DENY_ATTACH) + FishHook installer : NOP the whole thing
#define ADDR_PTRACE_FISHHOOK 0x10081D704
// JailbreakDetection orchestrator (8 checks) : always return 1 (clean)
#define ADDR_JB_ORCHESTRATOR 0x100824E9C
// Individual JB sub-checks : each returns 1 (no jailbreak found)
#define ADDR_JB_FILE_EXIST 0x100823E38 // type 1: stat/access
#define ADDR_JB_FILE_READABLE 0x100824238 // type 2: fopen/isReadable
#define ADDR_JB_SANDBOX_ESCAPE 0x1008245FC // type 3: write test + fork
#define ADDR_JB_SYMLINK 0x100824A14 // type 5: symlink resolve
#define ADDR_JB_DYLIB_NAMES 0x100824C7C // type 6: _dyld_get_image_name scan
#define ADDR_SECURITY_CHECK_BFC 0x100828BFC // sub_100828BFC(v10) & 1
#define ADDR_SECURITY_CHECK_D20 0x100828D20 // sub_100828D20() & 1
#define ADDR_SECURITY_CHECK_CD4 0x100828CD4 // sub_100828CD4() : bool
static void *(*orig_sysctl_debug)(void);
static void *hooked_sysctl_debug(void) { NSLog(@"[VWTweak] hooked_sysctl_debug called"); return 0; }
static void (*orig_ptrace_fishhook)(void);
static void hooked_ptrace_fishhook(void) { NSLog(@"[VWTweak] hooked_ptrace_fishhook called"); return; }
static uint64_t (*orig_jb_orchestrator)(void);
static uint64_t hooked_jb_orchestrator(void) { NSLog(@"[VWTweak] hooked_jb_orchestrator called"); return 1; }
static uint64_t (*orig_jb_file_exist)(void);
static uint64_t hooked_jb_file_exist(void) { NSLog(@"[VWTweak] hooked_jb_file_exist called"); return 1; }
static uint64_t (*orig_jb_file_readable)(void);
static uint64_t hooked_jb_file_readable(void) { NSLog(@"[VWTweak] hooked_jb_file_readable called"); return 1; }
static uint64_t (*orig_jb_sandbox_escape)(void);
static uint64_t hooked_jb_sandbox_escape(void) { NSLog(@"[VWTweak] hooked_jb_sandbox_escape called"); return 1; }
static uint64_t (*orig_jb_symlink)(void);
static uint64_t hooked_jb_symlink(void) { NSLog(@"[VWTweak] hooked_jb_symlink called"); return 1; }
static uint64_t (*orig_jb_dylib_names)(void);
static uint64_t hooked_jb_dylib_names(void) { NSLog(@"[VWTweak] hooked_jb_dylib_names called"); return 1; }
static uint64_t (*orig_security_check_bfc)(uint64_t);
static uint64_t hooked_security_check_bfc(uint64_t arg) { NSLog(@"[VWTweak] hooked_security_check_bfc(%llu) called", arg); return 0; }
static uint64_t (*orig_security_check_d20)(void);
static uint64_t hooked_security_check_d20(void) { NSLog(@"[VWTweak] hooked_security_check_d20 called"); return 0; }
static uint64_t (*orig_security_check_cd4)(void);
static uint64_t hooked_security_check_cd4(void) { NSLog(@"[VWTweak] hooked_security_check_cd4 called"); return 0; }
static void hookAt(uintptr_t base, uintptr_t ida_addr, void *hook, void **orig) {
uintptr_t real = base + (ida_addr - IDA_BASE);
MSHookFunction((void *)real, hook, orig);
NSLog(@"[VWTweak] Hooked 0x%lx (slide base 0x%lx)", real, base);
}
%ctor {
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
const char *name = _dyld_get_image_name(i);
if (!name || !strstr(name, TARGET_MODULE))
continue;
uintptr_t base = (uintptr_t)_dyld_get_image_header(i);
NSLog(@"[VWTweak] Found %s at base 0x%lx", name, base);
hookAt(base, ADDR_SYSCTL_DEBUG_CHECK, (void *)hooked_sysctl_debug, (void **)&orig_sysctl_debug);
hookAt(base, ADDR_PTRACE_FISHHOOK, (void *)hooked_ptrace_fishhook, (void **)&orig_ptrace_fishhook);
hookAt(base, ADDR_JB_ORCHESTRATOR, (void *)hooked_jb_orchestrator, (void **)&orig_jb_orchestrator);
// Optional: hook individual JB checks if you want finer control
// hookAt(base, ADDR_JB_FILE_EXIST, (void *)hooked_jb_file_exist, (void **)&orig_jb_file_exist);
// hookAt(base, ADDR_JB_FILE_READABLE, (void *)hooked_jb_file_readable, (void **)&orig_jb_file_readable);
// hookAt(base, ADDR_JB_SANDBOX_ESCAPE, (void *)hooked_jb_sandbox_escape,(void **)&orig_jb_sandbox_escape);
// hookAt(base, ADDR_JB_SYMLINK, (void *)hooked_jb_symlink, (void **)&orig_jb_symlink);
// hookAt(base, ADDR_JB_DYLIB_NAMES, (void *)hooked_jb_dylib_names, (void **)&orig_jb_dylib_names);
hookAt(base, ADDR_SECURITY_CHECK_BFC, (void *)hooked_security_check_bfc,(void **)&orig_security_check_bfc);
hookAt(base, ADDR_SECURITY_CHECK_D20, (void *)hooked_security_check_d20,(void **)&orig_security_check_d20);
hookAt(base, ADDR_SECURITY_CHECK_CD4, (void *)hooked_security_check_cd4,(void **)&orig_security_check_cd4);
return;
}
NSLog(@"[VWTweak] Target module '%s' not found in dyld image list", TARGET_MODULE);
}
At runtime, the syslog shows our hooks taking effect sequentially:
[VWTweak] Found Volkswagen at base 0x104890000
[VWTweak] Hooked 0x1050ad5a4 (slide base 0x104890000) // sysctl debug check
[VWTweak] Hooked 0x1050ad704 (slide base 0x104890000) // ptrace + FishHook
[VWTweak] Hooked 0x1050b4e9c (slide base 0x104890000) // JB orchestrator
[VWTweak] Hooked 0x1050b8bfc (slide base 0x104890000) // security check BFC
[VWTweak] Hooked 0x1050b8d20 (slide base 0x104890000) // security check D20
[VWTweak] Hooked 0x1050b8cd4 (slide base 0x104890000) // security check CD4
When execution runs, the hooks fire in order:
[VWTweak] hooked_ptrace_fishhook called
[VWTweak] hooked_security_check_bfc(50735101128) called
[VWTweak] hooked_security_check_d20 called
[VWTweak] hooked_security_check_cd4 called
[VWTweak] hooked_jb_orchestrator called
The application now bypasses the validation loops and launches successfully. If we disable only the hooked_jb_orchestrator hook, we can see the standard warning UI:

Keeping all hooks active, everything functions as expected:
Conclusion
Volkswagen’s choice of jailbreak detection relies on SecurityKit. Although it implements a robust matrix of filesystem probes, anti-hooking monitors, and process verification steps, the underlying checks ultimately funnel down to several key Swift functions.
No matter how complex the checks inside the app are, because this is client-side code running on a device we control, we can inspect and intercept its final decisions. High-level security libraries often suffer from this architectural limitation, once an analyst maps the core logic paths, they can render the entire defense engine inert with just a few target function hooks.
That’s it :p