Defeating Anti-Debug Techniques: macOS mach exception port stealing

Previously I showed you an anti-debug technique using the macOS exception ports API to detect the presence of a debugger. Today, I’m going to show you a different exception-port-based anti-debug trick which actually detaches the debugger by stealing the exception ports it is using. Some readers may be familiar with similar tricks done on Windows, however it’s very rare to see something like this done on macOS, though I have seen this kind of thing in the wild so here goes.

NOTE: All of these examples assume compilation for x86_64, the default now for years and soon may be the only option. Also, all bypasses will be done in-debugger, without patching the binary itself (which may not be feasible when dealing with packed code).

Let’s get right to the code:

#include <stdlib.h>
#include <stdio.h>
#include <mach/host_priv.h>
#include <mach/mach.h>
#include <mach/host_special_ports.h>
int remove_debugger() {
	mach_port_t service;
	kern_return_t kr;
	ipc_space_t selftask = mach_task_self();

	kr = mach_port_allocate(
		selftask,
		MACH_PORT_RIGHT_RECEIVE,
		&service
	);
	if (kr != KERN_SUCCESS) {
		printf("mach_port_allocate: %s\n", mach_error_string(kr));
		return 1;
	}

	kr = mach_port_insert_right(
		selftask,
		service,
		service,
		MACH_MSG_TYPE_MAKE_SEND
	);
	if (kr != KERN_SUCCESS) {
		printf("mach_port_insert_right: %s\n", mach_error_string(kr));
		return 1;
	}

	exception_mask_t exception_mask =
		EXC_MASK_BAD_ACCESS |
		EXC_MASK_BAD_INSTRUCTION |
		EXC_MASK_ARITHMETIC |
		EXC_MASK_EMULATION |
		EXC_MASK_SOFTWARE |
		EXC_MASK_BREAKPOINT |
		EXC_MASK_SYSCALL |
		EXC_MASK_MACH_SYSCALL |
		EXC_MASK_RPC_ALERT;
	kr = task_set_exception_ports(
		selftask,
		exception_mask,
		service,
		EXCEPTION_STATE,
		x86_THREAD_STATE64
	);
	if (kr != KERN_SUCCESS) {
		printf("task_set_exception_ports: %s\n", mach_error_string(kr));
		return 1;
	}
	return 0;
}

int main(int argc, const char * argv[]) {
	if (remove_debugger()) {
		printf("Error\n");
		return 1;
	}

	printf("No debugger currently attached\n");
	return 0;
}

Simple enough. Basically what we do here is take over the exception ports from the debugger with the mask of constants passed to task_set_exception_ports. When we run this code without a debugger, everything is fine:

$ ./main
No debugger currently attached

But watch what happens when we try to trace over that main function in lldb:

$ lldb main
(lldb) target create "main"
Current executable set to 'main' (x86_64).
(lldb) b main
Breakpoint 1: where = main`main, address = 0x0000000100000e80
(lldb) r
Process 20123 launched: '.../main' (x86_64)
Process 20123 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100000e80 main`main
main`main:
->  0x100000e80 <+0>: pushq  %rbp
    0x100000e81 <+1>: movq   %rsp, %rbp
    0x100000e84 <+4>: subq   $0x20, %rsp
    0x100000e88 <+8>: movl   $0x0, -0x4(%rbp)
Target 0: (main) stopped.
(lldb) ni
Process 20123 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000e81 main`main + 1
main`main:
->  0x100000e81 <+1>:  movq   %rsp, %rbp
    0x100000e84 <+4>:  subq   $0x20, %rsp
    0x100000e88 <+8>:  movl   $0x0, -0x4(%rbp)
    0x100000e8f <+15>: movl   %edi, -0x8(%rbp)
Target 0: (main) stopped.
(lldb) ni
Process 20123 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000e84 main`main + 4
main`main:
->  0x100000e84 <+4>:  subq   $0x20, %rsp
    0x100000e88 <+8>:  movl   $0x0, -0x4(%rbp)
    0x100000e8f <+15>: movl   %edi, -0x8(%rbp)
    0x100000e92 <+18>: movq   %rsi, -0x10(%rbp)
Target 0: (main) stopped.
(lldb) ni
Process 20123 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000e88 main`main + 8
main`main:
->  0x100000e88 <+8>:  movl   $0x0, -0x4(%rbp)
    0x100000e8f <+15>: movl   %edi, -0x8(%rbp)
    0x100000e92 <+18>: movq   %rsi, -0x10(%rbp)
    0x100000e96 <+22>: callq  0x100000d70               ; remove_debugger
Target 0: (main) stopped.
(lldb) ni
Process 20123 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000e8f main`main + 15
main`main:
->  0x100000e8f <+15>: movl   %edi, -0x8(%rbp)
    0x100000e92 <+18>: movq   %rsi, -0x10(%rbp)
    0x100000e96 <+22>: callq  0x100000d70               ; remove_debugger
    0x100000e9b <+27>: cmpl   $0x0, %eax
Target 0: (main) stopped.
(lldb) ni
Process 20123 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000e92 main`main + 18
main`main:
->  0x100000e92 <+18>: movq   %rsi, -0x10(%rbp)
    0x100000e96 <+22>: callq  0x100000d70               ; remove_debugger
    0x100000e9b <+27>: cmpl   $0x0, %eax
    0x100000e9e <+30>: je     0x100000ec1               ; <+65>
Target 0: (main) stopped.
(lldb) ni
Process 20123 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000e96 main`main + 22
main`main:
->  0x100000e96 <+22>: callq  0x100000d70               ; remove_debugger
    0x100000e9b <+27>: cmpl   $0x0, %eax
    0x100000e9e <+30>: je     0x100000ec1               ; <+65>
    0x100000ea4 <+36>: leaq   0xe9(%rip), %rdi          ; "Error\n"
Target 0: (main) stopped.
(lldb) ni


Process 20123 exited with status = -1 (0xffffffff) lost connection

Problem. See that whitespace following the attempt to step over the remove_debugger call? That’s me hitting enter and ultimately Ctrl + C due to the debugger no longer continuing, and being told the connection was lost. That function call successfully stole control away from the debugger, preventing me from stepping over that code (or hitting any breakpoints I might have set beyond that).

Nasty right? So what do we do about it?

Well you may have guessed it, all we need to do is change the mask passed to task_set_exception_ports by setting register rsi to 0 when the function is called. If you got this far you probably don’t need me to show you how, but I’ll do so anyhow.

$ lldb main
(lldb) target create "main"
Current executable set to 'main' (x86_64).
(lldb) b task_set_exception_ports
Breakpoint 1: where = libsystem_kernel.dylib`task_set_exception_ports, address = 0x000000000000c599
(lldb) r
Process 20547 launched: '.../main' (x86_64)
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00007fff54841599 libsystem_kernel.dylib`task_set_exception_ports
libsystem_kernel.dylib`task_set_exception_ports:
->  0x7fff54841599 <+0>: pushq  %rbp
    0x7fff5484159a <+1>: movq   %rsp, %rbp
    0x7fff5484159d <+4>: pushq  %rbx
    0x7fff5484159e <+5>: subq   $0x48, %rsp
Target 0: (main) stopped.
(lldb) reg w rsi 0
(lldb) fin
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
    frame #0: 0x0000000100000e22 main`remove_debugger + 194
main`remove_debugger:
->  0x100000e22 <+194>: movl   %eax, -0xc(%rbp)
    0x100000e25 <+197>: cmpl   $0x0, -0xc(%rbp)
    0x100000e29 <+201>: je     0x100000e57               ; <+247>
    0x100000e2f <+207>: movl   -0xc(%rbp), %edi
Target 0: (main) stopped.
(lldb) fin
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
    frame #0: 0x0000000100000e8b main`main + 27
main`main:
->  0x100000e8b <+27>: cmpl   $0x0, %eax
    0x100000e8e <+30>: je     0x100000eb1               ; <+65>
    0x100000e94 <+36>: leaq   0xe9(%rip), %rdi          ; "Error\n"
    0x100000e9b <+43>: movb   $0x0, %al
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000e8e main`main + 30
main`main:
->  0x100000e8e <+30>: je     0x100000eb1               ; <+65>
    0x100000e94 <+36>: leaq   0xe9(%rip), %rdi          ; "Error\n"
    0x100000e9b <+43>: movb   $0x0, %al
    0x100000e9d <+45>: callq  0x100000ee4               ; symbol stub for: printf
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000eb1 main`main + 65
main`main:
->  0x100000eb1 <+65>: leaq   0xd3(%rip), %rdi          ; "No debugger currently attached\n"
    0x100000eb8 <+72>: movb   $0x0, %al
    0x100000eba <+74>: callq  0x100000ee4               ; symbol stub for: printf
    0x100000ebf <+79>: movl   $0x0, -0x4(%rbp)
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000eb8 main`main + 72
main`main:
->  0x100000eb8 <+72>: movb   $0x0, %al
    0x100000eba <+74>: callq  0x100000ee4               ; symbol stub for: printf
    0x100000ebf <+79>: movl   $0x0, -0x4(%rbp)
    0x100000ec6 <+86>: movl   %eax, -0x18(%rbp)
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000eba main`main + 74
main`main:
->  0x100000eba <+74>: callq  0x100000ee4               ; symbol stub for: printf
    0x100000ebf <+79>: movl   $0x0, -0x4(%rbp)
    0x100000ec6 <+86>: movl   %eax, -0x18(%rbp)
    0x100000ec9 <+89>: movl   -0x4(%rbp), %eax
Target 0: (main) stopped.
(lldb) ni
No debugger currently attached
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000ebf main`main + 79
main`main:
->  0x100000ebf <+79>: movl   $0x0, -0x4(%rbp)
    0x100000ec6 <+86>: movl   %eax, -0x18(%rbp)
    0x100000ec9 <+89>: movl   -0x4(%rbp), %eax
    0x100000ecc <+92>: addq   $0x20, %rsp
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000ec6 main`main + 86
main`main:
->  0x100000ec6 <+86>: movl   %eax, -0x18(%rbp)
    0x100000ec9 <+89>: movl   -0x4(%rbp), %eax
    0x100000ecc <+92>: addq   $0x20, %rsp
    0x100000ed0 <+96>: popq   %rbp
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000ec9 main`main + 89
main`main:
->  0x100000ec9 <+89>: movl   -0x4(%rbp), %eax
    0x100000ecc <+92>: addq   $0x20, %rsp
    0x100000ed0 <+96>: popq   %rbp
    0x100000ed1 <+97>: retq
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000ecc main`main + 92
main`main:
->  0x100000ecc <+92>: addq   $0x20, %rsp
    0x100000ed0 <+96>: popq   %rbp
    0x100000ed1 <+97>: retq

main`mach_error_string:
    0x100000ed2 <+0>:  jmpq   *0x140(%rip)              ; (void *)0x0000000100000f00
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000ed0 main`main + 96
main`main:
->  0x100000ed0 <+96>: popq   %rbp
    0x100000ed1 <+97>: retq

main`mach_error_string:
    0x100000ed2 <+0>:  jmpq   *0x140(%rip)              ; (void *)0x0000000100000f00

main`mach_port_allocate:
    0x100000ed8 <+0>:  jmpq   *0x142(%rip)              ; (void *)0x00007fff54850302: mach_port_allocate
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000ed1 main`main + 97
main`main:
->  0x100000ed1 <+97>: retq

main`mach_error_string:
    0x100000ed2 <+0>:  jmpq   *0x140(%rip)              ; (void *)0x0000000100000f00

main`mach_port_allocate:
    0x100000ed8 <+0>:  jmpq   *0x142(%rip)              ; (void *)0x00007fff54850302: mach_port_allocate

main`mach_port_insert_right:
    0x100000ede <+0>:  jmpq   *0x144(%rip)              ; (void *)0x00007fff54850434: mach_port_insert_right
Target 0: (main) stopped.
(lldb) ni
Process 20547 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x00007fff54701015 libdyld.dylib`start + 1
libdyld.dylib`start:
->  0x7fff54701015 <+1>: movl   %eax, %edi
    0x7fff54701017 <+3>: callq  0x7fff547194c4            ; symbol stub for: exit
    0x7fff5470101c <+8>: hlt

libdyld.dylib`dyld3::kdebug_trace_dyld_image:
    0x7fff5470101d <+0>: pushq  %rbp
Target 0: (main) stopped.
(lldb) c
Process 20547 resuming
Process 20547 exited with status = 0 (0x00000000)
(lldb)

Take note of how I am able to finish the remove_debugger function and continue tracing over the main function all the way to the end.

Another anti-debug trick bites the dust.

Two concluding points to note though:

  1. It would be bad practice to register a handler and not handle the exceptions, which could mean a crash never properly crashes. In short, don’t actually use this code in production, it’s just a simple example on how to defeat a minimal example (I’m hear to show you how to get around anti-debug tricks, not how to add them properly).
  2. If a program is registering an exception handler, it may actually be doing self-debugging, in which case just removing the handler may not be enough to bypass the anti-debugging code as you would also need to emulate the self-debugging code. This is a complex topic, but I might try to put together a simple example later on.

Leave a Reply

Your email address will not be published. Required fields are marked *

Are you human? * Time limit is exhausted. Please reload the CAPTCHA.