🤔
内核漏洞 DirtyPipe 被报道存在于几乎所有 Linux 版本中,自 5.8 版本以来一直存在。利用此漏洞,不良操作者可以在不触发现有的内核保护和利用缓解措施的情况下实现特权升级,使该漏洞尤其令人不安。然而,DirtyPipe 漏洞的成功利用在很大程度上取决于此漏洞的能力(即通过 Linux 的管道将数据注入任意文件)。这种能力在其他内核漏洞中很少见,使得防御相对容易。只要 Linux 用户消除了漏洞,系统就可以相对安全。
本文提出了一种新的利用方法 - DirtyCred,将其他 Linux 内核漏洞推向 DirtyPipe 的级别。从技术上讲,给定一个 Linux 内核漏洞,我们的利用方法交换了非特权和特权内核凭证,从而为漏洞提供了类似 DirtyPipe 的可利用性。利用此漏洞能力,不良操作者可以获得提升特权甚至逃逸容器的能力。我们在一个完全受保护的 Linux 系统上评估了这种利用方法,并对 24 个真实世界内核漏洞进行了测试。我们发现 DirtyCred 可以在 16 个漏洞上展示可利用性,这意味着 DirtyCred 的安全严重性。在评估可利用性之后,本文进一步提出了一种新的内核防御机制。与现有的 Linux 内核防御不同,我们的新防御根据它们自己的特权将内核凭证对象隔离在非重叠的内存区域中。我们的实验结果表明,新的防御主要引入了可以忽略不计的开销。
如今,Linux 因为其在移动设备、云基础设施和 Web 服务器中的广泛应用而成为黑客攻击的热门目标。为了保护 Linux,内核开发人员和安全专家引入了各种内核保护和漏洞缓解技术(例如 KASLR [14] 和 CFI [19]),使内核利用变得前所未有的困难。为了成功实现攻击目标,恶意操作者今天必须确定那些具有能够禁用相应保护和缓解措施功能的强大内核漏洞。
然而,最近一个被标记为 CVE-2022-0847 [10] 的漏洞及其利用方法受到了网络安全界的广泛关注。由于其破坏性和影响,它甚至被冠以 DirtyPipe [30] 的外号。与其他未命名的内核漏洞不同,DirtyPipe 的利用可以实现特权升级,而无需去禁用广泛采用的内核保护和漏洞缓解措施。这一特点导致现有的 Linux 防御机制无效,因此导致许多以 Linux 内核为驱动的系统面临危险(例如 Android 设备)。
DirtyPipe虽然强大,但其可利用性与漏洞的能力密切相关(即滥用Linux内核管道机制向任意文件注入数据)。对于其他Linux内核漏洞,很少提供这种管道滥用能力。因此,Linux社区和设备制造商(例如Google)采取的措施是迅速发布针对内核漏洞的补丁,并消除攻击面。没有这个攻击面,对于完全受保护的Linux内核的利用仍然具有挑战性。对于其他内核漏洞,要实现与DirtyPipe相同级别的安全影响仍然很困难。
在本文中,我们提出了一种新颖的通用利用方法,即普通的内核漏洞也可以实现与DirtyPipe相同的利用目的。从技术角度来看,我们的利用方法与DirtyPipe不同。它不依赖于Linux的管道机制,也不依赖于CVE-2022-0847漏洞的本质。相反,它利用堆内存损坏漏洞将低特权内核凭证对象替换为高特权内核凭证对象。这种做法使Linux内核以为未经授权的用户可以获得操作高特权文件或进程的权限。因此,我们将此利用方法命名为DirtyCred。
要进行利用,DirtyCred面临三个关键技术挑战。首先,它需要将漏洞的能力转化为有用于凭证对象交换的能力,因为不同类型的漏洞在内存损坏方面提供了不同的能力,这些能力可能乍一看不足以进行凭证对象交换。其次,DirtyCred需要严格控制启动对象交换的时间窗口。正如我们将在第3节中讨论的那样,对于DirtyCred而言,时间窗口非常短暂。如果没有一个实际的机制来延长时间窗口,那么利用就会不稳定。第三,DirtyCred需要找到一种有效的机制,允许非特权用户以主动方式分配特权凭证,因为如果没有这个能力,凭证对象交换将是无效的。
为了应对以上的技术挑战,我们首先引入了一系列漏洞转移方案,使我们能够将任何基于堆的漏洞转化为一个释放凭据对象的无效方式,从而实现特权升级攻击。其次,我们利用三种不同的内核特性——userfaultfd [38]、FUSE [37]和文件系统中的锁定——来延长对象交换所需的时间窗口,从而稳定利用过程。最后,我们运用各种内核机制从用户空间和内核空间生成高特权线程,并主动分配特权对象。在这项工作中,我们通过使用24个真实世界的内核漏洞来评估DirtyCred的可利用性。令人惊讶的是,我们发现DirtyCred可以在16个漏洞和容器逃逸上展示特权升级攻击。我们将我们新提出的利用方法分享给了Google漏洞奖励计划(kCTF VRP [20]),并收到了他们的认可和20000美元的奖励。
由于DirtyCred的强大攻击能力和缺乏有效的防御措施,我们认为如果社区不立即采取行动来探索和部署新的防御机制,DirtyCred可能很快就会成为对Linux的严重威胁。因此,我们基于新的攻击方法提出了一种新的Linux内核防御机制。这种防御的基本思想是将高特权对象和低特权对象存储在不重叠的内存区域中。在这项工作中,我们通过利用vmalloc区域来存储高特权对象,使用普通区域来存储低特权对象来实现这一目标。我们将这种防御作为Linux内核原型实现,并使用标准基准测试评估其性能。我们表明,我们的防御主要引入可以忽略的开销。对于涉及文件操作的某些操作,它只会展示适度的性能开销。
与现有的内核利用技术相比,DirtyCred具有许多独特的特点。首先,它是一种通用的利用方法,因为它可以使任何基于堆的漏洞实现特权升级攻击。其次,它可以极大地减轻攻击迁移的负担,因为在使用DirtyCred之后,一个攻击者可以创建一个可以从一个内核版本或架构转移到另一个内核版本或架构的攻击代码,而不需要做出任何修改。第三,它可以绕过许多强大的内核保护和利用缓解机制(例如CFI [15]、KASLR [14]、SMEP/SMAP [7, 29]、KPTI [8]等)。最后,它可以超越特权升级,导致更严重的安全问题,例如Android取得root权限和逃逸容器。
总之,本文的贡献如下。
本文的其余部分组织如下。第2节介绍了本研究所需的背景知识并讨论了威胁模型。第3节介绍了DirtyCred的高层次思想,并总结了DirtyCred面临的技术挑战。第4、5和6节介绍了处理技术挑战的各种技术方法。第7节评估了所提出的利用方法在真实世界的Linux内核漏洞上的有效性。第8节介绍了一种新的防御机制,并在标准基准测试中评估了其性能。第9节讨论了相关工作,随后在第10节中讨论了一些相关问题和未来工作。最后,在第11节中总结了本研究的工作。
本节介绍了理解我们新提出的利用方法所需的一些技术背景。此外,我们讨论了我们的威胁模型和假设。
如[26]所定义,凭据是指包含特权信息的内核属性。通过这些属性,Linux内核可以检查用户的访问权限。在Linux内核中,凭据被实现为携带特权信息的内核对象。据我们所知,这些对象包括“cred”、“file”和“inode”。在本文中,我们仅使用“cred”和“file”对象设计了我们的利用方法。我们排除了“inode”对象,因为只有在文件系统上创建新文件时才能分配它,这不能提供足够的灵活性进行内存操作(在成功的程序利用中关键的操作)。以下是“cred”、“file”和“inode”对象的一些必要背景介绍。
每个Linux任务都包含一个指向“cred”对象的指针。'cred'对象包含UID字段,指示任务权限。例如,GLOBAL_ROOT_UID表示任务具有root特权。当任务尝试访问资源(例如文件)时,内核检查任务 cred
对象中的UID,确定是否可以授予访问权限。除UID之外,'cred'对象还包含功能。该功能指定了任务的细粒度特权。例如,CAP_NET_BIND_SERVICE表示任务可以将套接字绑定到Internet域特权端口。对于每个任务,他们的凭据是可配置的。在更改任务凭据时,内核遵循复制和替换原则。它首先复制凭证。其次,修改副本。最后,它将cred指针更改为任务以引用新修改的副本。在Linux中,每个任务只能更改自己的凭证。
在Linux内核中,每个文件都带有其所有者的UID和GID,其他用户的访问权限和功能。对于可执行文件,它们还具有SUID / SGID标志,表示特殊权限,允许其他用户以所有者的特权运行。在Linux内核实现中,每个文件都绑定到一个“inode”对象,连接到凭据。当任务尝试打开文件时,内核调用inode_permision函数,检查inode和相应的权限,然后授予文件访问权限。打开文件后,内核从“inode”对象中删除凭证,并将它们附加到“file”对象上。除了维护凭证之外,“file”对象还包含文件的读/写权限。通过“file”对象,内核可以索引到cred对象,从而检查特权。此外,它还可以检查读/写权限,从而确保任务不会以只读模式写入数据文件。
Linux内核设计了内存分配器来管理小内存分配,以提高性能和防止碎片化。虽然Linux内核中有三种不同的内存分配器,但它们都遵循相同的高级设计。具体而言,它们都使用缓存来维护相同大小的内存。对于每个缓存,内核分配内存页面并将内存分成多个相同大小的部分,每个部分是一个内存槽,用于托管对象。当缓存的内存页面用尽时,内核为缓存分配新页面。如果缓存不再使用内存页面,即内存页面上的所有对象都被释放,则内核相应地回收内存页面。在Linux内核中,主要有两种缓存,简要描述如下:
通用缓存: Linux内核具有不同的通用缓存来分配不同大小的内存。在从通用缓存分配内存时,内核首先将所需的大小向上舍入,并找到与大小请求匹配的缓存。然后,它从相应的缓存中分配一个内存槽。在Linux内核中,如果分配请求未指定要从哪些缓存分配,则默认情况下分配会在通用缓存中进行。对于落入同一通用缓存的分配,它们可能共享相同的内存地址,因为它们可能维护在同一内存页面上。
专用缓存: Linux内核为性能和安全目的创建专用缓存。由于某些对象在内核中经常使用,为这些对象专门创建缓存可以减少它们分配所花费的时间,从而提高系统性能。落入专用缓存的分配不与一般分配共享相同的内存页面。因此,在通用缓存中分配的对象不与专用缓存中的对象相邻。这可以看作是缓存级别的隔离,可以缓解一般缓存中对象的溢出威胁。
在我们的威胁模型中,我们假设一个非特权用户可以访问Linux系统,并试图利用内核中的堆内存破坏漏洞,从而提升自己的权限。此外,我们假设Linux启用了上游内核(版本5.15)中提供的所有漏洞缓解和内核保护机制。这些机制包括KASLR、SMAP、SMEP、CFI [7, 14, 15, 29]、KPTI [8]等。有了这些缓解和保护措施,内核地址是随机化的,在执行期间内核不能直接访问用户空间内存,并且其控制流完整性得到了保证。最后但同样重要的是,我们不假设存在硬件侧信道可以促进内核攻击。
在本节中,我们首先通过使用真实的例子介绍DirtyCred的高层次想法。然后,我们分析并讨论DirtyCred需要解决的技术挑战。
我们以一个真实的Linux内核漏洞(CVE-2021-4154 [9])为例,展示DirtyCred如何在高层次上工作。CVE-2021-4154是由于文件对象被错误地引用到fs_context对象的源字段中导致的类型混淆错误。在Linux内核中,文件对象的生命周期通过引用计数机制进行维护。当引用计数变为零时,文件对象将自动释放,这意味着文件对象不再被使用。然而,通过触发漏洞,即使文件仍在使用中,内核也会无效地释放文件对象。
如图1所示,DirtyCred首先打开一个可写的文件“/tmp/x”,这将在内核中分配一个可写的文件对象。通过触发漏洞,源指针将引用相应缓存中的文件对象。然后,DirtyCred尝试向打开的文件“/tmp/x”写入内容。在实际内容写入之前,Linux内核会检查当前文件是否有写入权限,以及位置是否可写等。通过检查后,DirtyCred保持这个实际的文件写入操作并进入第二步。在第二步中,DirtyCred触发fs_context对象的释放站点来释放文件对象,这将使文件对象成为一个已释放的内存空间。
接下来,在第三步中,DirtyCred打开一个只读文件“/etc/passwd”,这将触发内核为“/etc/passwd”分配文件对象。如图1所示,新分配的文件对象接管了已释放的存储空间。在这个设置完成之后,DirtyCred将释放其暂停的写操作,内核将执行实际内容写入。由于文件对象已经被交换,暂停的内容将被重定向到只读文件“/etc/passwd”。假设写入到“/etc/password”的内容是“hacker:x:0:0:root:/:/bin/sh”,那么恶意用户可以使用这个方案注入特权账户,从而实现提权。
以上示例仅是演示DirtyCred如何使用文件对象进行利用。如第2节所述,“cred”对象也被认为是凭证对象。与上面展示的文件交换类似,恶意用户也可以使用类似的思路来交换cred对象,从而实现提权。由于空间限制,我们不再详细阐述,感兴趣的读者可以参考我们在[2]上发布的利用演示。
从上面描述的现实世界的示例中,我们可以观察到DirtyCred没有改变控制流,而是利用了内核内存管理的本质来操纵内存中的对象。因此,许多现有的防御措施防止控制流篡改并不影响DirtyCred的利用。虽然一些最近的研究工作通过重新设计内存管理(例如AUTOSLAB [34])来启用内核防御,但它们也无法阻止DirtyCred。正如我们将在第8节中讨论的那样,新提出的内存管理方法仍然是粗粒度的,不足以阻碍我们的内存操作。
确实,虽然以上示例说明了DirtyCred如何执行利用以实现提权,但仍有许多技术细节需要进一步澄清和许多技术挑战需要解决。比如,如何确定被利用的对象在内存中的位置、如何在不损坏系统稳定性的前提下执行内核内存管理操纵,并且如何在避免影响系统性能的情况下完成内存操纵等等。这些问题都需要深入研究和解决,以提高内核安全防御水平。
正如上面所提到的,DirtyCred需要无效free能力来释放低权限对象(例如带有写权限的文件对象),然后重新分配高权限的对象(例如只读权限的文件对象)。在实践中,内核漏洞可能并不总是提供这样的能力。例如,一个漏洞可能只会提供越界写入的能力,而不是直接对凭证对象进行无效释放。因此,DirtyCred需要相应的方法来为具有不同能力的漏洞转换漏洞能力。在第4节中,我们将描述如何为不同类型的内核漏洞旋转能力。
如上面示例所述,DirtyCred需要在完成权限检查之后并在文件对象交换之前继续进行实际的文件写入。然而,保持实际写入是具有挑战性的。在Linux内核中,权限检查和实际内容写入很快地连续发生。如果没有一个实用的方案来准确控制文件对象交换的发生时间窗口,那么利用将不可避免地不稳定。在第5节中,我们介绍了一系列有效的机制,以确保文件对象交换可以在所需的时间窗口发生。
正如上面所讨论的,DirtyCred中最关键的一步之一是使用高权限凭证替换低权限凭证。为了做到这一点,DirtyCred会分配高权限对象,接管释放的内存空间。然而,一个低权限用户要想分配高权限凭证是具有挑战性的。虽然等待特权用户的活动可能会解决这个问题,但这种被动策略极大地影响了利用的稳定性。首先,DirtyCred不知道所需的内存位置何时才能被回收,因此继续其连续利用操作。其次,DirtyCred没有对新分配的对象进行控制。因此,有可能接管所需内存槽位的对象没有预期的权限级别。在第6节中,我们介绍了一种用户空间机制和内核空间方案来解决这个问题。
正如图1所示的示例所示,被编目为CVE-2021-4154的内核漏洞为DirtyCred提供了以无效方式取消分配文件对象的能力。然而,实际上,一个漏洞可能没有展示这样的能力。例如,双重释放(DF)或使用后释放(UAF)能力可能不直接与凭证对象相关联。一些漏洞,如越界访问(OOB),没有无效的free能力。为此,DirtyCred需要转换漏洞能力。接下来,我们将描述如何设计DirtyCred以实现能力转换。
对于具有覆盖缓存中数据的能力的OOB漏洞或具有覆盖缓存中数据的UAF漏洞,DirtyCred首先确定与包含引用凭证对象的指针的对象(即受害对象)共享同一缓存的对象。然后,它利用堆操作技术[6,49]在覆盖发生的内存区域中分配对象。如图2(a)所示,在旋转OOB漏洞时,受害者对象就在易受攻击对象之后。利用覆写能力,DirtyCred进一步修改了包含对象的指针。更具体地说,DirtyCred使用覆写能力将指向凭证对象的指针的最后两个字节写为零(参见图2(b))。
请回想一下,缓存是按连续页面组织的。在Linux内核中,内存页面的地址总是以最后一个字节为零的格式呈现。在新缓存中分配对象时,对象从内存页面的开头开始。因此,上面的零字节覆写将强制指针引用内存页面的开头。例如,如图2(b)所示,在将指向凭证对象的指针的最后两个字节设置为零之后,指针将引用另一个凭证对象所在的内存页面的开头。
正如图2(b)所示,在指针操作之后,DirtyCred获得了对内存页面第一个对象的附加引用。我们认为这个额外的对象引用意味着能力转换成功。原因是内核可以正常释放对象,将指针留在受害对象中作为悬空指针。然后,按照第3节中描述的类似过程,DirtyCred可以执行堆喷射,使用高特权凭证对象占据被释放的位置,从而实现特权提升。
在Linux内核中,一般缓存(例如kmalloc-96)和专用缓存(例如cred_jar)是分离的。这些缓存中包含的对象没有重叠。然而,Linux内核具有回收机制。当销毁内存缓存时,它会回收相应的未使用内存页面,然后将回收的页面分配给需要更多空间的缓存。这个特性使交叉缓存内存操作成为可能,为DirtyCred提供了通过双重释放漏洞进行能力转换的能力。
图3展示了DirtyCred如何将双重释放能力转换为所需的特权对象交换能力的过程。首先,DirtyCred在漏洞发生的缓存中分配许多对象。在这些新分配的对象中,有一个易受攻击的对象。使用两个不同的指针,DirtyCred可以以无效的方式两次取消分配易受攻击的对象。由于分配的数量很大,DirtyCred可以确保在大规模对象分配(见图3(a))之后,一个缓存充满新分配的对象。
在大规模分配之后,DirtyCred利用第一个指针以无效的方式取消分配易受攻击的对象,留下第二个指针(见图3(b))。然后,它重新分配易受攻击的对象,接管被释放的内存空间。如图3(c)所示,在重新分配之后,有三个指针引用易受攻击的对象。其中一个是第一个易受攻击的对象留下的指针。另外两个是与新分配的易受攻击对象进行双重释放能力关联的指针。
DirtyCred使用三个引用易受攻击对象的指针中的其中一个,进一步取消分配新分配的易受攻击对象,留下由两个悬空指针引用的已释放内存空间(见图3(d))。如上所述,如果缓存中不包含已分配的对象,则Linux内核会回收内存页面并将其分配给另一个缓存。因此,在易受攻击对象被取消分配之后,该缓存现在包含一个已释放的内存页面,它可以被分配给任何需要更多空间的缓存。DirtyCred可以从该缓存中分配一个新的对象,并将其作为特权对象使用。
使用三个引用易受攻击对象的指针中的一个,DirtyCred进一步取消分配新分配的易受攻击对象,留下由两个悬空指针引用的已释放内存空间(见图3(d))。如上所述,如果缓存中不包含已分配的对象,则Linux内核会回收内存页面并将其分配给另一个缓存。因此,在易受攻击对象被取消分配之后,该缓存现在包含一个已释放的内存页面,它可以被分配给任何需要更多空间的缓存。DirtyCred可以进一步取消分配缓存中的其他对象,从而相应地释放缓存(请参见图3(e))。
在回收的内存页面上,内核创建了一个新的缓存,用于存储凭据对象。新的缓存将页面内存分成slot。如图3(f)所示,如果易受攻击对象的大小与凭据对象的大小不同,则凭据对象的地址将不会与易受攻击对象的地址对齐,使得剩余的两个指针引用凭证对象的中间。在这种内存状态下,DirtyCred无法按照第3节中描述的利用过程进行操作,因为利用的成功需要具有取消分配凭证对象的能力。
为解决这个问题,DirtyCred首先使用一个剩余的指针来取消分配中间的凭证对象。如图3(g)所示,在此取消分配之后,内核创建了一个已释放的内存空间。该释放空间与凭证对象的大小相同。因此,当DirtyCred分配一个新的凭证对象时,内核将该释放空间填充为新的凭证对象。如图3(h)所示,在接管了释放空间后,剩余的最后一个指针引用新分配的凭证对象。这意味着能力转换的成功。原因是DirtyCred可以利用剩余的指针以无效的方式取消分配凭证对象,然后执行对象交换以进行特权提升。
回想一下,在执行文件写操作之前,Linux内核需要检查文件权限。DirtyCred需要在权限检查和实际文件写入之间执行文件对象交换。然而,这个时间窗口太短了,无法成功利用漏洞,因为交换需要触发漏洞并进行堆布局操作,这可能需要几秒钟的时间。为解决这个问题,DirtyCred利用了几种技术来扩展这个时间窗口,以确保它大于用于交换过程的时间。在这里,我们描述这些技术,并讨论它们如何促进利用。
用户故障处理器(Userfaultfd [38])和FUSE [37]是Linux内核中的两个重要功能。用户故障处理器(Userfaultfd)功能允许用户空间处理页面故障。当在用户故障处理器(Userfaultfd)中注册的内存上触发页面故障时,用户注册的页面故障处理器将得到通知以处理页面故障。与用户故障处理器(Userfaultfd)不同,FUSE是一个用户空间文件系统框架,允许用户实现用户空间文件系统。用户可以注册他们的处理器,为实现的用户空间文件系统指定如何响应文件操作请求。无论是用户故障处理器(Userfaultfd)还是FUSE,都可以被用来暂停Linux内核的执行时间,只要用户需要。
对于用户故障处理器(Userfaultfd),攻击者可以注册一个页面故障处理程序来处理内存页面。当内核试图访问该内存并触发页错误时,已注册的处理程序将被调用,使攻击者可以暂停内核执行。对于FUSE,攻击者可以从用户空间文件系统中分配内存。当内核访问这个内存时,它会调用预定义的文件访问处理器,从而暂停内核执行。
在这项工作中,DirtyCred利用这些功能来在文件权限检查完成后暂停内核执行。接下来,我们以userfaultfd为例,描述DirtyCred如何实现内核暂停并扩展攻击的时间窗口。对于FUSE,内核暂停过程类似。读者可以参考我们开发的漏洞样本[2]。
在执行文件写入时,DirtyCred调用syscall writev,即向量I/O的实现。与syscall write不同的是,这个syscall使用iovec结构从用户空间传递数据到内核空间。第1行到第5行的List 1定义了iovec结构。我们可以观察到,它包含一个用户空间地址和一个size字段,指示将传输的数据量。在Linux内核空间中,为了复制iovec中封装的数据,内核需要先将iovec导入内核空间。因此,在Linux内核版本v4.13之前(如List 1所示),writev的实现首先检查文件对象,确保当前文件处于打开状态并具有写入权限。一旦检查通过,它就会从用户空间导入iovec,并将用户数据写入相应的文件。在这种实现中,iovec的导入位于权限检查和数据写入之间。DirtyCred可以简单地利用上述的userfaultfd特性,在完成权限检查后暂停内核执行,从而赢得足够的时间来交换文件对象。据我们所知,Jann Horn在CVE-2016-4557的利用中首次使用了这种技术,但在内核v4.13之后不再可用。
在Linux内核版本v4.13之后,内核实现发生了变化。iovec的导入被提前到权限检查之前(参见List 2)。在这个新的实现中,DirtyCred仍然可以使用userfaultfd特性,在iovec导入的位置暂停内核执行。然而,它不再给DirtyCred扩展权限检查和实际文件写入之间时间窗口的能力。为了解决这个问题,DirtyCred利用了Linux文件系统的设计。
在Linux中,文件系统设计遵循严格的层次结构,其中高级接口通常用于文件写入操作,而低级接口则因文件系统而异。在写入文件时,内核首先调用高级接口。如List 3所示,generic_perform_write是文件写入操作的高级接口。我们可以看到,在第15-17行,generic_perform_write调用文件系统的写入操作,将数据写入文件。为了保证性能和兼容性,在写入操作之前,内核会针对iovec中包含的用户空间数据触发页错误。因此,在第10行使用userfaultfd特性,DirtyCred可以在实际文件写入之前暂停内核执行,从而获得足够的时间窗口进行特权文件对象交换。
与在iovec导入点暂停内核执行相比,我们认为利用文件系统设计更具挑战性,以便进行缓解。首先,正如Linux代码注释中所述, 在iovec中删除页面错误可能会导致死锁问题(请参见列表3)。如果页面未经预取,则某些文件系统不可避免地会遇到麻烦。其次,虽然将页面错误移至权限检查之前可能会解决问题, 但这种简单的防御反应会牺牲内核性能,并且更为重要的是,容易被潜在绕过。例如,DirtyCred可能在触发第一个页面错误后立即删除该页面。这样,内核必然会再次触发 页面错误,并因此在权限检查后暂停内核执行。
为了避免破坏文件内容,文件系统不允许两个进程同时写入一个文件。在Linux中,文件系统通过使用锁机制来实施此实践。为了说明这一点,列表4显示了在ext4文件系统中执行写操作的简化代码片段。正如我们所观察到的那样,文件系统首先尝试在第6行获取Inode锁。如果inode正在另一个文件的操作下(即其他人已经持有锁),则文件系统将等待锁被释放。在获取锁之后,文件系统调用generic_perform_write将数据写入文件。完成写入后,文件系统将释放锁并从函数返回。
上述锁机制可以确保写操作不会出错。不幸的是,它为DirtyCred留下了一个扩展时间窗口的机会,从而执行对象交换。具体而言,DirtyCred可以生成两个进程——进程A和进程B——同时在同一个文件上写入数据。假设进程A持有锁,正在写入大量数据。当进程A写入文件时,进程B将不得不等待一个延长的时间,直到在第10行释放锁。由于在调用generic_perform_write之前,进程B已经完成了文件权限检查,因此在锁等待期间花费的时间为DirtyCred提供了足够长的时间窗口,以完成文件对象交换,而无需担心权限检查块。根据我们的观察,在将4GB的文件写入硬盘驱动器时,等待时间可以延长到几十秒钟。在这个时间窗口内,触发漏洞和执行内存操作可以完成,而不会引起任何稳定性问题。
正如第3.2节所提到的那样,DirtyCred不能被动地等待特权用户的活动,并期望这些活动能导致一个特权对象接管所需的空闲空间,从而实现提权。因此,DirtyCred必须采取积极的行动来触发内核空间中的特权对象分配。本节讨论DirtyCred如何作为一个低特权用户执行特权对象分配。
在Linux内核中,“cred”对象代表相应内核任务的特权级别。root用户拥有一个特权cred对象,表示最高特权级别。 因此,如果DirtyCred可以积极触发root用户的活动,内核就可以相应地分配特权cred对象。在Linux中,当二进制文件具有SUID权限时,无论谁执行二进制文件,它都可以像所有者执行一样被执行。 利用这个特性,低特权用户可以在使用具有SUID权限设置的root用户拥有的二进制文件时生成root进程。
过去,攻击者专注于利用特权二进制文件中的漏洞,以实现提权。但在本文中,DirtyCred不依赖于特权二进制文件中存在的漏洞。相反,它滥用了上述功能,生成由root用户拥有的带有SUID权限设置的二进制文件,以分配特权cred对象来占用空闲内存位置。 在Linux中,符合该功能的二进制文件有很多,包括su、ping、sudo、mount、pkexec等可执行文件。
正如前面所讨论的,除了cred对象外,DirtyCred也可以使用文件对象来进行提权。与cred对象不同,文件对象的分配相对容易。回想一下,当交换文件对象时,DirtyCred将一个具有写入权限的文件对象替换为一个只读文件的对象。为了分配指定只读权限的文件对象,DirtyCred可以只使用读取权限打开多个目标文件。这样,内核就会在相应的内核内存中分配许多相应的文件对象。
上述描述的方法显示了一种从用户空间分配特权对象的方式。实际上,DirtyCred也可以从内核空间执行特权对象分配。当Linux内核启动一个新的内核线程时,它会复制当前正在运行的进程。随着进程复制,内核会相应地在内核堆上分配一个复制的cred对象。在Linux内核中,大多数内核线程都具有特权cred对象。因此,复制的cred对象也处于高特权级别。利用生成特权内核线程的能力,DirtyCred可以积极地分配特权cred对象。
据我们了解,分配高特权许可对象的方法有两种主要途径。第一种是与内核代码片段互动,触发内核在内部生成一个特权线程。例如,为内核工作队列创建工作线程也可以用于生成内核线程。在Linux内核中,工作队列被设计用于处理延迟函数。一个工作队列配有多个工作池。每个工作池都包含工作线程。工作线程是运行提交到工作队列上的工作的基础执行单元。每个工作池中的工作线程数量最多是CPU的数量。最初,内核为每个工作池只创建一个工作线程。当需要更多的工作线程或者说需要在工作队列中提交更多的工作时,内核会动态地创建工作线程。每个工作线程都是一个内核线程。因此,通过调整提交到内核工作队列的工作,可以相应地控制内核线程生成的活动。
除了上述方法之外,生成内核线程的第二种方法是调用用户模式助手。用户模式助手是一种允许内核创建用户模式进程的机制。用户模式助手最直接的应用之一是将内核模块加载到内核空间中。当加载内核模块时,内核调用用户模式助手API,进一步以高特权模式执行用户空间程序——modprobe,并在内核中创建高特权凭证对象。modprobe功能的一部分是搜索安装的标准模块目录以查找必要的驱动程序。在搜索期间,内核需要继续执行。因此,为了避免modprobe阻塞内核执行,当调用用户模式助手API时,内核还会生成一个新的内核线程。
在这个部分,我们设计了两个实验来评估DirtyCred对现实世界内核漏洞的可利用性。
正如上文所述,DirtyCred利用可利用对象(即包含凭证对象的对象)来执行内存操作,特别是针对像越界访问和使用后释放等漏洞。这种操作是DirtyCred实现权限提升的关键步骤之一。在执行内存操作时,DirtyCred将可利用对象分配到漏洞所在的缓存中。对于不同的漏洞,它们在不同的缓存上展示了内存破坏的能力。因此,DirtyCred的成功与否高度取决于它是否能够成功识别出适合于相应缓存的可利用对象。考虑到这一点,我们首先确定每个缓存可用的独特可利用对象。
为了指出这些对象,一个本能的反应是手动查找Linux内核代码,确定那些可利用的对象,并找出触发相应分配的输入。然而,Linux内核代码空间很大且复杂,使得代码检查变得不切实际。因此,为了解决这个问题,我们引入一种自动化方法来跟踪可利用的对象和触发它们分配的相应输入。在我们的评估中,我们将自动化方法应用于最新的稳定内核(即本文撰写时的5.16.15版本)。我们只考虑对象作为可利用对象,当且仅当自动化方法可以找到一个包含凭证对象的对象,并且可以演示一个输入以在内核堆上分配该对象。由于篇幅限制,我们详细介绍了自动化方法的设计和实现,请参见附录A。
除了识别可利用的内核对象之外,我们的实验还探究了DirtyCred针对现实世界漏洞的可利用性。回想一下,如果漏洞不能直接提供DirtyCred交换凭证对象的能力,DirtyCred需要转移漏洞的能力。正如第4.1节所讨论的那样,在执行漏洞转移时,DirtyCred可能需要覆盖可利用对象中的某些关键数据。对于不同的漏洞,它们的覆盖能力可能会有很大的差异,进一步影响权限提升的成功率。因此,我们使用DirtyCred来利用许多现实世界的漏洞,并研究它在对这些漏洞的利用方面表现如何,以评估DirtyCred的有效性。
我们假设Linux内核装备了最新的内核漏洞防护技术,在执行利用过程中需要考虑这些防护措施。因此,我们需要选择近年来开发的内核中发现的漏洞。在我们的评估中,我们仅选择了2019年之后报道的Linux内核CVE。在CVE选择过程中,我们过滤掉那些不会破坏内核堆数据的漏洞。此外,我们排除了那些我们不能重现相应内核崩溃的漏洞。最后,我们还淘汰了那些需要安装特定硬件才能触发的漏洞。遵循这些CVE选择标准,我们得到了一个包含24个唯一CVE的数据集。在表2中,我们列出了这些CVE的ID和相应的漏洞类型。正如我们所观察到的,我们选择的测试用例涵盖了内核堆上几乎所有类型的漏洞。
表1展示了在每个内核缓存中识别出的可利用对象。正如我们所观察到的,这些可利用对象涵盖了几乎所有通用缓存,除了kmalloc-8,在Linux内核中很少使用。对于大多数内存缓存,存在多个可利用的对象,这些对象对DirtyCred的权限提升可能非常有用。在每个可利用对象中,引用凭证对象的字段的偏移量也列在表1中。正如我们所观察到的,不同可利用对象的偏移量各不相同。这表明DirtyCred找到一个适合与漏洞能力匹配并执行成功利用的合适对象的机会更高。例如,如果漏洞展示覆盖8个字节到其第8个字节偏移量相邻对象的能力,具有关键数据在第8个字节处的可利用对象将极大地促进DirtyCred的权限提升。
从表1中,我们还发现了5个对象在5个通用缓存中。它们将对凭证对象的引用封装在对象开头。这意味着即使攻击者只获得非常有限的内存损坏能力(例如,在受害者对象的开头覆盖两个字节为零),他们仍然能够利用已识别的可利用对象发动DirtyCred攻击。值得注意的是,表1还区分了使用不同符号引用cred和file的可利用对象。正如我们将在第10节中讨论的那样,cred对象可以为容器逃逸提供更好的支持。因此,具有cred对象链接的充分可利用对象表明在docker逃逸方面提供了更实质性的支持。
表2展示了DirtyCred在不同漏洞上的可利用性。正如我们所观察到的,当底层Linux内核启用本文第2.3节讨论过的所有利用缓解机制时,DirtyCred成功地演示了对16个漏洞的内核防御绕过和权限提升。这一观察结果意味着DirtyCred可以用作强大的、通用的内核漏洞利用方法。在16个利用成功的测试用例中,有8个是越界访问或use-after-free漏洞,另外8个是双重释放漏洞。DirtyCred在所有双重释放测试用例上都获得了成功,因为双重释放能力总是可以转向无效释放凭证对象。
失败的案例主要来自越界访问和use-after-free漏洞。对于OOB漏洞,失败的案例展示了虚拟内存区域中的内存损坏情况。要使用DirtyCred,我们需要找到具有凭证信息的内核对象。这些对象通常分配在kmalloc的内存区域,而不是虚拟内存。因此,DirtyCred无法找到用于成功利用的必要对象。在表2中,我们用†符号标注了这些情况。正如我们将在第10节中讨论的那样,未能利用这些情况并不意味着DirtyCred不能利用虚拟内存上的漏洞。如果存在合适的可利用对象或使用其他能力转移技术,则虚拟内存上的内存损坏能力仍然可以转向对DirtyCred有用的能力。
对于UAF失败案例CVE-2022-24122,它并没有通过悬空指针展示出覆盖能力,而只是展示出了over-reading能力。正如第4节所讨论的那样,DirtyCred依赖于无效写入能力或无效释放能力。CVE-2022-24122的over-reading能力限制了DirtyCred执行成功的能力转移,从而导致攻击失败。对于CVE-2019-2215和CVE-2019-1566,它们展示了覆盖能力。然而,覆盖能力并没有发生在可利用对象的关键字段中。如果没有这样的能力,DirtyCred无法操作内核对象中必要的字段来释放凭证对象,从而导致攻击失败。
根据上面展示的攻击可行性,我们认为DirtyCred对现有的Linux系统构成了严重威胁。虽然滥用锁机制的技术可以通过重新设计文件系统来减轻,但这仍然不足以阻止DirtyCred的攻击,因为它可以从另一个路径——交换Credential对象的方式启动。因此,有效的方法是防止使用不同权限级别的凭证进行交换。从某种角度来看,用户空间堆防御措施对DirtyCred并不充分。内核希望内存分配/释放/访问尽可能快,否则会减慢用户空间程序和整个系统的速度。因此,内核中的内存分配器比用户空间中的分配器要简单得多(例如ptmalloc)。这一事实使得用户空间堆防御措施不适用于内核空间。从另一个角度来看,即使Linux内核引入了许多防御机制(例如CFI、SMEP、SMAP和KASLR等),目前不存在适用于DirtyCred的有效内核防御措施,其原因如下:
首先,DirtyCred并不违反任何控制流完整性,因此保护内核控制流的努力是徒劳的。其次,DirtyCred并不依赖于单个利用组件进行利用。正如第7节所示,用于利用的有价值对象分布在几乎所有一般缓存中。因此,通过消除可利用的对象来防御DirtyCred几乎是不可行的。第三,DirtyCred通过将合法的凭证对象放置到非法的内存位置来实现其利用目标,而不是篡改凭证对象的内容。这种利用实践使得现有的凭证完整性保护技术(例如三星Knox的实时内核保护[43])可能不太可能有效。最后但并非最不重要的,DirtyCred通过交换高特权和低特权凭证对象之间来执行特权升级。这种利用方法无法通过许多内核对象隔离方案(例如AUTOSLAB[34]和xMP[44])进行处理,因为它们根据对象类型而不是特权将关键内核对象分离在自己的内存区域中。
因此,我们认为一个有效的防御DirtyCred的解决方案是将高特权和低特权对象隔离开,使它们不能共享相同的内存空间。这样,DirtyCred就无法再将具有不同权限的对象重叠用于特权升级。为了实现上述目标,一个直接的反应是创建两个不同的缓存。一个用于存储高特权对象,另一个用于存储低特权对象。由于缓存是自然隔离的,这种设计可以确保具有不同特权的对象没有重叠。然而,正如我们在第4.2节中所讨论的,一旦内存缓存被破坏,Linux的伙伴分配器会回收底层内存页面。因此,DirtyCred仍然可以利用这个内存页面回收功能发起攻击。
表3显示我们的评估结果。首先,我们可以观察到,我们提出的方法大多数情况下几乎没有引入性能开销,表明我们的防御措施是轻量级的。其次,我们可以观察到,在LMbench的“10k文件创建”和“10k文件删除”测试用例中存在一些适度的性能下降。如表3所示,我们提出的防御引入了超过4%的开销。这种性能下降的原因是,文件对象通过vmalloc而非kmalloc分配到虚拟内存区域。与kmalloc相比,vmalloc相对较慢,因为虚拟内存必须将缓冲区空间重新映射为虚拟连续范围,而‘kmalloc’则不会重新映射。
需要注意的是,文件删除涉及的性能降级比文件创建要低(4.25%与7.17%)。差异背后的原因是文件对象的释放是通过RCU进行的,这与文件删除过程异步执行。虽然适度的开销可能引起某些生产系统的关注,但它极大地提高了内核对DirtyCred的保护。在本工作中,我们的主要目标是提高Linux社区的意识,而不是构建安全、高效的防御解决方案。我们将替代性防御解决方案的探索留给未来的研究工作。最后,需要注意的是,我们对Linux内核引入防御措施后,某些情况表现出轻微的性能改进。尽管我们尽力通过多次运行基准测试和在裸机上禁用CPU增强来尽可能地减少噪音,但这主要是由于实验的噪音造成的。
本文作者:Du4t
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!