pwnbox InCTFi 2019
pwnbox
This is a Kernel exploitation challenge I created for InCTFi 2019. 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: