After taking a look at the jailbreak detection implemented in the Banque Postale iOS app, I decided to look at other apps that implement similar protections. One such app is Crédit Agricole, which also implements jailbreak detection mechanisms.

Running the app on a jailbroken device, I got the following message: Crédit Agricole jailbreak detection message

Your smartphone looks “jailbroken”. For security reasons, access to this application is thus impossible.

Static analysis

Once again I dumped the .ipa, unzipped it, and searched for the “jailbreak” keyword in the app’s binary and resources using ripgrep.

% rg --binary "jailbreak"
tazSDK: binary file matches (found "\0" byte around offset 5)

Payload/nmb_prod.app/mabanqueexternal_mabanqueexternal.bundle/fr.lproj/LocalizableChalus.strings
1668:"acces_app_bloque_root_ios_titre" = "Votre smartphone semble «jailbreaké»";

Payload/nmb_prod.app/Frameworks/tazSDK.framework/tazSDK: binary file matches (found "\0" byte around offset 5)

Payload/nmb_prod.app/nmb_prod: binary file matches (found "\0" byte around offset 5)

The string acces_app_bloque_root_ios_titre is the one displayed in the popup, but searching for cross-references in the main binary (nmb_prod) didn’t lead directly to the usage code. This suggests the check logic is likely obfuscated or inside a specific security framework. However, by analyzing the binary functions, I found the detection logic.

The app is written in Swift, which is visible in the decompiled code (references to String.init, swift_bridgeObjectRetain, etc.).

acces_app_bloque_root_ios_titre -> doesn’t lead to any xref in the nmb_prod binary, so it’s likely not used in the app’s code but only in the resources.

1. Path list generation

This function initializes an array of strings containing paths to common jailbreak applications.

__int64 sub_100560120()
{
  // variables

  v3 = _allocateUninitializedArray<A>(_:)(9);
  v2 = v0;

  *v0 = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("/Applications/Cydia.app", 0x17u, 1);
  v2[1] = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("/Applications/blackra1n.app", 0x1Bu, 1);
  v2[2] = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("/Applications/FakeCarrier.app", 0x1Du, 1);
  v2[3] = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("/Applications/Icy.app", 0x15u, 1);
  v2[4] = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("/Applications/IntelliScreen.app", 0x1Fu, 1);
  v2[5] = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("/Applications/MxTube.app", 0x18u, 1);
  v2[6] = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("/Applications/RockApp.app", 0x19u, 1);
  v2[7] = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("/Applications/SBSettings.app", 0x1Cu, 1);
  v2[8] = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("/Applications/WinterBoard.app", 0x1Du, 1);

  return sub_10001F45C(v3, &type metadata for String);
}

2. File existence checker

This function iterates over an array of paths and uses NSFileManager to check if they exist.

__int64 sub_10055FC40()
{
  // ... initialization ...
  v11 = sub_100560120(); // Get the list of app paths
  v8 = sub_100005E3C(&unk_1026B12E8);
  v0 = sub_1000DF36C();
  Collection<>.makeIterator()(v8, v0);
  
  while ( 1 )
  {
    // Iterate through paths
    v1 = sub_100005E3C(&unk_1026C3550);
    IndexingIterator.next()(&v9, v1);
    v7 = v10;
    if ( !v10 ) break;
    
    v5 = objc_retainAutoreleasedReturnValue(objc_msgSend((id)objc_opt_self(&OBJC_CLASS___NSFileManager), "defaultManager"));
    swift_bridgeObjectRetain(v7);
    v4 = String._bridgeToObjectiveC()();
    swift_bridgeObjectRelease(v7);
    
    // Check if file exists at path
    v6 = (unsigned __int8)objc_msgSend(v5, "fileExistsAtPath:", v4);
    _objc_release(v4);
    _objc_release(v5);
    
    if ( (v6 & 1) != 0 )
    {
      swift_bridgeObjectRelease(v7);
      sub_10001553C(v12);
      v3 = 1; // Found a file -> Return True
      return v3 & 1;
    }
    swift_bridgeObjectRelease(v7);
  }
  sub_10001553C(v12);
  v3 = 0; // No files found -> Return False
  return v3 & 1;
}

3. The main detection orchestrator

This is the core function called by the security gate. It chains multiple checks together.

__int64 sub_10055F914()
{
  // ... setup UIDevice ...
  v3 = sub_100033CF0(); // Check if simulator
  
  _objc_release(v2);
  if ( (v3 & 1) != 0 )
  {
    v1 = 0;  // Simulator -> Not jailbroken
  }
  else if ( (sub_10055FA74() & 1) != 0 )  // 1. Cydia URL check
  {
    v1 = 1;  // Can open cydia:// -> Jailbroken
  }
  else if ( (sub_10055FC40() & 1) != 0 )  // 2. Jailbreak app paths check
  {
    v1 = 1;  // Found jailbreak apps -> Jailbroken
  }
  else if ( (sub_10055FDE8() & 1) != 0 )  // 3. Jailbreak system files check
  {
    v1 = 1;  // Found jailbreak files -> Jailbroken
  }
  else
  {
    v1 = sub_10055FF90();  // 4. Write test (seems to return 1 always in this logic)
  }
  return v1 & 1;
}

The other check, sub_10055FDE8, follows the same pattern as sub_10055FC40 but checks for different paths (system files).

// Inside sub_10055FDE8 initialization
v3 = _allocateUninitializedArray<A>(_:)(16);
v2 = v0;
*v0 = String.init(..., "/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist", ...);
v2[1] = String.init(..., "/Library/MobileSubstrate/DynamicLibraries/Veency.plist", ...);
v2[2] = String.init(..., "/private/var/lib/apt", ...);
v2[3] = String.init(..., "/private/var/lib/apt/", ...);
v2[4] = String.init(..., "/private/var/lib/cydia", ...);
v2[5] = String.init(..., "/private/var/mobile/Library/SBSettings/Themes", ...);
v2[6] = String.init(..., "/private/var/stash", ...);
v2[7] = String.init(..., "/private/var/tmp/cydia.log", ...);
// ... more checks for sshd, bash, etc ...

Dynamic analysis with LLDB

To understand exactly which check was failing on my Dopamine-jailbroken device, I decided to debug the app using LLDB.

First, I set up the connection to the device over USB:

% iproxy 6666 6666
Creating listening port 6666 for device port 6666
waiting for connection

On the device:

6s:~ root# debugserver 127.0.0.1:6666
debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-16.0.0
 for arm64.
Listening to port 6666 for a connection from 127.0.0.1....

On my Mac, I connected LLDB and attached to the process. First, I needed to find the base address of the binary to handle ASLR (Address Space Layout Randomization).

% lldb
(lldbinit) platform select remote-ios
(lldbinit) process connect connect://localhost:6666
(lldbinit) process attach --name nmb_prod --waitfor
(lldbinit) image list
[  0] 4B810E22-EDA9-3EC9-B503-68D1AD4AD1C7 0x0000000104fc4000 /private/var/containers/Bundle/Application/7A41E69C-8FB3-4654-AB8B-10BE87277B05/nmb_prod.app/nmb_prod (0x0000000104fc4000)
[  1] 444F5041-322E-342E-3700-6AE7FB25EB29 0x0000000107e14000 /private/preboot/3E41D289F448C01ACD54D44E35E91EBB8A13C3AD/dopamine-89YQUs/procursus/basebin/gen/dyld (0x0000000107e14000)
(lldbinit) image dump sections
Available completions:
        /private/var/containers/Bundle/Application/7A41E69C-8FB3-4654-AB8B-10BE87277B05/nmb_prod.app/nmb_prod
        nmb_prod
(lldbinit) image dump sections nmb_prod
Sections for '/private/var/containers/Bundle/Application/7A41E69C-8FB3-4654-AB8B-10BE87277B05/nmb_prod.app/nmb_prod(0x0000000104fc4000)' (arm64):
  SectID             Type                   Load Address                             Perm File Off.  File Size  Flags      Section Name
  ------------------ ---------------------- ---------------------------------------  ---- ---------- ---------- ---------- ----------------------------
  0x0000000000000100 container              [0x0000000000000000-0x0000000100000000)* ---  0x00000000 0x00000000 0x00000000 nmb_prod.__PAGEZERO
  0x0000000000000200 container              [0x0000000104fc4000-0x00000001073e8000)  r-x  0x00000000 0x02424000 0x00000000 nmb_prod.__TEXT
  0x0000000000000001 code                   [0x0000000104fc8000-0x0000000106e470c0)  r-x  0x00004000 0x01e7f0c0 0x80000400 nmb_prod.__TEXT.__text
  0x0000000000000002 code                   [0x0000000106e470c0-0x0000000106e4e320)  r-x  0x01e830c0 0x00007260 0x80000408 nmb_prod.__TEXT.__stubs
  ..............
  0x0000000000000034 zero-fill              [0x0000000107819a50-0x000000010781c400)  rw-  0x00000000 0x00000000 0x00000001 nmb_prod.__DATA.__common
  0x0000000000000500 container              [0x0000000107820000-0x0000000107b44000)  r--  0x02760000 0x003230e0 0x00000000 nmb_prod.__LINKEDIT

The base address is 0x104fc4000. The IDA base is usually 0x100000000.

(lldbinit) p/x 0x0000000104fc4000-0x100000000
(long) 0x0000000004fc4000

The slide is (here, it will change every time the app is launched) 0x4fc4000.

I set breakpoints at the return addresses of the specific check functions identified in IDA:

(lldb) breakpoint set -a 0x000000010055f914+0x04fc4000
# ... setting breakpoints for sub_10055FA74, sub_10055FC40, sub_10055FDE8, sub_10055FF90 ...
(lldb) breakpoint list
Current breakpoints:
1: address = nmb_prod`___lldb_unnamed_symbol_10055f914, locations = 1, resolved = 1, hit count = 0
2: address = nmb_prod`___lldb_unnamed_symbol_10055fa74 + 456, locations = 1, resolved = 1, hit count = 0
3: address = nmb_prod`___lldb_unnamed_symbol_10055fc40 + 420, locations = 1, resolved = 1, hit count = 0
4: address = nmb_prod`___lldb_unnamed_symbol_10055fde8 + 420, locations = 1, resolved = 1, hit count = 0
5: address = nmb_prod`___lldb_unnamed_symbol_10055ff90 + 320, locations = 1, resolved = 1, hit count = 0
6: address = nmb_prod`___lldb_unnamed_symbol_10055f914 + 220, locations = 1, resolved = 1, hit count = 0

I resumed the process and observed the results.

Breakpoint 1: Entry to sub_10055F914

(lldbinit) c
Process 7222 resuming
[ TiD: 1  ]-------------------------------------------------------------------------------------------[regs]
   X0:  0x000000028039A7A0   X8:  0x000041A1F521BA29  X16:  0x0000000197DD98AC  X24:  0x0000000000000000
   X1:  0x000000019A9A0A49   X9:  0x0000200000000000  X17:  0x00000001829C903C  X25:  0x00000001FA5D5AE0
   X2:  0x0000000000000000  X10:  0x0000000000000000  X18:  0x0000000000000000  X26:  0x0000000000000000
   X3:  0x0000000283F83CC0  X11:  0x000F000283F85900  X19:  0x00000001FA5D5AE0  X27:  0x00000002816CB200
   X4:  0x0000000283F83C80  X12:  0x000000000000000D  X20:  0x000000028039A7A0  X28:  0x0000000000000000
   X5:  0x000000210000000C  X13:  0x0000000283F859C0  X21:  0x000000014BF5B040   FP:  0x000000016FDDA080
   X6:  0x0000000000000001  X14:  0x0000000060400000  X22:  0x0000000000000000   LR:  0x0000000100496A64
   X7:  0x000000019B3D8249  X15:  0x00000001F521BA00  X23:  0x00000002816C5BF0   SP:  0x000000016FDD9F20
   PC:  0x0000000100583914  n z c v a i f
------------------------------------------------------------------------------------------------------[code]
@ /private/var/containers/Bundle/Application/7A41E69C-8FB3-4654-AB8B-10BE87277B05/nmb_prod.app/nmb_prod:
->  0x100583914 (0x10055f914): ff 03 01 d1  sub    sp, sp, #0x40
    0x100583918 (0x10055f918): f4 4f 02 a9  stp    x20, x19, [sp, #0x20]
    0x10058391c (0x10055f91c): fd 7b 03 a9  stp    x29, x30, [sp, #0x30]
    0x100583920 (0x10055f920): fd c3 00 91  add    x29, sp, #0x30
    0x100583924 (0x10055f924): ff 0f 00 f9  str    xzr, [sp, #0x18]
    0x100583928 (0x10055f928): f4 0f 00 f9  str    x20, [sp, #0x18]
    0x10058392c (0x10055f92c): 08 07 01 90  adrp   x8, 8416
    0x100583930 (0x10055f930): 00 b5 46 f9  ldr    x0, [x8, #0xd68]
------------------------------------------------------------------------------------------------------------
Process 7222 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100583914 nmb_prod`___lldb_unnamed_symbol_10055f914

Breakpoint 2: Return from sub_10055FA74 (Cydia URL)

(lldbinit) c
Process 7222 resuming
[ TiD: 1  ]-------------------------------------------------------------------------------------------[regs]
   X0:  0x0000000000000000   X8:  0x0000000000000012  X16:  0x0000000197DD88A4  X24:  0x0000000000000000
   ...
------------------------------------------------------------------------------------------------------[code]
@ /private/var/containers/Bundle/Application/7A41E69C-8FB3-4654-AB8B-10BE87277B05/nmb_prod.app/nmb_prod:
->  0x100583c3c (0x10055fc3c): c0 03 5f d6  ret    ; ___lldb_unnamed_symbol_10055f914 @ 0x10058398c @ /private/var/containers/Bundle/Application/7A41E69C-8FB3-4654-AB8B-10BE87277B05/nmb_prod.app/nmb_prod
    0x100583c40 (0x10055fc40): ff 03 03 d1  sub    sp, sp, #0xc0
    0x100583c44 (0x10055fc44): f4 4f 0a a9  stp    x20, x19, [sp, #0xa0]
    0x100583c48 (0x10055fc48): fd 7b 0b a9  stp    x29, x30, [sp, #0xb0]
------------------------------------------------------------------------------------------------------------
Process 7222 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x0000000100583c3c nmb_prod`___lldb_unnamed_symbol_10055fa74 + 456
(lldb) register read x0
      x0 = 0x0000000000000000

Result: 0 (Not detected).

Breakpoint 3: Return from sub_10055FC40 (App Paths)

(lldb) c
Process 7222 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x0000000100583de4 nmb_prod`___lldb_unnamed_symbol_10055fc40 + 420
->  0x100583de4: c0 03 5f d6  ret
(lldb) register read x0
      x0 = 0x0000000000000000

Result: 0 (Not detected).

Breakpoint 4: Return from sub_10055FDE8 (System Files)

(lldb) c
Process 7222 stopped
[ TiD: 1  ]-------------------------------------------------------------------------------------------[regs]
   X0:  0x0000000000000001   X8:  0x0000000000000001  X16:  0x00000001DB9D6070  X24:  0x0000000000000000
   ...
------------------------------------------------------------------------------------------------------[code]
@ /private/var/containers/Bundle/Application/7A41E69C-8FB3-4654-AB8B-10BE87277B05/nmb_prod.app/nmb_prod:
->  0x100583f8c (0x10055ff8c): c0 03 5f d6  ret    ; ___lldb_unnamed_symbol_10055f914 @ 0x1005839bc @ /private/var/containers/Bundle/Application/7A41E69C-8FB3-4654-AB8B-10BE87277B05/nmb_prod.app/nmb_prod
    0x100583f90 (0x10055ff90): f6 57 bd a9  stp    x22, x21, [sp, #-0x30]!
    0x100583f94 (0x10055ff94): f4 4f 01 a9  stp    x20, x19, [sp, #0x10]
    0x100583f98 (0x10055ff98): fd 7b 02 a9  stp    x29, x30, [sp, #0x20]
(lldb) register read x0
      x0 = 0x0000000000000001

Result: 1 (Detected!).

This function found one of these files on the device. Looking at the list in sub_1005602DC, the likely candidates on Dopamine are:

  • /private/var/lib/apt
  • /private/var/lib/cydia

Breakpoint 6: Return from sub_10055F914 (Main Orchestrator) The main function returns the result of the system file check.

(lldb) c
Process 7222 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1
    frame #0: 0x00000001005839f0 nmb_prod`___lldb_unnamed_symbol_10055f914 + 220
->  0x1005839f0: c0 03 5f d6  ret
(lldb) register read x0
      x0 = 0x0000000000000001

Final Result: 1 (Jailbroken).

LLDB patching

Since we identified the exact function returning the positive result (sub_10055F914), we can try to patch it live in LLDB to see if the app works. I set a breakpoint right at the end of that function and modified the return register x0.

(lldbinit) breakpoint set -a 0x000000010055F9F0+0x000000000467c000
Breakpoint 1: where = nmb_prod`___lldb_unnamed_symbol_10055f914 + 220, address = 0x0000000104bdb9f0
(llldbinitb) c
Process 7394 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000104bdb9f0 nmb_prod`___lldb_unnamed_symbol_10055f914 + 220
->  0x104bdb9f0: c0 03 5f d6  ret

(llldbinitb) register read x0
      x0 = 0x0000000000000001
(lldbinit) register write x0 0
(llldbinitb) c
Process 7394 resuming
lldb debug session showing bypass

The app resumed, and the jailbreak warning popup did not appear. The app loaded successfully, confirming that the detection was bypassed!

Frida bypass

Now that I knew exactly which function to hook, I wrote a simple Frida script to automate this bypass permanently:

var MODULE = "nmb_prod";
var IDA_BASE = 0x100000000;
var HOOKS = [{ ida: 0x10055f914, name: "sub_10055F914" }];

function hookOffset(mod, offset, name) {
  var addr = mod.base.add(offset);
  Interceptor.attach(addr, {
    onLeave: function (retval) {
      if (!retval.isNull()) {
        console.log("[*] " + name + " forced return 0 (was " + retval + ")");
        retval.replace(ptr(0x0));
      }
    },
  });
  console.log("[+] Hooked " + name + " at " + addr);
}

var mod = Process.getModuleByName(MODULE);
if (!mod) {
  console.log("[-] Module " + MODULE + " not found");
} else {
  console.log("[*] " + MODULE + " @ " + mod.base + " (size: 0x" + mod.size.toString(16)+ ")");
  HOOKS.forEach(function (h) {
    hookOffset(mod, h.ida - IDA_BASE, h.name);
  });
}

Running the script on the device with Frida:
```bash
% frida -H 192.168.1.31 -f fr.creditagricole.monbudget -l CA/bypass_jb.js
     ____
    / _  |   Frida 17.7.0 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to 192.168.1.31 ([email protected])
Spawning `fr.creditagricole.monbudget`...
[*] nmb_prod @ 0x10431c000 (size: 0x2424000)
[+] Hooked sub_10055F914 at 0x10487b914
Spawned `fr.creditagricole.monbudget`. Resuming main thread!
[Remote::fr.creditagricole.monbudget ]-> [*] sub_10055F914 forced return 0 (was 0x1)
[Remote::fr.creditagricole.monbudget ]->

The detection is successfully bypassed :p

Crédit Agricole jailbreak undetected

If you’re interested in an alternative approach, I’ve also created a Tweak using Logos and Theos to bypass the jailbreak detection.

See you in the next post :)