CVE-2017-5123 漏洞利用全攻略

原文:https://salls.github.io/Linux-Kernel-CVE-2017-5123/
译者:知道创宇404实验室

本文介绍如何利用Linux内核漏洞CVE-2017-5123提升权限,突破SEMP、SMAP、Chrome沙箱全方位保护。

背景

在系统调用处理阶段,内核需要具备读取和写入触发系统调用进程内存的能力。为此,内核设有copy_from_user与put_user等特殊函数,用于将数据复制进出用户区。在较高级别,put_user的功能大致如下:

put_user(x, void __user *ptr)
    if (access_ok(VERIFY_WRITE, ptr, sizeof(*ptr)))
        return -EFAULT
    user_access_begin()
    *ptr = x
    user_access_end()

access_ok() 调用检查ptr是否位于用户区而非内核内存。如果检查通过,user_access_begin()调用禁用SMAP,允许内核访问用户区。内核写入内存后重新启用SMAP。需要注意的一点是:这些用户访问函数在内存读写过程中处理页面错误,在访问未映射内存时不会导致崩溃。

漏洞

某些系统调用要求多次调用put/get_user以实现内核与用户区之间的数据复制。为避免重复检查和SMAP启用/禁用的额外开销,内核开发人员将缺少必要检查的不安全版本_put_user与unsafe_put_user涵盖进来。这样一来,忘记额外检查就在意料之中了。CVE-2017-5123就是一个很好的例子。在内核版本4.13中,为了能够正常使用unsafe_put_user,专门对waitid syscall进行了更新,但access_ok检查仍处于缺失状态。漏洞代码如下所示。

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();
        unsafe_put_user(signo, &infop->si_signo, Efault);    <-    no access_ok call
        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();
        return err;
Efault:
        user_access_end();
        return -EFAULT;
}

原语

缺少access_ok检查意味着允许提供内核地址并将其作为waitid syscall的infop参数。syscall将使用unsafe_put_user覆盖内核地址,因为此项操作可以逃避检查。该原语的棘手部分在于无法对写入内容(6个不同字段中的任何1个)施与足够控制。info.status 是32位int,但被限制为0 < status < 256。info.pid可在某种程度上通过重复fork操作进行控制,但最大值为0x8000。

以下是漏洞利用阶段将引用到的写入字段概况。

struct siginfo {
    int si_signo;
    int si_errno;
    int si_code;
    int padding;   // this remains unchanged by waitid
    int pid;       // process id
    int uid;       // user id
    int status;    // return code
}

谷歌Chrome沙箱

该漏洞的特色在于可从Chrome浏览器沙箱内部实现提权。首先介绍Chrome沙箱概况与工作原理。

谷歌Chrome采用沙箱保护浏览器,即便成功利用漏洞实现代码执行也无法touch系统其它部分。沙箱分两层:第一层通过改变user id与chroot限制资源访问;第二层尝试通过seccomp filter限制内核攻击面,阻止沙箱进程中不必要的系统调用。通常情况下,Chrome沙箱行之有效,因为Linux内核漏洞多位于syscall,由seccomp沙箱拦截。

然而,waitid syscall在seccomp沙箱中普遍存在,当然也包括Chrome沙箱(chrome seccomp source)。也就是说,可以通过攻击内核实现Chrome沙箱逃逸!

沙箱的局限性在于不允许使用fork,只能创建新线程而非进程。如果无法进行fork操作,waitid就会无法发挥作用,只能将0写入内核内存。

喘口气,进行 infoleak

所有困难都是暂时的,但无论采取哪种方式,都需要先获取内核基地址。 unsafe_put_user的一个优秀属性是在访问无效内存地址时不会崩溃,仅返回-EFAULT。因此,我们仅需猜测内核数据段潜在地址,直至显示不同错误代码、找到内核地址。有了内核地址就可以攻破KASLR了, 但注意不要覆盖任何重要信息 :)

我们可以用相同做法查找内核堆栈地址或内核内存其他区域。

SMAP绕过存在的局限性

现在,我想看看是否可以利用该漏洞突破所有防线。 结果发现目前能做的事情相当有限:

  • 只能写0;
  • 写24个字节的0,破坏附近内存;
  • 少量信息渗出,包括内核基地址与堆栈位置,但不包括堆栈目标位置。

辗转思考多种漏洞利用方法后确定了几个方向:

  • 在内核数据段找到一个对象,其索引/大小/值为零将导致超出内存访问边界;
  • 在内核中覆盖一个自旋锁,用来创建竞争条件;
  • 尝试覆盖内核堆栈上的基址指针或其他值;
  • 触发可能导致在内核堆栈上创建有用结构的操作,看看是否可以用任意写入的0命中对象。

我最终选取了第四个策略,进行堆喷射。

堆喷射

task_struct(代表每个进程和线程的结构)开始部分是一些flag,其中一个flag标记是否采用seccomp过滤器。如果能够用task_structs进行堆喷射,并且只覆盖那些起始flag,则可从其中一个进程移除seccomp,从而获取更多可能。

考虑到Linux内核堆栈并非自身擅长领域,先喷射10000个线程,然后使用调试器检查任务结构在堆栈中的位置。我注意到,喷射对象达到一定数量后,大部分任务结构将在堆栈较低地址处结束。这似乎意味着随着空闲槽被用完,堆栈将向下扩展。

接下来的计划是:

  • 创建10000个线程;
  • 从堆栈最低地址起继续猜测任务结构潜在地址;
  • 让10000个线程继续自检是否仍位于seccomp沙箱;
  • 当发现某个线程不再受seccomp影响时停止。

结果竟然奏效了!这种做法虽不可靠,但作为PoC已经足够。我认为增大喷射力度能够提升可靠程度。如果先喷洒其他对象填充,再创建10000个线程释放,可以更加确定目标任务结构将位于堆栈底部。截至目前,我电脑上的运行结果已达到50%成功率,其余半数则以内核崩溃告终。

获得更佳“任意”写入效果

现在,我们面临一项seccomp沙箱外围任务,目前已从上一步获知task_struct地址,仍需弄清如何利用内核漏洞升级到root权限并移除chroot。

好在原语已得到优化,可以使用fork() 来创建子对象,然后使waitid写入非零值。尽管如此,我们仍无法控制多数siginfo结构。唯一可用值是pid和status,两者都存在一定限制。 pid最大值是0x8000,状态是单字节。

但是,由于pid紧挨着一些未使用的填充(如前文所述),可以执行5次写入,每次都移回一个字节,构造一个任意写入的5字节。

5字节写入+ Physmap

5字节写入的使用方法并非显而易见,暂时仍无法创建任意地址。然而,我们可以创建外观类似 0x**********000000的地址,其中*可以是任意值。

在此,我从ret2dir获取灵感。有一段名为physmap的内核内存,其中内核保留一个映射到与用户区内存具有相同物理内存的“alias”(虚拟地址)。因此,在用户区创建一个填充0x41的页面后,内核中确实存在一个可以找到与该页面完全相同的网页地址。

我的策略是在用户区分配大量内存,然后尝试随机覆盖内核physmap中的页面,同时检查用户区页面是否已经改变。如果发现变化,则说明我们已经找到了一个与用户区地址相对应的内核虚拟地址,可以写入用户区并在内核内存中创建有效payload。我仅对内核physmap中以6个0结尾的页面进行了尝试,一旦找到“alias”,就可以构造一个指向内核地址的指针。

这部分内容非常可靠,但在罕见情况下也可以崩溃一个随机过程。

真正的任意读/写和Root操作!

现在我覆盖task_struct中的files指针,使其指向内核中的“alias”,在用户区构造一个伪造的files_struct对象,该对象也将位于alias.file对象,好处在于它们包含函数指针,即用来控制使用函数(如read,lseek,ioctl)的参数。通过将ioctl指向内核中的各种ROP小工具可以创建一个任意读写原语。于是,我修复了task_struct的clobbered部分,将creds结构改为root。最后,通过重置当前的fs移除chroot。现在我们已经完全实现沙箱逃逸,能够以root身份弹出一个计算器了!

完整漏洞参见https://github.com/salls/kernel-exploits/blob/master/CVE-2017-5123/exploit_smap_bypass.c

上一篇:glibc malloc学习笔记之fastbin

下一篇:WPA2 “KRACK” 漏洞简介与重现