Defeating debug protections with Corellium

Make iOS app debugging easier with a new technique that expedites analysis and bypasses slowdown attempts. Say goodbye to frustrations.
Defeating debug protections with Corellium

Debugging an iOS application or binary that employs anti-debugging techniques can be an immensely frustrating experience. As a researcher, you're often faced with a black box of opaque operations and deliberately obfuscated code designed to thwart your every attempt at understanding an application's behavior. The usual debugging tools and techniques, which typically streamline the process, become almost ineffective, turning what should be a straightforward task into a complex puzzle. This not only hampers the efficiency of the debugging process but also tests the patience and ingenuity of even the most experienced researchers. This article will demonstrate a new technique developed by the Corellium team to expedite your dynamic analysis efforts and defuse attempts to slow down your progress. 

What is Anti-debugging?

The OWASP Mobile Application Security Verification Standard (MASVS) is a comprehensive framework for ensuring the security of mobile applications. This standard, developed by the Open Web Application Security Project (OWASP), provides a baseline for mobile app security, consisting of various levels of security requirements and recommendations. Among its extensive guidelines, the MASVS addresses the need for mobile applications to implement measures against reverse engineering and debugging, recognizing these activities as potential threats to mobile app security and user privacy.  

Specifically, it emphasizes the importance of incorporating debug-checking mechanisms. This includes detecting and responding to the presence of a debugger, ensuring that code and sensitive data are not exposed during debugging sessions, and preventing debugging tools from modifying or tampering with the app.

By implementing these recommendations, developers can better protect their applications from unauthorized analysis and exploitation, which is crucial for maintaining the integrity and confidentiality of both the application and its user data.  

There are a few well-documented anti-debugging techniques like: 

  • Using sysctl to detect if the ptrace flag is set, which may cause the application to abort or behave differently while under introspection. [1] 
  • Using ptrace directly to set PTRACE_DENY_ATTACH, preventing the parent process from debugging it. [2] 
  • Using getppid()to determine if an application was launched by launchd (pid 1) 

The iOSSecuritySuite has a very clean implementation of these checks, which can easily be baked into an iOS application. For this demo, we will reimplement one of these checks into a simple binary we can run from the iOS console. The application is pretty straightforward: 

  • Set a random 8-byte flag to an NSString 
  • Print out some helpful debug information about the userland process, like the executable's base address and the flag's address. 
  • Wait for user input (mostly to pause for attaching a debugger) 
  • Exit 

Let's run the application first without any sort of debug protections enforced: 

iphone:/ root# ./main-allow  

2024-01-24 08:50:08.234 main-allow[736:32888] Flag value: aX1ZWAec 

2024-01-24 08:50:08.234 main-allow[736:32888] PID: 736 

2024-01-24 08:50:08.234 main-allow[736:32888] Base Address of Main: 0x10000423c 

2024-01-24 08:50:08.234 main-allow[736:32888] Address of NSString: 0x16fdff2f8 

2024-01-24 08:50:08.235 main-allow[736:32888] Address of actual NSString contents: 0x9142043a0 <--- save this for later 

2024-01-24 08:50:08.235 main-allow[736:32888] Pausing for user input... 

Then we can simply attach it to the process with the debugserver and open a port for our remote lldb client to talk to... 

iphone:/ root# ./debugserver16-3 0.0.0.0:3333 -a 736 

debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-1400.2.13.2 

 for arm64. 

Attaching to process 736... 

Listening to port 3333 for a connection from 0.0.0.0... 

and using the debug information provided by the binary, we can print out the contents of the flag 

(lldb) process connect connect://10.11.0.12:3333                                                                                                                                                                 Process 736 stopped 

* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP 

    frame #0: 0x00000001c5fe8e84 libsystem_kernel.dylib`__read_nocancel + 8 

libsystem_kernel.dylib`: 

->  0x1c5fe8e84 <+8>:  b.lo   0x1c5fe8ea4               ; <+40> 

    0x1c5fe8e88 <+12>: pacibsp  

    0x1c5fe8e8c <+16>: stp    x29, x30, [sp, #-0x10]! 

    0x1c5fe8e90 <+20>: mov    x29, sp 

Target 0: (main-allow) stopped. 

(lldb) x 0x9142043a0 

0x9142043a0: 61 58 31 5a 57 41 65 63 00 00 00 00 00 00 00 00  aX1ZWAec........ 

0x9142043b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................ 

(lldb)  

Excellent, we can see the contents of our NSString object right at the address supplied by the program.  

Now we will reimplement .denyDebugger() from the IOSSecuritySuite [3] into our application. 

static void denyDebugger() { 

    // Dynamic loading of ptrace 

    void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW); 

 

    pid_t pid = getpid(); 

 

    if (handle) { 

        typedef int (*PtraceType)(int request, pid_t pid, caddr_t addr, int data); 

        PtraceType ptraceFunction = (PtraceType)dlsym(handle, "ptrace"); 

 

        if (ptraceFunction) { 

            // PT_DENY_ATTACH == 31 

            int ptraceRet = ptraceFunction(31, 0, 0, 0); 

 

            if (ptraceRet != 0) { 

                NSLog(@"Error occurred when calling ptrace(). Denying debugger may not be reliable"); 

            } 

        } 

        dlclose(handle); 

    } 

} 

If we rerun the program with this added functionality, we get an entirely different experience: 

iphone:/ root# ./debugserver16-3 0.0.0.0:3333 -a 739 

debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-1400.2.13.2 

 for arm64. 

Attaching to process 739... 

Segmentation fault: 11 

iphone:/ root#  

By setting the PT_DENY_ATTACH flag, debugserver/lldb is unable to attach to the process. Techniques like these are not bulletproof and can usually be bypassed through clever combinations of hooking and patching. However, there are also ways for applications to detect if they are being instrumented or if the integrity of the application has been violated. Defeating these various mitigations is time wasted that could have been spent on proper dynamic analysis of your target.  

 What if we could debug this application without it even knowing it?

Debugging on Corellium

It's no secret that Corellium offers a gdb-stub in the kernel which you can use with both gdb and lldb. The kernel debugger is a valuable tool for gaining a deeper understanding of iOS internals and performing vulnerability research. It can even be chained together with our debug accelerator tool to provide a much faster debugging experience if you have to deal with latency in a cloud environment. 

Your Corellium VM exposes the kernel debug stub on service_ip:4000, which you can connect to with anything that speaks the gdb protocol. We will use lldb for this example: 

(lldb) gdb-remote 10.11.1.12:4000 

Kernel UUID: EE3449F0-DB47-3596-B253-084A0EBDBDC5 

Load Address: 0xfffffff00700c000 

WARNING: Unable to locate kernel binary on the debugger system. 

Process 1 stopped 

* thread #1, stop reason = signal SIGINT 

    frame #0: 0xfffffff007d2d0f0 

->  0xfffffff007d2d0f0: cbz    x30, -0xff82d2f08 

    0xfffffff007d2d0f4: ret     

    0xfffffff007d2d0f8: mov    x0, #0x1 

    0xfffffff007d2d0fc: bl     -0xff816d978 

Target 0: (No executable module.) stopped. 

(lldb) c 

Process 1 resuming 

You can also use the kernel debugger to debug userland processes, instantly defeating all of the above countermeasures. Using the monitor filter feature, we can specify a userland process by either its name, pid, or tid. You can also specify if you want to break on entering the userland process with the boe flag (optional) 

(lldb) process plugin packet monitor filter user pid:814 boe

Here, we've instructed the kernel to break into the process we specified with the pid: flag above.  

(lldb) process plugin packet monitor filter user pid:745 boe 

  packet: qRcmd,66696c7465722075736572207069643a37343520626f65 

response: OK 

Process 1 stopped 

* thread #2, stop reason = signal SIGSTOP 

    frame #0: 0x00000001c5fe8e84 

->  0x1c5fe8e84: b.lo   0x1c5fe8ea4 

    0x1c5fe8e88: pacibsp  

    0x1c5fe8e8c: stp    x29, x30, [sp, #-0x10]! 

    0x1c5fe8e90: mov    x29, sp 

Target 0: (No executable module.) stopped. 

As you can tell by the frames, we have switched our debugging context from the kernel to a userland process. The program also behaves normally, none the wiser that we are quietly debugging it. Now, using the debug information provided by the program, we can use lldb to print the contents of our userland NSString object. 

2024-01-24 09:31:26.294 main-deny[745:40019] Flag value: k5mLCxkR 

2024-01-24 09:31:26.295 main-deny[745:40019] PID: 745 

2024-01-24 09:31:26.295 main-deny[745:40019] Base Address of Main: 0x10000423c 

2024-01-24 09:31:26.295 main-deny[745:40019] Address of NSString: 0x16fdff2f8 

2024-01-24 09:31:26.295 main-deny[745:40019] Address of actual NSString contents: 0x7c24043e0 

2024-01-24 09:31:26.296 main-deny[745:40019] Pausing for user input... 

(lldb) x 0x7c24043e0 

0x7c24043e0: 6b 35 6d 4c 43 78 6b 52 00 00 00 00 00 00 00 00  k5mLCxkR........ 

0x7c24043f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................ 

(lldb)  

Voila! You can also use this technique to debug Corellium's non-jailbroken models if you wanted to debug com.apple.WebKit.WebContent on a non-jailbroken vm, just attach to the kernel debugger and follow the same steps above.  

webcontent-1

References

[1] - ptrace flag

[2] - OpenBSD ptrace

[3] - iOS Security Suite Github