背景
dirtypipe
dirtypipe(CVE-2022-0847)本质上是未初始化导致的逻辑漏洞,它可以实现任意写可读文件功能,提权不需要依赖内核函数地址等,且可以绕过各种内核保护和利用缓解措施。然而,该漏洞利用在很大基础上依赖此漏洞的能力,防御相对容易。该文章提出了一种新的利用手法,将其他linux 内核漏洞提升到dirtypipe级别。它不依赖于 Linux 的管道机制,也不依赖漏洞 CVE-2022-0847 的性质。相反,它利⽤堆内存损坏漏洞将低权限内核凭证对象(cred,file)替换为⾼权限内核凭证对象,使得非特权用户可以对高特权的文件或进程进行操作。
dirtycred优点:
- 它是一种通用的利用手法,可以对任意基于堆内存的漏洞进行提权。
- 可以显著减轻漏洞利用迁移的负担,也就是对任意适用的内核版本和架构,无需修改exp即可进行相同的漏洞利用。
- 可以绕过许多强大的内核保护/利用缓解机制(CFI,KASLR,SMEP,SMAP,KPTI等)
这是一个与dirtypipe相比的对比图:
slab
linux内核使用slab管理器对来实现对小内存的管理和分配,它主要维护了两种内存cache
1.通用缓存,即Generic Caches,普通的内存块缓存,大小为2的幂次方递增。
2.专用缓存,即Dedicated Caches,该缓存主要用于维护一些内核常用的对象,如cred,file等,由于这些常用对象在内核中被频繁使用,因此这些对象提供专用缓存可以减少分配它们所花费的时间,从而改善系统。
利用思路
文章以CVE-2021-4154为例,展示了大体的利用思路
主要分为三步:
- 利用漏洞等释放一个正在使用且没有特权的内核凭证对象(cred/file)
- 在被释放的对象内存处,申请一个新的有特权的内核凭证对象
- 以特权用户的权限进行操作
挑战
利用主要需要解决三个问题
1.如何去使用invalid-free来释放一个低权限的对象
2.作为低权限用户,我们如何分配高权限的内核凭证对象,且最终分配到相应的内存位置
3.如何实现稳定的file exploition
1.转化漏洞
OOB & UAF write
- 找到一个victim对象,该对象与我们需要攻击的对象共用同一缓存,且包含一个指向内存凭证对象的指针
- 利用oob或者uaf漏洞,将victim对象的凭证指针最后两字节覆盖为0
- 这样指针可能就指向了内存页开头的另一个凭证对象(内核凭证结构用特定的cache缓存)
- 这样就实现了有两个指针指向同一个凭证对象,这样我们就能实现UAF
- 接下来采用CVE-2021-4154的手法进行提权即可
Double free
一般通用缓存与专用缓存是隔离的,这些缓存中的对象没有重叠,然而linux内核有内存回收机制,当销毁内存缓存时,它会回收相应的未使用的内存页,然后分配给需要的别的缓存,此特性支持跨缓存内存操作,为dirtycred提供了针对double free漏洞的利用手段。
- 在有漏洞的cache中分配许多对象,在这些分配的对象中,有一个是易受攻击的对象(被两个悬垂指针ptr1,ptr2指向)
- 通过ptr1释放victim,然后重新分配对象,获得新的两个指针ptr1‘,ptr2’指向victim
- 通过ptr2释放victim,得到两个指针两个指针ptr1‘,ptr2’指向被free的victim内存区域
- 将该通用cache中所有的对象,页全部释放归还给slab
- 堆喷大量内核凭证对象
- 被归还的页被slab分配给专用缓存(缓存内核凭证对象),但指针不一定指向内核凭证对象起始位置(由于object大小不一定相等,并不一定是对齐的)
- 通过ptr2‘释放该凭证对象,再分配一个新的低权限的凭证对象,剩下ptr1’指向新的低权限的凭证对象
- 通过ptr1再次释放该凭证对象,然后申请新的高权限凭证对象
- 这样我们申请到的object的凭证对象就被替换了,完成提权
这里比较让人不明白的是,既然不是对齐的,那为什么可以直接调用free来释放内核凭证对象呢?
这里引用@bsauce以及@kiprey 师傅的回答:通过查阅 slab 分配器的 kfree 逻辑,发现它的释放逻辑与被释放地址高度相关。首先会尝试根据被释放地址获取其对应的 slab_cache 结构,然后再根据结构中所存放的信息来释放对应的 object size。换句话说,如果 kfree 释放的地址在 generic cache中,那就会走 generic cache 的释放逻辑;如果是在 dedicated cache 中,那就会走 dedicated cache 的释放逻辑。这么做或许是为了提高可用性,使得释放两个不同 cache 的内存块可以使用同一个 kfree 接口
尽管之前翻阅过slab的源码,但我的猪脑一下子就宕机了😥,差点忘了释放的object是直接链入的
2.成功分配高权限的内核凭证对象
为了确保我们能成功分配,增加race的成功率,我们需想办法延长时间窗口。
Userfaultfd & Fuse 延长时间窗口
关于userfaultfd这里我不多赘述了,经常用到,它允许用户自定义页错误时的处理操作,不过从v5.11开始,用户态的userfaultfd变为默认禁用的了。
Fuse可以参考 will’s root的文章以及一篇入门简介,这是一个接口,允许用户实现自己用户空间的文件系统,用户可注册handler函数来响应文件操作的请求。
借由@blue师傅的图和例子:
如题所示,通过hello程序把fuse文件系统挂载在/tmp/fuse目录下.此时如果在该目录中有相关操作时,请求会经过VFS到fuse的内核模块(上图中的步骤1),fuse内核模块根据请求类型,调用用户态应用注册的函数(上图中步骤2),然后将处理结果通过VFS返回给系统调用(步骤3)
只要有copy_from_user等从用户空间向内核空间的拷贝操作,以上两种方式都可以自定义阻塞(或者sleep之类的)来实现暂停,使得条件竞争的成功率大大增加
在v4.13之前,dirtycred可以通过系统调用writev进行文件的写操作, writev的实现⾸先检查⽂件对象,确保当 前⽂件处于打开状态并具有写权限。⼀旦检查通过,它就会从⽤⼾空间导⼊ iovec ,并将⽤⼾数据写⼊相应的⽂件。在这个实现中, iovec的导⼊是在权限 检查和数据写⼊之间。 DirtyCred 可以简单地利⽤前⾯提到的 userfaultfd 特性在完成权限检查后⽴即暂停内核执⾏,从⽽赢得⾜够的时间来交换⽂件对象。
论文中有相应的代码图
v4.13之前的代码
v4.13之后的patch
iovec的导入被移到了权限检查之前,这样的话尽管仍然可以通过userfaultfd暂停在iovec,但它不再能延长权限检查和实际文件写入之间的时间了。
利用文件系统的锁延长时间窗口
为了确保同步,文件系统不允许两个进程同时写入一个文件,通过锁机制实现。
ext4文件系统执行写入操作的简化代码:
可以看到第六行开始尝试请求索引节点锁,如果别人持有锁就要一直等待锁被释放。尽管它确实实现了写操作的同步,但也给dirtycred留下了延长时间窗口执行对象交换的时间。
具体的:
DirtyCred可以生成两个进程AB,这两个进程同时向一个文件写入数据,假如进程A持有锁,并写入大量数据,此时进程B不得不等待较长时间,直至锁被释放。然而在调用generic_perform_write()函数之前,进程B已经完成了文件权限的检查,锁等待的时间完全足以我们来完成文件对象交换。根据论文的数据,将4GB写入硬盘时,过程大概需要几十秒的时间,在这个时间内,就可以完成出发漏洞和执行内存操作,而不会在利用过程中引发任何不稳定的问题。
分配高权限内核凭证对象
用户空间分配:
- 低权限用户可以执行具有suid权限的二进制程序(ctf题中的busybox往往没有,但现实场景一般都有,如su/ping/sudo/mount/pkexec),或者频繁创建特权级守护进程(sshd等),这些进程都会分配高权限cred对象。
感觉还挺麻烦的 - 如果选择用file攻击的话,文件对象分配相对容易很多,我们直接打开只读的
/etc/passwd
就行
- 低权限用户可以执行具有suid权限的二进制程序(ctf题中的busybox往往没有,但现实场景一般都有,如su/ping/sudo/mount/pkexec),或者频繁创建特权级守护进程(sshd等),这些进程都会分配高权限cred对象。
内核空间分配:
- 内核启动新的内核线程时,会复制当前正在运行的进程,而绝大部分内核线程都有一个特权的cred对象,也就是我们可以通过创建线程的方法,分配高特权的cred对象,具体的有两种:
- 与内核代码交互,触发内核在内部产生一个新的特全线程,如通过工作队列创建工作线程
- 利用一种允许内核创建用户模式进程的机制,最直接的应用就是将内核模块加载进内核,在加载时,内核会调用usermode-helper API,以高权限模式执行用户空间的modprobe,从而在内核创建一个高权限cred对象(modprobe部分功能为搜索标准安装模块目录从而找到目标驱动,在搜索过程中,内核不应该阻塞,所以会创建新的内核线程来执行)
- 内核启动新的内核线程时,会复制当前正在运行的进程,而绝大部分内核线程都有一个特权的cred对象,也就是我们可以通过创建线程的方法,分配高特权的cred对象,具体的有两种:
3.可以利用的对象
论文中,作者利用了一种自动化跟踪可利用对象的方法,找出了v5.16.15下包含内核凭证对象的对象
符号★表⽰与file
凭证关联的对象,⽽符号†表⽰与 cred
对象关联的对象。 “Memory Cache”列指定存储内核对象的缓存。 structure列表⽰可利⽤对象的 类型。 Offset列描述了凭证对象的引⽤在可利⽤对象中的位置。
其中一些对象包含多个内核凭证对象(如ovl_dir_file
linux_binprm
)
评估
实验探索了dirtycred针对现实漏洞的可利用性,实验样例几乎涵盖了内核堆上所有类型的漏洞
可以看到,double-free的样例全部都是成功的,说明其转化能力非常强;而失败样例总是来自 OOB&UAF,对于OOB的样例,失败案例展示了虚拟内存区域的内存损坏,要使用dirtycred,我们需要找到带有凭证信息的内核对象,而这些对象通常在kmalloc内存区,而不是虚拟内存。
防御
- dirtycred不违反任何控制流完整性,CFI保护无效
- dirtycred不依赖单个开发组件,有很多可以利用的对象,因此无法消除对象来进行防御
- dirtycred替换内核凭证而不是篡改,这使得内核凭证完整性保护也失效
- dirtycred通过在彼此之间交换同一内核凭证对象,不受内核对象隔离机制的影响(如AUTOSLAB,xMP,因为它们根据对象类型而不是它们的特权在自己的内存区域分离相应内核对象)
解决方案:
隔离高权限和低权限的内核凭证对象,高权限对象放在虚拟内存中,调用vmalloc进行分配;低权限对象放在线性映射区中,使用kmalloc分配,这样高低权限对象区域就不会重叠了。
实验:
手动修改了内核中分配cred对象和file对象的方式,具体的:如果分配给特权⽤⼾,我们使⽤虚拟内存分配它们。具 体来说,在分配cred对象时,我们根据对象的UID来检查权限。如果UID与 GLOBAL_ROOT_UID 匹配,这意味着分配是针对特权 cred 对象的,我们使 ⽤vmalloc作为分配器为该对象分配虚拟内存。对于⽂件对象,我们检查⽂件 的模式。如果⽂件以写权限打开,我们将相应地使⽤vmalloc分配⽂件对象。
结果:
仅引入了可忽略的轻量开销,大概到4%。
讨论与未来工作
容器逃逸:
容器中没有file提供命名空间的权限, Using the Dirty Pipe Vulnerability to Break Out from Containers 不过最近这篇文章提出了一种攻击者被动等待runC进程,从而通过覆盖进程在主机上执行root命令的方法。若使用cred对象进行逃逸的话,就不需要被动等待了。这里我还没接触过,后面再看看。
安卓提权:
尽管Android基于linux内核,但它有更严格的访问控制和防护机制,更难以被利用。dirtycred有两种方法提权Android,一种是直接交换进程凭证,另一种是利用文件操作功能覆盖共享系统库,从而允许在沙盒中提权,然后再用恶意代码覆盖内核模块,实现任意读写,最终禁用SELinux,本文的作者们在撰写文章时就向谷歌提交了Android的提权0day。
跨平台/架构利用:
dirtycred不依赖内核函数地址,不包含任何特定版本或底层架构的数据,故跨平台/架构 通用。
其他漏洞转化:
在上面提到,虚拟内存上的漏洞更难以被利用,因为上面的可利用对象很少,但这并不意味着就没法利用了。对于 CVE-2021-34866,有一篇 文章 可以将 vmalloc 越界写转化为任意地址读写,因此也能转化为double free能力,DirtyCred 很有可能完成利用。
参考资料
https://zplin.me/papers/DirtyCred.pdf (论文)
https://zplin.me/papers/DirtyCred-BH22-Zhenpeng.pdf (blackhat)
https://www.maastaar.net/fuse/linux/filesystem/c/2016/05/21/writing-a-simple-filesystem-using-fuse/
https://kiprey.github.io/2022/10/dirty-cred/#%E4%B8%80%E3%80%81%E7%AE%80%E4%BB%8B&gid=1&pid=1