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.

The app crashing immediately upon 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:

brk_graph

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:

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-O load commands: For every loaded image, it finds LC_SYMTAB, LC_DYSYMTAB, and the __LINKEDIT segment
  • 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 ptrace directly from libsystem_kernel.dylib
  • Force-restores the pointer: It uses vm_protect to 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:

TagFunctionTechnique
0inlineCydia/package-manager URL scheme probe
1sub_100823E38Known jailbreak binary paths (read access)
2sub_100824238Restricted-path write probe
3sub_1008245FCWrite file to / (root write test)
4inlineEnv var check + fork() test
5sub_100824A14System-path symlink-farm check
6sub_100824C7CLoaded-dylib blocklist scan
9inlineShadowRuleset 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.

Source:

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.

Source:

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, "")
}
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).

Source:

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.

Source:

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.

Source:

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 function
void __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.

Source:

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.

Source:

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.

Source:

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 valueDecoded portWhat it detects
0x69A227042Frida server
0x115C4444Needle
0x1622SSH
0x2C44checkra1n

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.

Source:

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)

CheckWhat It Does
Bind info walkParses lazy + non-lazy binding opcodes looking for symbol "ptrace"
Export trie resolveWalks the compressed export trie in __LINKEDIT to find ptrace’s real address
Pointer overwriteUses 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)

GateTechniqueWhat It Detects
sub_100828BFCDYLD_INSERT_LIBRARIES env varFrida / Substrate / Cynject injection
sub_100828D20sysctl(KERN_PROC_PID): P_TRACED flagDebugger attached (lldb, Frida tracer)
sub_100828CD4Type dispatch table: 4 sub-checksJailbreak artifacts (files, dylibs, ports, debugger)

Layer 3: Case dispatch table (sub_100824E9C)

CaseTechniqueWhat It Detects
0canOpenURL:Cydia / Sileo / Zebra / Filza URL schemes
1access() on known pathsJailbreak binary presence on disk
2Write to restricted pathsWritable /private/ directories
3Write + delete in /Writable root filesystem
4Environment + fork()Jailbreak env vars + fork() succeeding
5stat() on symlink pathsJailbreak symlink farms (e.g. /Applications/Cydia.app)
6_dyld_get_image_name() blocklistLoaded jailbreak dylibs (expanded list including Substitute, tweak injectors)
9NSClassFromString(@"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: warning

Keeping all hooks active, everything functions as expected:

The app running successfully with the tweak enabled

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