在之前的博客文章中,我们重点分析了注册表的安全性以及如何有效寻找其中的漏洞。本文将重点关注基于hive的内存破坏漏洞的利用,即那些允许攻击者覆盖活动hive内存映射中数据的漏洞。这类问题是Windows注册表的典型特征,但也具有足够的通用性,本文描述的技术适用于我过去发现的17个漏洞,以及未来可能出现的任何类似漏洞。我们知道,hive在低级内存管理(如何在内存中映射)、自定义分配器对已分配和释放内存块的处理以及存储数据的性质方面表现出非常特殊的行为。所有这些特性使得从攻击安全的角度来看,这类漏洞的利用尤为有趣,这也是我想在此详细描述的原因。
与任何其他类型的内存破坏类似,绝大多数hive内存破坏问题可以分为两类:空间违规(如缓冲区溢出)和时间违规(如释放后使用条件)。在本文中,我们将选择一个最有潜力的漏洞候选,然后为其创建一个逐步利用的漏洞利用程序,将普通用户的权限从Medium IL提升到系统级权限。我们的目标是Windows 11,并且额外要求是成功绕过所有现代安全缓解措施。我之前在OffensiveCon 2024上就此主题进行了演讲,题为"Windows内核中注册表漏洞的实战利用",这篇博客文章可以视为该演讲信息的补充和扩展。对该主题深感兴趣的读者可以查看该演讲的幻灯片和录音。
从哪里开始:潜在选项的高级概述
让我们从回顾一些关键点开始。正如您可能记得的那样,Windows注册表单元分配器(即内部的HvAllocateCell、HvReallocateCell和HvFreeCell函数)以一种非常有利于利用的方式运行。首先,它完全缺乏任何防止内存破坏的保护措施;其次,它没有任何随机性元素,使其行为完全可预测。因此,不需要使用任何"hive喷射"或其他典型的堆利用技术——如果我们在测试机器上实现了所需的单元布局,它将在其他计算机上重现,无需任何额外步骤。一个潜在的例外可能是对HKLM和HKU中的全局共享hive进行攻击,因为我们不知道它们的初始状态,并且可能会由于其他应用程序同时执行的操作而产生一些随机性。尽管如此,即使这也不应构成特别重大的挑战。我们可以安全地假设,安排hive的内存布局是直接的,如果我们有某种内存破坏能力,通过一些耐心和实验,最终将能够覆盖任何类型的单元。
经典的利用内存破坏漏洞通常涉及以下步骤:
- 初始内存破坏原语
- …
- 利润(以任意代码执行、权限提升等形式)
漏洞利用开发人员的任务是填补此列表中的空白,设计出实现预期目标的中间步骤。通常有几个这样的中间步骤,因为考虑到当前的安全状态和缓解措施,漏洞很少能直接从内存破坏一步到位地导致代码执行。相反,采用逐步开发更强原语的策略,最终的利用链可能如下所示:
在这个模型中,第二/第三步是通过找到另一个有趣的对象,安排它分配在被覆盖的缓冲区附近,然后以某种方式破坏它以创建新的原语。然而,在hive的情况下,我们在这方面的选择似乎有限:我们假设可以完全控制hive中任何单元的表现形式,但问题是从利用的角度来看,hive中并没有立即有趣的数据。例如,regf格式不包含任何直接影响控制流的数据(例如函数指针),也不包含任何其他可以以某种巧妙方式覆盖以改进原始原语的虚拟内存地址。下面的图表描述了我们当前的情况:
这是否意味着hive内存破坏不可利用,它唯一允许的是在孤立的hive内存视图中破坏数据?并非如此。在以下小节中,我们将仔细考虑各种想法,探讨控制内部hive数据如何对系统的整体安全性产生更广泛的影响。然后,我们将尝试确定哪种可用方法最适合在实际漏洞利用中使用。
内部hive破坏
让我们首先研究覆盖内部hive数据是否像最初看起来那样不切实际。
在特权系统hive中执行仅hive攻击
需要明确的是,说hive不包含任何值得覆盖的数据并不完全准确。如果您仔细想想,情况恰恰相反——注册表存储了大量系统配置、注册服务信息、用户密码等。唯一的问题是所有这些关键数据都位于特定的hive中,即那些挂载在HKEY_LOCAL_MACHINE下的hive,以及HKEY_USERS中的一些hive(例如HKU.Default,对应于System用户的私有hive)。要能够通过仅破坏regf格式数据(不访问其他内核内存或实现任意代码执行)成功执行攻击并提升权限,必须满足两个条件:
- 漏洞必须仅通过API/系统调用触发,并且不需要对hive进行二进制控制,因为我们显然对任何系统hive都没有这种控制权。
- 目标hive必须至少包含一个具有足够宽松访问权限的键,允许非特权用户创建值(KEY_SET_VALUE权限)和/或新的子键(KEY_CREATE_SUB_KEY)。根据特定漏洞的先决条件,可能还需要一些其他访问权限。
在上述两点中,第一点肯定更难满足。许多hive内存破坏漏洞源于hive结构中一种奇怪的、不可预见的状态,这种状态只能通过完全控制给定文件的"离线"方式生成。仅API漏洞似乎相对罕见:例如,在我发现的17个基于hive的内存破坏案例中,理论上只有不到一半(具体来说是8个)可以通过仅对现有hive执行操作来触发。此外,仔细观察会发现,其中一些不符合针对系统hive的其他条件(例如,它们仅影响差异hive),或者非常不切实际,例如需要分配超过500 GB的内存,或需要许多小时才能触发。实际上,在广泛的漏洞范围内,只有两个非常适合直接攻击系统hive:CVE-2023-23420(在报告的"操作事务性重命名键的子键"部分讨论)和CVE-2023-23423(在"释放键节点的浅拷贝与CmpFreeKeyByCell"部分讨论)。
关于第二个问题——可写键的可用性——对攻击者来说情况要好得多。这有三个原因:
- 要成功对系统键执行仅数据攻击,我们通常不限于一个特定的hive,而是可以选择任何适合我们的hive。在大多数(如果不是全部)挂载在HKLM下的hive中利用hive破坏将使攻击者能够提升权限。
- Windows内核内部通过首先在注册表树中进行完整路径查找来实现键打开过程,然后才检查所需的用户权限。访问检查仅在特定键的安全描述符上执行,不考虑其祖先。这意味着为键设置过于宽松的安全设置会自动使其容易受到攻击,因为根据这种逻辑,它不会从其祖先键获得额外的保护,即使它们有更严格的访问控制。
- HKLM\SOFTWARE和HKLM\SYSTEM hive中有大量用户可写的键。它们在HKLM\BCD00000000、HKLM\SAM或HKLM\SECURITY中不存在,但正如我上面提到的,只需要一个这样的键即可成功利用。
要找到这类公开可访问键的具体示例,有必要编写自定义工具。该工具应首先递归列出低级\Registry\Machine和\Registry\User路径中的所有现有键,同时以最高可能的权限(理想情况下是System用户)运行。这将确保该进程可以看到注册表树中的所有键——即使是那些隐藏在受限父键后面的键。不值得尝试枚举\Registry\A的子键,因为任何对它的引用都会被Windows内核无条件阻止。同样,除非对容器化应用程序使用的差异hive感兴趣,否则可以跳过\Registry\WC。一旦我们有了所有键的完整列表,下一步就是验证哪些键可以被非特权用户写入。这可以通过读取它们的安全描述符(使用RegGetKeySecurity)并手动检查它们的访问权限(使用AccessCheck)来完成,或者完全将此任务委托给内核,只需在具有常规用户权限的情况下尝试以所需权限打开每个键。无论哪种情况,我们最终都应该能够获得可用于破坏系统hive的潜在键列表。
根据我的测试,在当前Windows 11系统上,HKLM中大约有1678个键授予普通用户创建子键的权限。其中,1660个位于HKLM\SOFTWARE,18个位于HKLM\SYSTEM。一些例子包括:
- HKLM\SOFTWARE\Microsoft\CoreShell
- HKLM\SOFTWARE\Microsoft\DRM
- HKLM\SOFTWARE\Microsoft\Input\Locales(及其一些子键)
- HKLM\SOFTWARE\Microsoft\Input\Settings(及其一些子键)
- HKLM\SOFTWARE\Microsoft\Shell\Oobe
- HKLM\SOFTWARE\Microsoft\Shell\Session
- HKLM\SOFTWARE\Microsoft\Tracing(及其一些子键)
- HKLM\SOFTWARE\Microsoft\Windows\UpdateApi
- HKLM\SOFTWARE\Microsoft\WindowsUpdate\UX
- HKLM\SOFTWARE\WOW6432Node\Microsoft\DRM
- HKLM\SOFTWARE\WOW6432Node\Microsoft\Tracing
- HKLM\SYSTEM\Software\Microsoft\TIP(及其一些子键)
- HKLM\SYSTEM\ControlSet001\Control\Cryptography\WebSignIn\Navigation
- HKLM\SYSTEM\ControlSet001\Control\MUI\StringCacheSettings
- HKLM\SYSTEM\ControlSet001\Control\USB\AutomaticSurpriseRemoval
- HKLM\SYSTEM\ControlSet001\Services\BTAGService\Parameters\Settings
如我们所见,有很多可能性。列表中的第二个键HKLM\SOFTWARE\Microsoft\DRM在过去有些流行,因为它曾被James Forshaw用来演示他在2019-2020年发现的两个漏洞(CVE-2019-0881,CVE-2020-1377)。随后,我也用它作为触发与注册表虚拟化相关的某些行为的方式(CVE-2023-21675,CVE-2023-21748,CVE-2023-35357),并作为填充SOFTWARE hive到其容量的一种潜在途径,从而在利用另一个漏洞(CVE-2023-32019)时导致OOM条件。这个键的主要优点是它存在于所有现代系统版本中(至少从Windows 7开始),并且它向所有用户(Everyone组,也称为World或S-1-1-0)授予广泛的权限。上面提到的其他键也允许普通用户进行写操作,但它们通常通过其他可能更受限制的组(如Interactive(S-1-5-4)、Users(S-1-5-32-545)或Authenticated Users(S-1-5-11))来实现,这可能需要注意。
除了全局系统hive外,我还发现了HKCU\Software\Microsoft\Input\TypingInsights键的奇怪情况,它存在于每个用户的hive中,允许系统中所有其他用户的读写访问。我在2023年12月向微软报告了这一点(报告链接),但它被认为严重性较低,到目前为止尚未修复。这一决定在某种程度上是可以理解的,因为该行为对系统安全没有直接、严重的后果,但它仍然可以作为一种有用的利用技术。由于任何用户都可以在任何其他用户的用户hive中打开一个键进行写入,他们获得了以下能力:
- 填满该hive的整个2 GiB空间,导致DoS条件(用户及其应用程序无法写入HKCU),并可能启用与hive内OOM条件处理不当相关的漏洞利用。
- 不仅写入HKCU本身的"TypingInsights"键,还写入覆盖在其上的差异hive中的任何对应键。这提供了攻击在该用户权限下在应用程序/服务器隔离中运行的应用程序的机会。
- 不仅对系统hive执行基于hive的内存破坏攻击,还对特定用户的hive执行攻击,从而实现更横向的权限提升场景。
如所示,即使是单个注册表键的安全描述符中看似微小的弱点也可能对系统安全产生重大影响。
总之,利用hive内存破坏攻击系统hive当然是可能的,但需要找到一个非常好的漏洞,该漏洞可以在现有键上触发,而无需加载自定义hive。这是一个很好的起点,但也许我们可以找到一种更通用的技术。
滥用regf不一致性触发内核池破坏
虽然hive在内存中的映射在某种程度上是孤立和自包含的,但它们并非存在于真空中。Windows内核在内核池空间中分配和管理许多与注册表相关的附加对象,如博客文章#6中所述。这些对象通过数据缓存作为优化,并帮助实现仅通过对hive空间的操作无法实现的某些功能(例如事务、分层键)。其中一些对象是长期存在的,只要hive挂载就保留在内存中。其他缓冲区在同一系统调用中分配并立即释放,仅用作临时数据存储。所有这些对象的内存安全性与hive映射中相应数据的一致性密切相关。在内核在CmCheckRegistry和相关函数中精心验证hive有效性后,它假设注册表hive的数据与其自身结构和相关辅助结构保持一致。
对于潜在的攻击者来说,这意味着hive内存破坏可能会升级为某些形式的池破坏。这为利用提供了更广泛的选择,因为内核的各个部分使用了各种池分配。事实上,我甚至在向微软的报告中利用了这种行为:在每次安全描述符的释放后使用情况下,我都会启用Special Pool并通过_CM_KEY_CONTROL_BLOCK.CachedSecurity字段在池上触发对该描述符缓存副本的引用。我这样做是因为通过访问池上的已释放分配比访问hive中已释放但仍映射的单元更容易生成可靠可重现的崩溃。
然而,这肯定不是通过修改regf格式的内部数据导致池内存破坏的唯一方法。另一个想法是,例如,在hive中创建一个非常长的"大数据"值(在版本≥1.4的hive中超过~16 KiB),然后使_CM_KEY_VALUE.DataLength与_CM_BIG_DATA.Count字段不一致,后者表示后备缓冲区中的16千字节块数。如果我们查看内部CmpGetValueData函数的实现,很容易看到它基于前一个值分配一个分页池缓冲区,然后基于后一个值将数据复制到其中。因此,如果我们将_CM_KEY_VALUE.DataLength设置为小于16344 × (_CM_BIG_DATA.Count - 1)的数字,那么下次请求该值的数据时,将发生线性池缓冲区溢出。
这种类型的原语很有前途,因为它打开了针对比之前可能更广泛的内存对象的大门。下一步可能涉及找到一个合适的对象立即放置在覆盖的缓冲区之后(例如,2020年这篇文章中提到的管道属性),然后破坏它以实现更强大的原语,如任意内核读/写。简而言之,这种攻击将归结为对基于池的内存破坏的相当通用的利用,这是一个在现有资源中广泛讨论的主题。我们不会在这里进一步探讨,而是鼓励感兴趣的读者自行研究。
跨hive内存破坏
到目前为止,在我们的分析中,我们假设使用基于hive的内存破坏漏洞,我们只能修改我们正在操作的特定hive中的数据。然而,在实践中,情况不一定如此,因为可能有其他数据位于我们的bin映射在内存中的紧邻位置。如果发生这种情况,使用线性缓冲区溢出可能无缝跨越原始hive和更高内存地址中一些更有趣的对象之间的边界。在以下部分中,我们将看两个这样的场景:一个是被攻击hive的映射位于"Registry"进程的用户模式空间中,另一个是它驻留在内核地址空间中。
Registry进程用户空间中的其他hive映射
在Registry进程的用户空间映射hive的节视图是绝大多数注册表的默认行为。各个映射在内存中的布局可以很容易地从WinDbg中观察到。为此,找到Registry进程(通常是系统进程列表中的第二个),切换到其上下文,然后发出!vad命令。下面显示了执行这些操作的示例。
我们可以看到,所有hive都非常接近彼此,实际上它们之间没有任何间隙。然而,这个例子并没有直接证明是否可以将SOFTWARE hive的映射直接放置在普通用户加载的app hive映射之后。系统hive的地址似乎已经确定,并且它们之间没有太多空间来注入我们自己的数据。幸运的是,hive可以动态增长,特别是当您开始向它们写入长值时。这会导致创建新的bin并将它们映射到Registry进程内存中的新地址。
为了测试目的,我编写了一个简单的程序,在给定键中创建连续的0x3FD8字节值。这会触发分配正好0x4000字节的新bin:0x3FD8字节的数据加上_HBIN结构的0x20字节、单元大小的4字节和填充的4字节。接下来,我在app hive和HKLM\SOFTWARE上并行运行它的两个实例,用字母"A"填充前者,用字母"B"填充后者。测试的结果立即在内存布局中可见:
我们在这里看到的是交错映射的受信任和不受信任的hive,每个2 MiB长,紧密打包了512个16 KiB的bin。重要的是,一个映射的结束和另一个映射的开始之间没有间隙,这意味着确实可以利用一个hive中的内存破坏来影响另一个hive的内部表示。以地址0x15280400000处的test.dat和SOFTWARE hive之间的边界为例。如果我们转储包含此页面边界前后几十个字节的内存区域,我们会得到以下结果:
我们可以清楚地看到,属于两个hive的字节存在于一个连续的存储区域中。这反过来意味着内存破坏确实可以从一个hive传播到另一个。然而,要成功实现这一结果,还需要确保目标hive的特定片段被标记为脏。否则,此内存页将被标记为PAGE_READONLY,尽管两个区域彼此直接相邻,但在尝试写入数据时会导致系统崩溃。
成功破坏全局系统hive中的数据后,攻击的其余部分可能涉及修改安全描述符以授予自己对特定键的写入权限,或直接更改配置数据以启用自己的代码以管理员权限执行。
攻击基于池的hive映射中的相邻内存
尽管hive文件视图通常映射在Registry进程的用户模式空间中(其中除了这些映射外没有其他内容),但在少数情况下,这些数据直接存储在内核模式池中。这些情况如下:
- 所有易失性hive,它们在磁盘上没有作为regf文件的持久表示。例如,根位于\Registry的虚拟hive,以及HKLM\HARDWARE hive。
- 整个HKLM\SYSTEM hive,包括其稳定和易失性部分。
- 所有最近通过在先前不存在的文件上调用NtLoadKey*系统调用创建的hive,包括新创建的app hive。
- 系统中每个活动hive的易失性存储空间。
第一点对潜在攻击者没有用,因为这些类型的hive不授予非特权用户写入权限。第二点和第三点也相当有限,因为它们只能通过不需要对输入hive进行二进制控制的内存破坏来利用。然而,第四点使得可以利用系统中任何hive的漏洞,包括app hive。这是因为创建易失性键不需要与常规键相比任何特殊权限。此外,如果我们有一种存储类型中的内存破坏原语,我们可以轻松影响另一种存储类型中的数据。例如,在稳定存储内存破坏的情况下,只需为单元索引_CM_KEY_VALUE.Data设置最高位,从而指向易失性空间。从这一点开始,我们可以任意修改位于该空间中的regf结构,并通过设置足够长的值大小(超过给定bin的边界)直接读/写越界池内存。这种情况如下图所示:
这种行为可以在特定示例上进一步验证。让我们考虑登录到Windows 11系统的用户的HKCU hive——由于存在"HKCU\Volatile Environment"键,它通常会有一些数据存储在易失性存储中。让我们首先使用!reg hivelist命令在WinDbg中找到该hive:
如所见,该hive有一个0x5000字节(5个内存页)的易失性空间。让我们尝试通过转换其对应的单元索引在内存中找到该hive区域的第二页:
它确实是一个内核模式地址,如预期的那样。我们可以转储其内容以验证它确实包含注册表数据:
一切看起来都很好。在页面的开头有一个bin头,在偏移0x20处,我们看到第一个对应于安全描述符(‘sk’)的单元。现在,让我们看看!pool命令对这个地址有什么说法:
我们正在处理一个由配置管理器请求的0x1000字节的分页池分配。它后面是什么?
接下来的两个内存页对应于池上完全无关的其他分配:一个与NT对象管理器相关,另一个与win32k.sys图形驱动程序相关。这清楚地表明,在内核空间中,包含易失性hive数据的区域与系统其他部分使用的各种其他分配混合在一起。此外,这种技术之所以吸引人,不仅因为它能够越界写入受控数据,还因为它能够事先读取这些OOB数据。因此,漏洞利用不必"盲目"操作,而是可以精确验证内存是否完全按预期排列,然后再继续攻击的下一个阶段。有了这些能力,编写漏洞利用的其余部分应该是正确梳理池布局并找到一些好的候选对象进行破坏的问题。
终极原语:越界单元索引
情况显然不像之前看起来那样无望,并且有相当多的方法可以将自己hive空间中的内存破坏转化为控制其他类型的内存。然而,它们都有一个小的缺陷:它们依赖于预先安排内存中对象的特定布局(例如,Registry进程中的hive映射或分页池上的分配),这意味着不能说它们是100%稳定或确定性的。内存布局的随机性带来了固有的风险,即漏洞利用要么根本不起作用,要么更糟,在此过程中导致操作系统崩溃。由于缺乏更好的替代方案,这些技术就足够了,特别是对于演示目的。然而,我找到了一种更好的方法,通过完全消除随机性元素来保证100%的有效性。我在本系列之前的博客文章中多次暗示甚至直接提到过这一点,当然,我指的是越界单元索引。
快速提醒一下,单元索引是hive中等同于指针的东西:它们是32位值,允许分配的单元相互引用。单元索引到其对应虚拟地址的转换是使用称为单元映射的特殊3级结构实现的,该结构类似于CPU页表:
负责执行单元映射遍历的内部HvpGetCellPaged函数的C-like伪代码如下:
_CELL_DATA *HvpGetCellPaged(_HHIVE *Hive, HCELL_INDEX Index) { _HMAP_ENTRY *Entry = &Hive->Storage[Index » 31].Map ->Directory[(Index » 21) & 0x3FF] ->Table[(Index » 12) & 0x1FF];
return (Entry->PermanentBinAddress & (~0xF)) + Entry->BlockOffset + (Index & 0xFFF) + 4; }
对应于单元映射各个级别的结构是_DUAL、_HMAP_DIRECTORY、_HMAP_TABLE和_HMAP_ENTRY,它们可以通过_CMHIVE.Hive.Storage字段访问。从利用的角度来看,这里有两个关键事实。首先,HvpGetCellPaged函数不对输入索引执行任何边界检查。其次,对于小于2 MiB的hive,Windows应用了一种称为"small dir"的额外优化。在这种情况下,内核不是分配整个1024个元素的Directory数组并仅使用其中一个,而是将_CMHIVE.Hive.Storage[…].Map指针设置为_CMHIVE.Hive.Storage[…].SmallDir字段的地址,该字段模拟单元素数组。这样,逻辑单元映射级别的数量保持不变,但系统使用少一个池分配来存储它们,每个hive节省约8 KiB内存。这种行为如下面的屏幕截图所示:
我们这里有一个hive,其稳定存储区为0xEE000字节(952 KiB),易失性存储区为0x5000字节(20 KiB)。这两个大小都小于2 MiB,因此在这两种情况下都应用了"small dir"优化。因此,Map指针(标记为橙色)直接指向SmallDir字段(标记为绿色)。
这种情况很有趣,因为如果内核尝试在这样一个hive的上下文中解析值为0x200000或更大的无效单元索引(即"Directory索引"部分非零),那么单元映射遍历的第一步将引用Guard、FreeDisplay等字段作为指针。这种情况如下图所示:
换句话说,通过完全控制单元索引的32位值,我们可以使转换逻辑通过从越界内存中获取的两个指针跳转,然后向结果添加一个受控的12位偏移量。另一个考虑因素是,在第一步中,我们引用了位于更大的_CMHIVE结构中的"数组"的越界索引,该结构在给定的Windows构建上始终具有相同的布局。因此,通过选择一个引用_CMHIVE中特定指针的目录索引,我们可以确保它在给定系统版本上始终以相同的方式工作,不受任何随机因素的影响。
另一方面,一个小的不便之处是_HMAP_ENTRY结构(即单元映射的最后一级)具有以下布局:
0: kd> dt _HMAP_ENTRY nt!_HMAP_ENTRY +0x000 BlockOffset : Uint8B +0x008 PermanentBinAddress : Uint8B +0x010 MemAlloc : Uint4B
最终返回的值是BlockOffset和PermanentBinAddress字段的总和。因此,如果这些字段之一包含我们想要引用的地址,另一个必须为NULL,这可能会稍微缩小我们的选择范围。
如果我们基于它们包含的指针从_CMHIVE开始创建结构之间关系的图形表示,它将类似于以下内容:
该图不一定完整,但它显示了从_CMHIVE开始最多通过两个指针解引用可以到达的一些对象的概述。然而,重要的是要记住,在实践中并非图中的每条边都是可遍历的。这是由于两个原因:首先,由于_HMAP_ENTRY结构的布局(即0x18字节对齐和需要给定指针相邻的0x0值);其次,由于这些对象中的每个指针并不总是初始化。例如,_CMHIVE.RootKcb字段仅对app hive有效(但对普通hive无效),而_CMHIVE.CmRm仅对标准hive设置,因为app hive从未启用KTM事务支持。因此,这个想法为我们的漏洞利用提供了一些良好的基础,但它确实需要额外的实验才能正确掌握每个技术细节。
继续前进,WinDbg中的!reg cellindex命令非常适合测试越界单元索引,因为它使用与HvpGetCellPaged完全相同的单元映射遍历逻辑,并且也不执行任何额外的边界检查。因此,让我们继续使用之前处理的HKCU hive,并尝试创建一个指向其_CMHIVE结构的单元索引。我们将使用_CMHIVE → _CM_RM → _CMHIVE路径来实现这一点。我们需要做出的第一个决定是为该索引选择存储类型:稳定(0)或易失性(1)。对于HKCU,两种存储类型都是非空的,并使用"small dir"优化,因此我们可以选择任何一种;假设选择易失性。接下来,我们需要计算目录索引,该索引将等于_CMHIVE.CmRm和_CMHIVE.Hive.Storage[1].SmallDir字段偏移量之间的差:
在这种情况下,它是(0xffff82828fc1b038 - 0xffff82828fc1a3a0) ÷ 8 = 0x193。下一步是计算表索引,该索引将是结构内_CM_RM.CmHive字段从结构开始的偏移量除以_HMAP_ENTRY的大小(0x18)。
因此,计算是(0xffff82828fdcc930 - 0xffff82828fdcc8e0) ÷ 0x18 = 3。接下来,我们可以验证CmHive指针落在_HMAP_ENTRY结构中的位置。
_CM_RM.CmHive指针与PermanentBinAddress字段对齐,这是个好消息。此外,BlockOffset字段为零,这也是可取的。在内部,它对应于ContainerSize字段,如果在此会话期间未对hive执行任何KTM事务,则该字段被清零——这对我们的示例来说已经足够。
我们现在已经计算了四个单元索引元素中的三个,最后一个元素是偏移量,我们将其设置为零,因为我们希望从最开始访问_CMHIVE结构。是时候在一个地方收集所有这些信息了;我们可以使用一个简单的Python函数构建最终的单元索引:
def MakeCellIndex(storage, directory, table, offset): … print(“0x%x” % ((storage « 31) | (directory « 21) | (table « 12) | offset)) …
然后传递我们到目前为止确定的值:
MakeCellIndex(1, 0x193, 3, 0) 0xb2603000
因此,指向给定hive的_CMHIVE结构的最终越界单元索引是0xB2603000。现在是时候在WinDbg中验证这个神奇索引是否真的按预期工作了。
确实,作为命令输入的_CMHIVE地址也出现在其输出中,这意味着我们的技术有效(输出地址中额外的0x4是为了考虑单元大小)。如果我们将此索引插入_CM_KEY_VALUE.Data字段,我们将能够通过注册表值读取和写入内核内存中的_CMHIVE结构。这代表了本地攻击者手中非常强大的能力。
编写漏洞利用
在这个阶段,我们已经有了一个坚实的计划,如何利用初始的hive内存破坏原语进行进一步的权限提升。是时候选择一个特定的漏洞并开始为其编写实际的漏洞利用了。这个过程在下面详细描述。
步骤0:选择漏洞
面对大约17个与hive内存破坏相关的漏洞,立即面临的挑战是选择一个进行演示漏洞利用。虽然这些漏洞中的任何一个最终都可以通过时间和实验被利用,但它们的难度各不相同。还有一个美学考虑:出于演示目的,如果漏洞利用的操作在Regedit中可见,那将是理想的,这缩小了我们的选择范围。尽管如此,仍有大量选择可用,我们应该能够确定一个合适的候选。让我们简要看一下两个不同的可能性。
CVE-2022-34707 在注册表上下文中,第一个总是浮现在我脑海中的漏洞是CVE-2022-34707。这部分是因为它是我在这项研究中手动发现的第一个漏洞,但主要是因为它非常方便利用。这个漏洞的本质是,可以加载一个安全描述符包含接近最大32位值(例如0xFFFFFFFF)的引用计数的hive,然后通过创建更多使用它的键来溢出它。这导致了一个非常强大的UAF原语,因为错误释放的单元可以随后被新对象填充,然后再次释放任意次数。通过这种方式,可以实现几种不同类型对象的类型混淆,例如,通过将同一单元随后作为安全描述符→值节点→值数据后备单元重用,我们可以轻松控制_CM_KEY_VALUE结构,从而允许我们使用越界单元索引继续攻击。
由于其特性,这个漏洞也是我在这项研究中为其编写完整漏洞利用的第一个漏洞。我在这里描述的许多技术都是在处理这个漏洞时发现的。此外,博客文章#1末尾显示权限提升的屏幕截图说明了CVE-2022-34707的成功利用。然而,在这篇博客文章的上下文中,它有一个根本性的缺陷:为了将初始引用计数设置为接近溢出32位范围的值,有必要手动制作输入regf文件。这意味着目标只能是app hive,因此我们将无法直接在注册表编辑器中观察利用。这将大大降低我通过视觉演示漏洞利用的能力,这最终导致我寻找一个更好的漏洞。
CVE-2023-23420 这让我们来到第二个漏洞,CVE-2023-23420。这也是hive中的一个UAF条件,但它涉及一个键节点单元而不是安全描述符单元。它是由事务性键重命名操作中的某些问题引起的。这些问题如此深入,影响了注册表的基本方面,以至于这个漏洞和相关的漏洞CVE-2023-23421、CVE-2023-23422和CVE-2023-23423通过完全移除对事务性键重命名操作的支持来修复。
在利用方面,这个漏洞特别独特,因为它可以仅使用API/系统调用触发,使得可以破坏攻击者具有写权限的任何hive。这使其成为编写漏洞利用的理想候选,其操作可以通过标准Windows注册表工具肉眼可见,所以这就是我们要做的。尽管在将hive布局按摩到所需状态方面的细节可能比CVE-2022-34707稍微困难一些,但这并不是我们无法处理的。所以让我们开始工作吧!
步骤1:滥用UAF建立动态控制的值单元
让我们首先澄清,我们的攻击将针对HKCU hive,更具体地说,是其易失性存储空间。这有望使漏洞利用更可靠一些,因为易失性空间在每次重新加载hive时重置,并且通常那里没有太多活动发生。利用过程始于键节点释放后使用,我们的目标是在第一阶段结束时(为什么是两个——我们稍后会讲到)完全控制两个注册表值的_CM_KEY_VALUE表示。一旦我们实现这一目标,我们将能够任意设置_CM_KEY_VALUE.Data字段,从而获得对任何选定的越界单元索引的读/写访问权限。有许多不同的方法可以实现这一点,但在我的概念验证中,我从以下数据布局开始:
在层次结构的顶部是HKCU\Exploit键,它是整个漏洞利用子树的根。它的唯一作用是作为我们创建的所有其他键和值的容器。在其下方,我们有"TmpKeyName"键,它之所以重要有两个原因:首先,它存储了四个值,这些值将在稍后阶段用于用受控数据填充已释放的单元(但目前为空)。其次,这是我们将执行"重命名"操作的键,这是CVE-2023-23420漏洞的基础。在其下方还有两个键,“SubKey1"和"SubKey2”,它们也是通过其父键的不同视图进行事务性删除所需的。
一旦我们在hive中安排了这种数据布局,我们就可以继续触发内存破坏。我们可以完全按照原始报告中"操作事务性重命名键的子键"部分所述进行操作,并在相应的InconsistentSubkeyList.cpp源代码中演示。简而言之,它涉及以下步骤:
- 通过调用NtCreateRegistryTransaction系统调用创建一个轻量级事务。
- 在我们新创建的事务中打开两个不同的HKCU\Exploit\TmpKeyName键句柄。
- 对其中一个句柄执行事务性重命名操作,将名称更改为"Scratchpad"。
- 事务性删除"SubKey1"和"SubKey2"键,每个通过不同的父句柄(一个重命名,另一个未重命名)。
- 通过调用NtCommitRegistryTransaction系统调用提交整个事务。
在易受攻击的系统上成功执行这些操作后,我们的对象在hive中的布局应相应更改:
我们看到"TmpKeyName"键已重命名为"Scratchpad",并且其两个子键已被释放,但第二个子键的释放单元仍出现在其父键的子键列表中。此时,我们希望使用"Scratchpad"键的四个值创建我们自己的伪造数据结构。根据它,释放的子键仍将显示为存在,并包含两个名为"KernelAddr"和"KernelData"的值。每个"Container"值负责模仿一种类型的对象,其中"FakeKeyContainer"值扮演最关键的角色。它的后备缓冲区必须与之前与"SubKey1"键节点关联的内存完美对齐。下面的图表说明了期望的结果:
所有突出显示的单元都包含攻击者控制的数据,这些数据表示描述HKCU\Exploit\Scratchpad\FakeKey键及其两个值的有效regf结构。一旦实现了这种数据布局,就可以使用RegOpenKeyEx等标准API打开"FakeKey"的句柄,然后通过其值操作任意单元索引。实际上,在触发UAF后制作这些对象的过程比仅设置四个不同值的数据稍微复杂一些,需要以下步骤:
- 使用"FakeKey"键的初始基本表示写入"FakeKeyContainer"值。在这个阶段,键节点完全正确并不重要,但它必须具有适当的长度,从而精确覆盖"Scratchpad"键的子键列表当前指向的释放单元。
- 设置其他三个容器值的数据——同样还不是最终的,但那些具有适当长度并填充了唯一标记的值,以便以后可以轻松识别它们。
- 启动信息泄漏循环以找到对应于"ValueListContainer"、“KernelAddrContainer"和"KernelDataContainer"值的数据单元的单元索引,以及有效安全描述符的单元索引。此逻辑依赖于滥用"FakeKey"的_CM_KEY_NODE.Class和_CM_KEY_NODE.ClassLength字段,将它们指向我们想要读取的hive中的数据。具体来说,ClassLength成员设置为0xFFC,Class成员在后续循环迭代中设置为索引0x80000000、0x80001000、0x80002000等。这启用了一种"任意hive读取"原语,可以通过在"Scratchpad"键上调用带有KeyNodeInformation类的NtEnumerateKey系统调用来实现读取,该调用除其他外返回给定子键的类属性。这样,我们获得了构建每个模拟单元的最终形式所需的关于内部hive布局的所有信息。
- 使用上述信息为四个单元中的每一个设置正确的数据:“FakeKey"键的键节点(带有有效的安全描述符和指向值列表的索引)、值列表本身以及"KernelAddr"和"KernelData"的值节点。这使得"FakeKey"成为Windows视为一个完整的键,但其所有内部regf结构完全由我们控制。
如果所有这些步骤都成功,我们应该能够在Regedit中打开HKCU\Exploit\Scratchpad键并查看当前的漏洞利用进度。我的测试系统上的示例如屏幕截图所示。额外的"Filler"值用于填充在重命名操作期间释放的旧"TmpKeyName"键节点占用的空间。这是必要的,以便"FakeKeyContainer"值的数据正确对齐"SubKey1"键的释放单元,但为了清晰起见,我在上述逻辑的高级描述中跳过了这个小的实现细节。
步骤2:获取对CMHIVE内核对象的读/写访问权限
既然我们现在完全控制了一些注册表值,下一个逻辑步骤将是使用特殊制作的OOB单元索引初始化它们,然后检查我们是否可以实际访问它代表的内核结构。假设我们将"KernelData"值的类型设置为REG_BINARY,其长度设置为0x100,数据单元索引设置为之前计算的0xB2603000值,该值应指向内核池中hive的_CMHIVE结构。如果我们这样做,然后在注册表编辑器中浏览到"FakeKey"键,我们将遇到一个不愉快的意外:
这绝对不是我们期望的结果,一定是出了问题。如果我们在WinDbg中调查系统崩溃,我们将获得以下信息:
我们看到了bugcheck代码0x51(REGISTRY_ERROR),这表明它是故意触发的,而不是通过错误的内存访问