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:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// clang -o main main.c
#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:

1
$ ./main No debugger currently attached

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

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
$ 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.

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
$ 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 here 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 beyond the scope of this article.

Comments