Two Bugs, One Func()
› part iii: a kernel heap overflow
4/24/2017
love these blog posts? support my tools & writing on patreon! Mahalo :)
Background
The first part of this multi-series blog series 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-by-one read error would trigger a kernel panic if auditing was enabled:
While this bug did not appear exploitable, after analyzing Apple's 'fix' (released in macOS 10.12.4), I realized that they:
- did not fix the kernel panic
- introduced a kernel info leak, that could leak sensitive information or be used to bypass KASLR:
$ sudo hexdump -C /var/audit/current | 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
The second blog post in this series, "a kernel info leak 0day, thanks to Apple's fix" covered the gory technical details of this 0day kernel bug.
Where There's Smoke, There's Fire
So we've blogged about a kernel panic and a kernel memory leak. But are (was) there more? Yes! Turns out that within the same function, audit_arg_sockaddr(), a subtle issue existed that could result in an exploitable ring-0 heap overflow.
It's been said that "where there's smoke, there's fire." After finding the off-by-one error in audit_arg_sockaddr(), I decided to poke on the code within this function a bit more. Here's some code from the start of the function:
void audit_arg_sockaddr(struct kaudit_record *ar, struct vnode *cwd_vp, struct sockaddr *sa)
{
int slen;
struct sockaddr_un *sun;
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);
...
}
We've discussed the audit_arg_sockaddr function in detail before, but just to reiterate the function attempts to audit (log) a socket's address. On the surface this seems like a fairly trivial task...however as we'll see, there is more than meets the eye!
After some basic parameter validation (for example checking for NULLs), the audit_arg_sockaddr function copies the passed in sockaddr structure ('sa') into a kernel audit record structure, (kaudit_record, 'ar') via a call to bcopy():
void audit_arg_sockaddr(struct kaudit_record *ar, struct vnode *cwd_vp, struct sockaddr *sa)
{
...
bcopy(sa, &ar->k_ar.ar_arg_sockaddr, sa->sa_len);
}
The kaudit_record structure is defined in /bsd/security/audit/audit_private.h:
/*
* In-kernel version of audit record; the basic record plus queue meta-data.
* This record can also have a pointer set to some opaque data that will be
* passed through to the audit writing mechanism.
*/
struct kaudit_record {
struct audit_record k_ar;
u_int32_t k_ar_commit;
void *k_udata;
u_int k_ulen;
struct uthread *k_uthread;
TAILQ_ENTRY(kaudit_record) k_q;
};
The first member in this structure is another structure: struct audit_record k_ar. The call to bcopy()copies the sockaddr structure into this structure...specifically, into 'k_ar.ar_arg_sockaddr'.
From the above kaudit_record structure definition, we can see 'k_ar' is an audit_record structure. This rather large structure is also defined in the same audit_private.h file:
struct audit_record {
u_int32_t ar_magic;
int ar_event;
int ar_retval;
...
struct sockaddr_storage ar_arg_sockaddr;
int ar_arg_fd2;
...
};
From this definition, its easy to see that 'ar_arg_sockaddr' is yet another structure, this time of type 'sockaddr_storage'.
We met the 'sockaddr_storage' structure in previous blog posts. It's 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];
};
Note the maximum size of this structure ('_SS_MAXSIZE') 128.
Ok, so back to the bcopy():
bcopy(sa, &ar->k_ar.ar_arg_sockaddr, sa->sa_len);
Apple's man pages (man bcopy) provide a function declaration: void bcopy(const void *src, void *dst, size_t len); and state, "The bcopy() function copies len bytes from string src to string dst."
Let's summarize the bcopy() call in audit_arg_sockaddr():
- source ('src'): struct sockaddr *sa
- destination ('dst'): struct sockaddr_storage k_ar.ar_arg_sockaddr
- bytes to copy ('len'): sa->sa_len
As we showed in the previous blog, one can (legally) craft sockets larger than 128 bytes. For example, here is a socket that's 200 bytes long:
#define SOCKET_SIZE 200
//create socket
int unixSocket = socket(AF_UNIX, SOCK_STREAM, 0);
//alloc/fill
char* addr = malloc(SOCKET_SIZE);
memset(addr, 0x41, SOCKET_SIZE);
//init
((struct sockaddr_un*)addr)->sun_len = SOCKET_SIZE;
((struct sockaddr_un*)addr)->sun_family = AF_UNIX;
//bind
bind(unixSocket, (struct sockaddr *)addr, SOCKET_SIZE));
On macOS 10.12.3 and before, since the bcopy() operation uses the source socket's len ('sa->sa_len'), and performs no bounds checking, sockets larger than 128 ('_SS_MAXSIZE') will overflow the kernel audit record's 128-byte sockaddr_storage ('ar_arg_sockaddr') buffer.
Oops!!
Ok let's summarize this vulnerability:
- When (network) auditing is enabled, binding a socket will trigger a call to audit_arg_sockaddr().
- The audit_arg_sockaddr function will copy the socket to audit into an audit record via bcopy().
- If the socket is greater than 128 bytes ('_SS_MAXSIZE'), this copy operation (which uses the size of the socket to determine how many bytes to copy) will overflow the statically sized struct sockaddr_storage ('ar_arg_sockaddr').
Hooray; kernel heap-overflow!
Towards Exploitation
In order to exploit a heap overflow, one generally has to control (or partially control) two things:
- The number of bytes copied, such that # bytes > the destination buffer size
- The values of the bytes copied
As this bug fulfills both these 'requirements', it should be exploitable.
Given such a heap-overflow vulnerability, there are many papers and talks that describe exactly how to gain control of the instruction pointer in order to 'weaponize' the bug:
As such (and due to the fact that yes, I'm somewhat lazy), we won't cover all of these details here again.
Normally, as covered within aforementioned macOS heap-exploitation papers/talks, to gain control of the instruction pointer (RIP), one overflows a heap buffer into an adjacent object. If this object is something 'owned' by the attacker, such as a C++ object, it can be corrupted in an exploitable manner. For example, one may be able to hijack an object's vTable, or corrupting it's method pointers.
I've (ab)used this technique to exploit a kernel-mode heap overflow I discovered within LittleSnitch:
For the vulnerable bcopy() within the audit_arg_sockaddr() function, overflowing into an adjacent object is possible. For example, we can easily corrupt a random adjacent object in a random manner (0x4141....), which leads to a kernel panic:
kernel.debug`panic(str="\"a freed zone element has been modified in zone %s: expected %p but found %p, bits changed %p, at offset %d of %d in element %p, cookies %p %p\"@/Library/Caches/com.apple.xbs/Sources/xnu_debug/xnu-3789.41.3/osfmk/kern/zalloc.c:651") + 261 at debug.c:460
However, we may be able to stay wholly self-contained, and gain execution by simply corrupting other members within the kernel audit record structure. This is because following the sockaddr_storage ('ar_arg_sockaddr') structure, are various pointers and length variables:
struct kaudit_record {
struct audit_record k_ar;
//below this, can be overflowed
void *k_udata;
u_int k_ulen;
struct uthread *k_uthread;
TAILQ_ENTRY(kaudit_record) k_q;
};
Via the overflow, carefully corrupting these values, may lead to control of RIP. Below is an example the kernel dereferencing a pointer value, we fully control (0x4141....):
Generally speaking, hijacking fields within the same object or structure can often more lead to a more reliable exploit. This is due to the fact that such 'self-contained' corruption isn't susceptible to the rather indeterministic nature of the heap nor relies on corrupting external objects.
Conclusions
I reported this bug to Apple at the start of March. They closed it and the off-by-one bug (discussed in part one of this blog series) as a duplicate of same bug. Obviously these are two totally different bugs...oh well :|
Though the the release notes (AFAIK), did not mention it, this bug was fixed in macOS 10.12.4. How? Quite simply! Apple added a new check in order to ensure the maximum number of bytes copied into the kernel audit record do not exceed the size of the sockaddr_storage structure, 'ar_arg_sockaddr':
if (sa->sa_len > sizeof(ar->k_ar.ar_arg_sockaddr))
bcopy(sa, &ar->k_ar.ar_arg_sockaddr, sizeof(ar->k_ar.ar_arg_sockaddr));
else
bcopy(sa, &ar->k_ar.ar_arg_sockaddr, sa->sa_len);
Unlike Apple's fix for the off-by-one error that didn't fixed the bug and, worse, introduced a kernel-mode information leak (0day PoC here)...this fix looks solid :)