Gone in 59 seconds: tips and tricks to bypass AppMinder’s Jailbreak detection

There’s a new attempt at jailbreak detection available at http://appminder.nesolabs.de. It is mostly aimed at Enterprise applications and not AppStore usage. I am not sure about AppStore rules but those tricks will most probably not pass the approval process.

AppMinder provides three levels of jailbreak detection and anti-debugging measures. The different levels are related to self-integrity checking and code obfuscation rates. When you generate a new protection, it will give you some plug’n’pray code to plug in into your existent code base. It is very easy to integrate. There is some polymorphism on each generation – code is different but the high-level operations will be the same. For my analysis I have used the C variant – self-integrity checking level and code obfuscation rate both high.

The core of jailbreak detection is located in a big inline assembler function with random name on each generation, and in a single line to make it a bit more annoying to read. In OS X you can easily convert it to line by line with sed “s/;/\echo -e ‘\n\r’/g”. IDA has some trouble disassembling it but you can help it by manually defining code. It was late and I did not bothered to verify these IDA troubles.

First thing that you easily notice are the SVC 0x80 instructions. This is the INT 80 equivalent in ARM (there’s also the old name SWI). The system call number should be passed on R12 register. There is some small obfuscation to try to hide the syscall numbers but nothing hard to follow. You can quickly understand that there are no new jailbreak detection tricks – fork, ptrace, exit are being called and tested. Old tricks indeed.
This function is also responsible for setting the contents of four arrays, that will be used for self-checksum. Sample code:

#if !(TARGET_IPHONE_SIMULATOR)
unsigned int so[2];
unsigned int sr[2];
unsigned int sn[2];
unsigned int cs[2];
unsigned int a = 0x96ed;
unsigned int b = 1;

unsigned int i = 0;

(void) memset (so, 0x00, sizeof (so));
(void) memset (sr, 0x00, sizeof (sr));
(void) memset (sn, 0x00, sizeof (sn));
(void) memset (cs, 0x00, sizeof (cs));

gKLuXlstmJe (so, sr, sn, cs);
#endif

__attribute__ ((always_inline)) static void gKLuXlstmJe (unsigned int *so, unsigned int *sr, unsigned int *sn, unsigned int *cs)

While it does not matter much as I will show you next, there is no protection whatsoever against hardware breakpoints. These can be used and abused to debug this. Oh, iOS GDB does not have this feature enabled. I have to find some time to port and fix that.

The syscall calls give up the location of this function. We just need to find the 0x80DF binary pattern to locate the first syscall and the last one (word of caution: the self-integrity checks also use this instruction some times). One way to bypass it is to jump from the first SVC 0x80 to the next instruction after the last one. You can either do this by patching and resigning the binary or with a debugger.

The next step is to beat the self-integrity checksums. They should be inserted in the same function where above’s jailbreak detection code is used. Sample code:

#if !(TARGET_IPHONE_SIMULATOR)
for (i = 0; i < (sizeof (so) / sizeof (unsigned int)); i++) {     
if (((unsigned short)so[i] != (a + 0x4893) &&
    ((unsigned short)(so[i] >> 16) != (a + 0x4893)))) {
        asm volatile ("pop {r1-r15};");
    }
}
#endif

#if !(TARGET_IPHONE_SIMULATOR)
for (i = 0; i < (sizeof (sr) / sizeof (unsigned int)); i++) {
    if (sr[i] != (a - a + b)) {
        asm volatile ("movw r6, #96;movt r6, #107;bx r6;");
    }
}
#endif

#if !(TARGET_IPHONE_SIMULATOR)
for (i = 0; i < (sizeof (sn) / sizeof (unsigned int)); i++) {
    if (sn[i] != (a - a + (b + b))) {
        asm volatile ("pop {r2-r15};");
    }
}
#endif

#if !(TARGET_IPHONE_SIMULATOR)
if (checksum ((char *)(cs[0] - 6), (cs[1] - (cs[0] - 6))) != CHECKSUM) {
    asm volatile ("mov r0, #1;mov r12, #1;svc 0x80;");
}
#endif

What we have here are four different checks that use the unsigned int arrays. If checks fail they will crash or exit the application by setting program counter to a bogus value and some other tricks. The inline assembly can’t be used for detection. It is mutated in each generation. The same applies to checksum and “a” values. But… the arrays are always referenced and used. And… they are initialized using memset. Ah… Breakpoint memset and get the addresses of the four arrays. Watchpoints can be set on each and the location of each check easily found (assuming watchpoints are set to read only). A4 CPU has 4 watchpoints available. I am not sure about A5, the ARM specification supports up to 16. Voilá, everything detected and bypassed. Game over!

The problem is very difficult to solve if not impossible for MDM solutions, in particular in iOS due to code signing restrictions (no packing, encryption, and so on). If the device is jailbroken, MDM solutions are at mercy of being modified and any jailbreak detection code will be easily bypassed. Detection is usually based on certain folders, syscalls or functions such as fork and ptrace. Be very careful with the assumptions you make when creating/designing MDM solutions. If the enterprise application depends on not being used in jailbroken devices then you have a big problem in its design (only real solution might be a corporate policy that prohibits jailbroken devices). The protection page says a kernel-level rootkit is required to bypass all checks. This is not true as code injection can be used for the same purpose.

Greetings to Nesolabs for their effort. The mutation engine is interesting, even with the SVC vulnerability. I hope this will help you to improve the next version. It is a hopeless game to definitely win but time can be bought. In the end time might be the only thing you need, although that applies more to DRM than MDM solutions.

Have fun,
fG!