Windows注册表探险 #2:功能简史
作者:Mateusz Jurczyk,Google Project Zero
在深入探讨注册表的底层安全特性之前,了解它在操作系统中的作用及其背后的历史背景非常重要。本质上,注册表是一个由命名的“键”和“值”组成的层次化数据库,Windows 和应用程序用它来存储各种设置和配置数据。它表现为一种树形结构,其中键可以有一个或多个子键,每个子键都恰好与一个父键相关联。此外,每个键还可以包含一个或多个值,这些值具有类型(整数、字符串、二进制数据块等),用于在注册表中存储实际数据。每个键都可以通过其名称及其所有祖先的名称(用特殊反斜杠字符 '' 分隔)来唯一标识,并以某个顶级键(HKEY_LOCAL_MACHINE、HKEY_USERS 等)的名称开头。例如,一个完整的注册表路径可能看起来像这样:HKEY_CURRENT_USER\Software\Microsoft\Windows。从高层次来看,这非常类似于文件系统的结构,其中顶级键相当于已挂载磁盘分区的根目录(例如 C:\),键相当于目录,值相当于文件。然而,一个重要的区别是,键是注册表中唯一可保护的对象类型,并且值在数据库中的作用远不如文件在文件系统中的作用重要。此外,注册表的特定子树以称为注册表配置单元(registry hive)的二进制文件形式存储在磁盘上,并且配置单元的挂载点不一定与顶级键一一对应(例如,C:\Windows\system32\config\SOFTWARE 配置单元挂载在 HKEY_LOCAL_MACHINE\Software 下,这是一个一级嵌套键)。
从根本上说,在注册表中只能执行少数几种基本操作。这些操作总结在下表中:
| 类别 | 操作 |
|---|---|
| 配置单元 | 加载配置单元 |
| 卸载配置单元 | |
| 将配置单元刷新到磁盘 | |
| 键 | 打开键 |
| 创建键 | |
| 删除键 | |
| 重命名键 | |
| 设置/查询键安全属性 | |
| 设置/查询键标志 | |
| 枚举子键 | |
| 键变更通知 | |
| 查询键路径 | |
| 查询打开的子键数量 | |
| 关闭键句柄 | |
| 值 | 设置值 |
| 删除值 | |
| 枚举值 | |
| 查询值数据 |
在我们深入探讨其中任何一项具体操作之前,让我们先追溯注册表的演变历程以及导致其当前状态的路径。
Windows 3.1
注册表最早于 1992 年发布的 Windows 3.1 中引入。它被设计为一个集中式配置存储库,旨在解决 MS-DOS 中基本文本配置文件(例如 config.sys)以及早期 Windows 版本中结构稍强的 .INI 文件的诸多缺点。但最初的注册表与我们今天所知的完全不同:只有一个顶级键(相当于 HKEY_CLASSES_ROOT)和一个配置单元(C:\windows\reg.dat),大小限制为 64 KB,采用自定义二进制格式,其魔数(magic bytes)为 "SHCC3.10"。当时没有值(数据直接分配给键),注册表仅用于 OLE/COM 和文件类型注册。这是第一个 Regedit.exe 在高级模式下启动时的样子:

运行在 Windows 3.1 上的第一个注册表编辑器
尽管有其局限性,Windows 3.1 的注册表是一个重要的里程碑,因为它确立了其层次结构等持久的概念,并为当今先进的注册表功能铺平了道路。
Windows NT 3.1、3.5 和 3.51
一年后的 1993 年,基于全新且更健壮的内核设计的新版本 Windows 发布:Windows NT 3.1。时至今日,最初的 NT 内核仍然是所有现代版本 Windows(包括 Windows 11)的基础——其注册表实现也是如此。与 Windows 3.1 相比,Windows NT 3.x 中最大的功能性注册表变化包括:
- 引入了许多新的顶级键(HKLM、HKCU、HKU),从而扩展了注册表预期存储信息的范围。
- 将单个 reg.dat 配置单元文件替换为多个独立的配置单元(位于 C:\winnt\system32\config 的 default、sam、security、software、system)。
- 引入了具有多种可能数据类型的命名值。
- 使注册表键可保护。
- 取消了 64 KB 注册表配置单元限制。
为了适应这些新功能,Windows 采用了一种名为 "regf" 的新型二进制格式,该格式专门设计用于支持扩展的功能。该格式的核心原则在 NT 3.x 版本系列中保持不变,但其内部持续演进,这体现在配置单元文件头中编码的版本号不断增加。具体来说,Windows NT 3.1 的预发布版本使用 regf v1.0,Windows NT 3.1 RTM 使用 regf v1.1,而 Windows NT 3.5 和 3.51 使用 regf v1.2。
最后,虽然 Regedit.exe 仍然是简单的“注册信息编辑器”,但新增了一个实用程序 RegEdt32.exe,它提供了更多选项,并且可以不受限制地访问系统注册表。尽管其外观过时,但其用户界面的结构开始类似于现代注册表的形态以及当今注册表编辑器背后的核心概念:

运行在 Windows NT 3.1 上的 RegEdt32.exe
值得注意的是,Windows NT 3.1 是第一个其部分代码至今仍在 Windows 11 中使用的系统。基于这一观察,我们现在可以自信地声称注册表代码库已有超过 30 年的历史。
Windows 95
不久之后,1995 年夏天,Windows 95 正式向公众发布。它迅速取得了巨大成功,主要归功于用户界面的创新——它是第一个具有任务栏、开始菜单以及我们现在与 Windows 相关联的整体外观和感觉的版本。然而,就注册表内部机制而言,它并不特别有趣。它延续了 Windows NT 3.x 开始的趋势,将注册表扩展为操作系统中更核心的部分,并借鉴了许多相同的高层概念。但是,由于它基于与 NT 完全不同的内核,底层的注册表实现也有所不同。所有的注册表数据通常只存储在两个文件中:C:\WINDOWS\System.dat 和 C:\WINDOWS\User.dat。它们采用另一种由 "CREG" 签名标识的二进制格式编码,该格式比 Win3.1 格式更强大,但不如 WinNT 的 regf(例如,它不支持安全描述符)。相同的格式后来被 9x 系列的后续系统继承,即 Windows 98 和 Me,但其遗产就此终结。据我所知,CREG 格式对 NT 系列中注册表的发展影响甚微,因此无需深入讨论其内部机制。
可以说,Windows 95 中与注册表相关且影响最持久的一件事是 Regedit.exe 在功能和视觉上的完全重新设计。它获得了浏览整个注册表树、读取现有值并创建新值、重命名键以及在键、值和数据中搜索文本字符串的能力。乍一看,它几乎与现代注册表编辑器完全相同,只是缺少一些选项,例如加载自定义配置单元或管理键安全属性。甚至程序图标也基本保持不变,对于许多高级用户来说,直到今天它仍然是 Windows 注册表的代名词:

运行在 Windows 95 上的重新设计的 Regedit.exe
Windows NT 4.0
1996 年 Windows NT 4.0 的首次亮相标志着注册表的另一个重要里程碑,但这次主要是在技术方面。在视觉方面,NT 4.0 采用了与 Windows 95 相同的图形界面,包括新的改进版 Regedit.exe。由于添加了 Regedit,Windows NT 4.0 现在包含了两个相互竞争的注册表编辑器:来自 Windows 95 的 Regedit 和来自 Windows NT 3.x 的 RegEdt32。它们共享一些重叠的功能(例如,手动遍历注册表和检查单个值的能力),但各自也提供了一些独特的功能:只有 Regedit 能够在值中搜索数据,而只有 RegEdt32 支持管理注册表键的安全属性。我怀疑,对于想要修改系统内部设置的用户来说,存在两个不同的工具一定很令人困惑:他们不仅需要理解注册表的结构以及如何导航它,还需要知道针对特定任务使用哪个工具。这两个实用程序都进入了 Windows 2000,但最终在 Windows XP 中合并为一个单一的 Regedit.exe 程序。RegEdt32.exe 仍然可以在现代 Windows 版本的 C:\Windows\system32 中找到,作为历史遗留物,但它目前所做的只是启动 Regedit.exe 然后终止。
如前所述,NT 4.0 中真正重要的变化发生在底层。在 NT 3.51 和 NT 4.0 发布之间,内核开发人员更新了 regf 格式的一些内部方面,以简化它并提高其效率。此外,引入了一种称为“快速叶子”(fast leaves)的新优化,它在子键列表中添加了特殊的四字节提示,以加速键查找。这些变化是实质性的且不向后兼容,因此版本必须再次增加,导致了 regf v1.3。这一点值得注意,因为 1.3 是最早被认为是现代版本并且至今仍受 Windows 10 和 11 支持的配置单元类型,尽管现在也存在高达 1.6 的更新格式版本。这意味着,人们可以从 Windows NT 4.0 系统上复制一个配置单元文件,在 Windows 11 的 Regedit 中加载、检查和修改它,然后再复制回去,这些步骤中的每一步都将毫无问题地工作。更重要的是,这种支持不仅仅是为了读取存档配置单元——在诸如 RegSaveKeyExA 这样的文档化 API 函数中,版本 1.3 由 REG_STANDARD_FORMAT 枚举表示,表明即使到今天它仍被认为是“标准”格式。事实上,Windows 11 中确实有一些核心系统配置单元,例如挂载在 HKEY_USERS<SID>_Classes 下的 UsrClass.dat,仍然以 regf v1.3 格式编码。因此,从这个意义上说,Windows NT 4.0 和 11,尽管发布相隔数十年,代表着截然不同的技术时代,却展现出一种根本的联系。
现代时期
基于 regf 配置单元格式和 Regedit 的图形界面在 1996 年至 2024 年间基本保持不变这一事实,人们可能会认为注册表的内部实现也没有太大变化。我们可以尝试通过一个小实验来证明或反驳这个假设,测量每个连续 Windows 版本中与注册表相关的代码量。为了确保方法一致并使调查与安全相关,我们将重点关注配置管理器(Configuration Manager)的内核模式部分,这部分在很大程度上构成了本地攻击面。这样的分析在技术上是可行的,甚至相对容易实现,因为:
- 所有与内核注册表相关的代码都被编译到单个可执行映像中:ntoskrnl.exe。
- 所有 NT 家族系统内核的调试符号(PDB/DBG 文件)都已由 Microsoft 公开提供,无论是通过 Microsoft 符号服务器、可从 Microsoft 网站下载的符号包,还是与系统安装介质捆绑的符号文件。
- 内核代码遵循一致的命名约定,所有与注册表相关的函数名都以 "Hv"(代表 Hive)、"Cm"(代表 Configuration Manager)或 "Vr"(可能代表 Virtualized Registry)开头,只有少数例外。
- 如今有一些非常好的逆向工程工具,可以帮助我们计算汇编指令的数量,甚至计算与注册表引擎对应的反编译类 C 源代码行数。
就我而言,我使用 IDA Pro 和 Hex-Rays 反编译了每个 NT 系列系统的整个内核,然后运行了一个后处理脚本来提取与注册表相关的函数。在计算行数并将其绘制在图表上之后,我们得到以下结果:

正如我们所见,代码库经历了巨大而稳定的增长,从 NT 4.0 的大约 10,000 行代码开始,增加到 Windows 11 的大约 100,000 行,增长了十倍。需要重申的是,这仅涵盖了注册表的内核部分,忽略了在用户模式库(如 advapi32.dll、KernelBase.dll 或 ntdll.dll)中找到的代码。此外,我预计反编译的代码比原始源代码更密集,因为它不包含任何注释或空白。考虑到所有这些因素,Microsoft 管理的注册表代码的总量可能比上面显示的数字大得多。
回到内核注册表代码,其随时间的扩展是巨大的,无论是绝对还是相对而言。但如果这些发展对普通用户来说是不可见的,那么所有这些新代码是做什么的呢?这些变化可以分为三大类:
- 优化:使注册表更高效的更改,例如在 regf v1.5 中引入“哈希叶子”(hash leaf)子键索引类型以使键查找更快,或者添加一个原生系统调用来就地重命名键,而无需对整个子树进行昂贵的复制+删除操作。
- 向后兼容性:旨在使遗留应用程序在现代系统上无缝运行的更改,例如注册表虚拟化。
- 新功能:为注册表添加新功能或使其适应新用例的更改。这些更改要么通过新的 API 提供(因此主要与软件开发人员相关),要么根本没有文档化,仅由 Windows 内部使用。示例包括支持大于 1 MB 的值、注册表回调、支持事务、应用程序配置单元 和差异配置单元。
有趣的是,最大的变化并非定期发生,而是集中在仅四个 Windows 版本中:NT 3.1–4.0、XP、Vista 和 10 周年更新(1607)。这在下面的时间线中有所说明:

这当然不是一个详尽的列表:它包含了我认为在安全审计期间最有趣的功能,但缺少与增量日志记录、配置单元文件在内存中管理和映射方式的改进,以及 Microsoft 多年来实施的许多其他优化、稳定性改进和重构相关的修改。但这表明注册表是 Windows 内核中一个高度复杂的部分,并且具有大量潜在的有趣深层漏洞等待被发现。
在下一篇文章中,我将分享在研究注册表过程中发现的一些有用的信息来源。其中一些可能比另一些更明显,但它们都极大地帮助我理解了该技术的某些方面,或者提供了我所缺少的必要背景。下次见!