Friday 10 August 2012

Manual Bypass of Naive Jailbreak Detection Methods

Apple is a strict vendor. Out of the box, you cannot just push any app on your device and start running stuff. There is no magic check box that allows you to change this (a la Android) - you have the AppStore and that's it. Jailbreaking primarily means that this restriction is patched (hacked) out of the operating system - on a jailbroken device, any binary, compiled for iOS, can be executed.

Before going into details, it might be useful to clarify what iOS checks before loading and executing a binary.
  1. A digital signature is enclosed in every installation package (IPA). The signature contains a hash for each file in the installation package and a cryptographic signature, provided by Apple (directly or indirectly). Broken IPAs cannot be installed. Jailbreaking means that this restriction is relieved so that any signing authority is accepted - that is, a self-signed certificate is sufficient for signing IPAs.
  2. Every executable is supplied with an embedded XML, which gives a detailed description about what that particular executable is allowed to do. For instance this is the place to list the allowed keychain access groups, or flags that indicate that the application is allowed to hook into other processes through the kernel (~gdb).
  3. Memory pages are signed and the kernel checks their signature before executing their contents. Therefore, it is very hard to inject payload (shellcode) directly into a process memory space - however, this restriction does not mean that we are unable to tamper with the process at runtime.
Having said that, consider a Proof-of-Concept example for very basic jailbreak checking. The most naive approach checks for known residual artifacts of the jailbreaking process ('/Applications/Cydia.app', '/var/log/cydia.log' etc.) The process utilises an array of strings and existence of any of the files indicates a jailbroken device. 

BOOL isJailbroken(AppDelegate* delegate) {
    return [[NSFileManager defaultManager] fileExistsAtPath:@"/Applications/Cydia.app"];
}

- (void)jailbreakDetection
{
    if (isJailbroken(self))
    {
        [self vanIlyen];
    }
    else
    {
        [self nincsIlyen];
    }
}

- (void)vanIlyen
{
    [[[UIAlertView alloc] initWithTitle:@"Jailbroken device!"
                               message:@"Indeed, it is."
                              delegate:nil
                     cancelButtonTitle:@"Got it."
                     otherButtonTitles: nil] show];
}

- (void)nincsIlyen
{
    [[[UIAlertView alloc] initWithTitle:@"Clean device.:)"
                               message:@"Oh yes."
                              delegate:nil
                     cancelButtonTitle:@"Got it."
                     otherButtonTitles: nil] show];

The algorithm is simple. We check whether the '/Applications/Cydia.app' exists and update the user interface accordingly. From a pentester's point of view, the source is the documentation and it is a rare and happy moment when we have access to it while testing. Therefore, take a look at the disassembly view:


; =============== S U B R O U T I N E =======================================


; AppDelegate - (void)jailbreakDetection
; Attributes: bp-based frame
; void __cdecl __AppDelegate jailbreakDetection_(struct AppDelegate *self, SEL)
__AppDelegate_jailbreakDetection_       ; DATA XREF: __objc_const:00003318 o
                PUSH            {R4,R7,LR}
                MOV             R4, R0
                MOV             R0, (selRef_isJailbroken - 0x2406) ; selRef_isJailbroken
                ADD             R7, SP, #4
                ADD             R0, PC ; selRef_isJailbroken
                LDR             R1, [R0] ; "isJailbroken"
                MOV             R0, R4
                BLX             _objc_msgSend
                TST.W           R0, #0xFF
                BEQ             loc_241E
                MOV             R0, (selRef_vanIlyen - 0x241E) ; selRef_vanIlyen
                ADD             R0, PC ; selRef_vanIlyen
                B               loc_2428
; ---------------------------------------------------------------------------
loc_241E                                ; CODE XREF: -[AppDelegate jailbreakDetection]+1C j
                MOV             R0, (selRef_nincsIlyen - 0x242A) ; selRef_nincsIlyen
                ADD             R0, PC ; selRef_nincsIlyen

loc_2428                                ; CODE XREF: -[AppDelegate jailbreakDetection]+28 j
                LDR             R1, [R0]
                MOV             R0, R4
                BLX             _objc_msgSend
                POP             {R4,R7,PC}
; End of function -[AppDelegate jailbreakDetection]
; ---------------------------------------------------------------------------

The assembly view of the isJailbroken function looks as follows:

; =============== S U B R O U T I N E =======================================
; AppDelegate - (char)isJailbroken
; char __cdecl __AppDelegate isJailbroken_(struct AppDelegate *self, SEL)
__AppDelegate_isJailbroken_             ; DATA XREF: __objc_const:00003324 o
                PUSH            {R7,LR}
                MOVW            R1, #(selRef_defaultManager - 0x244C) ; selRef_defaultManager
                MOV             R7, SP
                MOVT.W          R1, #0
                MOV             R0, (classRef_NSFileManager - 0x244E) ; classRef_NSFileManager
                ADD             R1, PC ; selRef_defaultManager
                ADD             R0, PC ; classRef_NSFileManager
                LDR             R1, [R1] ; "defaultManager"
                LDR             R0, [R0] ; _OBJC_CLASS_$_NSFileManager
                BLX             _objc_msgSend
                MOV             R1, (selRef_fileExistsAtPath_ - 0x2460) ; selRef_fileExistsAtPath_
                ADD             R1, PC ; selRef_fileExistsAtPath_
                LDR             R1, [R1] ; "fileExistsAtPath:"
                MOV             R2, (cfstr_ApplicationsCy - 0x246C) ; "/Applications/Cydia.app"
                ADD             R2, PC  ; "/Applications/Cydia.app"
                BLX             _objc_msgSend
                POP             {R7,PC}
; End of function -[AppDelegate isJailbroken]

There are plenty of ways to bypass this check. The easiest and most trivial is to patch the binary so that the file name string is different - indeed, the patch works in this case perfectly.


However, it is much more interesting to do the whole magic in dynamic mode - although string patching is viable and perfectly working option in this case, it is not applicable in most cases (especially when it comes to more complex checks).

We use gdb to perform the bypass. In order to stop execution in the correct moment, we create a breakpoint in the [NSFileManager fileExistsAtPath:] API call.

Attaching to process 1172.
Reading symbols for shared libraries + done
Reading symbols for shared libraries ++ done
Reading symbols for shared libraries + done
0x2feb8470 in __dyld_strcmp ()
(gdb) b fileExistsAtPath:
Breakpoint 1 at 0x37fd9c3a
(gdb) c

The app runs and we hit the breakpoint.

Breakpoint 1, 0x37fd9c3a in -[NSFileManager fileExistsAtPath:] ()
(gdb)

Now take a look at the source in the beginning. It is easy to assemble the calling stack, even manually:
[NSFileManager fileExistsAtPath:] 
[AppDelegate isJailbroken]
[AppDelegate jailbreakDetection]
[...]

gdb confirms our theory:

(gdb) bt
#0  0x37fd9c3a in -[NSFileManager fileExistsAtPath:] ()
#1  0x000a146e in ?? ()
#2  0x000a140c in ?? ()
#3  0x000a13ec in ?? ()
#4  0x31515c30 in -[UIApplication _stopDeactivatingForReason:] ()
#5  0x31503914 in -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] ()
[...]

We create another breakpoint, in the [AppDelegateisJailbroken] function, right after the return of the [NSFileManager fileExistsAtPath:].

(gdb) b *0x000a146e
Breakpoint 2 at 0xa146e
(gdb) c
Continuing.

We hit the second breakpoint - at this point, in accordance with ARM calling conventions, the returned value from the fileExistsAtPath: is in $r0. All we need to do is to set it to 0.

(gdb) i r $r0
r0             0x1 1
(gdb) set $r0=0
(gdb) c
Continuing.

...and the application pops up an alert with "Clean device. :)" Easy, isn't it?

1 comment:

  1. More there is two ways to bypass Jailbreak checks
    1. Runtime Modification using GDB
    2. Method Swizzling using Cycript
    have a look if you like it...

    http://hackoftheday.securitytube.net/2013/04/bypassing-jailbroken-checks-in-ios.html

    ReplyDelete