Windows注册表探索之旅 #3:学习资源

访问原始链接 Google 翻译

作者:Mateusz Jurczyk,Google Project Zero

在着手研究一个新的漏洞研究目标时,尤其是闭源目标,我优先考虑尽可能多地收集相关信息。当目标是一个像 Windows 注册表这样古老且基础的子系统时,这就变得特别有趣。在这种情况下,有价值的信息碎片可能隐藏在被人遗忘的文档、绝版的书籍和尘封的开源代码中——每一份都可能提供拼图中关键的一块。发掘它们需要一些努力,但回报往往是巨大的。这些信息碎片可能包含关于软件某些部分如何实现以及为何如此实现的线索——是哪些设计决策导致了特定的结果等等。当看到全局时,就更容易推理软件的行为、理解原始开发者的意图,并思考可能的边界情况。在其他时候,如果别人已经投入了时间和精力,它只是简单地加速了逆向工程的过程,节省了用于推导某些逻辑部分的时间。

关于如何超越二进制文件并利用所有可用信息来源,Alex Ionescu 在 OffensiveCon 2019 的主题演讲 "Reversing Without Reversing" 中给出了一个很好的解释。我的注册表安全审计也确实涉及了大量动手的逆向工程,但它也大量补充了并非直接来自 ntoskrnl.exe 的信息。虽然 Alex 的演讲讨论了整体研究 Windows,但这篇博文提供了一个具体的案例研究,说明如何在实践中应用这些想法。本文的第二个目标是将所有收集到的材料整合成一个全面、易于未来研究者查阅的总结。完整的列表可能看起来有些庞大,因为它包含了一些重叠信息的引用,所以我发现关键的部分都用 🔑 符号标记了。我强烈建议查阅这些资源,因为它们提供了有助于理解未来文章的上下文。

Microsoft Learn

官方文档可能是处理新 API 时首先要研究的最直观的东西。对于微软来说,这意味着 Microsoft Learn(前身为 MSDN Library),这是一个为 Windows 软件开发人员维护的庞大技术信息库。它完全在线提供,并包含以下专门针对注册表的部分和文章:

博客和在线资源

由于注册表存储了大量用户活动的痕迹,它是取证调查中一个流行的信息来源。因此,多年来发表了许多文章和博客文章,重点关注内部配置单元结构、与注册表相关的内核对象以及恢复已删除的数据。以下是我在网上找到的非官方注册表资源列表,从最早到最新:

  • WinReg.txt,作者未知(署名为 B.D.)– 基于逆向工程的 Windows 3.x (SHCC3.10)、Windows 95 (CREG) 和 Windows NT (regf) 配置单元二进制格式的文档。它可能是第一份公开描述配置单元未记录结构的文章。
  • 安全帐户管理器,作者未知(署名为 [email protected])– 一篇主要关注 Windows 2000 和 XP 中用户管理内部原理的综合性文章,剖析了 SAM 组件使用的许多二进制结构。由于用户和凭据管理与注册表高度相关(所有身份验证数据都存储在那里),文章还包括一个“注册表结构”部分,解释了 regf 配置单元文件的编码。
  • 🔑 Windows 注册表文件格式规范,Maxim Suhanov – 一份高质量且相对最新的 regf 格式 1.3 到 1.6 版本的规范,并包含关于古老版本 1.1 和 1.2 的额外信息。
  • Windows NT 注册表文件 (REGF) 格式规范,Joachim Metz – 另一份与 libregf 库相关的独立开发的 regf 格式规范。
  • Push the Red Button,Brendan Dolan-Gavitt (moyix) – 一个专注于安全、逆向工程和取证的个人博客。它包含许多有趣的与注册表相关的帖子,可追溯到 2007-2009 年。
  • Windows 事件响应,Harlan Carvey – 一个专注于 Windows 事件响应和数字分析的技术博客,包含 2006 年至 2022 年间发表的涉及注册表的各种帖子。
  • 我的 DFIR 博客,Maxim Suhanov – 另一个专注于数字取证的博客,多次提及 Windows 注册表。它提供了一些其他地方难以找到的原始信息,例如参见Windows 中的容器化注册表配置单元。
  • 挖掘过去:Windows 注册表取证再探,David Via – Mandiant 的一篇博客文章,讨论从注册表配置单元和事务日志中恢复数据。
  • 创建注册表链接 和 注册表的奥秘,Pavel Yosifovich – 两篇博客文章,涵盖在注册表中创建符号链接及其整体内部结构。
  • Windows 注册表,维基百科贡献者 – 一如既往,维基百科没有让人失望,尽管文章包含的深度技术细节不多,但它有关于注册表历史、其高级设计以及在系统中作用的广泛章节。

此外,The Old New Thing 是一个极好的技术博客,探索 Windows 功能背后的怪癖、设计决策和历史背景。它由一位在微软工作超过 30 年的员工 Raymond Chen 撰写,每天发布一篇帖子,令人惊叹地保持了一致性。虽然这些博客文章在技术上不是文档,但它们在社区中备受推崇,可以被视为事实上的微软知识资源——只是比 Microsoft Learn 更有趣。在过去的 20 多年里,Raymond 有时会写关于注册表的文章,分享有关此功能的有趣幕后故事和轶事。我试图在下面的单一列表中查找并汇编所有相关的与注册表相关的帖子:

学术论文和演示文稿

在数字取证过程中从注册表中恢复有意义的痕迹也是学术界已知的问题。为了找到相关的工作,我通常首先在 Google Scholar 中输入几篇已知论文的标题,然后深入进行其参考文献的广度优先搜索。以下是我找到的与注册表相关的内容:

开源软件

套用一句名言,源代码胜过千言万语。有时,直接查看代码比阅读英文解释更容易掌握一个概念或设计。虽然注册表的规范实现是 Windows 内核,但多年来已经开发了许多开源项目来操作注册表配置单元。它们通常要么基于开发者自己进行的 regf 格式分析,要么基于现有的文档和其他开源工具。它们存在的三个主要原因是:a) 计算机取证,b) 在其他主机平台上模拟 Windows 行为,c) 直接访问 SAM 配置单元以更改/重置本地用户凭据。无论出于何种原因,此类项目可能有助于更好地理解内部配置单元格式,并在必要时帮助构建概念验证配置单元。我找到的所有相关开源库和实用程序列表如下所示:

  • libregf – 一个用 C 编写的库,带有 Python 绑定。
  • hivex – 一个用 C 编写的库,作为 libguestfs 项目的一部分,支持 OCaml、Perl、Python 和 Ruby 绑定。
  • cmlib – 一个用 C 实现的模块,作为 ReactOS 的一部分,与 Windows 的实现非常相似。
  • chntpw (离线 Windows 密码编辑器) – 一个在 1997 年至 2014 年间用 C 开发的工具,用于直接在 SAM 配置单元中离线管理 Windows 用户密码。与注册表相关的代码位于 ntreg.c (regf 解析器) 和 reged.c (基本注册表编辑器) 中。
  • Samba – Samba 项目包含了 Windows 注册表的另一个实现(位于 source3/registry 和 source4/lib/registry 下)。
  • regipy – 一个 Python 注册表配置单元解析库及配套工具。
  • yarp – 字面意思是又一个注册表解析器(用 Python 编写)。
  • Registry – 一个用 C# 编写的配置单元解析器。
  • nt-hive – 一个用 Rust 编写的配置单元解析器(具有只读功能)。
  • Notatin – 另一个用 Rust 编写的配置单元解析器,包括 Python 绑定和辅助二进制文件。

最后,在撰写本文时,只需在 GitHub 上搜索一些内部内核函数名称,就可能揭示 20 多年前 Windows 本身是如何实现某些功能的。

SDK 头文件

随软件开发工具包分发的头文件是一个有趣的情况,因为一方面它们是微软希望开发者使用的官方资源,但另一方面——它们有点隐蔽,因为在线文档并不总是与它们的内容保持同步。因此,我们可以探索它们在磁盘上的本地副本,有时会发现未在线公开记录的工件(函数声明、结构定义、注释)。一些与注册表最相关的头文件是:

  • winreg.h (用户模式) – 列表中的主要注册表头文件,包含来自官方注册表 API 的函数和结构原型。
  • wdm.h (内核模式) – 指定了许多由注册表系统调用接口使用的有趣常量/标志和类型,例如配置单元加载标志(NtLoadKey2 的第三个参数,如 REG_LOAD_HIVE_OPEN_HANDLE 等)或键/值查询结构(KEY_TRUST_INFORMATION、KEY_VALUE_LAYER_INFORMATION 等)。
  • ntddk.h (内核模式) – 包含一些其他地方找不到的类型,例如 KEY_LAYER_INFORMATION。
  • winnt.h (用户模式) – 大多与 wdm.h 等效。
  • winternl.h (用户模式) – 包含一些与注册表相关的系统调用声明(NtRenameKey、NtSetInformationKey)。

安全研究

在开始一个新项目时,了解先前的安全研究尤其有用。它不仅经常揭示目标的深层技术细节,而且来自志同道合的专业人士,他们通过安全视角查看代码,并可能激发关于进一步弱点或需要更多关注领域的想法。就注册表而言,我认为与其高复杂性和在 Windows 操作系统中扮演的关键角色相比,公开领域所做的工作相对较少。尽管如此,我还是发现了一些非常有见地的材料,尤其是来自 Project Zero 的同事 James Forshaw 的材料。我在这个主题上收集到的所有与安全相关的资源完整列表如下所示(包括一些对我过去发表作品的引用):

  • 近期 Windows 漏洞案例研究 (2010),Gynvael Coldwind,Mateusz Jurczyk – 关于 Gynvael 和我在 2009/2010 年短暂的注册表研究期间发现的几个安全漏洞的演示。
  • 微软内核整数溢出漏洞 (2016),Honggang Ren – 关于 CVE-2016-0070 的文章,这是一个在加载畸形配置单元文件时的 Windows 内核漏洞。
  • Project Zero 漏洞跟踪器 (2016),James Forshaw,Mateusz Jurczyk – 作为简单注册表配置单元 fuzzing 的结果提交给微软的四份漏洞报告。
  • Project Zero 漏洞跟踪器 (2014-2020),James Forshaw – James 发现的 17 个与注册表相关的漏洞,其中许多是注册表与其他系统机制(安全模拟、文件系统)交叉处的逻辑问题。

书籍

对于一个像注册表这样有 20 多年历史的代码库,可以预期早期一些涵盖它的资源是以纸质形式而非互联网形式发布的。因此,我标准流程的一部分是在 Google Books 中搜索与特定技术相关的各种技术术语和关键词,看看会出现什么。对于注册表,这些可能是例如 "regedit"、"regf"、"hbin"、"LOG1"、"RegCreateKey"、"NtCreateKey"、"HvAllocateCell"、"\Registry\Machine"、"key control block" 等等。在某些情况下,这会得到具有独特、严格技术信息的书籍,而在其他情况下,最有见地的部分是历史视角,能够看到该技术首次推出后不久是如何被看待的。有时,书籍的价值直到它到达邮箱才完全显现,因为它既没有作为电子书出售,也没有在 Google Books 中提供预览,因此需要实体副本。

我找到的完全或部分专门介绍 Windows 注册表的书籍如下(从最新到最旧):

专利

另一个可能难以找到的有用信息来源是专利,由 Google Patents 索引。我通过这种方式找到的一个特别有价值的结果是 🔑 容器化配置 (US20170279678A1),这是微软 2016 年的专利,它彻底解释了注册表中差异配置单元和分层键背后的核心概念。这些机制是 Windows 10 周年更新中引入的新功能的一部分,旨在更好地支持容器化,但关于其工作原理的任何官方文档都无处可寻。因此,该专利是理解这种新注册表功能复杂方面的绝佳辅助工具,增加了必要的上下文,并帮助理解原本高度隐晦的内核函数,如 CmpPrepareDiscardAndReplaceKcbAndUnbackedHigherLayers。

手动分析

到目前为止,我们讨论的所有资源都可以通过网页浏览器、文本编辑器或物理形式访问。但还有另一种类型的信息源,它同样重要(如果不是更重要的话),并且需要更专业的工具来理解它。我指的是我们可以从 Windows 中负责处理注册表的可执行映像中提取的知识,既包括“标准”的逆向工程,也包括充分利用其中或其周围的任何有用工件。我将在后续文章中更多地介绍动手逆向过程,现在我们将注意力转向那些无需推理就能向我们呈现清晰信息的工件。

顺便说一下,迄今为止最需要查看的文件是 ntoskrnl.exe,即核心 NT 内核映像。它包含了内核空间注册表实现的全部内容,无论是从安全角度还是功能角度来看都很有趣。我个人 99% 的手动分析时间都在查看那个特定的二进制文件,但值得注意的是,还有一些其他与注册表相关的可执行文件和库:

  • winload.exe – Windows 引导加载程序,在 Windows 内核之前执行。其职责之一是将 SYSTEM 配置单元加载到内存中并从中读取一些配置,因此它包含了 ntoskrnl.exe 中注册表代码的部分副本。
  • offreg.dll – 离线注册表库,也与内核共享一些注册表代码(但在用户模式下执行)。
  • kernelbase.dll – 主要的 WinAPI 库之一,实现了大部分用户空间的注册表 API。
  • ntdll.dll – 另一个核心用户模式库,在注册表 API 和内核注册表实现之间提供桥梁。
  • regsvc.dll – 实现远程注册表服务的 DLL。

让我们研究一下通过运行反汇编器/反编译器可以轻松获得哪些关于注册表的信息。我个人使用 IDA Pro + Hex-Rays,因此下面的示例基于它们。

🔑 公共符号 (PDB)

微软为 C:\Windows 中找到的大多数可执行映像提供公共符号,以方便开发人员和安全研究人员。所谓“公共”符号,我指的是主要包含二进制文件中函数名称的 PDB 文件,这些名称有助于在调试期间或在崩溃事件中符号化系统堆栈跟踪。过去,符号曾经捆绑在系统安装介质或单独的 Resource Kit 光盘上,后来以预打包存档的形式从微软网站下载。这两种渠道都已弃用,目前唯一支持的获取符号的方式是按文件从 Microsoft 符号服务器 获取。PDB 文件可以使用官方的 SymChk 工具直接下载,也可以通过支持符号服务器的软件(例如 IDA Pro、WinDbg)间接下载。

就 ntoskrnl.exe 而言,其附带的符号是最宝贵的信息来源之一。正如之前的文章所述,Windows 内核遵循一致的命名约定,因此我们可以立即看到哪些内部例程与注册表相关,以及我们可能开始分析的入口点(与注册表相关的系统调用处理程序)在哪里。它向我们展示了我们正在处理的代码范围(1000 多个注册表函数),并使得可以执行诸如博客文章 #2 中所示的分析(统计每个系统版本的行数),而无需进行任何逆向工程工作。也许最重要的是,函数名称在进行实际逆向时大大简化了代码推理,特别是对于具有非常描述性名称的函数,如 CmpCheckAndFixSecurityCellsRefcount 或 CmpPromoteSingleKeyFromParentKcbAndChildKeyNode。

显示名称和地址的截图

我们可以在内核调试符号中找到的另一种信息是类型:枚举、结构和联合。然而,有两个注意事项。首先,只有一些类型包含在 PDB 中,并且不清楚微软使用什么标准来决定是否发布它们。我粗略估计大约 50% 的注册表类型可以在那里找到,主要是基础类型。其次,即使某些类型的原型在符号中,函数参数和局部变量也没有用它们的类型进行注释,因此仍然需要确定相应的类型并手动注释变量,以便反编译的输出有意义。尽管如此,能够访问这些信息对于在局部层面理解代码以及把握大局仍然是一个巨大的帮助。

可以在公共符号中找到的结构有:

  • 配置单元描述符(HHIVE、CMHIVE)及相关结构
  • 配置单元 bin 和 cell 结构(HBIN、CM_KEY_NODE、CM_KEY_VALUE、CM_KEY_SECURITY、...)
  • 键对象相关结构(CM_KEY_BODY、CM_KEY_CONTROL_BLOCK、...)
  • 一些事务相关结构(CM_TRANS、CM_KCB_UOW、...)
  • 一些分层键相关结构(CM_KCB_LAYER_INFO、...)

同时,缺失且需要手动重建的结构有:

  • 解析上下文和路径信息结构(如 CmpParseKey 所用)
  • 一些事务相关结构(磁盘上的事务日志记录、轻量级事务对象描述符、...)
  • 虚拟化相关结构
  • 大多数分层键相关结构

大多数相关的类型名称以“CM”开头,因此很容易在 IDA 的 Local Types 窗口中找到它们:

IDA 截图显示以 CM 开头的 Local Types

我想借此机会感谢微软提供符号供下载,并鼓励其他供应商也为其产品这样做。 🙂

Windows 的调试/Checked 版本

微软过去曾发布 Windows 的调试/checked 版本(除了“free”版本),从 Windows NT 到早期的 Windows 10。它们之间的区别在于,调试/checked 版本禁用了某些编译器优化,并启用了额外的调试检查,以尽早识别内部系统状态不一致。内核模式驱动程序的开发人员被鼓励在将其视为稳定并交付给客户之前,在调试/checked Windows 版本上进行测试。不幸的是,这些特殊版本已经停产,不再适用于最新的 Windows 10 和 11。

这些旧版本在逆向工程背景下可能非常有价值,因为额外的检查可能揭示代码所做的一些不变量和假设,但在查看零售版本时并不明显。更重要的是,这些检查通常是详细的,并包括调用诸如 RtlAssert、DbgPrint、DbgPrintEx 等函数,传递失败断言的文本表示、源代码文件名和/或行号。这些可能泄露变量、结构成员、枚举、常量和其他类型信息的名称。让我们看一些例子:

DbgPrintEx(DPFLTR_CONFIG_ID, 24u, "\tImplausible size %lx\n", v13);
DbgPrintEx(DPFLTR_CONFIG_ID, 24u, "\tKey is bigger than containing cell.\n");
DbgPrintEx(DPFLTR_CONFIG_ID, 0, "invalid name starting with NULL on key %08lx\n", a3);
DbgPrintEx(DPFLTR_CONFIG_ID, 0, "invalid (ODD) name length on key %08lx\n", a3);
DbgPrintEx(DPFLTR_CONFIG_ID, 24u, "\tNo key signature\n");
DbgPrintEx(DPFLTR_CONFIG_ID, 0, "\tData:%08lx - unallocated Data\n", v20);
DbgPrintEx(DPFLTR_CONFIG_ID, 24u, "Class:%08lx - Implausible size \n", v20);
DbgPrintEx(DPFLTR_CONFIG_ID, 24u, "SecurityCell is HCELL_NIL for (%p,%08lx) !!!\n", a1, v67);
DbgPrintEx(DPFLTR_CONFIG_ID, 24u, "SecurityCell %08lx bad security for (%p,%08lx) !!!\n", v86, a1, v73);
DbgPrintEx(DPFLTR_CONFIG_ID, 0, "Root cell cannot be a symlink or predefined handle\n");
DbgPrintEx(DPFLTR_CONFIG_ID, 0, "invalid flags on root key %lx\n", v31);
DbgPrintEx(DPFLTR_CONFIG_ID, 24u, "\tWrong parent value.\n");

CmpCheckKey 函数负责验证新加载的配置单元中每个键的结构正确性,对于遇到的每个问题,它都会打印一条或多或少的详细消息。这可以帮助我们更好地理解这些检查各自旨在完成什么。

DbgPrintEx(DPFLTR_CONFIG_ID, 0, "CmKCBToVirtualPath ==> Could not get name even from parent KCB = %p!!!!\n", a1);

这条消息可以解释为某种回退机制在转换注册表路径时失败。这可能表明一个有趣/脆弱的代码结构,事实上,周围的代码确实受到了 16 位整数溢出和由此产生的池内存损坏的影响(在 Project Zero issue #2341 中报告)。因此,整个代码块(包括漏洞)被移除,因为它在功能上是冗余的,没有任何实际用途。

RtlAssert("(*VirtContext) & CMP_VIRT_IDENTITY_RESTRICTED", "minkernel\\ntos\\config\\cmveng.c", 3554u, 0i64);

CmpIsSystemEntity 中的这行代码揭示了几条信息:函数参数名称(VirtContext)、一个未在任何其他资源中记录的标志的内部名称(CMP_VIRT_IDENTITY_RESTRICTED),以及表达式的源文件名和行号(minkernel\ntos\config\cmveng.c:3554)。这些信息可以移植到我们的主反汇编器数据库(如 .idb)中,并在以后帮助我们更好地理解使用相同对象/标志的其他代码区域。

DbgPrintEx(DPFLTR_CONFIG_ID, 22u, "Error[1] %lx while processing CmLogRecActionDeleteKey\n", v12);

CmpDoReDoRecord 中的这个和类似调用告知我们事务记录类型的内部名称(CmLogRecActionCreateKey、CmLogRecActionDeleteKey 等),这些名称同样没有在其他任何地方公开提及。

调试和实验

探测正在运行的 Windows 系统的注册表是我想到的最后一种了解它的方式。在某种意义上,这是一个必要的步骤,因为我们只能通过阅读静态文档和代码走这么远。在某个时刻,我们将被迫调查与配置单元对应的真实内存映射,探索内存中注册表对象的内容,或者验证特定函数的行为是否符合我们的预期。幸运的是,有一些工具可以让我们窥视内部注册表状态,超越了 Regedit 等标准实用程序允许的范围。它们在下面的章节中简要描述。

扩展的 Regedit 替代品

内置的 Regedit.exe 实用程序功能相当基础,虽然它足以满足大多数修补和系统管理目的,但一些第三方开发人员创建了具有扩展选项集的自定义注册表编辑器。我个人没有使用过它们,所以我无法证明它们的质量,但它们可能为其他研究人员提供一些好处。一个例子是 Total Registry,其主要优点是除了标准的带有五个