前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CVE-2017-5123 漏洞利用全攻略

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

作者头像
Seebug漏洞平台
发布2018-03-16 12:16:02
1.6K0
发布2018-03-16 12:16:02
举报
文章被收录于专栏:Seebug漏洞平台

原文:https://salls.github.io/Linux-Kernel-CVE-2017-5123/

译者:hello1900@知道创宇404实验室

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

背景

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

代码语言:txt
复制
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_userunsafe_put_user涵盖进来。这样一来,忘记额外检查就在意料之中了。CVE-2017-5123就是一个很好的例子。在内核版本4.13中,为了能够正常使用unsafe_put_user,专门对waitid syscall进行了更新,但access_ok检查仍处于缺失状态。漏洞代码如下所示。

代码语言:txt
复制
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。

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

代码语言:txt
复制
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结构。唯一可用值是pidstatus,两者都存在一定限制。 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对象,好处在于它们包含函数指针,即用来控制使用函数(如readlseekioctl)的参数。通过将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

最后,感谢Chrome/Chromium安全团队对我的漏洞报告给予快速响应!

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 漏洞
  • 原语
  • 谷歌Chrome沙箱
  • 喘口气,进行 infoleak
  • SMAP绕过存在的局限性
  • 堆喷射
  • 获得更佳“任意”写入效果
  • 5字节写入+ Physmap
  • 真正的任意读/写和Root操作!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档