MASTG-KNOW-0085: Anti-Debugging Detection
Exploring applications using a debugger is a very powerful technique during reversing. You can not only track variables containing sensitive data and modify the control flow of the application, but also read and modify memory and registers.
There are several anti-debugging techniques applicable to iOS which can be categorized as preventive or as reactive. When properly distributed throughout the app, these techniques act as a supportive measure to increase the overall resilience.
- Preventive techniques act as a first line of defense to impede the debugger from attaching to the application at all.
- Reactive techniques allow the application to detect the presence of a debugger and have a chance to diverge from normal behavior.
Using ptrace¶
As seen in Debugging, the iOS XNU kernel implements a ptrace
system call that's lacking most of the functionality required to properly debug a process (e.g. it allows attaching/stepping but not read/write of memory and registers).
Nevertheless, the iOS implementation of the ptrace
syscall contains a nonstandard and very useful feature: preventing the debugging of processes. This feature is implemented as the PT_DENY_ATTACH
request, as described in the official BSD System Calls Manual. In simple words, it ensures that no other debugger can attach to the calling process; if a debugger attempts to attach, the process will terminate. Using PT_DENY_ATTACH
is a fairly well-known anti-debugging technique, so you may encounter it often during iOS pentests.
Before diving into the details, it is important to know that
ptrace
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 aptrace
function pointer is obtained viadlsym
.
The following is an example implementation of 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
}
Bypass: To demonstrate how to bypass this technique we'll use an example of a disassembled binary that implements this approach:
Let's break down what's happening in the binary. dlsym
is called with ptrace
as the second argument (register R1). The return value in register R0 is moved to register R6 at offset 0x1908A. At offset 0x19098, the pointer value in register R6 is called using the BLX R6 instruction. To disable the ptrace
call, we need to replace the instruction BLX R6
(0xB0 0x47
in Little Endian) with the NOP
(0x00 0xBF
in Little Endian) instruction. After patching, the code will be similar to the following:
Armconverter.com is a handy tool for conversion between bytecode and instruction mnemonics.
Bypasses for other ptrace-based anti-debugging techniques can be found in "Defeating Anti-Debug Techniques: macOS ptrace variants" by Alexander O'Mara.
Using sysctl¶
Another approach to detecting a debugger that's attached to the calling process involves sysctl
. According to the Apple documentation, it allows processes to set system information (if having the appropriate privileges) or simply to retrieve system information (such as whether or not the process is being debugged). However, note that just the fact that an app uses sysctl
might be an indicator of anti-debugging controls, though this won't be always be the case.
The Apple Documentation Archive includes an example which checks the info.kp_proc.p_flag
flag returned by the call to sysctl
with the appropriate parameters. According to Apple, you shouldn't use this code unless it's for the debug build of your program.
Bypass: One way to bypass this check is by patching the binary. When the code above is compiled, the disassembled version of the second half of the code is similar to the following:
After the instruction at offset 0xC13C, MOVNE R0, #1
is patched and changed to MOVNE R0, #0
(0x00 0x20 in in bytecode), the patched code is similar to the following:
You can also bypass a sysctl
check by using the debugger itself and setting a breakpoint at the call to sysctl
. This approach is demonstrated in iOS Anti-Debugging Protections #2.
Using getppid¶
Applications on iOS can detect if they have been started by a debugger by checking their parent PID. Normally, an application is started by the launchd process, which is the first process running in the user mode and has PID=1. However, if a debugger starts an application, we can observe that getppid
returns a PID different than 1
. This detection technique can be implemented in native code (via syscalls), using Objective-C or Swift as shown here:
func AmIBeingDebugged() -> Bool {
return getppid() != 1
}
Bypass: Similarly to the other techniques, this has also a trivial bypass (e.g. by patching the binary or by using Frida hooks).