Windows注册表内核对象深度解析

本文深入分析Windows注册表的内核模式对象实现,涵盖_CMHIVE和_HHIVE结构体、内存映射管理、单元映射机制、密钥控制块等核心组件,揭示注册表子系统在运行时的内部工作原理和数据结构关系。

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字节,如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
+-------------------+
|     _CMHIVE       |
|  +-------------+  |
|  |  _HHIVE     |  |
|  | (0x600 bytes)| |
|  +-------------+  |
|                   |
|  (remaining data) |
|                   |
+-------------------+

这种关系反映了其他常见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个顶级成员,其中大多数将在下面按较大的组进行描述:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
0: kd> dt _HHIVE
nt!_HHIVE
   +0x000 Signature        : Uint4B
   +0x008 GetCellRoutine   : Ptr64     _CELL_DATA* 
   +0x010 ReleaseCellRoutine : Ptr64     void 
   +0x018 Allocate         : Ptr64     void* 
   +0x020 Free             : Ptr64     void 
   +0x028 FileWrite        : Ptr64     long 
   +0x030 FileRead         : Ptr64     long 
   // ... 更多字段

签名

等于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符号中。我设法通过逆向工程推断了我观察到的常量的含义,得到以下枚举:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
enum _HV_HIVE_FLAGS
{
  HIVE_VOLATILE                      = 0x1,
  HIVE_NOLAZYFLUSH                   = 0x2,
  HIVE_PRELOADED                     = 0x10,
  HIVE_IS_UNLOADING                  = 0x20,
  HIVE_COMPLETE_UNLOAD_STARTED       = 0x40,
  HIVE_ALL_REFS_DROPPED              = 0x80,
  HIVE_ON_PRELOADED_LIST             = 0x400,
  HIVE_FILE_READ_ONLY                = 0x8000,
  HIVE_SECTION_BACKED                = 0x20000,
  HIVE_DIFFERENCING                  = 0x80000,
  HIVE_IMMUTABLE                     = 0x100000,
  HIVE_FILE_PAGES_MUST_BE_KEPT_LOCAL = 0x800000,
};

日志文件信息

在偏移量0xA4到0xCC之间,有许多与日志文件管理相关的字段,即伴随主hive文件在磁盘上的.LOG1/.LOG2文件。

Hive版本

Version字段存储hive的次要版本,理论上应该是3-6之间的整数。然而,如上一篇博客文章所述,可以通过将主版本设置为0和任何所需的次要版本,或者诱使内核从日志文件恢复hive头,并利用HvAnalyzeLogFiles函数比HvpGetHiveHeader更宽松的事实,将其设置为任意32位值。然而,我没有发现这种行为有任何安全影响。

视图映射

视图映射包含关于hive如何在内存中映射的所有基本信息。注册表内存管理的具体实现多年来已经显著发展,其细节在连续的系统版本之间发生变化。在最新版本中,视图映射由顶级_HVP_VIEW_MAP公共结构表示:

1
2
3
4
5
6
7
8
0: kd> dt _HVP_VIEW_MAP
nt!_HVP_VIEW_MAP
   +0x000 SectionReference : Ptr64 Void
   +0x008 StorageEndFileOffset : Int8B
   +0x010 SectionEndFileOffset : Int8B
   +0x018 ProcessTuple     : Ptr64 _CMSI_PROCESS_TUPLE
   +0x020 Flags            : Uint4B
   +0x028 ViewTree         : _RTL_RB_TREE

单元映射

正如第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》:

1
2
3
4
5
6
7
+-------------------------------+
|   Cell Index (32 bits)        |
| +---+----------+-------+----+ |
| | T | Directory| Table |Offset| |
| | (1 bit) | (10 bits)|(9 bits)|(12 bits)| |
| +---+----------+-------+----+ |
+-------------------------------+

考虑到数据结构的性质,相应的cell映射遍历涉及基于cell索引的连续1、10和9位部分解引用三个嵌套数组,然后将最终12位偏移量添加到目标块的页对齐地址。匹配cell映射相应层的内核内部结构是_DUAL、_HMAP_DIRECTORY、_HMAP_TABLE和_HMAP_ENTRY,所有这些都可以通过ntoskrnl.exe PDB符号公开访问。cell映射的入口点是_HHIVE结构末尾的Storage数组:

1
2
3
4
0: kd> dt _HHIVE
nt!_HHIVE
[...]
   +0x118 Storage          : [2] _DUAL

双元素数组的索引表示存储类型,0表示稳定,1表示易失,因此单个_DUAL结构描述了特定存储空间的2 GiB视图:

1
2
3
4
5
6
0: kd> dt _DUAL
nt!_DUAL
   +0x000 Length           : Uint4B
   +0x008 Map              : Ptr64 _HMAP_DIRECTORY
   +0x010 SmallDir         : Ptr64 _HMAP_TABLE
   // ... 更多字段

小目录优化

关于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的所有密钥体都在双链表中连接,如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
+----------------+    +----------------+    +----------------+
| _CM_KEY_BODY   |    | _CM_KEY_BODY   |    | _CM_KEY_BODY   |
| +------------+ |    | +------------+ |    | +------------+ |
| | KeyBodyList|----->| | KeyBodyList|----->| | KeyBodyList| |
| +------------+ |    | +------------+ |    | +------------+ |
| | KCB ptr   |----+  | | KCB ptr   |--+   | | KCB ptr   |--+
| +------------+ |  |  | +------------+ |   | +------------+ |
+----------------+  |  +----------------+   +----------------+
                    |
                    |  +-----------------------+
                    +->| _CM_KEY_CONTROL_BLOCK |
                       |                       |
                       +-----------------------+

结论

本文的目标是全面概述Windows中配置管理器子系统使用的结构,特别强调最重要和最常用的结构,即描述hive和密钥的结构。我想分享这些知识,因为公开可用的准确描述注册表实现方面操作的来源不多,特别是与Windows 10和11中最新的代码开发相关的信息。

这篇文章结束了该系列中仅关注注册表内部工作的部分。在接下来的第七部分中,我们将转变视角,研究注册表在整体系统安全中的作用,并深入关注漏洞研究。敬请期待!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计