Windows注册表内核对象深度解析
内核模式对象概述
在Windows注册表系列的前一篇文章中,我们深入研究了regf hive格式的内部结构。理解注册表的这一基础方面至关重要,因为它揭示了该机制背后的设计原则及其固有的优缺点。存储在regf文件中的数据代表了hive的最终状态。了解如何解析这些数据足以处理以此格式编码的静态文件,例如在编写自定义regf解析器以检查从硬盘提取的hive时。
然而,对于那些感兴趣的是Windows在运行时如何管理regf文件,而不仅仅是它们孤立行为的人来说,还有另一个维度需要探索:在活动hive的整个生命周期中分配和维护的大量内核模式对象。这些辅助对象至关重要的原因有几个:
- 跟踪所有当前加载的hive、它们的属性(例如加载标志)、它们的内存映射以及它们之间的关系(尤其是彼此叠加的delta hive)
- 在多线程Windows环境中同步对密钥和hive的访问
- 缓存hive信息以实现比直接内存映射查找更快的访问
- 将注册表与NT对象管理器集成并支持标准操作(打开/关闭句柄、设置/查询安全描述符、强制执行访问检查等)
- 在待处理事务完全提交到底层hive之前管理其状态
为了满足这些多样化的需求,Windows内核采用了众多相互连接的结构。在本文中,我们将研究其中一些最关键的结构,它们如何运作,以及如何使用WinDbg有效地枚举和检查它们。
需要注意的是,Microsoft仅通过ntoskrnl.exe的PDB符号提供了一些注册表相关结构的官方定义。在许多情况下,我必须对相关代码进行逆向工程以恢复结构布局,并推断特定字段和枚举的类型和名称。在整篇文章中,我将明确指出每个结构定义是官方的还是逆向工程的。
Hive结构
考虑到hive是最复杂的注册表对象类型,它们的内核模式描述符同样复杂和冗长也就不足为奇了。Windows中主要的hive描述符结构称为_CMHIVE,跨越了相当大的0x12F8字节 - 超过了x86系列架构上的标准内存页面大小4 KiB。在_CMHIVE内部,偏移量0处包含另一个类型为_HHIVE的结构,它占用0x600字节,如下图所示:
|
|
这种关系反映了其他常见Windows对象对的关系,例如_EPROCESS / _KPROCESS和_ETHREAD / _KTHREAD。由于_HHIVE总是作为更大的_CMHIVE结构的一个组件分配,它们的指针类型实际上是可以互换的。如果您遇到使用_HHIVE*指针的反编译访问超出了结构的大小,几乎可以肯定它表示对包含的_CMHIVE对象中字段的引用。
但是为什么需要两个不同的结构来表示单个注册表hive?虽然技术上不是必需的,但这种分离可能用于划分与hive不同抽象层相关的字段。具体来说:
- _HHIVE管理hive的低级方面,包括hive头、bin和cell,以及内存映射和与其磁盘对应物的同步状态(例如脏扇区)
- _CMHIVE处理关于hive的更抽象信息,例如安全描述符缓存、指向高级内核对象(如根密钥控制块(KCB))的指针以及相关的事务资源管理器(_CM_RM结构)
接下来的小节将更深入地研究这两个结构的职责和内部工作原理。
_HHIVE结构概述
_HHIVE结构的主要作用是管理hive的内存相关状态。这使得更高级别的注册表代码可以执行诸如分配、释放和将cell标记为"脏"的操作,而无需处理低级实现细节。_HHIVE结构包含49个顶级成员,其中大多数将在下面按较大的组进行描述:
|
|
签名
等于0xBEE0BEE0,它是_HHIVE / _CMHIVE结构的唯一签名。在数字取证中,它可能有助于在原始内存转储中识别这些结构,并且是Windows注册表实现中另一个对蜜蜂的引用。
函数指针
接下来是六个函数指针,在HvHiveStartFileBacked和HvHiveStartMemoryBacked中初始化,并指向以下操作的内核处理程序:
| 指针名称 | 指针值 | 操作 |
|---|---|---|
| GetCellRoutine | HvpGetCellPaged或HvpGetCellFlat | 将cell索引转换为虚拟地址 |
| ReleaseCellRoutine | HvpReleaseCellPaged或HvpReleaseCellFlat | 释放先前转换的cell索引 |
| Allocate | CmpAllocate | 在全局注册表配额内分配内核内存 |
| Free | CmpFree | 在全局注册表配额内释放内核内存 |
| FileWrite | CmpFileWrite | 将数据写入hive文件 |
| FileRead | CmpFileRead | 从hive文件读取数据 |
正如我们所看到的,这些函数提供了操作内核内存、cell索引和hive文件的基本功能。在我看来,其中最重要的是GetCellRoutine,其典型目标HvpGetCellPaged执行cell映射遍历,以便将cell索引转换为hive映射内的相应地址。
很自然地认为,如果攻击者通过缓冲区溢出或use-after-free条件破坏这些指针,这些函数指针可能对利用有用。在Windows 10及更早版本中确实如此,但在Windows 11中,这些调用现在已被去虚拟化,大多数调用站点直接引用HvpGetCellPaged / HvpGetCellFlat和HvpReleaseCellPaged / HvpReleaseCellFlat之一,而不引用指针。这对安全性很有好处,因为它完全消除了这些字段在任何攻击场景中的有用性。
Hive加载失败信息
这是一个指向公共_HIVE_LOAD_FAILURE结构的指针,每次在加载hive时发生错误时,它都作为第一个参数传递给SetFailureLocation函数。它可以帮助跟踪给定hive的哪些有效性检查失败,而无需跟踪整个加载过程。
基础块
指向hive头副本的指针,由_HBASE_BLOCK结构表示。
同步锁
有两个锁,用途如下:
- FlusherLock - 在更改cell内数据的客户端和刷新线程之间同步对hive的访问
- WriterLock - 在修改bin/cell布局的写入器之间同步对hive的访问
它们正式类型为_CMSI_RW_LOCK,但它们归结为_EX_PUSH_LOCK,并且它们与标准内核API(如ExAcquirePushLockSharedEx)一起使用。
脏块信息
在偏移量0x58和0x84之间,_HHIVE存储了几个表示内存中和磁盘上hive实例之间同步状态的数据结构。
Hive标志
首先,在偏移量0x8C处有两个标志,指示hive映射是否是flat的以及hive是否是只读的。其次,有一个32位的HiveFlags成员,存储进一步的标志,这些标志(据我所知)不包含在任何公共Windows符号中。我设法通过逆向工程推断了我观察到的常量的含义,得到以下枚举:
|
|
日志文件信息
在偏移量0xA4到0xCC之间,有许多与日志文件管理相关的字段,即伴随主hive文件在磁盘上的.LOG1/.LOG2文件。
Hive版本
Version字段存储hive的次要版本,理论上应该是3-6之间的整数。然而,如上一篇博客文章所述,可以通过将主版本设置为0和任何所需的次要版本,或者诱使内核从日志文件恢复hive头,并利用HvAnalyzeLogFiles函数比HvpGetHiveHeader更宽松的事实,将其设置为任意32位值。然而,我没有发现这种行为有任何安全影响。
视图映射
视图映射包含关于hive如何在内存中映射的所有基本信息。注册表内存管理的具体实现多年来已经显著发展,其细节在连续的系统版本之间发生变化。在最新版本中,视图映射由顶级_HVP_VIEW_MAP公共结构表示:
|
|
单元映射
正如第5部分所讨论的,几乎每个cell都包含对hive中其他cell的引用,形式为cell索引。因此,实际上每个注册表操作都涉及多轮将cell索引转换为其相应的虚拟地址,以便遍历注册表结构。section视图存储在红黑树中,因此搜索复杂度为O(log n)。这可能看起来不错,但如果我们考虑到在典型系统上,注册表的读取频率远高于扩展/收缩频率,那么以较低的插入/删除效率为代价进一步优化搜索操作就变得有意义了。这正是cell映射的作用:以O(n)而非O(log n)的较慢插入/删除复杂度为代价,实现O(1)的更快搜索复杂度。由于这种技术,HvpGetCellPaged - 可能是Windows注册表实现中最热门的函数 - 在恒定时间内执行。
在技术术语上,cell映射是类似页表的结构,将32位hive地址空间划分为较小的嵌套层,包括所谓的目录、表和条目。提醒一下,cell索引和cell映射的布局如下图所示,基于《Windows Internals》书中类似的图表,该图表本身源自Mark Russinovich 1999年的文章《Inside the Registry》:
|
|
考虑到数据结构的性质,相应的cell映射遍历涉及基于cell索引的连续1、10和9位部分解引用三个嵌套数组,然后将最终12位偏移量添加到目标块的页对齐地址。匹配cell映射相应层的内核内部结构是_DUAL、_HMAP_DIRECTORY、_HMAP_TABLE和_HMAP_ENTRY,所有这些都可以通过ntoskrnl.exe PDB符号公开访问。cell映射的入口点是_HHIVE结构末尾的Storage数组:
|
|
双元素数组的索引表示存储类型,0表示稳定,1表示易失,因此单个_DUAL结构描述了特定存储空间的2 GiB视图:
|
|
小目录优化
关于cell映射还有一个实现细节值得在这里提及 - 小目录优化。让我们从观察开始,Windows中的大多数注册表hive相对较小,大小低于2 MiB。这可以通过在WinDbg中使用!reg hivelist命令并注意"Stable Length"和"Volatile Length"列中的值来轻松验证。它们中的大多数通常包含几千字节到几百千字节的值。这意味着如果内核为这些hive分配完整的第一级目录(在64位平台上占用1024个条目×8字节=8 KiB),它们仍然只使用其中的第一个元素,导致不小的内存浪费 - 特别是在20世纪90年代早期首次实现注册表的背景下。为了优化这种常见情况,Windows开发人员采用了一种非常规方法来模拟一个1项长的"数组",使用_DUAL结构中类型为_HMAP_TABLE的SmallDir成员,并在可能时让_DUAL.Map指针指向它而不是单独的内存池分配。后来,当hive增长并需要超过一个cell映射目录元素时,内核回退到标准行为并为目录数组执行正常的内存池分配。
_CMHIVE结构概述
除了偏移量0处类型为_HHIVE的第一个成员外,_CMHIVE结构还包含超过3 KiB的进一步信息,描述活动hive。这些数据涉及比内存管理更抽象的概念,例如注册表树结构本身。下面,我们将不进行逐字段分析,而是专注于_CMHIVE内的信息的一般类别,大致按数据结构的复杂性递增组织:
- 引用计数:一个32位的引用计数,主要用于在hive上的短期操作期间使用,以防止对象在主动操作时被释放
- 文件句柄和大小:磁盘上hive文件的句柄和当前大小,例如主hive文件(.DAT)和伴随的日志文件(.LOG、.LOG1、.LOG2)
- 文本字符串:一些信息性字符串,在尝试基于其_CMHIVE结构识别hive时可能有用
- 时间戳:可以在hive描述符中找到的几个时间戳
- 列表条目:_LIST_ENTRY结构的实例,用于将hive链接到各种双链列表中
- 同步机制:用于同步对整个hive或其某些部分的访问的各种对象
- 卸载历史:一个128元素的数组,存储了在卸载hive过程中已成功完成的步骤数
- 延迟卸载状态:与注册表hive的延迟卸载相关的对象
- Hive布局信息:Windows中的hive重组过程尝试通过将系统运行时访问的密钥分组,然后是系统启动期间访问的密钥,最后是完全未使用的密钥来优化hive
- 刷新状态和脏块信息:与将cell标记为脏并将其内容同步到磁盘相关的任何状态
- 卷上下文:指向公共_CMP_VOLUME_CONTEXT结构的指针,提供有关hive文件磁盘卷的扩展信息
- KCB表和根KCB:与hive中密钥对应的全局可见KCB(密钥控制块)结构表,以及指向根密钥KCB的指针
- 安全描述符缓存:hive中存在的所有安全描述符的缓存,从内核池分配,因此比底层hive映射更高效地访问
- 事务相关对象:指向描述与hive关联的资源管理器对象的_CM_RM结构的指针,如果为其启用了"重量级"事务(即KTM事务)
密钥结构
注册表中第二重要的对象是密钥。它们基本上可以被认为是注册表的本质,因为几乎每个注册表操作都以某种方式涉及它们。它们也是与Windows NT对象管理器紧密集成的唯一注册表元素。这带来了许多好处,因为客户端应用程序可以使用标准化句柄操作注册表,并可以利用自动安全检查和对象生命周期管理。然而,这种集成也带来了自己的挑战,因为它要求配置管理器正确与对象管理器交互,并安全地处理其复杂性和边缘情况。因此,内部密钥相关结构在注册表实现中起着至关重要的作用。
Windows内核中的两个基本密钥结构是密钥体(_CM_KEY_BODY)和密钥控制块(_CM_KEY_CONTROL_BLOCK)。密钥体直接与NT对象管理器中的密钥句柄关联,类似于_FILE_OBJECT结构对文件句柄的作用。换句话说,这是内核在调用ObReferenceObjectByHandle引用用户提供的句柄时获得的初始对象。对于单个密钥,可能同时存在多个密钥体结构,只要有几个程序持有该密钥的活动句柄。相反,密钥控制块表示特定密钥的全局状态,并用于管理其一般属性。这意味着对于系统中的大多数密钥,最多同时分配一个KCB。对于尚未访问的密钥(因为内核延迟初始化它们),可能没有KCB,并且对于同一注册表路径可能有多个KCB,如果密钥已被删除并再次创建(这两个密钥实例被视为单独的实体,其中一个被标记为已删除/不存在)。考虑到这一点,密钥体和KCB之间的关系是多对一的,单个KCB的所有密钥体都在双链表中连接,如下图所示:
|
|
结论
本文的目标是全面概述Windows中配置管理器子系统使用的结构,特别强调最重要和最常用的结构,即描述hive和密钥的结构。我想分享这些知识,因为公开可用的准确描述注册表实现方面操作的来源不多,特别是与Windows 10和11中最新的代码开发相关的信息。
这篇文章结束了该系列中仅关注注册表内部工作的部分。在接下来的第七部分中,我们将转变视角,研究注册表在整体系统安全中的作用,并深入关注漏洞研究。敬请期待!