原文:https://salls.github.io/Linux-Kernel-CVE-2017-5123/
译者:hello1900@知道创宇404实验室
本文介绍如何利用Linux内核漏洞CVE-2017-5123提升权限,突破SEMP、SMAP、Chrome沙箱全方位保护。
背 景
在系统调用处理阶段,内核需要具备读取和写入触发系统调用进程内存的能力。为此,内核设有copy_from_user与put_user等特殊函数,用于将数据复制进出用户区。在较高级别,put_user的功能大致如下:
代码语言:javascript复制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检查仍处于缺失状态。漏洞代码如下所示。
代码语言:javascript复制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。
以下是漏洞利用阶段将引用到的写入字段概况。
代码语言:javascript复制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
最后,感谢Chrome/Chromium安全团队对我的漏洞报告给予快速响应!