Showing posts with label jailbreak. Show all posts
Showing posts with label jailbreak. Show all posts

Tuesday, 16 October 2012

Offline Patching of iOS Applications

During penetration tests, we frequently utilise offline patching of iOS applications to bypass restrictions and protection mechanisms. Offline patching is a convenient way to overcome jailbreak detection, certificate pinning or poorly implemented authentication/authorisation in a permanent fashion: we perform binary autopsy of the application, fire up our favourite hexeditor, change a few bytes and voila! the restrictions are gone.

Let's consider our favourite dummy jailbreak detection application first.

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

- (void)jailbreakDetection
{
    if (isJailbroken(self))
    {
        [self dirtyDevice];
    }
    else
    {
        [self cleanDevice];
    }
}

The algorithm relies on the fileExistAtPath: API call to decide whether or not the Cydia app is installed. Our first attempt for patching targets the string object. At this point, all we need is a good hexeditor to pinpoint the string location within the binary and change a few characters. Using IDA, we inspect the cstrings section of the binary. NSStrings in ObjC are immutable (unless created otherwise), and they are stored in the cstring section of the mach-o binary. 

__cstring:00002DC0 ; ===========================================================================
__cstring:00002DC0
__cstring:00002DC0 ; Segment type: Pure data
__cstring:00002DC0                 AREA __cstring, DATA, ALIGN=0
__cstring:00002DC0                 ; ORG 0x2DC0
__cstring:00002DC0 aViewcontroll_4 DCB "ViewController",0  ; DATA XREF: __cfstring:cfstr_Viewcontroll_4 o
__cstring:00002DCF aApplicationsCy DCB "/Applications/Cydia.app",0
__cstring:00002DCF                                         ; DATA XREF: __cfstring:cfstr_ApplicationsCy o

Using a hexeditor, we quickly patch the corresponding section, paying close attention not to change the length of the string (overwriting is the keyword here):


So far so good. Having patched the binary, we re-create the installation package, update the application and push it to the device using iPhone Configuration Utility. However, if we attempt to start the modified binary, the kernel flat refuses to load it, since our tweaked binary fails at the signature (rather integrity) checking step. In order to overcome the issue, we use this script to re-create the signature of the binary:

D:JBT_v2 mac$ resigner.sh JBT_unsigned.ipa randomkey JBT_signed.ipa
APP_NAME=JailbreakDetectionTest.ipa
Resigning application using certificate: keydumperhez
temp/Payload/JailbreakDetectionTest.app: replacing existing signature

Repackaging as JBT_signed.ipa  

Upload the signed package again, and the jailbreak check algorithm is blinded. Easy, isn't it? The above method is the Sex Pistols of patching methods - kind of a no brainer attack, but it works like charm against such very dumb jailbreak checking algorithms. The method however, can be used for more intelligent attacks. Consider the same app, but now take a look at it in 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
                [...]
                BLX             _objc_msgSend
                POP             {R4,R7,PC}
; End of function -[AppDelegate jailbreakDetection]

Consider the instructions highlighted in red.
                TST.W           R0, #0xFF
                BEQ             loc_241E
The idea here is to change the branching instruction, therefore the direction of the condition check will be the other way around: our app will consider any clean device jailbroken. IDA could help us but unfortunately, there is no ARM assembler included in the demo version, therefore we have to use manual pen-and-paper methods - this is the point where we need the ARM Assembly Reference Guide, attached to every xCode installation.  It can be found in /Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/Frameworks/DTISAReferenceGuide.framework/Versions/A/Resources/ARMISA.pdf on my test machine.

Most ARM instructions include a condition section, which makes the difference i.e. between BEQ (Branch-if-equal) and BNE (Branch-if-not-equal). Take a look at page 314 in the Reference Guide, which contains the list of possible condition flags.

Take a look at the Hex view of the BEQ loc_241e line in IDA (highlighted in red):

 05 D0 41 F2 CE 00 C0 F2  00 00 78 44 04 E0 41 F2  ..A.......xD..A.

To manually disassemble the instruction, 0xD005 is the correct form (because of the endianness of iOS). In binary format, 0xd005 is b1101 0000 0000 0101. On page 350, the following table represents the meaning of the individual bits. We recognise that the compiler used the Thumb1 encoding of the B** instruction in this case:


Therefore, the 'cond' bits correspond to the EQUAL condition, which in this case is the b0000 flagbit combination. In order to 'reverse' the condition, we need to change the bits to b0001, which in turn corresponds to 0x05D1. In order to verify our results, we use IDA before and after patching and compare the results:

Before patching:
After patching:



Again, we re-create and resign the modified installation package, and now the app is blind again.

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?