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.

r_con

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.


dlsym-ptrace

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

viewDidLoad-1

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