前段时间,在群里小伙伴发了最新的linux内核利用,影响版本还挺多,自己也利用国外的exploit来进行实验linux提权……阅读本文需要的基础:c语言,操作系统,内核调用……(虽然没有,也是可以看的 。。偷笑)
在这篇博客文章中,我将解释如何利用CVE-2017-5123这一我在Linux内核中发现的bug,并展示如何使用它来提升权限,即使使用者使用的环境是SMEP,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中,waitid()在系统调用时被更新成使用unsafe_put_user,但没有进行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系统调用的infop参数,然后系统调用将简单地使用该内核地址进行写操作,因为unsafe_put_user函数它从不被进行检查。但是最初的一个很棘手的部分是我们不能很好地控制写的东西。因为它写入了6个不同的区域,我们没有完全控制任何一个区域。 info.status是一个32位的int类型,但是大小被限制为0 <status <256。info.pid可以通过重复分叉来稍微控制下,但是它的最大值为0×8000。
以下是我们将在漏洞利用期间提到的代码内容概述。
- 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
- }
是什么让这个bug比权限升级更有趣的呢?哈哈,它可以在Chrome沙箱中使用。首先,我们来解释一下Chrome 沙箱是如何工作的。
Google Chrome使用沙箱来保护浏览器,即使攻击者获得代码执行权,也无法触及系统的其他部分。有两层沙箱。第一个通过改变用户ID和做一个chroot来限制对资源的访问。第二个尝试使用seccomp过滤器来限制内核攻击,以阻止沙盒进程中不需要的系统调用。这通常是相当有效的,因为大多数Linux内核漏洞都在系统调用中被seccomp沙箱阻止了。然而,waitid()系统调用很有趣,因为它通常在seccomp沙箱中是被允许的,当然,这也包括了Chrome沙箱(chrome seccomp源码)。这意味着我们可以攻击内核以逃离Chrome沙箱!
沙盒强加的一个限制 是不允许这样做的。我们只能创建新的线程而不是进程。这是一个问题,因为如果我们不能进行系统调用,waitid()将会失败,只允许我们写0到内核内存中。
你必须相信我,我们才能最终完成。但无论哪种方式,我们都需要知道内核的利用地址,所以我们先来看看。unsafe_put_user存在的一个不错的特性,它访问无效的内存地址时不会崩溃,而只是返回-EFAULT。因此,我们可以只保留内核数据段可能在的地址,直到我们得到不同的错误代码,然后我们就有可能找到内核地址,并可以打败KASLR。只是要小心不要覆盖掉任何重要的东西。
我们可以这样做来查找内核堆的地址,或者内核内存存在的其他区域。
在这一点上,我想看看是否有可能充分利用这个bug。总而言之,目前我们所能做的事情相当有限:
我们只能写0。我们写24个字节的0,并且会破坏掉附近的内存。没有更多的信息泄露出来。我们只知道内核库的位置,堆的位置,而不是堆中的对象。我想了一会儿,决定用不同的方法来利用这个bug,并确定了几个方向:
在内核数据部分找到一个对象,其索引/大小/值为零时会导致超出内存访问的效果。在内核中覆盖一个自旋锁让我们创建一个条件竞争。尝试覆盖内核堆栈上的基址指针或其他值。触发将导致在内核堆上创建有用结构的操作,然后看看我们是否可以用我们任意写入的0来命中它们。
task_struct(表示每个进程和线程的结构)的开头是一些标志,如果使用了seccomp过滤器,那么覆写掉将是会出现一个标志。如果我们可以用task_structs释放堆,并且只覆盖那些起始标记,那么我们可以从我们的一个进程中移除seccomp,然后我们就会有更多的希望。
由于我不是Linux内核堆的专家,因此我首先进行了10000个线程,然后使用调试器来检查任务结构在堆中的位置。我注意到,当我使用足够多的任务时,大部分的任务结构最终都会在堆中的一个较低的地址上,而不是之前的任何结构。这似乎意味着随着内存中空闲槽被用完,堆将向下扩展。
那么计划是:
创建10000个线程从堆中最低的地址开始,继续猜测任务结构可能在的地址让10000个线程继续检查它们是否仍在seccomp沙箱中当发现它不再受seccomp影响时停止。虽然它不可靠,但它足以证明概念,而且我认为通过更多地调整释放可能使其更可靠。也许如果你先释放其他对象来填充内存的“空洞”,然后在创建10000个线程后释放它们,你可以更确定目标任务结构将在堆的底部。我还没有探索到这一点,但目前似乎在我的电脑上有大约50%的时间在工作,内核崩溃了使用了另外50%的时间。
在这一点上,有一个任务已经不在seccomp沙盒中了,但我们知道task_struct在上一步的地址。我们仍然需要弄清楚如何从这里利用内核升级到root权限并删除chroot。
幸运的是,我们的基础工作已经变得更好了,现在我们可以使用fork()创建子元素,然后waitid写入非零值。不幸的是,我们仍然不能控制很多的siginfo结构。唯一可以使用的值是pid和status,而这两者都是有限的。最大pid是0×8000,状态是一个字节。
但是,由于在pid中内存有一些未使用的填充(在前面的结构中显示),我们可以执行5次写入,每次都会移回一个字节。构造一个任意的5字节写入。
使用我们上面构建的5字节写法并不简单。我们仍然不能创建出任意地址写入。然而,我们可以创建看起来像0x ** 000000的地址,其中的*可以是任何东西。
在这里的时候,我从ret2dir中获得了灵感。有一部分内核内存称为physmap,内核会保留一个“别名”,第二个虚拟地址会映射到与用户空间内存相同的物理内存。因此,通过在用户空间中创建一个填充0×41的内存页面,实际上在内核中有一个地址与之对应,所以我们可以找到与填充0×41完全相同的页面。
我的策略是在用户空间分配大量的内存。然后尝试随机覆盖内核的physmap中的页面,同时检查userland页面是否已经改变。如果我们看到一个变化,那么我们已经找到了一个与用户状态地址对应的内核虚拟地址,我们可以写入userland在内核内存中来创建我们的payload。我只尝试内核地址结尾为6的内核中的页面,这样一旦我们找到一个地址的“别名”,就可以构造一个指向该内核内存地址的指针。
这部分是非常可靠的,但在罕见的情况下可能崩溃一个随机进程。
现在我能覆盖掉filestask_struct中的指针,将其指向我们在内核中找到的“别名”,userland中我构造一个假的files_struct对象,它也将会是alias、file对象,这种情况很不错,因为它们含有函数指针,你能控制参数来实现一些功能,如read,lseek,ioctl。通过指向ioctl内核中的各种ROP小工具,我们可以创建一个任意的读写权限。我修复了task_struct的clobbered部分,将我们的creds结构改为root。最后,我通过重置当前的fs来删除chroot。现在的情况是已经完全逃脱了沙箱,可以以root身份弹出一个计算器!
这个完整的漏洞可以在github.com/salls/kerne… 看到。感谢Chrome / Chromium安全人员对我的错误报告的快速响应!
来源: https://juejin.im/entry/5a0ebc8151882534af258777