Introduction
This blog post focuses specifically on dynamically bypassing ptrace
iOS anti-debugging defence which prevents an iOS mobile application from entering into a debugging state. The radare2
tool used, as well as the r2frida
and r2ghidra
plugins to perform static and dynammic analysis. The ptrace
syscall can be found several *nix operating systems. It is generally used for debugging breakpoints and tracing system calls. It is used from native debuggers to keep track. Also, this blog post covers only one feature of the ptrace
syscall, the 'PT_DENY_ATTACH'
.
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.
For more examples and bypasses of other security mechanisms such as bypassing different anti-RE defences on iOS, including getppid()
, sysctl()
, jailbreak detection, certificate pinning, dynamic instrumentation, you can refer to my other blog post at this link
For the purpose of this blog post the ios-challenge-2
application used to showcase the identification of the ptrace
anti-debugging technique as well as to present a way to bypass it.
Installing r2frida plugin
Assuming that radare2
is already installed on the local machine. Also, the r2frida
plugin is installed which aims to join the capabilities of static analysis of radare2
and the instrumentation provided by frida. The recommended way to install r2frida
is by using r2pm
The following command initializes the package control
~/ r2pm init
Afterwards, the following command used to install r2frida
plugin
~/ r2pm -ci r2frida [....] pkg-config --cflags r_core -I/usr/local/Cellar/radare2/5.8.8/include/libr cc src/io_frida.o -o io_frida.dylib -fPIC -g -L/usr/local/Cellar/radare2/5.8.8/lib -lr_core -lr_config -ldl -lr_debug -ldl -lr_bin -ldl -lr_lang -ldl -lr_anal -ldl -lr_bp -ldl -lr_egg -ldl -lr_asm -ldl -lr_flag -ldl -lr_search -ldl -lr_syscall -ldl -lr_fs -ldl -lr_magic -ldl -lr_arch -ldl -lr_esil -ldl -lr_reg -ldl -lr_io -ldl -lr_socket -ldl -lr_cons -ldl -lr_crypto -ldl -lr_util -ldl -shared -fPIC -Wl,-exported_symbol,_radare_plugin -Wl,-no_compact_unwind ext/frida/libfrida-core.a -framework Foundation -lbsm -framework AppKit -lresolv mkdir -p /"/Users/xenovas/.local/share/radare2/plugins" mkdir -p /"/Users/xenovas/.local/share/radare2/prefix/bin" rm -f "//Users/xenovas/.local/share/radare2/plugins/io_frida.dylib" cp -f io_frida.dylib* /"/Users/xenovas/.local/share/radare2/plugins" cp -f src/r2frida-compile /"/Users/xenovas/.local/share/radare2/prefix/bin"
The following command shows the installed apps as well as the running apps on the virtual device
~/ r2 frida://apps/usb PID Name Identifier ----------------------------------- [..] - Podcasts com.apple.podcasts - Reminders com.apple.reminders - Safari com.apple.mobilesafari - Settings com.apple.Preferences - Shortcuts com.apple.shortcuts - Stocks com.apple.stocks - Substitute com.ex.substitute.settings - TV com.apple.tv - Tips com.apple.tips - Translate com.apple.Translate - Voice Memos com.apple.VoiceMemos - Wallet com.apple.Passbook - Watch com.apple.Bridge - Weather com.apple.weather - iTunes Store com.apple.MobileStore 2754 ios-challenge-2 re.murphy.ios-challenge-2
Installing r2ghidra plugin
In order to enhance reverse engineering capabilities provided by radare2
we integrate the Ghidra
decompiler by installing the r2ghidra
plugin. The following command used to install the plugin
~/ r2pm -ci r2ghidra [.....] make install PLUGDIR=/Users/xenovas/.local/share/radare2/plugins BINDIR=/Users/xenovas/.local/share/radare2/prefix/bin mkdir -p /Users/xenovas/.local/share/radare2/prefix/bin cp -f sleighc /Users/xenovas/.local/share/radare2/prefix/bin mkdir -p /Users/xenovas/.local/share/radare2/plugins for a in *.dylib ; do rm -f "//Users/xenovas/.local/share/radare2/plugins/$a" ; done cp -f *.dylib /Users/xenovas/.local/share/radare2/plugins rm -f /Users/xenovas/.local/share/radare2/plugins/asm*ghidra*.dylib rm -f /Users/xenovas/.local/share/radare2/plugins/anal*ghidra*.dylib codesign -f -s - /Users/xenovas/.local/share/radare2/plugins/*.dylib [....]
Furthermore, we also install SLEIGH dicompiler / disassembler that comes with r2ghidra
using the following command
~/ r2pm -ci r2ghidra-sleigh [....] inflating: r2ghidra_sleigh-5.7.6/ppc_32_quicciii_le.sla inflating: r2ghidra_sleigh-5.7.6/JVM.ldefs inflating: r2ghidra_sleigh-5.7.6/6502.sla inflating: r2ghidra_sleigh-5.7.6/x86.sla inflating: r2ghidra_sleigh-5.7.6/ARM7_le.sla inflating: r2ghidra_sleigh-5.7.6/x86-16.pspec inflating: r2ghidra_sleigh-5.7.6/tricore.pspec inflating: r2ghidra_sleigh-5.7.6/ppc_64.cspec inflating: r2ghidra_sleigh-5.7.6/ARM4_le.sla inflating: r2ghidra_sleigh-5.7.6/riscv64-fp.cspec inflating: r2ghidra_sleigh-5.7.6/RV64IC.pspec inflating: r2ghidra_sleigh-5.7.6/x86gcc.cspec inflating: r2ghidra_sleigh-5.7.6/hexagon.cspec inflating: r2ghidra_sleigh-5.7.6/atmega256.pspec inflating: r2ghidra_sleigh-5.7.6/ppc_64_le.sla inflating: r2ghidra_sleigh-5.7.6/65c02.sla inflating: r2ghidra_sleigh-5.7.6/AARCH64.sla inflating: r2ghidra_sleigh-5.7.6/AARCH64BE.sla inflating: r2ghidra_sleigh-5.7.6/avr8xmega.sla inflating: r2ghidra_sleigh-5.7.6/ppc_64_be.sla inflating: r2ghidra_sleigh-5.7.6/avr8.sla inflating: r2ghidra_sleigh-5.7.6/ARM5_le.sla inflating: r2ghidra_sleigh-5.7.6/MCS96.sla
Application dynamic analysis
After installing and running the application it exits immediately.
The following command spawns the application and after it exits, the detach reason and the process termination message shows up on the output. Lets see this in practice
~/ r2 frida://spawn/usb//re.murphy.ios-challenge-2
INFO: Using safe io mode.
-- git pull now
[0x00000000]> INFO: DetachReason: FRIDA_SESSION_DETACH_REASON_PROCESS_TERMINATED
Now lets spawn the application again but this time we use the :dtf
command which traces the address of the ptrace
syscall and also shows the arguments in integer format
[0x00000000]> oo INFO: Using safe io mode. INFO: resumed spawned process [0x00000000]> :dtf ptrace ii true [0x00000000]> :dc INFO: resumed spawned process [0x00000000]> [dtf onLeave][Wed Aug 30 2023 00:57:33 GMT-0700]ptrace@0x1f9970560
- args:31
, 0.Retval: 0x0
INFO: DetachReason:FRIDA_SESSION_DETACH_REASON_PROCESS_TERMINATED
As we see the application terminated again and from the args value (31
) we are able to determine that the feature of the ptrace
syscall is the 'PT_DENY_ATTACH'
.
According with OWASP-MASTG and iOS Anti-Reversing Defenses
, the ptrace
syscall is not part of the public iOS API. Non-public APIs are prohibited, and the App Store may reject apps that include them. Because of this, ptrace is not directly called in the code; it's called when a ptrace function pointer is obtained via dlsym
. The following code snippet represents the above logic
#import <dlfcn.h>
#import <sys/types.h>
#import <stdio.h>
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
void anti_debug() {
ptrace_ptr_t ptrace_ptr = (ptrace_ptr_t)dlsym(RTLD_SELF, "ptrace");
ptrace_ptr(31, 0, 0, 0); // PTRACE_DENY_ATTACH = 31
}
Application static analysis
At this point and after we gained all the needed knowledge regarding the ptrace
anti-debugging technique, we can move forward to perform a static analysis.
First we unzip the .ipa
file in order to statically examine the application using radare2
~/ unzip ios-challenge-2.ipa [..] ~/ r2 -A Payload/ios-challenge-2.app/ios-challenge-2 INFO: Analyze all flags starting with sym. and entry0 (aa) INFO: Analyze all functions arguments/locals (afva@@@F) INFO: Analyze function calls (aac) INFO: Analyze len bytes of instructions for references (aar) INFO: Check for objc references (aao) INFO: Parsing metadata in ObjC to find hidden xrefs INFO: Found 38 objc xrefs INFO: Found 38 objc xrefs in 0 dwords INFO: Finding and parsing C++ vtables (avrr) INFO: Finding function preludes (aap) INFO: Finding xrefs in noncode section (e anal.in=io.maps.x) INFO: Analyze value pointers (aav) INFO: aav: 0x100000000-0x10000c000 in 0x100000000-0x10000c000 INFO: Emulate functions to find computed references (aaef) INFO: Type matching analysis for all functions (aaft) INFO: Propagate noreturn information (aanr) INFO: Use -AA or aaaa to perform additional experimental analysis -- Your problems are solved in an abandoned branch somewhere [0x100008e44]> axt? Usage: axt[?gq*] find data/code references to this address | axtj [addr] find data/code references to this address and print in json format | axtg [addr] display commands to generate graphs according to the xrefs | axtq [addr] find and list the data/code references in quiet mode | axtm [addr] show xrefs to in 'make' syntax (see aflm and axfm) | axt* [addr] same as axt, but prints as r2 commands [0x100008e44]>
As seen previously, the ptrace
syscall is generally invoked via dlsym
so we search for it as follows
[0x100008e44]> axt sym.imp.dlsym sym.func.100008864 0x100008888 [CALL:--x] bl sym.imp.dlsym [0x100008e44]>
At this point we continue using radare2
in order to visualize the execution flow and to examine some assembly instructions in order to have insights of the validation checks in a lower level
[0x100008e44]> s sym.func.100008864 [0x100008864]> VV
As we see at the screenshot below we have obtained a lot of information regarding the ptrace implementation. Specifically we see that the ptrace
is called by Challenge1.viewDidLoad
and also we are able to determine the feature of the ptrace
from the 0xf1
hex value which is 31
in decimal indicating the 'PT_DENY_ATTACH'
feature.
At this point we are able to examine the viewDidLoad
method as we know it implements the ptrace
syscall.
[0x100008864]> ic Challenge1 class Challenge1 0x100008a4c method Challenge1 viewDidLoad 0x100008abc method Challenge1 jailbreakTest1Tapped: 0x100008b14 method Challenge1 showAlertWithMessage: 0x100008c3c method Challenge1 isJailbroken [0x100008864]>
We can see that the viewDidLoad
method is located at 0x100008a4c
address as seen above, so lets further check the validations on radare2
[0x100008864]> s 0x100008a4c [0x100008a4c]> VV
If we examine further we see that except the ptrace syscall there are other anti-reversing defences enabled, but as we mentioned earlier this blog post is focusing only to bypass ptrace
syscall.
Lets decompile the code using r2ghidra
in order to have a high level view of the viewDidLoad
implementation
[0x100008a4c]> pdg
void method.Challenge1.viewDidLoad(ulong param_1)
{
int32_t iVar1;
char *pcVar2;
ulong uStack_20;
ulong uStack_18;
uStack_18 = *0x10000db80;
uStack_20 = param_1;
sym.imp.objc_msgSendSuper2(&uStack_20, *0x10000d958);
sym.func.100008864();
iVar1 = sym.func.100008a30();
if ((iVar1 == 0) && (iVar1 = sym.func.10000898c(), iVar1 == 0)) {
iVar1 = sym.func.1000088b4();
if (iVar1 == 0) {
return;
}
pcVar2 = "";
}
else {
pcVar2 = "";
}
sym.imp.NSLog(pcVar2);
// WARNING: Subroutine does not return
sym.imp.exit(0);
}
As seen from the decompiled code above, the first check is implemented using the ptrace
( sym.func.100008864
) syscall. At this point we can bypass ptrace
syscall using r2frida
Hooking with r2frida
As we saw earlier the argument passed to ptrace
is 0xf1
in hex which indicates the ptrace
feature used. In order to disable ptrace
syscall we can change this value to a non existing identifier, for example passing the value -1
. The following radare2
code snippet can be used to dynamically manipulate the argument passed to ptrace
Interceptor.attach(Module.findExportByName(null, 'ptrace'), {
onEnter: function (args) {
args[0] = ptr(-1);
}
});
The following output indicates that the ptrace
syscall has been disabled
~/ r2 frida://spawn/usb//re.murphy.ios-challenge-2 INFO: Using safe io mode. -- Thank you for using radare2. Have a nice night! [0x00000000]> [0x00000000]> :eval Interceptor.attach(Module.findExprtByName*null, 'ptrace'),{onEnter: function (args) { args[0] = ptr(-1) }}) {} [0x00000000]> :dtf ptrace iiii [0x00000000]> :dc INFO: resumed spawned process [0x00000000]> [dtf onLeave][Wed Aug 30 2023 06:50:41 GMT-0700] ptrace@0x1f9970560 - args: 18446744073709551000, 0, 0, 0. Retval: 0xffffffffffffffff