tl;dr

  • double fput() leading to uaf

pwnbox

Challenge source files : pwnbox

We are given the following files

$ ls
 bzImage  config_x86_64  mod.ko  rootfs.img  run.sh
  • bzImage : kernel image
  • config_x86_64 : kernel config file
  • mod.ko : Vulnerable kernel module
  • rootfs.img : root filesystem
  • run.sh : runner script
Boot took 0.48 seconds


_ ____      ___ __ | |__   _____  __
| '_ \ \ /\ / / '_ \| '_ \ / _ \ \/ /
| |_) \ V  V /| | | | |_) | (_) >  < 
| .__/ \_/\_/ |_| |_|_.__/ \___/_/\_\ 
|_|                                


/ $  uname -a 
Linux (none) 4.9.193 #3 SMP Thu Sep 19 19:33:54 IST 2019 x86_64 GNU/Linux

Inspecting /proc/cpuinfo we can see that smep and smap protections are enabled. For debugging it would be better to turn off kaslr by appending nokaslr to kernel parameters. While inspecting the init file we can see that mod.ko kernel module is loaded and /dev/mod character device is created. Since /proc/kallsyms is restricted, to get the address of the module we can edit init to get root shell and then dump kallsyms.

Analysing Kernel Module

After reversing the kernel module we can see that it provides ioctl interface through /dev/mod. And we can request for the following requests.

#define NEW_BOX 0x1337
#define UNLOCK_BOX 0x1338
#define LOCK_BOX 0x1339
#define DELETE_BOX 0x133a
#define SET_BOX 0x133b

NEW_BOX takes key as parameter and creates a box object with that key and buffer of size 0x100. A new encBox fd is created and returned to the user this is used to interface with the object. We can use read and write syscall on the returned fd to write and read data from box buffer.

struct encBox {
  size_t key;
  char *ptr;
};

SET_BOX takes encBox fd parameter and set this as the primary box, On which further operations are performed. LOCK_BOX does a repeated key xor of buffer with the specified key value. UNLOCK_BOX decrypts the buffer if correct key is specified. DELETE_BOX destroys the current box.

The bugs is in the SET_BOX request handler


static int box_set(struct file *file, char *attr) {

...

  if (file->private_data) {
    encfile = fget((unsigned int)(unsigned long)file->private_data);
    f = check_encfile(encfile);
    if (!IS_ERR(f))
      fput(f);
    fput(encfile);             <--
  }

...

}

struct file *check_encfile(struct file *encfile) {
  if (!encfile)
    return ERR_PTR(-EBADF);

  if (encfile->f_op != &encBox_fops) {
    fput(encfile);            <--
    return ERR_PTR(-EINVAL);
  }
  return encfile;
}

In box_set function if a fd is already present, it retrieves the file object by calling fget which takes an fd and returns a file object, also increment’s its reference count . This reference is droped by calling fput function. The check_encfile function checks if fd given is of the type encBox, if not it’s reference is decremented. For the case of fd not being of type encBox the reference is droped twice. With this we can create uaf of file object.

Exploitation

For triggering the bug we need to get non encBox fd as the current fd, but the set_box adds fd only after checking. So we need some other methord. One way to achieve this is to set one box then call close syscall on that fd, so next open will return same fd number.

  int box = box_new(mod_fd,0x1337);
  set_box(mod_fd,box);
  close(box);

  uaf_fd = open("/dev/null", O_RDWR | O_CREAT);

uaf_fd will have the same file descriptor number as box, since fd number is not cleared while box is freed. The next call to set_box will over decrement the file object.

Since we don’t have any info leaks, so it’s not feasible to overwrite the file object’s function pointers.

The idea is to open a writable file and after the kernel finish checking if it’s writable, use the bug to free the object and open a read only file in it’s place. Since the checks are passed kernel writes content to the file. One good candidate for such a file is to open /etc/passwd and overwrite the password of root user.

The current issue is that the check and writing gives us a small window which might be hard to race, so we need a better way to extend the race window.

writev

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

The writev() system call writes iovcnt buffers of data described by iov to the file associated with the file descriptor fd

The code for writev syscall is defined in fs/read_write.c

SYSCALL_DEFINE3(writev, unsigned long, fd, const struct iovec __user *, vec,
        unsigned long, vlen)
{
    return do_writev(fd, vec, vlen, 0);
}
static ssize_t do_writev(unsigned long fd, const struct iovec __user *vec,
             unsigned long vlen, int flags)
{

    if (f.file) {
    ...
        ret = vfs_writev(f.file, vec, vlen, &pos, flags);
    ...
  }

}
ssize_t vfs_writev(struct file *file, const struct iovec __user *vec,
           unsigned long vlen, loff_t *pos, int flags)
{
    if (!(file->f_mode & FMODE_WRITE))
        return -EBADF;
    if (!(file->f_mode & FMODE_CAN_WRITE))
        return -EINVAL;

    return do_readv_writev(WRITE, file, vec, vlen, pos, flags);
}

The vfs_writev checks if the file is writable and calls do_readv_writev function.


/**
 * import_iovec() - Copy an array of &struct iovec from userspace
 *     into the kernel, check that it is valid, and initialize a new
 *     &struct iov_iter iterator to access it.
**/
static ssize_t do_readv_writev()
{
    ...
    ret = import_iovec(type, uvector, nr_segs,
               ARRAY_SIZE(iovstack), &iov, &iter);
    ...
}

import_iovec function copy the iovec from userspace and later the write to the file happens. We can use userfaultfd to handle the page-fault of iovec access and extend the race window.

Other thing to keep in mind is that we need some mechanism to find if the newly opened file object take the place of freed object. One way is to spray by opening large number of files. Or we can use kcmp syscall to check if two fd have the same file object.

So the exploit is as follows:

  • mmap a region with userfaultfd enabled
  • open a writable file and call writev syscall with iovec pointing to mmaped region
  • after the write checks are passed, the read of iovec will trigger page-fault and our handler is called.
  • inside the handler trigger the bug and open /etc/passwd file.

conclusion

You can find the full exploit at github . The challenge idea and exploit was inspired from Jann Horn’s p0 report

Reference: