Defeating Anti-Debug Techniques: macOS mach exception ports

Continuing my series on anti-debug technique and how to defeat them, today we have a mach exception ports based trick on macOS.

For those unfamiliar with how the macOS flavor of Unix does things, these ports are used by debuggers to handle exceptions like breakpoints and bad-access. With this knowledge, a piece of software that does not want to be debugged, like a piece of malware trying to prevent analysis, can check if these ports are open and do something else in response, like shutdown.

The code for mach exception handling is fairly complex and poorly documented, but I’ve created a fairly simple example of debugger-detection code so we can dive right in.

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).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// clang -o main main.c
#include <stdlib.h>
#include <stdio.h>
#include <mach/mach.h>

int main(int argc, const char * argv[]) {
exception_mask_t exception_masks[EXC_TYPES_COUNT];
mach_msg_type_number_t exception_count = 0;
mach_port_t exception_ports[EXC_TYPES_COUNT];
exception_behavior_t exception_behaviors[EXC_TYPES_COUNT];
thread_state_flavor_t exception_flavors[EXC_TYPES_COUNT];

kern_return_t kr = task_get_exception_ports(
mach_task_self(),
// In earlier header versions EXC_MASK_ALL could have been used, but it now includes too much.
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
| EXC_MASK_CRASH
,
exception_masks,
&exception_count,
exception_ports,
exception_behaviors,
exception_flavors
);
if (kr == KERN_SUCCESS) {
for (mach_msg_type_number_t i = 0; i < exception_count; i++) {
if (MACH_PORT_VALID(exception_ports[i])) {
printf("DEBUGGER DETECTED!\n");
return 1;
}
}
}
else {
printf("ERROR: task_get_exception_ports: %s\n", mach_error_string(kr));
return 1;
}

printf("No debugger detected\n");

return 0;
}

Now when run directly, no debugger is detected:

1
2
$ ./main
No debugger detected

But when run with LLDB, a debugger is detected:

1
2
3
4
5
6
7
8
$ lldb main
(lldb) target create "main"
Current executable set to 'main' (x86_64).
(lldb) r
Process 3281 launched: '/.../main' (x86_64)
DEBUGGER DETECTED!
Process 3281 exited with status = 1 (0x00000001)
(lldb)

Alright, so how do we defeat this? There are a few way, and the best approach may depend on the code doing the detection, but in this simple example we can simply mask out all the exceptions by changing the second argument (register rsi) to task_get_exception_ports to a 0 in a debugger.

Allow me to demonstrate just how easy that is.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ lldb main
(lldb) target create "main"
Current executable set to 'main' (x86_64).
(lldb) b task_get_exception_ports
Breakpoint 1: where = libsystem_kernel.dylib`task_get_exception_ports, address = 0x000000000000c675
(lldb) r
Process 3408 launched: '/.../main' (x86_64)
Process 3408 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00007fff7b058675 libsystem_kernel.dylib`task_get_exception_ports
libsystem_kernel.dylib`task_get_exception_ports:
-> 0x7fff7b058675 <+0>: pushq %rbp
0x7fff7b058676 <+1>: movq %rsp, %rbp
0x7fff7b058679 <+4>: pushq %r15
0x7fff7b05867b <+6>: pushq %r14
Target 0: (main) stopped.
(lldb) reg w rsi 0
(lldb) c
Process 3408 resuming
No debugger detected
Process 3408 exited with status = 0 (0x00000000)
(lldb)

Easy, even easier than the better-known sysctl technique, once you know what’s going on. If this function gets called a lot, you will probably want to automate it in your debugger of choice, but I leave that as an exercise for the reader.

You may have noticed in the comments that it was formerly possible to use EXC_MASK_ALL (which actually does not include “all”) instead of listing the exceptions individually. This is no longer possible since the newer EXC_MASK_RESOURCE and EXC_MASK_GUARD (along with EXC_MASK_CORPSE_NOTIFY) ports are present outside a debugger. You can find a list of all of these constants in the XNU header file osfmk/mach/exception_types.h.

Stay tuned for more on mach task ports, because next up we will have force-detaching of debuggers with the mach exception ports API.

Comments