Windows注册表之旅 #6:内核模式对象
欢迎回到Windows注册表之旅!在本系列的前一篇文章中,我们深入研究了regf hive格式的内部结构。理解注册表的这一基础方面至关重要,因为它揭示了该机制背后的设计原则及其固有的优缺点。regf文件中存储的数据代表了hive的最终状态。了解如何解析这些数据足以处理以此格式编码的静态文件,例如在编写自定义regf解析器以检查从硬盘提取的hive时。然而,对于那些对Windows在运行时如何管理regf文件感兴趣,而不仅仅是它们独立行为的人来说,还有另一个维度需要探索:在活动hive的整个生命周期中分配和维护的大量内核模式对象。这些辅助对象至关重要,原因如下:
- 跟踪所有当前加载的hive、它们的属性(例如加载标志)、它们的内存映射以及它们之间的关系(尤其是彼此叠加的差异hive)。
- 在多线程Windows环境中同步对键和hive的访问。
- 缓存hive信息以实现比直接内存映射查找更快的访问。
- 将注册表与NT对象管理器集成并支持标准操作(打开/关闭句柄、设置/查询安全描述符、强制执行访问检查等)。
- 在待处理事务完全提交到底层hive之前管理其状态。
为了满足这些多样化的需求,Windows内核采用了众多互连的结构。在本文中,我们将研究其中一些最关键的结构,它们如何运作,以及如何使用WinDbg有效地枚举和检查它们。需要注意的是,Microsoft仅通过ntoskrnl.exe的PDB符号提供了一些与注册表相关的结构的官方定义。在许多情况下,我必须对相关代码进行逆向工程以恢复结构布局,并推断特定字段和枚举的类型和名称。在整篇文章中,我将明确指出每个结构定义是官方的还是逆向工程的。如果您发现任何不准确之处,请告知我。这里提供的定义主要源自应用了2022年3月补丁的Windows Server 2019(内核版本10.0.17763.2686),这是我进行大部分注册表代码分析时使用的内核版本。然而,此版本与最新的Windows 11之间超过99%的注册表结构定义似乎是相同的,使得这些信息也直接适用于最新系统。
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头、bins和cells,以及内存映射和与其磁盘对应物的同步状态(例如,脏扇区)。
- _CMHIVE处理关于hive的更抽象信息,例如安全描述符缓存、指向高级内核对象(如根键控制块(KCB))的指针,以及相关的事务资源管理器(_CM_RM结构)。
接下来的小节将更深入地探讨这两个结构的职责和内部运作。
_HHIVE结构概述
_HHIVE结构的主要作用是管理hive的内存相关状态。这使得更高级别的注册表代码能够执行诸如分配、释放和将cells标记为“脏”的操作,而无需处理低级实现细节。_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 map遍历,以便将cell索引转换为hive映射内的相应地址。
很自然地会认为,如果攻击者通过缓冲区溢出或释放后使用条件破坏这些函数指针,它们可能对利用有用。在Windows 10及更早版本中确实如此,但在Windows 11中,这些调用现在已被去虚拟化,大多数调用站点直接引用HvpGetCellPaged / HvpGetCellFlat和HvpReleaseCellPaged / HvpReleaseCellFlat之一,而不引用指针。这对安全性非常有利,因为它完全消除了这些字段在任何攻击场景中的用处。
以下是Windows 10中GetCellRoutine调用的示例,在IDA Pro中反汇编:
以及Windows 11中的相同调用:
Hive加载失败信息
这是一个指向公共_HIVE_LOAD_FAILURE结构的指针,每次在加载hive时发生错误时,它都会作为第一个参数传递给SetFailureLocation函数。它有助于跟踪给定hive的哪些有效性检查失败,而无需跟踪整个加载过程。
基础块
指向hive头的副本,由_HBASE_BLOCK结构表示。
同步锁
有两个锁,用途如下:
- FlusherLock – 同步在cells内更改数据的客户端与刷新线程之间对hive的访问。
- WriterLock – 同步修改bin/cell布局的写入器之间对hive的访问。
它们官方类型为_CMSI_RW_LOCK,但归结为_EX_PUSH_LOCK,并且与标准内核API(如ExAcquirePushLockSharedEx)一起使用。
脏块信息
在偏移量0x58和0x84之间,_HHIVE存储了几个表示内存中和磁盘上hive实例之间同步状态的数据结构。
Hive标志
首先,在偏移量0x8C处有两个标志,指示hive映射是否是平坦的以及hive是否是只读的。其次,有一个32位的HiveFlags成员,存储进一步的标志,据我所知,这些标志未包含在任何公共Windows符号中。我设法逆向工程并推断出我观察到的常量的含义,得到以下枚举:
|
|
以下是每个标志的单行解释:
- HIVE_VOLATILE:hive仅存在于内存中;例如,为\Registry和\Registry\Machine\HARDWARE设置。
- HIVE_NOLAZYFLUSH:对hive的更改不会自动刷新到磁盘,需要手动刷新;例如,为\Registry\Machine\SAM设置。
- HIVE_PRELOADED:hive是默认的系统hive之一;例如,为\Registry\Machine\SOFTWARE、\Registry\Machine\SYSTEM等设置。
- HIVE_IS_UNLOADING:hive当前正在另一个线程中加载或卸载,在操作完成之前不应访问。
- HIVE_COMPLETE_UNLOAD_STARTED:hive的卸载过程已在CmpCompleteUnloadKey中开始。
- HIVE_ALL_REFS_DROPPED:通过KCBs对hive的所有引用都已删除。
- HIVE_ON_PRELOADED_LIST:hive通过PreloadedHiveList字段链接到链表中。
- HIVE_FILE_READ_ONLY:底层hive文件是只读的,不应修改;指示hive是使用REG_OPEN_READ_ONLY标志加载的。
- HIVE_SECTION_BACKED:hive使用section views在内存中映射。
- HIVE_DIFFERENCING:hive是一个差异hive(版本1.6,加载在\Registry\WC下)。
- HIVE_IMMUTABLE:hive是不可变的,无法修改;指示它是使用REG_IMMUTABLE标志加载的。
- HIVE_FILE_PAGES_MUST_BE_KEPT_LOCAL:内核始终维护hive每个页面的本地副本,要么通过将其锁定在物理内存中,要么通过CoW机制创建私有副本。
日志文件信息
在偏移量0xA4到0xCC之间,有许多与日志文件管理相关的字段,即伴随主hive文件在磁盘上的.LOG1/.LOG2文件。
Hive版本
Version字段存储hive的次要版本,理论上应该是3到6之间的整数。然而,如上一篇博客文章所述,可以通过将主版本指定为0和任何所需的次要版本,或者通过诱使内核从日志文件恢复hive头,并滥用HvAnalyzeLogFiles函数比HvpGetHiveHeader更宽松的事实,将其设置为任意32位值。尽管如此,我尚未发现此行为的任何安全影响。
视图映射
视图映射保存了关于hive如何在内存中映射的所有基本信息。注册表内存管理的具体实现多年来已经显著发展,其细节在连续的系统版本之间发生变化。在最新的版本中,视图映射由顶级_HVP_VIEW_MAP公共结构表示:
|
|
其各自字段的语义如下:
- SectionReference:包含对应于hive文件的内核模式句柄,通过CmSiCreateSectionForFile中的ZwCreateSection创建。
- StorageEndFileOffset:存储在任何给定时间可以通过文件支持的sections表示的hive的最大大小。初始设置为加载的hive的大小,对于可变(普通)hive,可以在运行时动态增加或减少。
- SectionEndFileOffset:表示加载时hive文件section的大小。在HvpViewMapStart中的第一次初始化之后永远不会修改,并且似乎主要用作防止将不可变hive文件扩展到其原始大小之外的保障。
- ProcessTuple:类型为_CMSI_PROCESS_TUPLE的结构,它标识hive的section views的主机进程。此字段当前始终指向全局CmpRegistryProcess对象,该对象对应于系统中托管所有hive映射的专用“注册表”进程。然而,如果Microsoft选择实现这样的功能,此字段可以实现在多个进程之间更细粒度的hive映射分离。
- Flags:表示与整个hive相关的内存管理标志集。这些标志没有公开记录;然而,通过逆向工程,我确定了它们的用途如下:
- VIEW_MAP_HIVE_FILE_IMMUTABLE (0x1):指示hive已作为不可变加载,意味着没有数据 ever 保存回底层hive文件。
- VIEW_MAP_MUST_BE_KEPT_LOCAL (0x2):指示所有hive数据必须持久存储在内存中,而不仅仅通过文件支持的sections可访问。这可能是为了保护免受涉及从远程网络共享加载的hive的双重获取条件的影响。
- VIEW_MAP_CONTAINS_LOCKED_PAGES (0x4):指示hive的一些页面当前使用ZwLockVirtualMemory锁定在物理内存中。
- ViewTree:这是视图树结构的根,包含内存中映射的每个连续section view的描述符。
总的来说,Windows中低级hive内存管理的实现比最初看起来必要的更复杂。这种复杂性源于内核需要优雅地处理各种角落案例和交互。例如,hive可能作为不可变加载,这表明hive可以在内存中操作,但更改不得刷新到磁盘。同时,系统必须支持从.LOG文件恢复数据,包括将hive扩展到其原始磁盘长度之外的可能性。在运行时,还必须能够高效地修改注册表数据,以及根据需要收缩和扩展它。更复杂的是,Windows根据文件的备份卷对锁定hive页面在内存中强制执行不同的规则,仔细平衡最佳内存使用和系统安全保证。这些以及许多其他因素共同导致了hive内存管理的复杂性。
为了更好地理解视图树的组织方式,我们首先分析hive映射代码的一般逻辑。
Hive映射逻辑
负责在内存中映射hive的主要内核函数是HvLoadHive。它实现了整体