iOS apps reverse engineering - Solving crackmes - part 2
In this post I will go through the steps required to solve a slightly more complicated crackme (Level 2). I will show the steps that I took, but you can find your own way and use this post just as a reference. If you have not prepared your reversing/cracking environment yet, please refer to this post. If you would like to start with a simpler example, check this post.
Step 1 - Download the crackme
Clone the Owasp Mobile Security Testing Guide repository with the following coomand:
git clone https://github.com/OWASP/owasp-mstg
You should have two iOS crackmes under owasp-mstg/Crackmes/iOS
: Level_01
and Level_02
.
In this blog post I will show you how to solve Level_02
.
Step 2 - Install the iOS app on your device
If you follwed this blog post,
you should already have everything you need to install iOS apps. If you installed all the tools, you will have two options to install the app UnCrackable_Level_2.ipa
:
- You can use ideviceinstaller from you computer:
- Connect USB cable and run
ideviceinstaller -i UnCrackable_Level_2.ipa
- Connect USB cable and run
- Or transfer the file via SCP and install it directly from your device:
- To Transfer your application, run:
scp UnCrackable_Level_2.ipa root@<deviceip>:~/
- To install it, run:
ssh root@<deviceip> appinst UnCrackable_Level_2.ipa
- To Transfer your application, run:
Step 3 - Run the application on your device
If you run the application you have just installed, you will notice that the app doesn’t seem to open. We need to figure out why (no, the app is not broken), and we’ll use our reversing skills to figure out what is going on.
Step 4 - Extract the app
As you may already know, .ipa files are regular zip archives which can be extracted with the command below:
unzip UnCrackable_Level_2.ipa
Once the archive has been decompressed, the first thing to do it to check what kind of executable we have with the file utility:
$ file Payload/UnCrackable\ Level\ 2.app/UnCrackable\ Level\ 2
Payload/UnCrackable Level 2.app/UnCrackable Level 2: Mach-O universal binary with 2 architectures: [armv7:Mach-O armv7 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>] [arm64:Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>]
The output shows that we have a Mach-O universal binary with 2 architectures. That means that for this application to be compatible with both armv7 (32 bits processor in older devices) and arm64 (64 bits processor for newer devices), both executables have been packed in one single “fat” binary.
As a result, the first thing to do in this case is to extract the thin binaries and load the one you need in you reverse engineering tool of choice. If you have jailbroken your device with checkra1n, it must be a 64 bits device.
If you have installed radare2, you can extract both versions by executing the following:
rabin2 -x UnCrackable\ Level\ 2
Otherwise, download jtool2 from here and execute the following to get the arm64 binary:
export ARCH=arm64; ./jtool2.ELF64 -e arch UnCrackable\ Level\ 2
Step 5 - Defeating anti-debugging
Once we have loaded our thin binary in our reversing tool of choice (I will use radare2), let’s see if we can find anything interesting in the method executed when the main view is loaded:
[0x100005520]> pdf @ method.ViewController.viewDidLoad
; CODE XREF from method.ViewController.viewDidLoad @ 0x100005528
;-- func.1000054f4:
┌ 912: method.ViewController.viewDidLoad (int64_t arg1);
│ ; var void *var_8h @ sp+0x8
│ ; var void *instance @ sp+0x10
│ ; var int64_t var_0h_2 @ sp+0x18
│ ; var int64_t var_20h @ sp+0x20
│ ; var int64_t var_20h_2 @ sp+0x28
│ ; var int64_t var_30h @ sp+0x30
│ ; var int64_t var_30h_2 @ sp+0x38
│ ; var int64_t var_40h @ sp+0x40
│ ; var int64_t var_40h_2 @ sp+0x48
│ ; var int64_t var_50h @ sp+0x50
│ ; var int64_t var_50h_2 @ sp+0x58
│ ; var int64_t var_60h @ sp+0x60
│ ; var int64_t var_60h_2 @ sp+0x68
│ ; arg int64_t arg1 @ x0
│ 0x1000054f4 ffc301d1 sub sp, sp, 0x70
│ 0x1000054f8 fa6702a9 stp x26, x25, [var_20h]
│ 0x1000054fc f85f03a9 stp x24, x23, [var_30h]
│ 0x100005500 f65704a9 stp x22, x21, [var_40h]
│ 0x100005504 f44f05a9 stp x20, x19, [var_50h]
│ 0x100005508 fd7b06a9 stp x29, x30, [var_60h]
│ 0x10000550c fd830191 add x29, var_60h
│ 0x100005510 f30300aa mov x19, x0 ; arg1
│ 0x100005514 f30b00f9 str x19, [instance]
│ 0x100005518 1f2003d5 nop
│ 0x10000551c 684c0458 ldr x8, section.21.__DATA.__objc_superrefs ; 0x10000dea8 ; section.23.__DATA.__objc_data
│ 0x100005520 e80f00f9 str x8, [var_0h_2]
│ 0x100005524 1f2003d5 nop
│ 0x100005528 81360458 ldr x1, str.viewDidLoad ; 0x100009c94 ; char *selector
│ 0x10000552c e0430091 add x0, instance ; void *instance
│ 0x100005530 89100094 bl sym.imp.objc_msgSendSuper2 ; void *objc_msgSendSuper2(void *instance, char *selector)
│ ; void *objc_msgSendSuper2(0x0000000000000000, "viewDidLoad")
│ 0x100005534 41018052 movz w1, 0xa
│ 0x100005538 000080d2 movz x0, 0
│ 0x10000553c 3b100094 bl sym.imp.dlopen
│ 0x100005540 f40300aa mov x20, x0
│ 0x100005544 c1cc0210 adr x1, str.ptrace ; 0x10000aedc
│ 0x100005548 1f2003d5 nop
│ 0x10000554c 3a100094 bl sym.imp.dlsym
│ 0x100005550 e80300aa mov x8, x0
│ 0x100005554 e0130032 orr w0, wzr, 0x1f
│ 0x100005558 01008052 movz w1, 0
│ 0x10000555c 020080d2 movz x2, 0
│ 0x100005560 03008052 movz w3, 0
│ 0x100005564 00013fd6 blr x8 ; pstate(0x1f, 0x100000000, 0x0, 0x0)
│ 0x100005568 e00314aa mov x0, x20
│ 0x10000556c 2c100094 bl sym.imp.dlclose
│ 0x100005570 1f2003d5 nop
│ 0x100005574 60460458 ldr x0, reloc.NSThread ; 0x10000de40 ; void *instance
│ 0x100005578 1f2003d5 nop
│ 0x10000557c 22340458 ldr x2, 0x10000dc00
│ 0x100005580 1f2003d5 nop
│ 0x100005584 21340458 ldr x1, str.detachNewThreadSelector:toTarget:withObject: ; 0x100009ca4 ; char *selector
│ 0x100005588 e30313aa mov x3, x19
│ 0x10000558c 040080d2 movz x4, 0
│ 0x100005590 6e100094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector) ; "X"
│ ; void *objc_msgSend(-1, "detachNewThreadSelector:toTarget:withObject:")
In the code fragment above we can notice a suspicious sequence of method calls:
- Call to
dlopen(0, 0xa)
: obtains a handle for the current process - Call to
dlsym(x0, "ptrace")
obtains the address of ptrace - Call to
ptrace(0x1f, 0, 0, 0)
executes ptrace with 0x1f as first argument - Call to
detachNewThreadSelector(0x10000dc00, x19, 0)
to create a new thread
You might wonder why an app like this tries to call ptrace… it’s not supposed to debug anything, right? A little search on Apple’s implementation of ptrace.h will give us a hint of what its arguments mean:
#define PT_TRACE_ME 0 /* child declares it's being traced */
#define PT_READ_I 1 /* read word in child's I space */
#define PT_READ_D 2 /* read word in child's D space */
#define PT_READ_U 3 /* read word in child's user structure */
#define PT_WRITE_I 4 /* write word in child's I space */
#define PT_WRITE_D 5 /* write word in child's D space */
#define PT_WRITE_U 6 /* write word in child's user structure */
#define PT_CONTINUE 7 /* continue the child */
#define PT_KILL 8 /* kill the child process */
#define PT_STEP 9 /* single step the child */
#define PT_ATTACH ePtAttachDeprecated /* trace some running process */
#define PT_DETACH 11 /* stop tracing a process */
#define PT_SIGEXC 12 /* signals as exceptions for current_proc */
#define PT_THUPDATE 13 /* signal for thread# */
#define PT_ATTACHEXC 14 /* attach to running process with signal exception */
#define PT_FORCEQUOTA 30 /* Enforce quota for root */
#define PT_DENY_ATTACH 31
#define PT_FIRSTMACH 32 /* for machine-specific requests */
As can be observed above, PT_DENY_ATTACH
has value 31
which is the same as 0x1f
in hex.
Cool, we are one step closer to understanding what’s going on, let’s see what the man page for ptrace
says about this flag:
PT_DENY_ATTACH
This request is the other operation used by the traced
process; it allows a process that is not currently being
traced to deny future traces by its parent. All other
arguments are ignored. If the process is currently being
traced, it will exit with the exit status of ENOTSUP; oth-erwise, otherwise,
erwise, it sets a flag that denies future traces. An
attempt by the parent to trace a process which has set this
flag will result in a segmentation violation in the parent.
Bingo! If the process is currently being traced it will exit! But are we tracing this app at all? The answer is yes, because if you followed my introduction post, you have frida installed on your device! Let’s quickly nop the call to ptrace to prevent the app from exiting:
:> s 0x100005564
:> oo+
:> wa nop
Written 4 byte(s) (nop) = wx 1f2003d5
We have seen how easily we could bypass this anti-debugging measure, but there will be more surprises :)
There is a call to detachNewThreadSelector(0x10000dc00, x19, 0)
, but what is it doing?
- By looking at the disassembly we realize that
x19
is a pointer to theViewController
object - The first argument is pointing to a pointer to the string “abc”:
[0x10000557c]> ps @ [0x10000dc00]
abc
So this call looks roughly like this: detachNewThreadSelector(ptr2ptr2abc, ViewController, 0)
, which means
the method ViewController.abc
will be executed in a new thread. Let’s see what is happening in this thread:
[0x100005488]> pdf @ method.ViewController.abc
; CODE XREF from method.ViewController.viewDidLoad @ 0x10000557c
; CODE XREF from str.v32_0:8__UIApplication_16__UIUserNotificationSettings_24 @ +0x15
;-- func.1000053f4:
┌ 256: method.ViewController.abc (int64_t arg1);
│ ; var int64_t var_ch @ sp+0xc
│ ; var int64_t var_44h @ sp+0x44
│ ; var int64_t var_7ch @ sp+0x7c
│ ; var int64_t var_b4h @ sp+0xb4
│ ; var int64_t var_ech @ sp+0xec
│ ; var int64_t var_f0h @ sp+0xf0
│ ; var int64_t var_f8h @ sp+0xf8
│ ; var int64_t var_0h @ sp+0x118
│ ; var int64_t var_119h @ sp+0x119
│ ; var int64_t var_0h_2 @ sp+0x380
│ ; var int64_t var_0h_3 @ sp+0x388
│ ; var int64_t var_0h_4 @ sp+0x38c
│ ; var int64_t var_40h @ sp+0x390
│ ; var int64_t var_40h_2 @ sp+0x398
│ ; var int64_t var_10h @ sp+0x3a0
│ ; var int64_t var_10h_2 @ sp+0x3a8
│ ; var int64_t var_20h @ sp+0x3b0
│ ; var int64_t var_20h_2 @ sp+0x3b8
│ ; var int64_t var_30h @ sp+0x3c0
│ ; var int64_t var_30h_2 @ sp+0x3c8
│ ; arg int64_t arg1 @ x0
│ 0x1000053f4 fc6fbca9 stp x28, x27, [var_40h]!
│ 0x1000053f8 f65701a9 stp x22, x21, [var_10h]
│ 0x1000053fc f44f02a9 stp x20, x19, [var_20h]
│ 0x100005400 fd7b03a9 stp x29, x30, [var_30h]
│ 0x100005404 fdc30091 add x29, var_30h
│ 0x100005408 ff430ed1 sub sp, sp, 0x390
│ 0x10000540c f3c30391 add x19, var_f0h
│ 0x100005410 ff1b01b9 str wzr, [var_0h] ; arg1
│ 0x100005414 ffef00b9 str wzr, [var_ech] ; arg1
│ 0x100005418 e80b1fb2 orr x8, xzr, 0xe0000000e
│ 0x10000541c 280080f2 movk x8, 0x1
│ 0x100005420 684a01f9 str x8, [var_0h_2]
│ 0x100005424 e8030032 orr w8, wzr, 1 ; arg1
│ 0x100005428 a8831cb8 stur w8, [var_0h_3]
│ 0x10000542c 8b100094 bl sym.imp.getpid ; int getpid(void)
│ ; int getpid(void)
│ 0x100005430 a0c31cb8 stur w0, [var_0h_4]
│ 0x100005434 1f2003d5 nop
│ 0x100005438 d45f0358 ldr x20, reloc.mach_task_self_ ; 0x10000c030
│ 0x10000543c f5f30191 add x21, var_7ch
│ 0x100005440 16518052 movz w22, 0x288
│ ┌─< 0x100005444 05000014 b 0x100005458
│ │ ; CODE XREF from method.ViewController.abc @ 0x1000054c8
│ ┌──> 0x100005448 e8674439 ldrb w8, [var_119h] ; [0x119:4]=-1 ; 281
│ ┌───< 0x10000544c 08051837 tbnz w8, 3, 0x1000054ec ; unlikely
│ │╎│ 0x100005450 800c8052 movz w0, 0x64 ; 'd'
│ │╎│ 0x100005454 fc100094 bl sym.imp.usleep ; int usleep(int s)
│ │╎│ ; int usleep(-1)
│ │╎│ ; CODE XREF from method.ViewController.abc @ 0x100005444
│ │╎└─> 0x100005458 800240b9 ldr w0, [x20]
│ │╎ 0x10000545c e1231f32 orr w1, wzr, 0x3fe
│ │╎ 0x100005460 e2d30291 add x2, var_b4h
│ │╎ 0x100005464 e3b30391 add x3, var_ech
│ │╎ 0x100005468 e4f30191 add x4, var_7ch
│ │╎ 0x10000546c e5130191 add x5, var_44h
│ │╎ 0x100005470 e6330091 add x6, var_ch
│ │╎ 0x100005474 f1100094 bl sym.imp.task_get_exception_ports
│ │╎ 0x100005478 e8ef40b9 ldr w8, [var_ech] ; [0xec:4]=-1 ; 236
│ │╎ 0x10000547c 1f000071 cmp w0, 0
│ │╎ 0x100005480 0409407a ccmp w8, 0, 4, eq
│ │╎┌─< 0x100005484 20010054 b.eq 0x1000054a8 ; likely
│ │╎│ 0x100005488 090080d2 movz x9, 0
│ │╎│ ; CODE XREF from method.ViewController.abc @ 0x1000054a4
│ ┌────> 0x10000548c aa7a69b8 ldr w10, [x21, x9, lsl 2]
│ ╎│╎│ 0x100005490 4a050011 add w10, w10, 1
│ ╎│╎│ 0x100005494 5f090071 cmp w10, 2
│ ┌─────< 0x100005498 a2020054 b.hs 0x1000054ec ; unlikely
│ │╎│╎│ 0x10000549c 29050091 add x9, x9, 1
│ │╎│╎│ 0x1000054a0 3f0108eb cmp x9, x8
│ │└────< 0x1000054a4 43ffff54 b.lo 0x10000548c ; likely
│ │ │╎│ ; CODE XREF from method.ViewController.abc @ 0x100005484
│ │ │╎└─> 0x1000054a8 760200f9 str x22, [x19]
│ │ │╎ 0x1000054ac a00301d1 sub x0, var_0h_2
│ │ │╎ 0x1000054b0 e1031e32 orr w1, wzr, 4
│ │ │╎ 0x1000054b4 e2e30391 add x2, var_f8h
│ │ │╎ 0x1000054b8 e3c30391 add x3, var_f0h
│ │ │╎ 0x1000054bc 040080d2 movz x4, 0
│ │ │╎ 0x1000054c0 050080d2 movz x5, 0
│ │ │╎ 0x1000054c4 da100094 bl sym.imp.sysctl
│ │ │└──< 0x1000054c8 00fcff34 cbz w0, 0x100005448 ; unlikely
│ │ │ 0x1000054cc 00cd0210 adr x0, str.__ViewController_abc_ ; 0x10000ae6c
│ │ │ 0x1000054d0 1f2003d5 nop
│ │ │ 0x1000054d4 61cd0250 adr x1, str._Users_berndt_Projects_uncrackable_app_iOS_Level2_UnDebuggable_ViewController.m ; 0x10000ae82
│ │ │ 0x1000054d8 1f2003d5 nop
│ │ │ 0x1000054dc a3cf0250 adr x3, str.junk__0 ; 0x10000aed2
│ │ │ 0x1000054e0 1f2003d5 nop
│ │ │ 0x1000054e4 c2118052 movz w2, 0x8e
│ │ │ 0x1000054e8 1a100094 bl sym.imp.__assert_rtn ; void __assert_rtn(const char *assertion, const char *file, unsigned int line, const char *function)
│ │ │ ; void __assert_rtn(-1, -1, -1, -1)
│ │ │ ; CODE XREFS from method.ViewController.abc @ 0x10000544c, 0x100005498
│ └─└───> 0x1000054ec 00008052 movz w0, 0
└ 0x1000054f0 54100094 bl sym.imp.exit ; void exit(int status) ; method.ViewController.viewDidLoad
└ ; void exit(-1)
Without going into too much detail, from the above method we can see the following sequence:
- There is a loop that is calling:
task_get_exception_ports(mac_task_self, 0x3fe, var_b4h, var_ech, var_7ch, var_44h, var_ch)
sysctl(var_0h_2, 4, var_f8h, var_f0h, 0, 0)
- Depending on some value returned by the above calls, break from the loop and exit the app.
Let’s dig a little bit deeper, and try to understand what task_get_exception_ports
is doing. From
Apple’s man page we can see the following information:
Function - Return send rights to the target task's exception ports.
SYNOPSIS
kern_return_t task_get_exception_ports
(task_t task,
exception_mask_t exception_types,
exception_mask_array_t old_exception_masks,
old_exception_masks old_exception_count,
exception_port_array_t old_exception_ports,
exception_behavior_array_t old_behaviors,
exception_flavor_array_t old_flavors);
PARAMETERS
task
[in task send right] The task for which to return the exception ports.
exception_types
[in scalar] A flag word indicating the types of exceptions for which the exception ports are desired:
EXC_MASK_BAD_ACCESS
Could not access memory.
EXC_MASK_BAD_INSTRUCTION
Instruction failed. Illegal or undefined instruction or operand.
EXC_MASK_ARITHMETIC
Arithmetic exception
EXC_MASK_EMULATION
Emulation instruction. Emulation support instruction encountered.
EXC_MASK_SOFTWARE
Software generated exception.
EXC_MASK_BREAKPOINT
Trace, breakpoint, etc.
EXC_MASK_SYSCALL
System call requested.
EXC_MASK_MACH_SYSCALL
System call with a number in the Mach call range requested.
EXC_MASK_RPC_ALERT
Exceptional condition encountered during execution of RPC.
old_exception_masks
[out array of exception_mask_t] An array, each element being a mask specifying for which exception types the corresponding element of the other arrays apply.
old_exception_count
[pointer to in/out scalar] On input, the maximum size of the array buffers; on output, the number of returned sets returned.
You can check Apple source code
to see all the values for EXC_MASK_*
, but I will save you some time and tell you that the argument 0x3fe
represents all the EXC_MASK_*
mentioned in the man page above.
In iOS and Mac OS exception ports are used when a program is running under a debugger. What the app does with this method is to simply retrieve the list of exceptions being handled for the current task. If any of the exceptions mentioned above is being handled, the app will assume it is running under a debugger, will break the loop and exit. The only thing we need to do at this point is to nop the break instruction to keep the app alive:
[0x100005604]> s 0x100005498
[0x100005498]> oo+
[0x100005498]> wa nop
Written 4 byte(s) (nop) = wx 1f2003d5
We have seen how to identify and bypass another anti-debugging technique. This anti-debugging technique is not novel and with a quick Google search you can find more resources such as this blog post.
Let’s now take a look at Apple’s man pages for sysctl. We can see that the sysctl propotype looks as follows:
int sysctl(int *name, u_int namelen, void *oldp, size_t *oldlenp, void *newp, size_t newlen);
By observing the last two sysctl arguments set to 0
in the app’s code, we can conclude that it is being called to
retrieve (and not set) information. Also the first argument is a pointer to the requested information,
so it is key to see what values are being set in order to understand what this code is doing. This is performed in the
following code:
0x100005418 e80b1fb2 orr x8, xzr, 0xe0000000e ; KERN_PROC
0x10000541c 280080f2 movk x8, 0x1 ; CTL_KERN
0x100005420 684a01f9 str x8, [var_0h_2]
0x100005424 e8030032 orr w8, wzr, 1 ; arg1 ; KERN_PROC_PID
0x100005428 a8831cb8 stur w8, [var_0h_3]
0x10000542c 8b100094 bl sym.imp.getpid ;[1] ; int getpid(void)
0x100005430 a0c31cb8 stur w0, [var_0h_4]
If you have the impression that the first two values are in inverse order, you are totally right. This is not an error, but it it’s because we are looking at a little-endian binary.
The constants commented above can be found in Apple’s source code.
What the above code suggests is that information is being requested for the current process and the result will be written to oldp
.
Let’s see what information is returned by KERN_PROC
in Apple’s man pages for sysctl
KERN_PROC
Return the entire process table, or a subset of it. An array of
pairs of struct proc followed by corresponding struct eproc
structures is returned, whose size depends on the current number
of such objects in the system. The third and fourth level names
are as follows:
Third level name Fourth level is:
KERN_PROC_ALL None
KERN_PROC_PID A process ID
KERN_PROC_PGRP A process group
KERN_PROC_TTY A tty device
KERN_PROC_UID A user ID
KERN_PROC_RUID A real user ID
In sysctl.h and proc.h
we can find more information about the struct kinfo_proc
returned by KERN_PROC:
struct kinfo_proc {
struct extern_proc kp_proc; /* proc structure */
struct eproc {
struct proc *e_paddr; /* address of proc */
struct session *e_sess; /* session pointer */
struct _pcred e_pcred; /* process credentials */
struct _ucred e_ucred; /* current credentials */
struct vmspace e_vm; /* address space */
pid_t e_ppid; /* parent process id */
pid_t e_pgid; /* process group id */
short e_jobc; /* job control counter */
dev_t e_tdev; /* controlling tty dev */
pid_t e_tpgid; /* tty process group id */
struct session *e_tsess; /* tty session pointer */
#define WMESGLEN 7
char e_wmesg[WMESGLEN + 1]; /* wchan message */
segsz_t e_xsize; /* text size */
short e_xrssize; /* text rss */
short e_xccount; /* text references */
short e_xswrss;
int32_t e_flag;
#define EPROC_CTTY 0x01 /* controlling tty vnode active */
#define EPROC_SLEADER 0x02 /* session leader */
#define COMAPT_MAXLOGNAME 12
char e_login[COMAPT_MAXLOGNAME]; /* short setlogin() name */
int32_t e_spare[4];
} kp_eproc;
};
struct extern_proc {
union {
struct {
struct proc *__p_forw; /* Doubly-linked run/sleep queue. */
struct proc *__p_back;
} p_st1;
struct timeval __p_starttime; /* process start time */
} p_un;
#define p_forw p_un.p_st1.__p_forw
#define p_back p_un.p_st1.__p_back
#define p_starttime p_un.__p_starttime
struct vmspace *p_vmspace; /* Address space. */
struct sigacts *p_sigacts; /* Signal actions, state (PROC ONLY). */
int p_flag; /* P_* flags. */
char p_stat; /* S* process status. */
/* These flags are kept in extern_proc.p_flag. */
#define P_ADVLOCK 0x00000001 /* Process may hold POSIX adv. lock */
#define P_CONTROLT 0x00000002 /* Has a controlling terminal */
#define P_LP64 0x00000004 /* Process is LP64 */
#define P_NOCLDSTOP 0x00000008 /* No SIGCHLD when children stop */
#define P_PPWAIT 0x00000010 /* Parent waiting for chld exec/exit */
#define P_PROFIL 0x00000020 /* Has started profiling */
#define P_SELECT 0x00000040 /* Selecting; wakeup/waiting danger */
#define P_CONTINUED 0x00000080 /* Process was stopped and continued */
#define P_SUGID 0x00000100 /* Has set privileges since last exec */
#define P_SYSTEM 0x00000200 /* Sys proc: no sigs, stats or swap */
#define P_TIMEOUT 0x00000400 /* Timing out during sleep */
#define P_TRACED 0x00000800 /* Debugged process being traced */
Now if we look back at our sysctl call sysctl(var_0h_2, 4, var_f8h, var_f0h, 0, 0)
, we know that
the array returned by sysctl will be stored on the stack at var_f8h (which means sp+0xf8)
. If we look
at what happens right after the sysctl call, we will notice the following sequence of instructions:
0x100005448 ldrb w8, [var_119h]
0x10000544c tbnz w8, 3, 0x1000054ec
We can observe that it is reading some value from the stack at var_119h (sp+0x119)
and if a certain bit is set, it will
break from the loop. Let’s calculate at which offset inside the returned structure we are reading in order to identify
its meaning:
0x119 - 0xf8 = 33
So at offset 33 inside the returned data structure, check if the 4th least significant bit is set. If you look at the
above code in proc.h, you will notice that extern_proc.p_flag
starts at offset 32. So when the app is loading
a 32 bits value from offset 33, it is actually loading the 3 most significat bytes of p_flag
, plus the p_stat
byte.
So even though the code works as expected, thechnically there is an (innocuous in this case) off-by-one error
when reading the p_stat
byte. We know that 1000
in binary means 8
in hex or decimal, so if we look back at proc.h
we will notice that the P_TRACED
flag has value 0x00000800
. As you can see, the only bit set here would be the 12th
(not the 4th), but since we are just reading the 3 most significant bytes of p_flag
, our value is going to have its 4th
least significant bit set if it’s being traced.
Cool, we have identified another anti-debugging trick! If you have struggled to follow the low-level details of this trick, you can find more information on how this is done at the source code level here. Let’s now nop the break, so if we ever need to trace the app, we can do it without any issues:
[0x100005400]> s 0x10000544c
[0x10000544c]> oo+
[0x10000544c]> wa nop
Written 4 byte(s) (nop) = wx 1f2003d5
Let’s now replace the original binary with the one we have just modified, repackage the .ipa and install it on the device. If everything went well we should be welcomed by the following screen:
Step 6 - Did the app just freeze?
At this point, if you try to insert anything in the textbox and tap verify, the app will become unresponsive. Let’s see what happens when we tap verify:
[0x100005884]> pdf @ method.ViewController.handleButtonClick:
;-- func.100005884:
┌ 532: method.ViewController.handleButtonClick: (int64_t arg1);
│ ; var int64_t var_0h @ sp+0x0
│ ; var int64_t var_10h @ sp+0x10
│ ; var int64_t var_10h_2 @ sp+0x18
│ ; var int64_t var_20h @ sp+0x20
│ ; var int64_t var_20h_2 @ sp+0x28
│ ; var int64_t var_30h @ sp+0x30
│ ; var int64_t var_30h_2 @ sp+0x38
│ ; var int64_t var_40h @ sp+0x40
│ ; var int64_t var_40h_2 @ sp+0x48
│ ; arg int64_t arg1 @ x0
│ 0x100005884 ff4301d1 sub sp, sp, 0x50
│ 0x100005888 f85f01a9 stp x24, x23, [var_10h]
│ 0x10000588c f65702a9 stp x22, x21, [var_20h]
│ 0x100005890 f44f03a9 stp x20, x19, [var_30h]
│ 0x100005894 fd7b04a9 stp x29, x30, [var_40h]
│ 0x100005898 fd030191 add x29, var_40h
│ 0x10000589c f40300aa mov x20, x0 ; arg1
│ 0x1000058a0 93470430 adr x19, 0x10000e191
│ 0x1000058a4 1f2003d5 nop
│ 0x1000058a8 e00313aa mov x0, x19
│ 0x1000058ac 6ffeff97 bl sym.func.100005268 ; sym.func.100005268(0x10000e191)
│ 0x1000058b0 f30300f9 str x19, [sp]
│ 0x1000058b4 a05f0310 adr x0, str.cstr.Code_Signature:__s ; 0x10000c4a8
│ 0x1000058b8 1f2003d5 nop
│ 0x1000058bc 1c0f0094 bl sym.imp.NSLog
│ 0x1000058c0 1f2003d5 nop
│ 0x1000058c4 f52c0458 ldr x21, 0x10000de60
│ 0x1000058c8 58000090 adrp x24, 0x10000d000
│ 0x1000058cc 003747f9 ldr x0, [x24, 0xe68] ; [0x10000de68:4]=0
│ ; reloc.NSString ; void *instance
│ 0x1000058d0 1f2003d5 nop
│ 0x1000058d4 b61c0458 ldr x22, str.stringWithCString:encoding: ; 0x100009da1
│ 0x1000058d8 42390410 adr x2, section.24.__DATA.__data ; 0x10000e000
│ 0x1000058dc 1f2003d5 nop
│ 0x1000058e0 e3030032 orr w3, wzr, 1
│ 0x1000058e4 e10316aa mov x1, x22 ; char *selector ; "stringWithCString:encoding:" str.stringWithCString:encoding:
│ 0x1000058e8 980f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ ; void *objc_msgSend(-1, "stringWithCString:encoding:")
│ 0x1000058ec fd031daa mov x29, x29
│ 0x1000058f0 a80f0094 bl sym.imp.objc_retainAutoreleasedReturnValue ; void objc_retainAutoreleasedReturnValue(void *instance)
│ ; void objc_retainAutoreleasedReturnValue(-1)
│ 0x1000058f4 f70300aa mov x23, x0
│ 0x1000058f8 003747f9 ldr x0, [x24, 0xe68] ; [0x10000de68:4]=0
│ ; reloc.NSString ; void *instance
│ 0x1000058fc e3030032 orr w3, wzr, 1
│ 0x100005900 e10316aa mov x1, x22 ; char *selector ; "stringWithCString:encoding:" str.stringWithCString:encoding:
│ 0x100005904 e20313aa mov x2, x19
│ 0x100005908 900f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ ; void *objc_msgSend(-1, "stringWithCString:encoding:")
│ 0x10000590c fd031daa mov x29, x29
│ 0x100005910 a00f0094 bl sym.imp.objc_retainAutoreleasedReturnValue ; void objc_retainAutoreleasedReturnValue(void *instance)
│ ; void objc_retainAutoreleasedReturnValue(-1)
│ 0x100005914 f60300aa mov x22, x0
│ 0x100005918 1f2003d5 nop
│ 0x10000591c a11a0458 ldr x1, str.decrypt:password: ; 0x100009dbd ; char *selector
│ 0x100005920 e00315aa mov x0, x21 ; void *instance
│ 0x100005924 e20317aa mov x2, x23
│ 0x100005928 e30316aa mov x3, x22
│ 0x10000592c 870f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ ; void *objc_msgSend(0x000000010000df60, "decrypt:password:")
│ 0x100005930 fd031daa mov x29, x29
│ 0x100005934 970f0094 bl sym.imp.objc_retainAutoreleasedReturnValue ; void objc_retainAutoreleasedReturnValue(void *instance)
│ ; void objc_retainAutoreleasedReturnValue(0x000000010000df60)
│ 0x100005938 f50300aa mov x21, x0
│ 0x10000593c e00316aa mov x0, x22 ; void *instance
│ 0x100005940 8b0f0094 bl sym.imp.objc_release ; void objc_release(void *instance)
│ ; void objc_release(-1)
│ 0x100005944 e00317aa mov x0, x23 ; void *instance
│ 0x100005948 890f0094 bl sym.imp.objc_release ; void objc_release(void *instance)
│ ; void objc_release(-1)
│ ┌─< 0x10000594c 750100b4 cbz x21, 0x100005978 ; unlikely
│ │ 0x100005950 480000b0 adrp x8, section.24.__DATA.__data ; 0x10000e000
│ │ 0x100005954 08414639 ldrb w8, [x8, 0x190] ; [0x10000e190:4]=0
│ │ ; section.25.__DATA.__bss
│ │ 0x100005958 1f050071 cmp w8, 1
│ ┌──< 0x10000595c 61020054 b.ne 0x1000059a8 ; likely
│ ││ 0x100005960 1f2003d5 nop
│ ││ 0x100005964 a1180458 ldr x1, str.showJailbreakAlert ; 0x100009dcf ; char *selector
│ ││ 0x100005968 e00314aa mov x0, x20 ; void *instance
│ ││ 0x10000596c 770f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ ││ ; void *objc_msgSend(-1, "showJailbreakAlert")
│ ││ 0x100005970 140080d2 movz x20, 0
│ ┌───< 0x100005974 3a000014 b 0x100005a5c
│ │││ ; CODE XREF from method.ViewController.handleButtonClick: @ 0x10000594c
│ ││└─> 0x100005978 1f2003d5 nop
│ ││ 0x10000597c e0250458 ldr x0, reloc.UIAlertView ; 0x10000de38 ; void *instance
│ ││ 0x100005980 1f2003d5 nop
│ ││ 0x100005984 a1120458 ldr x1, str.alloc ; 0x10000dbd8 ; char *selector ; section.3.__TEXT.__objc_methname
│ ││ 0x100005988 700f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector) ; "H"
│ ││ ; void *objc_msgSend(-1, "alloc")
│ ││ 0x10000598c 48000090 adrp x8, 0x10000d000
│ ││ 0x100005990 01f145f9 ldr x1, [x8, 0xbe0] ; [0xbe0:4]=-1 ; 3040 ; (pstr 0x100009c46) "initWithTitle:message:delegate:cancelButtonTitle:otherButtonTit" ; str.initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:
│ ││ 0x100005994 a2590310 adr x2, str.cstr.Decryption_Failed. ; 0x10000c4c8
│ ││ 0x100005998 1f2003d5 nop
│ ││ 0x10000599c 635a0310 adr x3, str.cstr.TAMPERING_DETECTED_ ; 0x10000c4e8
│ ││ 0x1000059a0 1f2003d5 nop
│ ││┌─< 0x1000059a4 28000014 b 0x100005a44
│ │││ ; CODE XREF from method.ViewController.handleButtonClick: @ 0x10000595c
│ │└──> 0x1000059a8 1f2003d5 nop
│ │ │ 0x1000059ac a1160458 ldr x1, 0x10000dc80 ; char *selector
│ │ │ 0x1000059b0 e00314aa mov x0, x20 ; void *instance
│ │ │ 0x1000059b4 650f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ │ │ ; void *objc_msgSend(-1, "theTextField")
│ │ │ 0x1000059b8 fd031daa mov x29, x29
│ │ │ 0x1000059bc 750f0094 bl sym.imp.objc_retainAutoreleasedReturnValue ; void objc_retainAutoreleasedReturnValue(void *instance)
│ │ │ ; void objc_retainAutoreleasedReturnValue(-1)
│ │ │ 0x1000059c0 f60300aa mov x22, x0
│ │ │ 0x1000059c4 1f2003d5 nop
│ │ │ 0x1000059c8 01160458 ldr x1, str.text ; 0x100009def ; char *selector
│ │ │ 0x1000059cc 5f0f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ │ │ ; void *objc_msgSend(-1, "text")
│ │ │ 0x1000059d0 fd031daa mov x29, x29
│ │ │ 0x1000059d4 6f0f0094 bl sym.imp.objc_retainAutoreleasedReturnValue ; void objc_retainAutoreleasedReturnValue(void *instance)
│ │ │ ; void objc_retainAutoreleasedReturnValue(-1)
│ │ │ 0x1000059d8 f70300aa mov x23, x0
│ │ │ 0x1000059dc 1f2003d5 nop
│ │ │ 0x1000059e0 81150458 ldr x1, str.isEqualToString: ; 0x100009df4 ; char *selector
│ │ │ 0x1000059e4 e20315aa mov x2, x21
│ │ │ 0x1000059e8 580f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ │ │ ; void *objc_msgSend(-1, "isEqualToString:")
│ │ │ 0x1000059ec f80300aa mov x24, x0
│ │ │ 0x1000059f0 e00317aa mov x0, x23 ; void *instance
│ │ │ 0x1000059f4 5e0f0094 bl sym.imp.objc_release ; void objc_release(void *instance)
│ │ │ ; void objc_release(-1)
│ │ │ 0x1000059f8 e00316aa mov x0, x22 ; void *instance
│ │ │ 0x1000059fc 5c0f0094 bl sym.imp.objc_release ; void objc_release(void *instance)
│ │ │ ; void objc_release(-1)
│ │ │ 0x100005a00 1f2003d5 nop
│ │ │ 0x100005a04 a0210458 ldr x0, reloc.UIAlertView ; 0x10000de38 ; void *instance
│ │ │ 0x100005a08 1f2003d5 nop
│ │ │ 0x100005a0c 610e0458 ldr x1, str.alloc ; 0x10000dbd8 ; char *selector ; section.3.__TEXT.__objc_methname
│ │ │ 0x100005a10 4e0f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector) ; "H"
│ │ │ ; void *objc_msgSend(-1, "alloc")
│ │ │ 0x100005a14 48000090 adrp x8, 0x10000d000
│ │ │ 0x100005a18 01f145f9 ldr x1, [x8, 0xbe0] ; [0xbe0:4]=-1 ; 3040 ; (pstr 0x100009c46) "initWithTitle:message:delegate:cancelButtonTitle:otherButtonTit" ; str.initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:
│ │┌──< 0x100005a1c d8000034 cbz w24, 0x100005a34 ; likely
│ │││ 0x100005a20 42570310 adr x2, str.cstr.Congratulations_ ; 0x10000c508
│ │││ 0x100005a24 1f2003d5 nop
│ │││ 0x100005a28 03580310 adr x3, str.cstr.You_found_the_secret__ ; 0x10000c528
│ │││ 0x100005a2c 1f2003d5 nop
│ ┌────< 0x100005a30 05000014 b 0x100005a44
│ ││││ ; CODE XREF from method.ViewController.handleButtonClick: @ 0x100005a1c
│ ││└──> 0x100005a34 a2580310 adr x2, str.cstr.Verification_Failed. ; 0x10000c548
│ ││ │ 0x100005a38 1f2003d5 nop
│ ││ │ 0x100005a3c 63590310 adr x3, str.cstr.This_is_not_the_string_you_are_looking_for._Try_again. ; 0x10000c568
│ ││ │ 0x100005a40 1f2003d5 nop
│ ││ │ ; CODE XREFS from method.ViewController.handleButtonClick: @ 0x1000059a4, 0x100005a30
│ └──└─> 0x100005a44 25490310 adr x5, 0x10000c368
│ │ 0x100005a48 1f2003d5 nop
│ │ 0x100005a4c e40314aa mov x4, x20
│ │ 0x100005a50 060080d2 movz x6, 0
│ │ 0x100005a54 3d0f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ │ ; void *objc_msgSend(-1, "initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:")
│ │ 0x100005a58 f40300aa mov x20, x0
│ │ ; CODE XREF from method.ViewController.handleButtonClick: @ 0x100005974
│ └───> 0x100005a5c 7f7e01a9 stp xzr, xzr, [x19, 0x10]
│ 0x100005a60 7f7e00a9 stp xzr, xzr, [x19]
│ 0x100005a64 1f2003d5 nop
│ 0x100005a68 010c0458 ldr x1, str.show ; 0x100009c8a ; char *selector
│ 0x100005a6c e00314aa mov x0, x20 ; void *instance
│ 0x100005a70 360f0094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ ; void *objc_msgSend(-1, "show")
│ 0x100005a74 e00314aa mov x0, x20 ; void *instance
│ 0x100005a78 3d0f0094 bl sym.imp.objc_release ; void objc_release(void *instance)
│ ; void objc_release(-1)
│ 0x100005a7c e00315aa mov x0, x21
│ 0x100005a80 fd7b44a9 ldp x29, x30, [var_40h]
│ 0x100005a84 f44f43a9 ldp x20, x19, [var_30h]
│ 0x100005a88 f65742a9 ldp x22, x21, [var_20h]
│ 0x100005a8c f85f41a9 ldp x24, x23, [var_10h]
│ 0x100005a90 ff430191 add sp, sp, 0x50 ; 0x178000
└ ┌─< 0x100005a94 360f0014 b sym.imp.objc_release
└ │ ; void objc_release(0x000000010000df60)
[0x100005884]>
We see that there is a call to sym.func.100005268
. The disassembly is shown below:
[0x1000053ec]> pdf @ sym.func.100005268
; STRING XREF from sym.func.100005268 @ 0x100005298
; CALL XREF from method.ViewController.handleButtonClick: @ 0x1000058ac
┌ 396: sym.func.100005268 (int64_t arg1);
│ ; var int64_t var_0h @ sp+0x0
│ ; var int64_t var_8h @ sp+0x8
│ ; var int64_t var_10h @ sp+0x10
│ ; var int64_t var_28h @ sp+0x28
│ ; var int64_t var_38h @ sp+0x38
│ ; var int64_t var_40h @ sp+0x40
│ ; var int64_t var_40h_2 @ sp+0x48
│ ; var int64_t var_50h @ sp+0x50
│ ; var int64_t var_50h_2 @ sp+0x58
│ ; var int64_t var_60h @ sp+0x60
│ ; var int64_t var_60h_2 @ sp+0x68
│ ; var int64_t var_70h @ sp+0x70
│ ; var int64_t var_70h_2 @ sp+0x78
│ ; var int64_t var_80h @ sp+0x80
│ ; var int64_t var_80h_2 @ sp+0x88
│ ; arg int64_t arg1 @ x0
│ 0x100005268 ff4302d1 sub sp, sp, 0x90
│ 0x10000526c fa6704a9 stp x26, x25, [var_40h]
│ 0x100005270 f85f05a9 stp x24, x23, [var_50h]
│ 0x100005274 f65706a9 stp x22, x21, [var_60h]
│ 0x100005278 f44f07a9 stp x20, x19, [var_70h]
│ 0x10000527c fd7b08a9 stp x29, x30, [var_80h]
│ 0x100005280 fd030291 add x29, var_80h
│ 0x100005284 f30300aa mov x19, x0 ; arg1
│ 0x100005288 1f2003d5 nop
│ 0x10000528c 686c0358 ldr x8, reloc.__stack_chk_guard ; 0x10000c018
│ 0x100005290 080140f9 ldr x8, [x8]
│ 0x100005294 e81f00f9 str x8, [var_38h]
│ 0x100005298 80feff10 adr x0, sym.func.100005268 ; 0x100005268
│ 0x10000529c 1f2003d5 nop
│ 0x1000052a0 e1230091 add x1, var_8h
│ 0x1000052a4 db100094 bl sym.imp.dladdr ; "`"
│ ┌─< 0x1000052a8 60000034 cbz w0, 0x1000052b4 ; unlikely
│ │ 0x1000052ac f60b40f9 ldr x22, [var_10h] ; [0x10:4]=-1 ; 16
│ ┌──< 0x1000052b0 560100b5 cbnz x22, 0x1000052d8 ; unlikely
│ ││ ; CODE XREF from sym.func.100005268 @ 0x1000052a8
│ │└─> 0x1000052b4 a0860310 adr x0, str.cstr._Error:_Could_not_resolve_symbol_xyz ; 0x10000c388
│ │ 0x1000052b8 1f2003d5 nop
│ │ 0x1000052bc 9c100094 bl sym.imp.NSLog
│ │ 0x1000052c0 1f2003d5 nop
│ │ 0x1000052c4 e05b0458 ldr x0, reloc.NSThread ; 0x10000de40 ; void *instance
│ │ 0x1000052c8 1f2003d5 nop
│ │ 0x1000052cc 21490458 ldr x1, str.exit ; 0x100009c8f ; char *selector
│ │ 0x1000052d0 1e110094 bl sym.imp.objc_msgSend ; void *objc_msgSend(void *instance, char *selector)
│ │ ; void *objc_msgSend(-1, "exit")
│ │ 0x1000052d4 f60b40f9 ldr x22, [var_10h] ; [0x10:4]=-1 ; 16
│ │ ; CODE XREF from sym.func.100005268 @ 0x1000052b0
│ └──> 0x1000052d8 d5720091 add x21, x22, 0x1c
│ 0x1000052dc d81240b9 ldr w24, [x22, 0x10] ; [0x10:4]=-1 ; 16
│ 0x1000052e0 d4db0230 adr x20, str.__TEXT ; 0x10000ae59
│ 0x1000052e4 1f2003d5 nop
│ ; CODE XREFS from sym.func.100005268 @ 0x1000052f8, 0x100005324
│ ┌┌─> 0x1000052e8 19008012 movn w25, 0
│ ╎╎ 0x1000052ec f70315aa mov x23, x21
│ ╎╎ ; CODE XREF from sym.func.100005268 @ 0x100005320
│ ┌───> 0x1000052f0 39070011 add w25, w25, 1 ; 0x100000000
│ ╎╎╎ ; sym.__mh_execute_header
│ ╎╎╎ 0x1000052f4 3f03186b cmp w25, w24
│ ╎└──< 0x1000052f8 82ffff54 b.hs 0x1000052e8 ; unlikely
│ ╎ ╎ 0x1000052fc e80240b9 ldr w8, [x23]
│ ╎ ╎ 0x100005300 1f050071 cmp w8, 1
│ ╎┌──< 0x100005304 a1000054 b.ne 0x100005318 ; likely
│ ╎│╎ 0x100005308 e0220091 add x0, x23, 8 ; const char *s1
│ ╎│╎ 0x10000530c e10314aa mov x1, x20 ; const char *s2 ; "__TEXT" str.__TEXT
│ ╎│╎ 0x100005310 3e110094 bl sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
│ ┌────< 0x100005314 a0000034 cbz w0, 0x100005328 ; unlikely
│ │╎│╎ ; CODE XREF from sym.func.100005268 @ 0x100005304
│ │╎└──> 0x100005318 e80640b9 ldr w8, [x23, 4] ; [0x4:4]=-1 ; 4
│ │╎ ╎ 0x10000531c f702088b add x23, x23, x8
│ │└───< 0x100005320 97feffb5 cbnz x23, 0x1000052f0 ; likely
│ │ └─< 0x100005324 f1ffff17 b 0x1000052e8
│ │ ; CODE XREF from sym.func.100005268 @ 0x100005314
│ └────> 0x100005328 f4e20091 add x20, x23, 0x38
│ 0x10000532c f83240b9 ldr w24, [x23, 0x30] ; [0x30:4]=-1 ; 48
│ ┌─< 0x100005330 98010034 cbz w24, 0x100005360 ; unlikely
│ │ 0x100005334 19008052 movz w25, 0
│ │ 0x100005338 55d90210 adr x21, str.__text ; 0x10000ae60
│ │ 0x10000533c 1f2003d5 nop
│ │ ; CODE XREF from sym.func.100005268 @ 0x10000535c
│ ┌──> 0x100005340 e00314aa mov x0, x20 ; const char *s1
│ ╎│ 0x100005344 e10315aa mov x1, x21 ; const char *s2 ; "__text" str.__text
│ ╎│ 0x100005348 30110094 bl sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
│ ┌───< 0x10000534c a0000034 cbz w0, 0x100005360 ; unlikely
│ │╎│ 0x100005350 94120191 add x20, x20, 0x44
│ │╎│ 0x100005354 39070011 add w25, w25, 1
│ │╎│ 0x100005358 3f03186b cmp w25, w24
│ │└──< 0x10000535c 23ffff54 b.lo 0x100005340 ; likely
│ │ │ ; CODE XREFS from sym.func.100005268 @ 0x100005330, 0x10000534c
│ └─└─> 0x100005360 88064429 ldp w8, w1, [x20, 0x20]
│ 0x100005364 e91a40b9 ldr w9, [x23, 0x18] ; [0x18:4]=-1 ; 24
│ 0x100005368 0801160b add w8, w8, w22
│ 0x10000536c 0801094b sub w8, w8, w9
│ 0x100005370 007d4093 sxtw x0, w8
│ 0x100005374 f5a30091 add x21, var_28h
│ 0x100005378 e2a30091 add x2, var_28h
│ 0x10000537c 57100094 bl sym.imp.CC_MD5
│ 0x100005380 160080d2 movz x22, 0
│ 0x100005384 14d70270 adr x20, str._02x ; 0x10000ae67
│ 0x100005388 1f2003d5 nop
│ ; CODE XREF from sym.func.100005268 @ 0x1000053b4
│ ┌─> 0x10000538c a86a7638 ldrb w8, [x21, x22]
│ ╎ 0x100005390 e80300f9 str x8, [sp]
│ ╎ 0x100005394 02008092 movn x2, 0
│ ╎ 0x100005398 e00313aa mov x0, x19
│ ╎ 0x10000539c 01008052 movz w1, 0
│ ╎ 0x1000053a0 e30314aa mov x3, x20 ; "%02x" str._02x
│ ╎ 0x1000053a4 6e100094 bl sym.imp.__sprintf_chk
│ ╎ 0x1000053a8 d6060091 add x22, x22, 1
│ ╎ 0x1000053ac 730a0091 add x19, x19, 2
│ ╎ 0x1000053b0 df4200f1 cmp x22, 0x10
│ └─< 0x1000053b4 c1feff54 b.ne 0x10000538c ; likely
│ 0x1000053b8 e81f40f9 ldr x8, [var_38h] ; [0x38:4]=-1 ; 56
│ 0x1000053bc 1f2003d5 nop
│ 0x1000053c0 c9620358 ldr x9, reloc.__stack_chk_guard ; 0x10000c018
│ 0x1000053c4 290140f9 ldr x9, [x9]
│ 0x1000053c8 280108cb sub x8, x9, x8
│ ┌─< 0x1000053cc 280100b5 cbnz x8, 0x1000053f0 ; likely
│ │ 0x1000053d0 00008052 movz w0, 0
│ │ 0x1000053d4 fd7b48a9 ldp x29, x30, [var_80h]
│ │ 0x1000053d8 f44f47a9 ldp x20, x19, [var_70h]
│ │ 0x1000053dc f65746a9 ldp x22, x21, [var_60h]
│ │ 0x1000053e0 f85f45a9 ldp x24, x23, [var_50h]
│ │ 0x1000053e4 fa6744a9 ldp x26, x25, [var_40h]
│ │ 0x1000053e8 ff430291 add sp, sp, 0x90 ; 0x178000
│ │ 0x1000053ec c0035fd6 ret
│ │ ; CODE XREF from sym.func.100005268 @ 0x1000053cc
└ └─> 0x1000053f0 5e100094 bl sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) ; method.ViewController.abc
└ ; void __stack_chk_fail(void)
[0x1000053ec]>
This method can be summarized as follows:
- It obtains the image base address
- Starts parsing the Mach header
- Loops through load commands
- Finds the text section and calculates its MD5
- Returns the text section’s MD5
Unfortunately this code was originally written for 32 bits devices, so even if the binary runs on 64 bits processors, this method will create an infinite loop due to the differences between 32 bits and 64 bits mach headers not being handled properly. I wrote the following set of patches that fixes the code to work on 64 bits devices:
# re-open file in read/write mode
oo+
# Fix offset to LC_COMMAND_64
s 0x1000052d8
wa add x21, x22, 0x20
# Remove unnecessary check for LC_COMMAND==LC_SEGMENT
s 0x100005304
wa nop
# Fix sizeof(segment_command_64)
s 0x100005328
wa add x20, x23, 0x48
# Fix sizeof(section_64)
s 0x100005350
wa add x20, x20, 0x50
# Fix register size for section_64.addr and section_64.size
s 0x100005360
wa ldp x8, x1, [x20, 0x20]
# Fix register size for segment_command_64.vmaddr:
s 0x100005364
wa ldr x9, [x23, 0x18]
# Fix register size for baseaddr and section_64.addr
s 0x100005368
wa add x8, x8, x22
# Fix register size for section_64.addr and segment_command_64.vmaddr
s 0x10000536c
wa sub x8, x8, x9
# Fix register size for CC_MD5's first argument
s 0x100005370
wa mov x0,x8
# Write secret
s 0x10000e000
wz uMqEK/JCNg+njduTS840mrac3zjLP1kpwV508f0119E=
You may notice that the last patch is replacing a secret which is tied to the text section’s
MD5. Since we patched the binary to work on 64 bits devices, the corresponding secret must be updated
as well. You can save the patches above in patches.r2
and apply them with the following command:
r2 -i patches.r2 UnCrackable\ Level\ 2
Now if we repackage the app we should see the following alert when clicking verify:
Step 7 - Bypass anti-tampering
That message means the challenge has detected that we patched the code (remember those anti-debug patches?), so we need to find a way to bypass this integrity check and let it think the binary is correct. Since we know the integrity check is based on the code section’s MD5 we can plan our attack as follows:
- Take the original IPA and extract the thin binaries
- Take the 64 bit binary and apply the 64 bit patches to get the “unmodified” binary
- Calculate the text section’s MD5
- Use frida to spoof the calculated MD5 at runtime and bypass inegrity checks
In order to dump the text section I will use this little script which uses radare2:
#!/bin/bash
rm -f textSection
out=$(r2 -q -c "iS" "$1" | grep text)
echo $out
addr=$(echo $out | cut -d ' ' -f4)
size=$(echo $out | cut -d ' ' -f3)
echo $addr $size
r2 -q -c "pr $size @ $addr > textSection" "$1"
We can save it as dumpTextSection.sh and run it as follows:
./dumpTextSection.sh UnCrackable\ Level\ 2
0 0x000051f8 0x4280 0x1000051f8 0x4280 -r-x 0.__TEXT.__text
0x1000051f8 0x4280
And calculate the MD5:
md5sum textSection
19bfedd6f969b290dd236a201b057e5a textSection
Now we need to find a good place to inject the original MD5 at runtime to fool integrity checks. After
looking at the ViewController.handleButtonClick
method, we notice that the MD5 is passed as argument
to stringWithCString:encoding:
before being passed to decrypt:password:
. From that we can make a
guess that the binary’s MD5 is used to decrypt the app secret! That’s also why the only solution to this
challenge is to either spoof the right MD5 or keep the code section intact. Unfortunately, just spoofing
the MD5 would not work on the old challenge since the secret was not encrypted with either the 32 or the
64 bits’s binary code section. That is the reason why those patches were needed in order to create a
challenge that is actually solvable.
We can now hook stringWithCString:encoding:
with frida and replace the MD5 at runtime:
/*
* Auto-generated by Frida. Please modify to match the signature of +[NSString stringWithCString:encoding:].
* This stub is currently auto-generated from manpages when available.
*
* For full API reference, see: https://frida.re/docs/javascript-api/
*/
{
/**
* Called synchronously when about to call +[NSString stringWithCString:encoding:].
*
* @this {object} - Object allowing you to store state for use in onLeave.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {array} args - Function arguments represented as an array of NativePointer objects.
* For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8.
* It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
* @param {object} state - Object allowing you to keep state across function calls.
* Only one JavaScript function will execute at a time, so do not worry about race-conditions.
* However, do not use this to store function arguments across onEnter/onLeave, but instead
* use "this" which is an object for keeping state local to an invocation.
*/
onEnter(log, args, state) {
if (args[2].readUtf8String().length==32){
md5="19bfedd6f969b290dd236a201b057e5a"
args[2].writeUtf8String(md5)
}
},
/**
* Called synchronously when about to return from +[NSString stringWithCString:encoding:].
*
* See onEnter for details.
*
* @this {object} - Object allowing you to access state stored in onEnter.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {NativePointer} retval - Return value represented as a NativePointer object.
* @param {object} state - Object allowing you to keep state across function calls.
*/
onLeave(log, retval, state) {
}
}
Save the above file in __handlers__/NSString/stringWithCString_encoding_.js
and run frida as follows:
frida-trace -U Uncrackable2 -m "+[NSString stringWithCString:encoding:]"
You sohuld now see the following message:
Cool, we have bypassed the anti-tampering! We will now go ahead and bypass jailbreak detection.
Step 8 - Bypassing jailbreak detection
If we look back at the method ViewController.viewDidLoad
we can see the following jailbreak checks:
- Check if the following resources exist:
/Applications/Cydia.app
/Library/MobileSubstrate/MobileSubstrate.dylib
/bin/bash
/usr/bin/sshd
/etc/apt
- Check if the following file can be written:
/private/wut.txt
- Check if the following URL can be opened to check if Cydia is installed:
cydia://package/com.example.package
We could patch each of those checks, but there is a more elegant way to rule them all: change the code responsible for setting the “jailbroken” flag:
:> s 0x1000057c4
:> oo+
:> wa mov w8, wzr
Written 4 byte(s) (orr w8, wzr, wzr) = wx e8031f2a
Let’s repackage, install the app and see what happens (remember to keep replacing the MD5 with frida):
Good! We have bypassed jailbreak detection! We just need to type the right secret to complete the challenge.
Step 9 - Finding the secret and solving the challenge
We have seen a call to decrypt:password:
in ViewController.handleButtonClick
, so in order to
obtain the secret we can hook the decrypt:password:
method with frida and print the returned
secret. We can do that with the following code:
/*
* Auto-generated by Frida. Please modify to match the signature of +[AESCrypt encrypt:password:].
* This stub is currently auto-generated from manpages when available.
*
* For full API reference, see: https://frida.re/docs/javascript-api/
*/
{
/**
* Called synchronously when about to call +[AESCrypt encrypt:password:].
*
* @this {object} - Object allowing you to store state for use in onLeave.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {array} args - Function arguments represented as an array of NativePointer objects.
* For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8.
* It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
* @param {object} state - Object allowing you to keep state across function calls.
* Only one JavaScript function will execute at a time, so do not worry about race-conditions.
* However, do not use this to store function arguments across onEnter/onLeave, but instead
* use "this" which is an object for keeping state local to an invocation.
*/
onEnter(log, args, state) {
},
/**
* Called synchronously when about to return from +[AESCrypt encrypt:password:].
*
* See onEnter for details.
*
* @this {object} - Object allowing you to access state stored in onEnter.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {NativePointer} retval - Return value represented as a NativePointer object.
* @param {object} state - Object allowing you to keep state across function calls.
*/
onLeave(log, retval, state) {
var secret = new ObjC.Object(ptr(retval)).toString()
log(`Decrypted secret: ${secret}`)
}
}
Let’s save this code as __handlers__/AESCrypt/decrypt_password_.js
and run the following
commands:
$ frida-trace -U Uncrackable2 -m "+[NSString stringWithCString:encoding:]" &
frida-trace -U Uncrackable2 -m "+[AESCrypt decrypt:password:]"
[1] 133301
Attaching...
Instrumenting...
+[NSString stringWithCString:encoding:]: Loaded handler at "/home/kali/owasp-mstg/Crackmes/iOS/Level_02/original/__handlers__/NSString/stringWithCString_encoding_.js"
+[AESCrypt decrypt:password:]: Loaded handler at "/home/kali/owasp-mstg/Crackmes/iOS/Level_02/original/__handlers__/AESCrypt/decrypt_password_.js"
Started tracing 1 function. Press Ctrl+C to stop.
Started tracing 1 function. Press Ctrl+C to stop.
/* TID 0x403 */
4226 ms +[AESCrypt decrypt:0x283dc4600 password:0x283dc6e40]
4226 ms Decrypted secret: MySuperSecretString
Excellent! We’ve found the secret! Let’s verify it and see if it is accepted as the right solution:
Conclusions
In this blog post we have seen a few possible ways of bypassing some anti-debugging, anti-tampering, and anti-jailbreak protections. We also did the impossible and solved a challenge that could not be completed without fixing the 64 bits parsing code and updating the secret string accordingly.
If you have any questions, comments, alternative solutions, or would simply like to discuss, you can reach out to me via DM on my Twitter handle at the bottom of this page.