Little Snitch is a program for which I was very curious to hack around and try to beat it’s protection. I had a feeling it would be a very nice challenge and I can say it didn’t disappointed me!

The target is version 2.0.3, running on Tiger 10.4.11.

First protection to be defeated was the “classical” PTRACE_DENY_ATTACH. You Control Desktops explains and has links to this protection. If we try to attach gdb to one Little Snitch process (it has at least 3) we get a segmentation fault, so this should be PTRACE_DENY_ATTACH “protection”. Use otx or IDA to disassemble the binaries and search for ptrace calls. Yes, it’s there! Nop those calls and off we go!

Now the real interesting stuff…
After patching the PTRACE_DENY_ATTACH trick, we still can’t attach gdb to processes. We get a misterious EPERM error due to lack of permissions?!?!? Trying to attach gdb running as root gets the same result, so this must be another trick in the bag… To test some ideas, I tried to unload the kernel extension (Little snitch installs a kernel extension) but it was protected against unloading (which makes sense because a program could unload it and off it goes our firewall protection).
My attack point was trying to understand how could the extension be protected against unloading… After some web searching I found an interesting Apple article about KAUTH
This talks about a kernel authorization subsystem. It supports various authorizations scopes, where the most interesting and which seems to fit into our problem is:

KAUTH_PROCESS_CANTRACE — Authorizes whether the current process can trace the target process. arg0 (of type proc_t) is the process being traced. arg1 (of type (int *)) is a pointer to an an errno-style error code; if the listener denies the request, it must set this value to a non-zero value.

Kauth uses SCOPES to define an area of interest for authorizations. For example, the authorization we are looking at is located in Process Scope. Each scope has actions, for example KAUTH_PROCESS_CANTRACE. A listener is who makes the authorization decision for a request. You can create or use a default listener. After that, you register the listener into the scope of interest.

Resuming, it should be something like:
Create Listener (make decisions for actions) -> Register Listener

You can find demo code at

To register a listener you use function kauth_listen_scope so we should look first for this one.
Disassemble the Little Snitch kernel extension and look for function calls (extension is located at /System/Library/Extensions) There are two calls: First one:

00006D40 55                  push    ebp
00006D41 89 E5               mov     ebp, esp
00006D43 83 EC 18            sub     esp, 18h
00006D46 C7 44 24 08 00 00+  mov     [esp+18h+var_10], 0
00006D4E C7 44 24 04 28 00+  mov     [esp+18h+var_14], 28h ; '('
00006D56 C7 04 24 BC 00 01+  mov     [esp+18h+var_18], offset unk_100BC
00006D5D E8 1E E9 FF FF      call    sub_5680
00006D62 C7 44 24 08 4B 00+  mov     [esp+18h+var_10], 4Bh ; 'K'
00006D6A C7 44 24 04 00 02+  mov     [esp+18h+var_14], 200h
00006D72 C7 04 24 04 FE 00+  mov     [esp+18h+var_18], offset off_FE04
00006D79 E8 12 D2 FF FF      call    sub_3F90
00006D7E C7 44 24 08 00 00+  mov     [esp+18h+var_10], 0
00006D86 C7 44 24 04 F0 6B+  mov     [esp+18h+var_14], offset sub_6BF0
00006D8E C7 04 24 88 DA 00+  mov     [esp+18h+var_18], offset aCom_apple_kaut ; "" <- SCOPE
00006D95 A3 C8 00 01 00      mov     ds:dword_100C8, eax
00006D9A E8 D1 29 02 00      call    near ptr _kauth_listen_scope
00006D9F A3 C4 00 01 00      mov     ds:dword_100C4, eax
00006DA4 C9                  leave
00006DA5 C3                  retn

Second one:

0000BC40 55                  push    ebp
0000BC41 89 E5               mov     ebp, esp
0000BC43 83 EC 18            sub     esp, 18h
0000BC46 E8 05 D5 01 00      call    near ptr _IOLockAlloc
0000BC4B C7 44 24 08 4B 00+  mov     [esp+18h+var_10], 4Bh ; 'K'
0000BC53 C7 44 24 04 08 00+  mov     [esp+18h+var_14], 8
0000BC5B C7 04 24 48 FE 00+  mov     [esp+18h+var_18], offset off_FE48
0000BC62 A3 20 91 02 00      mov     ds:dword_29120, eax
0000BC67 E8 24 83 FF FF      call    sub_3F90
0000BC6C C7 44 24 08 00 00+  mov     [esp+18h+var_10], 0 ; idata
0000BC74 C7 44 24 04 50 BB+  mov     [esp+18h+var_14], offset sub_BB50 ; <- listener callback
0000BC7C C7 04 24 50 DE 00+  mov     [esp+18h+var_18], offset aCom_apple_ka_0 ; "" <- SCOPE
0000BC83 A3 1C 91 02 00      mov     ds:dword_2911C, eax
0000BC88 E8 E3 DA 01 00      call    near ptr _kauth_listen_scope
0000BC8D A3 18 91 02 00      mov     ds:dword_29118, eax
0000BC92 C9                  leave
0000BC93 C3                  retn

The prototype for this function is:

extern kauth_listener_t kauth_listen_scope(
 const char *           identifier,
 kauth_scope_callback_t callback,
 void *                 idata

Where the most interesting parameter is the callback because “callback is the address of your listener callback function”, meaning it’s the function making the decisions :-).
You can see the two scopes being used, the and The process scope deals with processes operations and fileop with file operations. We are interested in the process operations.

IDA identified the callback location so let’s see what is there.

0000BB50                   ; =============== S U B R O U T I N E =======================================
0000BB50                   ; Attributes: bp-based frame
0000BB50                   sub_BB50        proc near               ; DATA XREF: sub_BC40+34o
0000BB50                   var_18          = dword ptr -18h <- local variable
0000BB50                   arg_8           = dword ptr  10h <- action
0000BB50                   arg_C           = dword ptr  14h <- idata
0000BB50                   arg_10          = dword ptr  18h <- credential
0000BB50 55                  push    ebp
0000BB51 89 E5               mov     ebp, esp
0000BB53 53                  push    ebx
0000BB54 83 EC 14            sub     esp, 14h
0000BB57 C7 04 24 14 91 02+  mov     [esp+18h+var_18], offset dword_29114
0000BB5E E8 45 D6 01 00      call    near ptr _OSIncrementAtomic
0000BB63 A1 84 FE 00 00      mov     eax, ds:dword_FE84
0000BB68 85 C0               test    eax, eax
0000BB6A 75 06               jnz     short loc_BB72  ; change to jump and so give a always defer result ?
0000BB6C 83 7D 10 02         cmp     [ebp+arg_8], 2 <- Compare Action in argument against 2. For this scope, value 2 action is KAUTH_PROCESS_CANTRACE, so this should be testing if action is trace attempt
0000BB70 74 1E               jz      short loc_BB90 <- if equal then process this action
0000BB72                   loc_BB72:                               ; CODE XREF: sub_BB50+1Aj
0000BB72                                                           ; sub_BB50+45j ...
0000BB72 BB 03 00 00 00      mov     ebx, 3          ; default is to return KAUTH_RESULT_DEFER (3)
0000BB77                   looc_BB77:                               ; CODE XREF: sub_BB50+6Cj
0000BB77 C7 04 24 14 91 02+  mov     [esp+18h+var_18], offset dword_29114
0000BB7E E8 1D D6 01 00      call    near ptr _OSDecrementAtomic
0000BB83 89 D8               mov     eax, ebx
0000BB85 83 C4 14            add     esp, 14h
0000BB88 5B                  pop     ebx
0000BB89 C9                  leave
0000BB8A C3                  retn
0000BB8A                   ; ---------------------------------------------------------------------------
0000BB8B 90 90 90 90 90      align 10h
0000BB90                   loc_BB90:                               ; CODE XREF: sub_BB50+20j
0000BB90 8B 45 14            mov     eax, [ebp+arg_C]
0000BB93 85 C0               test    eax, eax
0000BB95 74 DB               jz      short loc_BB72
0000BB97 8B 45 14            mov     eax, [ebp+arg_C]
0000BB9A 89 04 24            mov     [esp+18h+var_18], eax
0000BB9D E8 0A DC 01 00      call    near ptr _proc_pid
0000BBA2 89 04 24            mov     [esp+18h+var_18], eax
0000BBA5 E8 B6 FE FF FF      call    sub_BA60
0000BBAA 85 C0               test    eax, eax
0000BBAC 74 C4               jz      short loc_BB72
0000BBAE 8B 45 18            mov     eax, [ebp+arg_10]
0000BBB1 BB 02 00 00 00      mov     ebx, 2          ; <- KAUTH_RESULT_DENY (2)
0000BBB6 C7 00 01 00 00 00   mov     dword ptr [eax], 1 ; <- EPERM (1)
0000BBBC EB B9               jmp     short loc_BB77
0000BBBC                   sub_BB50        endp

If you check the KauthORama example, you can see the OSIncrementAtomic being used in the listeners routines. Let’s assume the author used this example to create his code. We should be in the right track.
To analyse this function we need to give a look to the include files (you need to install XCode and SDKs).

/usr/include/sys/errno.h:#define EPERM 1 /* Operation not permitted */

/Developer/SDKs/MacOSX10.4u.sdk/System/Library/Frameworks/Kernel.framework/Headers/sys/kauth.h:#define KAUTH_PROCESS_CANTRACE 2
/Developer/SDKs/MacOSX10.4u.sdk/System/Library/Frameworks/Kernel.framework/Headers/sys/kauth.h:#define KAUTH_RESULT_ALLOW (1)
/Developer/SDKs/MacOSX10.4u.sdk/System/Library/Frameworks/Kernel.framework/Headers/sys/kauth.h:#define KAUTH_RESULT_DENY (2)
/Developer/SDKs/MacOSX10.4u.sdk/System/Library/Frameworks/Kernel.framework/Headers/sys/kauth.h:#define KAUTH_RESULT_DEFER (3)

With these values we can try to understand what is going on (without kernel debugging, I can only do a dead listing attempt so I have to assume lots of stuff and cross fingers :-)).

The listener prototype is:

static int MyListener(
 kauth_cred_t   credential,
 void *         idata,
 kauth_action_t action,   <- Requested Action
 uintptr_t      arg0,
 uintptr_t      arg1,
 uintptr_t      arg2, 
 uintptr_t      arg3

Checking the arguments passed to the function, we see the action KAUTH_PROCESS_CANTRACE being tested. The listener must return one of three possible results: ALLOW, DENY or DEFER.
A request might go thru various listeners, but a single DENY from one listener is sufficient to DENY the request. If you check the demo code from the Technical Note, it returns DEFER as the default action. That makes some sense, because if we are not interested in processing that action, then we should let other listeners decide over it.

The logic for this piece of code could be something like this:


To patch this, we just need to return always the default value. We can force the first JNZ or NOP the second JZ. I patched the first JNZ to JMP. Patch the kernel extension, reboot and try to attach to the process… Hurray, no more errors!

Another one bites the dust… This is a very interesting feature of Mac OS X but we were able to somewhat easily beat it due to the ability to understand what was happening by dead listing the code. If code was obfuscated in some way it would be much harder because not everyone has two Macs to do kernel debugging.