Preventing an iOS mobile application from being debugged — The secure way

Kok Sang Teo
CSG @ GovTech
Published in
9 min readOct 14, 2020

[Disclaimer: The goal of this article is to provide insights on mobile penetration testing focusing on iOS Anti-Reversing Defences — to prevent an iOS mobile application from entering into a debugging state (as described in OWASP Mobile Security Testing Guide item “MSTG-RESILIENCE-2”). The insights shared are purely for learning purposes and are based on intensive collective research done through online and offline public materials. The author and CSG does not condone, encourage, nor intend for the lessons described below to be used for any purposes other than cybersecurity research.]

Introduction to anti-debugging

This article introduces anti-debugging, a technique that prevents mobile applications from being debugged. This prevents attackers from tampering with iOS mobile applications and exploring its deep logic under the hood. By running a debugger over the app, hackers not only track variables containing sensitive data, but can also directly modify the control flow of the application, lower level memory, and registers. This allows them to reverse engineer the mobile application, access the ‘crown jewels’ (logic source code), and decode the workings of the application.

Are we vulnerable?

The answer is: it depends. Apps from Apple’s App Store are typically signed with entitlements that prevent debugger attachment. The bad news, however, is that jailbroken devices can re-sign your application and enable the Get Task Allow entitlement, allowing users to attach a debugger to the application process. As such, it is not wise to rely solely on iOS for anti-debugging. Implementing anti-debugging techniques serves as a good deterrent against reverse engineering attempts by hackers.

Introducing ptrace

To employ anti-debugging techniques, I used a function called the “Process trace”, also known as ptrace. Ptrace is a system call found in Unix and several Unix-like operating systems. Ptrace lets users control and inspect the targeted app behaviour. It also enables the controller to inspect and manipulate the internal state of its target. Additionally, ptrace is used by debuggers and other code-analysis tools, mainly as aids in software development.

This article covers only one feature of the ptrace syscall, namely ‘PT_DENY_ATTACH’. This syscall is fairly well-known among iOS application hackers and one of the most common anti-debugging techniques in iOS applications. You can find out more about ptrace in the official Apple documentation, which provides a brief description of what ‘PT_DENY_ATTACH’ does and how it can be used to bypass a mobile application’s debugger safeguards.

PT Deny Attach description

Simply put, the use of ptrace ‘PT_DENY_ATTACH’ ensures that no debugger can attach to the calling process. If an attempt is made to attach a debugger, the process exits automatically.

Implementing PT_DENY_ATTACH

It is important to note that the ‘PT_DENY_ATTACH’ is not a public Application Programming Interface (API) on iOS. As per the App Store publishing policy, the use of non-public APIs is prohibited and may lead to the rejection of the app from the App Store. As such, ptrace is not called directly in the code, but only when a ptrace function pointer is obtained via dlsym function.

The following PT_DENY_ATTACH code snippet written in Swift is for demonstration only; the base code can be referenced here. Below are the steps I took:

  1. Using Xcode, launch your project, create the main.swift file, and paste the code snippet as follows:
import Foundation
import UIKit
autoreleasepool {
disable_gdb()
UIApplicationMain(
CommandLine.argc,
UnsafeMutableRawPointer(CommandLine.unsafeArgv)
.bindMemory(
to: UnsafeMutablePointer<Int8>.self,
capacity: Int(CommandLine.argc)),
nil,
NSStringFromClass(AppDelegate.self)
)
}

2. Create a file called denyptrace.c, along the corresponding bridging header if prompted to do so. Paste the code snippet below into denyptrace.c:

#include <stdio.h>
#import <dlfcn.h>
#import <sys/types.h>
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif
void disable_gdb() {
void* handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
dlclose(handle);
}

3. Add the following line of code into the bridging header file:

#import "DenyPtrace.c"
void disable_gdb();

4. Lastly, launch Appdelegate.swift and comment out the @UIApplicationMain label.

If you compile and run this code and attempt to attach a debugger*, it will immediately detach itself with an exit status as shown in the screenshot below.

*: To view the debugger rejection message, you will need to run debugserver from your mobile phone using the –waitfor “<Mobile app name>” flag, following which you will need to run and attach a Low Level Debugger called lldb (from the host MacBook) immediately upon launching the app.

Presence of an anti-debugging technique is presented

The message Process 20008 exited with status = 45 (0x0000002d) is a tell-tale sign that the debug target is using PT_DENY_ATTACH. If you attempt to attach the debugserver directly after you launch the mobile application, you will get a Segmentation Fault: 11 error. This hints to the attacker that an anti-debugging technique is implemented, though the specific technique remains unknown.

Anti-debugging is presented

Bypassing the technique

A hacker would then find ways to bypass ‘PT_DENY_ATTACH’ to disable the anti-debugging mechanism. One way to do so is to stub the call and bypass our anti-debugging code**. That is bad news! All a hacker needs to do is to set a breakpoint on the ptrace symbol and change the value to 0, as shown below:

<<In Desktop directory>> lldb
(lldb) platform select remote-ios
Platform: remote-ios
<<snipped for brevity>>
(lldb) process connect connect://xxx.xxx.1.109:9876 //attaching debugger
Process 20028 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00000001b2fbb25c libsystem_kernel.dylib`read + 8
libsystem_kernel.dylib`read:
-> 0x1b2fbb25c <+8>: b.lo 0x1b2fbb278 ; <+36>
0x1b2fbb260 <+12>: stp x29, x30, [sp, #-0x10]!
0x1b2fbb264 <+16>: mov x29, sp
0x1b2fbb268 <+20>: bl 0x1b2f97f4c ; cerror
Target 0: (pt-deny-attach-test) stopped.
(lldb) b disable_gdb //this is the function name to set breakpoint based on sample code above
Breakpoint 1: where = pt-deny-attach-test`disable_gdb + 12 at denyPtrace.c:20:20, address = 0x0000000104a731b4
(lldb) c
Process 20028 resuming
Process 20028 stopped
<<snipped for brevity>>
Target 0: (pt-deny-attach-test) stopped. // the breakpoint for disable_gdb has been reached
<<snipped for brevity>>
(lldb) b ptrace // I will set another breakpoint called ptrace
Breakpoint 2: where = libsystem_kernel.dylib`__ptrace, address = 0x00000001b2fb8f2c
(lldb) c // continue after set breakpoint
Process 20028 resuming
Process 20028 stopped // the ptrace function has been reached
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x00000001b2fb8f2c libsystem_kernel.dylib`__ptrace
libsystem_kernel.dylib`__ptrace:
-> 0x1b2fb8f2c <+0>: adrp x9, 290480
0x1b2fb8f30 <+4>: add x9, x9, #0xd68 ; =0xd68
0x1b2fb8f34 <+8>: str wzr, [x9]
0x1b2fb8f38 <+12>: mov x16, #0x1a
Target 0: (pt-deny-attach-test) stopped.
(lldb) reg read //I will now reach the reg for x0 and it is ‘0x1f’ which is ‘31’. The hacker aim is to replace ‘31’ to ‘0’ to avoid calling ptrace altogether
General Purpose Registers:
x0 = 0x000000000000001f
<<snipped for brevity>>
(lldb) reg write x0 0 //overwrite this ‘31’ to 0 to bypass PT_DENY_ATTACH
(lldb) reg read
General Purpose Registers:
x0 = 0x0000000000000000 //I have overwritten it successfully
<<snipped for brevity>>
(lldb) c
Process 20028 resuming //successful bypass
(lldb)

To prevent a hacker from performing the above process to bypass our debugging code, I will attempt to obfuscate ptrace.

**: A hacker can attempt to bypass the anti-debugger by running the debugserver from your mobile phone with the –waitfor “<Mobile app name>” flag, then running and attaching lldb (from the host MacBook) immediately upon launching the app to see the crash message.

Obfuscation of ptrace through ARM system calls

As much as possible, we want to deter hackers from debugging a program. Conducting an obfuscation of ptrace through ARM system calls makes it difficult for a hacker to attempt a bypass.

For the purpose of this demonstration, ptrace is listed under syscall number 26; as such, I can simply call ptrace using syscall with 26. The documentation for syscall can be found here for further reading.

Syscall documentation

Thereafter, I constructed a ptrace syscall in ARM with the value 31 or 0x1f. Using assembly (ARM64), ptrace will not be called directly and will be called via syscall and this should make it harder for hackers to conduct symbol hooking.

Implementation of ARM syscall

Rather than calling the PT_DENY_ATTACH, I called a ptrace syscall to accomplish this step. This sample code snippet illustrates the ptrace syscall process:

"mov x0, #26\n" // this is ptrace syscall which is #26
"mov x1, #31\n" // PT_DENY_ATTACH (0x1f) - first argument
"mov x2, #0\n"
"mov x3, #0\n"
"mov x16, #0\n"
"svc #128\n" // syscall

The next step is to incorporate assembly calls into our project. The following code snippet implements ARM syscall in a swift project for demonstration purposes. Developers are always recommended to implement it in their own way based on their project requirements in order to achieve the same overall outcome. Please feel free to experiment!

1. Create a C file with header file, with the names arm_syscall.c and arm_syscall.h respectively. Create the corresponding bridging header if prompted to do so.

2. Copy and paste the code snippet into arm_syscall.c, as follows:

#include "arm_syscall.h"Void plokij() {__asm (
"mov x0, #31\n" // to define PT_DENY_ATTACH (31) to x0
"mov x1, #0\n”
"mov x2, #0\n"
"mov x3, #0\n" //I am actually writing ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0) in the above instruction set
"mov x16, #26\n" // set the intra-procedural to syscall #26 to invoke ‘ptrace’
"svc #0x80\n" // SVC generate supervisor call. Supervisor calls are normally used to request privileged operations or access to system resources from an operating system
);
}

3. Copy and paste the code snippet into arm_syscall.h:

#ifndef arm_syscall_h
#define arm_syscall_h
#include <stdio.h>
void plokij(void) __attribute__((always_inline));
#endif /* arm_syscall_h */

4. Add the following line of code into the bridging header file:

#include "arm_syscall.h"

5. Create a main.swift file and paste the code snippet into it:

import Foundation
import UIKit
plokij () // function callUIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))

6. Lastly, launch Appdelegate.swift and comment out the @UIApplicationMain label.

Note: This will detach even the XCode debugger, so use it only on release builds.

As a best practice, I will further obfuscate the whole binary by performing the inline function as the setting is available. To do so, I implement the __attribute__((always_inline)) property*** on the code snippet above.

Whether the obfuscation happens next will depend on how the compiler compiles the app. If this fails, the function calls for an anti-debugger check in the ‘TEXT’ segment of the binary and defines itself as the function name. A skilled hacker would then be able identify the syscall function using a binary disassembler tool if the name is not sufficiently obfuscated.

***: Function inline is not officially supported yet and Apple App Store may reject its use. The technique of inlining demonstrated is meant for educational purposes and developers are encouraged to perform it using different methods (that can pass the App Store checks) to achieve the same overall outcome. Here’s an article written by Galvin Li on the inline function and the benefits of using it.

Note: Code inlining feature may not work at times

In my case, the inline function does not work.

Finally, there is a feature called symbol stripping in Xcode (an Integrated Development Environment in Mac that supports the development of MacOS and iOS applications) that is enabled by default. However, do note that Xcode may not be able to strip symbols successfully depending on the app build.

After implementation, we should see a segmentation fault: 11 error (as with PT_DENY_ATTACH) when a hacker attempts to attach a debugger to the mobile application.

Successful anti-debugging using syscall

Conclusion

Exploring applications using a debugger is a very powerful technique in reverse engineering. To protect a mobile application from a malicious actor, it is crucial to implement anti-debugging techniques and obfuscation through system calls. These safeguards make it difficult for the attacker to break down the application’s defence mechanisms.

The cyber landscape is ever-changing. Despite the best efforts of cybersecurity specialists in securing systems, there will always be new methods of breaking down these security safeguards. The technique demonstrated in this article is just one of many to deter hackers. In any case, a determined hacker will employ various methods to eventually bypass it. Hence, it is good practice to continuously review and refresh techniques to keep an application secure. Do refer to the OWASP Mobile Security Testing Guide frequently for new techniques and methods to secure mobile applications.

--

--

Kok Sang Teo
CSG @ GovTech

White Hat and Security Enthusiast in Web and Mobile Offensive Security