CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go系统接口编程
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go系统接口编程
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • Windows 10系统编程 引言
  • 第1章:基础
  • 第2章:对象和句柄
  • 第3章:进程
  • 第4章:作业(Jobs)
  • 第5章:线程基础
  • 第6章:线程调度
  • 第7章:进程内线程同步
  • 第8章:进程间线程同步
  • 第9章:线程池
  • 第10章:高级线程
  • 第11章:文件和设备输入输出
  • 第12章:内存管理基础
  • 第13章:内存操作
  • 第14章:内存映射文件
  • 第15章:动态链接库
  • 第16章:安全性
  • 第17章:注册表
    • 注册表项
      • HKEYLOCALMACHINE
      • HKEY_USERS
      • HKEYCURRENTUSER (HKCU)
      • HKEYCLASSESROOT (HKCR)
      • HKEYCURRENTCONFIG (HKCC)
      • HKEYPERFORMANCEDATA
    • 32位特定的注册表项
    • 操作键和值
      • 读取值
      • 写入值
      • 删除键和值
      • 创建注册表链接
      • 枚举键和值
    • 注册表通知
    • 事务性注册表
    • 远程注册表
    • 其他注册表函数
    • 总结
目录

第17章:注册表

# 第17章:注册表

Windows注册表自Windows NT诞生之初便是其基础组成部分。它是一个分层数据库,存储着与系统及其用户相关的信息。其中部分数据是易失性的,即这些数据在系统运行时生成,系统关闭时便会被删除。而另一方面,非易失性数据则会持久保存在文件中。

Windows有多个用于查看和操作注册表的内置工具。主要的图形用户界面(GUI)工具是Regedit.exe,经典的命令行工具则是reg.exe。此外,还可以使用PowerShell进行批处理或命令行风格的操作。

使用reg.exe和PowerShell不在本章讨论范围内。

过去,软件开发人员常常大量使用注册表来存储应用程序的各类信息,在一定程度上,现在依然如此。不过,建议不要使用注册表来存储应用程序或与用户相关的数据。相反,应用程序应将信息存储在文件系统中,通常采用诸如INI、XML、JSON或YAML等方便的格式(仅列举一些常见格式)。注册表应当只供Windows使用。话虽如此,如果信息较少,有时将其存储在注册表中会很便捷。一种常见的技巧是在注册表值中存储文件路径,这样大部分信息存储在文件中,注册表仅起到指向该文件的作用。

在本章中,我们将探讨注册表最重要的部分,以及如何通过编程对其进行操作。

  • 注册表项(The Hives)
  • 32位特定的注册表项
  • 操作键和值(Working with Keys and Values)
  • 注册表通知(Registry Notifications)
  • 事务性注册表(Transactional Registry)
  • 其他注册表函数(Miscellaneous Registry Functions)

# 注册表项

注册表被划分为多个注册表项(hive),每个注册表项都公开特定的信息。尽管RegEdit.exe显示有5个注册表项,但实际上只有两个“真正的”注册表项,即HKEY_USERS和HKEY_LOCAL_MACHINE。其他所有注册表项都是由这两个“真正的”注册表项中的数据组合而成的。图17-1展示了Regedit.exe中的注册表项。

img

图17-1:注册表项

以下各节将对这些注册表项进行简要介绍。

# HKEY_LOCAL_MACHINE

这个注册表项存储与计算机相关的信息,与任何特定用户无关。其中许多数据对于系统正常启动至关重要,因此在进行任何更改时都必须格外小心。默认情况下,只有管理员级别的用户才能对这个注册表项进行更改。以下是一些重要的子键:

  • SOFTWARE:已安装的应用程序通常在此存储与用户无关的信息。常见的子键模式是SOFTWARE[公司名称][产品名称][版本](“版本”部分并非总是使用),后面可能还会有更多子键。例如,Microsoft Office将其与计算机相关的信息存储在SOFTWARE\Microsoft\Office,部分信息存储在版本子键中。
  • SYSTEM:大多数系统参数存储在此处,各种系统组件在启动时会从此处读取信息。以下是一些对开发人员有用的子键示例:
    • SYSTEM\CurrentControlSet\Services:存储系统上安装的服务和设备驱动程序的信息。我们将在第19章(“服务”)中更深入地研究这个键。
    • SYSTEM\CurrentControlSet\Enum:此子键是硬件设备驱动程序的父键。
    • SYSTEM\CurrentControlSet\Control:此子键是许多系统组件会查看的设置的父键,这些组件包括内核本身、会话管理器(Smss.exe)、Win32子系统进程(csrss.exe)、服务控制管理器(Services.exe)等。
    • SYSTEM\BCD00000000:存储启动配置数据(Boot Configuration Data,BCD)信息。
    • SYSTEM\SECURITY:存储本地安全策略的信息(默认情况下,管理员无法访问此键,但SYSTEM账户可以访问)。
    • SYSTEM\SAM:存储本地用户和组的信息(与上述键的访问限制相同)。

你可以使用(例如)PsExec Sysinternals工具,以SYSTEM账户运行Regedit.exe来查看SAM和SECURITY子键,命令如下:psexec -s -i -d regedit。另一种方法是行使“取得所有权”特权并更改这些键的自由访问控制列表(Discretionary Access Control List,DACL)以允许管理员访问(见第16章),但不建议这样做。

子键SYSTEM\CurrentControlSet是指向SYSTEM\ControlSet001子键的链接。这种间接引用的原因与Windows NT的一个旧功能“最后一次正确配置(Last Known Good)”有关。在某些情况下,可能会有多个“控制集”。子键SYSTEM\Select保存着指示哪个是“当前”控制集的值。

关于“最后一次正确配置”的一些详细信息在第19章中讨论。

此注册表项中的信息大多持久存储在%SystemRoot%\System32\Config目录下的文件中。表17-1列出了子键及其对应的存储文件(这些文件的格式未公开)。 表17-1:一些注册表项的备份文件

子键 文件名
HKEY_LOCAL_MACHINE\SAM SAM
HKEY_LOCAL_MACHINE\Security SECURITY
HKEY_LOCAL_MACHINE\Software SOFTWARE
HKEY_LOCAL_MACHINE\System SYSTEM

完整的注册表项及其存储文件列表可以在注册表中HKLM\System\CurrentControlSet\Control\hivelist键下找到。

# HKEY_USERS

HKEY_USERS注册表项存储了曾经在本地系统上登录过的每个用户的所有用户特定信息。图17-2展示了这样一个注册表项的示例。每个用户都由其安全标识符(Security Identifier,SID,作为字符串)表示。

img

图17-2:HKEY_USERS注册表项

.DEFAULT子键存储新创建用户获取的默认值。接下来的三个短SID值在第16章中应该很熟悉,它们分别对应SYSTEM、本地服务(Local Service)和网络服务(Network Service)账户。然后,那个看起来很长且类似随机的SID代表一个“普通”用户。第二个看起来与上述类似且后缀为“_Classes”的SID与HKEY_CLASSES_ROOT注册表项相关,后续章节将对此进行描述。

如果你打开其中一个SID子键,会发现各种与桌面、控制台、环境变量、颜色、键盘、打印机等相关的用户特定设置。Windows资源管理器等各种组件会读取这些设置,根据用户的需求定制环境。

# HKEY_CURRENT_USER (HKCU)

HKEY_CURRENT_USER注册表项是指向当前运行RegiEdit.exe的用户的链接,显示的是HKEY_USERS中该用户的相同信息。此注册表项中的数据持久存储在用户目录(例如c:\users\username)下一个名为NtUser.dat的隐藏文件中。

# HKEY_CLASSES_ROOT (HKCR)

这是一个相当特别的注册表项,它由现有键构建而成,组合了以下内容:

  • HKEY_LOCAL_MACHINE\Software\Classes
  • HKEY_CURRENT_USER\Software\Classes(即HKEY_USERS{用户SID}_Classes)

如果存在冲突,HKEY_CURRENT_USER的设置会覆盖HKEY_LOCAL_MACHINE的设置,因为用户的选择优先级应高于计算机的默认设置。HKEY_CLASSES_ROOT包含两类信息:

  • 资源管理器外壳数据:文件类型和关联,以及外壳扩展信息。
  • 组件对象模型(Component Object Model,COM)相关信息。

资源管理器外壳信息包括文件类型和关联操作。例如,在HKEY_CLASSES_ROOT中搜索.txt会找到一个键,其默认值为txtfile。查找txtfile键会定位到shell\open\command子键,其默认值为%SystemRoot%\System32\NOTEPAD.EXE %1,这清楚地表明记事本(Notepad)是打开txt文件的默认应用程序。

其他与外壳相关的键包括资源管理器外壳支持的各种外壳扩展,如自定义图标、自定义上下文菜单、项目预览,甚至是功能完备的外壳扩展(本书不涉及外壳自定义内容)。

HKEY_CLASSES_ROOT中更重要的基本信息与COM注册有关。这些细节很重要,将在第21章中详细讨论。

# HKEY_CURRENT_CONFIG (HKCC)

这个注册表项只是指向HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current的链接。在本书的内容中,它大多不太重要。

# HKEY_PERFORMANCE_DATA

这个注册表项在Regedit.exe中不可见,这是有充分理由的。此注册表项是用于使用性能计数器(在第20章中讨论)的旧机制。从Windows 2000开始,有了一个用于处理性能计数器的新应用程序编程接口(API),相比使用HKEY_PERFORMANCE_DATA的注册表API,它更受青睐。

# 32位特定的注册表项

在64位系统上,注册表的某些部分对于32位和64位应该有不同的键。例如,应用程序安装信息通常存储在HKLM\Software{公司名称}{应用名称}中。

有些应用程序可以在同一系统上同时安装32位和64位版本。在这种情况下,必须有办法区分32位设置和64位设置。

打开上述键的32位进程会收到另一个以HKLM\Software\Wow6432N开头的键。

这种重定向是透明的,应用程序无需进行任何特殊操作。64位进程看到的注册表是其实际状态,不会发生这种重定向。

这种重定向通常称为注册表虚拟化(Registry Virtualization)。

HKLM\Software并非32位进程中唯一会进行重定向的键。一些与COM相关的信息也会从HKCR重定向到HKCR\Wow3264Node。以下是一些会被重定向的子键示例:

  • HKCR\CLSID:所有进程内(DLL)COM组件(见第21章)的此键会被重定向到HKCR\Wow6432Node\CLSID。
  • HKCR\AppID、HKCR\Interface和HKCR\TypeLib也会类似地重定向到Wow64子键。

32位进程会自动进行这种重定向,但这些进程可以通过在RegCreateKeyEx和RegOpenKeyEx函数(见下一节)中指定KEY_WOW64_64KEY访问标志来选择不进行重定向。也存在相反的标志(KEY_WOW64_32KEY),用于允许64位进程在键名中不指定Wow6432Node的情况下访问32位注册表部分。

# 操作键和值

打开现有注册表键可通过RegOpenKeyEx函数实现:

LSTATUS RegOpenKeyEx(
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpSubKey,
    _In_opt_ DWORD ulOptions,
    _In_ REGSAM samDesired,
    _Out_ PHKEY phkResult);
1
2
3
4
5
6

与大多数其他应用程序编程接口(API)相比,第一个明显的不同是返回值。它是一个32位有符号整数,返回操作的错误代码。这与其他返回BOOL、HANDLE等类型的API中使用的GetLastError返回的值相同。这意味着成功时返回ERROR_SUCCESS(即0);此外,调用GetLastError没有意义,实际上应该避免调用,因为注册表API调用不会改变它的值。

现在来看看这个函数的具体参数。hKey是用于解释lpSubKey的基键。它可以是预定义键之一(如HKEY_LOCAL_MACHINE、HKEY_CURRENT_USER等),也可以是之前注册表API调用返回的键句柄。顺便说一下,子键是不区分大小写的。

ulOptions可以为零或REG_OPTION_OPEN_LINK。在后一种情况下,如果键是一个链接(指向另一个键),则打开链接键本身,而不是链接的目标。通常情况下指定为零。

samDesired是打开键所需的访问掩码。如果无法授予所需的访问权限,调用将失败并返回“访问被拒绝”错误代码(5)。常见的访问掩码包括用于所有查询/枚举操作的KEY_READ,以及用于写入/修改值和创建子键操作的KEY_WRITE。正如上一节所述,这里也可以指定32位或64位注册表视图。你可以在文档中找到完整的注册表键访问掩码列表。

最后,phkResult是调用成功时返回的键句柄。请注意,注册表键有其自己的类型(HKEY),它与其他HANDLE类型没有区别(都是不透明的void*),但注册表键有自己的关闭函数:

LSTATUS RegCloseKey(_In_ HKEY hKey);
1

你可能想知道为什么注册表键句柄是“特殊的”。其中一个原因是注册表键可能会打开到另一台计算机的注册表(如果启用了远程注册表服务),这意味着某些关闭操作可能涉及与远程系统的通信。

非扩展版本的函数(RegOpenKey)仍然存在并受支持,主要是为了与16位Windows兼容。没有充分的理由使用这个(以及其他类似的)函数。

RegOpenKeyEx用于打开现有键,如果键不存在则调用失败。要创建新键,需调用RegCreateKeyEx函数:

LSTATUS RegCreateKeyEx(
    _In_ HKEY hKey,
    _In_ LPCTSTR lpSubKey,
    _Reserved_ DWORD Reserved,
    _In_opt_ LPTSTR lpClass,
    _In_ DWORD dwOptions,
    _In_ REGSAM samDesired,
    _In_opt_ CONST LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _Out_ PHKEY phkResult,
    _Out_opt_ LPDWORD lpdwDisposition);
1
2
3
4
5
6
7
8
9
10

hKey是起始基键,它可以是之前调用RegCreateKeyEx/RegOpenKeyEx获得的值,也可以是标准预定义键之一。调用者创建新子键的能力取决于键hKey的安全描述符,而不是打开hKey时使用的访问掩码。lpSubKey是要创建的子键。它必须位于hKey之下(直接或间接),这意味着子键可以有多个用反斜杠分隔的子键。如果调用成功,该函数会创建所有中间子键。

Reserved应设为零,lpClass应设为NULL,这两个参数没有实际作用。dwOptions通常为零(相当于REG_OPTION_NON_VOLATILE)。此值表示一个非易失性键,在保存注册表项键时会被保存。此外,还可以指定以下值的组合:

  • REG_OPTION_VOLATILE:与REG_OPTION_NON_VOLATILE相反,该键被创建为易失性的,意味着它存储在内存中,但在卸载注册表项键时会被丢弃。
  • REG_OPTION_CREATE_LINK:创建一个符号链接键,而不是一个“真正的”键(见本章后面的“创建注册表链接”部分)。
  • REG_OPTION_BACKUP_RESTORE:该函数会忽略samDesired参数,如果调用者的令牌(token)中具有SeBackupPrivilege权限,则使用ACCESS_SYSTEM_SECURITY和KEY_READ权限创建/打开键。如果调用者的令牌中具有SeRestorePrivilege权限,则授予ACCESS_SYSTEM_SECURITY、DELETE和KEY_WRITE权限。如果同时具备这两个权限,那么最终的访问权限是两者的并集,实际上就是授予对该键的完全访问权限。

samDesired是调用者请求的常规访问掩码。lpSecurityAttributes是我们熟悉的常规SECURITY_ATTRIBUTES结构体。phkResult是操作成功时返回的键。最后,最后一个(可选)参数lpdwDisposition返回键是实际创建的(REG_CREATED_NEW_KEY)还是打开了现有键(REG_OPENED_EXISTING_KEY)。新创建的键没有值。

# 读取值

使用一个已打开的键(无论是创建的还是打开的),可以进行多种操作。最基本的操作是读取和写入值。可以使用RegQueryValueEx函数来读取值:

LSTATUS RegQueryValueEx(
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpValueName,
    _Reserved_ LPDWORD lpReserved,
    _Out_opt_ LPDWORD lpType,
    _Out_ LPBYTE lpData,
    _Inout_opt_ LPDWORD lpcbData
);
1
2
3
4
5
6
7
8

hKey是要从中读取值的键,它可以是之前打开的键,也可以是预定义键之一(包括不太常见的键,如HKEY_PERFORMANCE_DATA)。lpValueName是要查询的值的名称。如果它为NULL或空字符串,则检索该键的默认值(如果有)。

lpReserved如其名,应设置为NULL。lpType是一个可选指针,用于返回返回数据的类型,其值为表17 - 2中所示的值之一。

表17 - 2:注册表值类型

值 描述
REG_NONE (0) 无值类型
REG_SZ (1) 以NULL结尾的Unicode字符串
REG_EXPAND_SZ (2) 以NULL结尾的Unicode字符串(可能包含未展开的环境变量,用%%表示)
REG_BINARY (3) 二进制(任意)数据
REG_DWORD (4) 32位数字(小端序)
REG_DWORD_LITTLE_ENDIAN (4) 与上述相同
REG_DWORD_BIG_ENDIAN (5) 32位数字(大端序)
REG_LINK (6) 符号链接(Unicode)
REG_MULTI_SZ (7) 由NULL分隔的多个Unicode字符串,第二个NULL表示结束
REG_RESOURCE_LIST (8) CM_RESOURCE_LIST结构(仅在内核模式下有用)
REG_FULL_RESOURCE_DESCRIPTOR (9) CM_FULL_RESOURCE_DESCRIPTOR(仅在内核模式下有用)
REG_RESOURCE_REQUIREMENTS_LIST (10) 仅在内核模式下有用
REG_QWORD (11) 64位数字(小端序)
REG_QWORD_LITTLE_ENDIAN (11) 与上述相同

如果调用者对该信息不感兴趣,可以将lpType指定为NULL。当调用者知道预期结果时,通常会这样做。lpData是调用者分配的用于存储数据本身的缓冲区。对于某些值类型,大小是固定的(例如REG_DWORD为4字节),但其他类型是动态的(例如REG_SZ、REG_BINARY),这意味着调用者需要分配足够大的缓冲区,否则只会复制部分数据,并且函数会返回ERROR_MORE_DATA。调用者缓冲区的大小由最后一个参数指定。在输入时,它应包含调用者缓冲区的大小。在输出时,它包含写入的字节数。如果调用者需要数据大小,可以将lpData设置为NULL,并在lpcbData中获取大小。

以下示例展示了如何读取HKCU\Console\FaceName处的字符串值(省略错误处理):

HKEY hKey;
::RegOpenKeyEx(HKEY_CURRENT_USER, L"Console", 0, KEY_READ, &hKey);
DWORD type;
DWORD size;
// 首次调用以获取大小
::RegQueryValueEx(hKey, L"FaceName", nullptr, &type, nullptr, &size);
assert(type == REG_SZ);
// 返回的大小包括NULL终止符
auto value = std::make_unique<BYTE[]>(size);
::RegQueryValueEx(hKey, L"FaceName", nullptr, &type, value.get(), &size);
::RegCloseKey(hKey);
printf("Value: %ws\n", (PCWSTR)value.get());
1
2
3
4
5
6
7
8
9
10
11
12

还有另一个函数RegGetValue可用于检索值:

LSTATUS RegGetValue(
    _In_ HKEY hkey,
    _In_opt_ LPCSTR lpSubKey,
    _In_opt_ LPCSTR lpValue,
    _In_ DWORD dwFlags,
    _Out_opt_ LPDWORD pdwType,
    _Out_ PVOID pvData,
    _Inout_opt_ LPDWORD pcbData
);
1
2
3
4
5
6
7
8
9

该函数与RegQueryValueEx类似,但通过dwFlags参数增加了一个不错的选项,即可以限制可能返回的值类型。这使得调用者在预期的值不是期望的类型时能够得到失败结果(并且节省了检索数据所需的时间)。dwFlags的值可以是表17-3中所示值的组合。

表17-3:RegGetValue的标志

值 描述
RRF_RT_REG_NONE (1) 允许REG_NONE类型
RRF_RT_REG_SZ (2) 允许REG_SZ类型
RRF_RT_REG_EXPAND_SZ (4) 允许REG_EXPAND_SZ类型。除非指定了RRF_NOEXPAND标志,否则展开环境变量
RRF_RT_REG_BINARY (8) 允许REG_BINARY类型
RRF_RT_REG_DWORD (0x10) 允许REG_DWORD类型
RRF_RT_REG_MULTI_SZ (0x20) 允许REG_MULTI_SZ类型
RRF_RT_REG_QWORD (0x40) 允许REG_QWORD类型
RRF_RT_DWORD
RRF_RT_REG_BINARY | RRF_RT_REG_DWORD
组合标志
RRF_RT_QWORD
RRF_RT_REG_BINARY | RRF_RT_REG_QWORD
组合标志
RRF_RT_ANY (0x0000ffff) 无类型限制
RRF_SUBKEY_WOW6464KEY (0x10000)(Win 10+) 打开64位键(如果子键不为NULL)
RRF_SUBKEY_WOW6432KEY (0x20000)(Win 10+) 打开32位键(如果子键不为NULL)
RRF_NOEXPAND (0x10000000) 不展开REG_EXPAND_SZ结果
RRF_ZEROONFAILURE (0x20000000) 在失败时用零填充缓冲区

# 写入值

要将值写入注册表键,可调用RegSetValueEx函数:

LSTATUS RegSetValueEx(
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpValueName,
    _Reserved_ DWORD Reserved,
    _In_ DWORD dwType,
    _In_ CONST BYTE* lpData,
    _In_ DWORD cbData
);
1
2
3
4
5
6
7
8

此时,大多数参数应该很容易理解。hKey是要写入的键,它必须至少具有KEY_SET_VALUE访问掩码(通常,该键是使用包含KEY_SET_VALUE的KEY_WRITE访问掩码打开的)。lpValueName是要设置的值的名称,并且不区分大小写。如果此名称为NULL或空字符串,则设置默认值(可以是任何类型)。Reserved参数必须为零。

数据类型由dwType参数指定,并且必须是表17 - 2中的值之一(有关REG_LINK的特殊情况,请参阅“创建注册表链接”部分)。数据本身由一个通用指针(lpData)和大小组成。数据必须与指定的类型相匹配。有些类型具有固定大小(例如REG_DWORD),而其他类型可以是任意长度(例如REG_SZ、REG_BINARY、REG_MULTI_SZ)。确保字符串(REG_SZ)以NULL结尾,MULTI_SZ以两个NULL结尾。注意,指定的大小(cbData)始终以字节为单位,无论值的类型如何。对于字符串,此大小必须包括终止NULL字符。

如果指定的值已经存在,则会用新值覆盖它。

以下示例将HKEY_CURRENT_USER\Console处的FaceName值更改为“Arial”(省略错误处理):

HKEY hKey;
::RegOpenKeyEx(HKEY_CURRENT_USER, L"Console", 0, KEY_WRITE, &hKey);
WCHAR value[] = L"Arial";
::RegSetValueEx(hKey, L"FaceName", 0, REG_SZ, (const BYTE*)value, sizeof(value));
::RegCloseKey(hKey);
1
2
3
4
5

还有一个替代函数RegSetKeyValue可用于相同目的:

LSTATUS RegSetKeyValue(
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpSubKey,
    _In_opt_ LPCTSTR lpValueName,
    _In_ DWORD dwType,
    _In_reads_bytes_opt_(cbData) LPCVOID lpData,
    _In_ DWORD cbData
);
1
2
3
4
5
6
7
8

该函数与RegSetValueEx几乎相同。有时使用它更方便,因为它允许指定相对于hKey的子键(lpSubKey),hKey是最终要设置值的键。如果没有现成的子键句柄,这就避免了显式打开子键的需要。

# 删除键和值

可以通过调用RegDeleteKey或RegDeleteKeyEx来删除注册表键:

LSTATUS RegDeleteKey (
    _In_ HKEY hKey,
    _In_ LPCTSTR lpSubKey
);
LSTATUS RegDeleteKeyEx(
    _In_ HKEY hKey,
    _In_ LPCTSTR lpSubKey,
    _In_ REGSAM samDesired,
    _Reserved_ DWORD Reserved
);
1
2
3
4
5
6
7
8
9
10

RegDeleteKey是最简单的函数,它删除相对于打开的hKey的子键(及其所有值)。用于打开键的访问掩码并不重要——是键上的安全描述符指示调用者是否可以执行删除操作。

RegDeleteKeyEx增加了通过为samDesired参数指定KEY_WOW64_32KEY或KEY_WOW64_64KEY来更改注册表视图的选项。请参阅本章前面“32位特定配置单元”部分的讨论。

被删除的键会被标记为删除,并且只有在所有打开的该键句柄都关闭后才会真正被删除。上述删除函数只能删除没有子键的键。如果键有子键,函数将失败并返回ERROR_ACCESS_DENIED (5)。

要删除包含所有子键的键,可调用RegDeleteTree:

LSTATUS RegDeleteTree(
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpSubKey
);
1
2
3
4

hKey必须使用以下权限打开:DELETE、KEY_ENUMERATE_SUB_KEYS、KEY_QUERY_VALUE以及(如果键有任何值)KEY_SET_VALUE。如果lpSubKey为NULL,则会从hKey指定的键中删除所有键和值。

使用RegDeleteKeyValue或RegDeleteValue删除值相当简单:

LSTATUS RegDeleteValue(
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpValueName
);

LSTATUS RegDeleteKeyValue(
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpSubKey,
    _In_opt_ LPCTSTR lpValueName
);
1
2
3
4
5
6
7
8
9
10

hKey必须使用KEY_SET_VALUE访问掩码打开。RegDeleteValue删除hKey中的给定值,而RegDeleteKeyValue允许指定从给定键到达的子键,以便从中删除值。

出于性能原因,RegDeleteValue更好,因为在内部,只要提供了子键,API就会打开子键、执行操作并关闭子键。对于其他可选子键的函数也是如此。如果有直接的键句柄,使用RegDeleteValue总是更快。

# 创建注册表链接

注册表支持链接,即指向其他键的键。我们已经见过一些这样的键。例如,HKEY_LOCAL_MACHINE\System\CurrentControlSet键在大多数情况下是指向HKEY_LOCAL_MACHINE\System\ControlSet001的符号链接。使用RegEdit.exe查看此类键时,符号链接看起来与普通键一样,因为它们的行为与链接的目标相同。图17-3展示了上述提到的键,它们的外观和行为完全相同。

我自己的注册表编辑器RegEditX.exe会用不同的图标显示链接(图17-4)。它还揭示了链接目标的存储方式——使用名为SymbolicLinkName的值。

图17-4:RegEditX.exe中一个键及其链接

微软文档确实提供了关于如何创建注册表链接的完整信息。第一步是创建键,并将其指定为链接而非普通键。 假设我们打算在 HKEY_CURRENT_USER 下创建一个名为 DesktopColors 的链接,该链接指向 HKEY_CURRENT_USER\Control Panel\Desktop\Colors。以下代码片段通过在调用 RegCreateKeyEx 时指定 REG_OPTION_CREATE_LINK 选项,将所需的键创建为链接(省略错误处理):

HKEY hKey;
::RegCreateKeyEx(HKEY_CURRENT_USER, L"DesktopColors", 0, nullptr,
    REG_OPTION_CREATE_LINK, KEY_WRITE, nullptr, &hKey, nullptr);
1
2
3

现在到了第一个棘手的部分。文档指出,链接的目标应写入名为 SymbolicLinkValue 的值中,并且它必须是绝对注册表路径。这里的问题是,所需的 “绝对路径” 并非像 HKEY_CURRENT_USER\Control Panel\Desktop\Colors 这样的路径。相反,它必须是内核视角下的注册表绝对路径。如果你在 RegEditX.exe 中打开名为 “Registry” 的树节点,就能看到这种路径的样子(图17-5)。

这意味着 HKEY_CURRENT_USER 必须转换为 HKEY_USERS\。这可以通过硬编码安全标识符(Security Identifier,SID)字符串来实现,但动态获取会更好。幸运的是,根据第16章详述的信息,我们可以像这样获取当前用户的SID字符串(再次省略错误处理):

HANDLE hToken;
::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &hToken);
// Win8+: HANDLE hToken  =  ::GetCurrentProcessToken();
BYTE buffer[sizeof(TOKEN_USER) + SECURITY_MAX_SID_SIZE];
DWORD len;
::GetTokenInformation(hToken, TokenUser, buffer, sizeof(buffer), &len);
::CloseHandle(hToken);
auto user = (TOKEN_USER*)buffer;
PWSTR stringSid;
::ConvertSidToStringSid(user->User.Sid, &stringSid);

// use stringSid...

::LocalFree(stringSid);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

现在我们可以这样组合绝对路径:

// 为方便起见,使用std::wstring
std::wstring path = L"\\REGISTRY\\USER\\";
path += stringSid;
path += L"\\Control Panel\\Desktop\\Colors";
1
2
3
4

第二个棘手的部分是,链接的路径必须在不包含终止NULL字节的情况下写入注册表中的 SymbolicLinkValue 值:

::RegSetValueEx(hKey, L"SymbolicLinkValue", 0, REG_LINK, (const BYTE*)path.c_str(),
    path.size() * sizeof(WCHAR));  // 注意 - NULL终止符不计入
1
2

这样就完成了创建操作。删除注册表链接无法使用上一节讨论的 RegDeleteKey(Ex) 来完成。如果你尝试这么做,它会删除链接的目标,而不是链接本身。只能使用伪文档化的原生函数 NtDeleteKey(来自 Ntdll.dll)来删除链接。要使用它,我们首先必须声明它,并链接到 ntdll 导入库(另一种链接方式是动态调用 GetProcAddress 来获取 NtDeleteKey 的地址):

extern "C" int NTAPI NtDeleteKey(HKEY);
#pragma comment(lib, "ntdll")
1
2

现在我们可以像这样删除符号链接键:

HKEY hKey;
::RegOpenKeyEx(HKEY_CURRENT_USER, L"DesktopColors", REG_OPTION_OPEN_LINK,
    DELETE, &hKey);
::NtDeleteKey(hKey);
::RegCloseKey(hKey);
1
2
3
4
5

最后,RegCreateKeyEx 无法打开现有链接,它只能创建链接。这与 “普通” 键形成对比,普通键可以通过 RegCreateKeyEx 打开或创建。打开链接必须使用 RegOpenKeyEx,并在选项参数中使用 REG_OPTION_OPEN_LINK 标志。

# 枚举键和值

诸如RegEdit.exe之类的工具需要枚举某个键下的子键,或者该键上设置的值。枚举子键是通过RegEnumKeyEx函数完成的:

LSTATUS RegEnumKeyEx(
    _In_ HKEY hKey,
    _In_ DWORD dwIndex,
    _Out_ LPTSTR lpName,
    _Inout_ LPDWORD lpcchName,
    _Reserved_ LPDWORD lpReserved,
    _Out_ LPTSTR lpClass,
    _Inout_opt_ LPDWORD lpcchClass,
    _Out_opt_ PFILETIME lpftLastWriteTime);
1
2
3
4
5
6
7
8
9

为使调用生效,hKey句柄必须使用KEY_ENUMERATE_SUB_KEYS访问掩码打开。枚举的执行方式是将dwIndex指定为零,并在循环中递增,直到调用返回ERROR_NO_MORE_ITEMS,这表明没有更多的键了。返回的结果始终包含键的名称(lpName),并且必须附带其大小(lpcchName,在输入时设置为缓冲区可以存储的最大字符数,包括NULL终止符,函数在输出时会将其更改为实际写入的字符数,不包括终止NULL)。该名称是键的简单名称,而不是从配置单元根目录开始的绝对名称。如果lpName缓冲区不够大,无法容纳键名,函数将失败并返回ERROR_MORE_DATA,并且不会向lpName缓冲区写入任何内容。

键名的最大长度为255个字符。

可能会返回另外两个可选信息:类名(一个很少使用的可选用户定义值)和键的最后修改时间。

以下示例取自DumpKey.exe应用程序(本章代码示例的一部分),展示了如何枚举键:

#include <atltime.h>

void DumpKey(HKEY hKey, bool dumpKeys, bool dumpValues, bool recurse) {
    FILETIME modified;
    // ...
    if (dumpKeys) {
        printf("Keys:\n");
        WCHAR name[256];
        for (DWORD i = 0; ; i++) {
            DWORD cname = _countof(name);
            auto error = ::RegEnumKeyEx(hKey, i, name, &cname, nullptr, nullptr, nullptr, &modified);
            if (error == ERROR_NO_MORE_ITEMS)
                // 枚举完成
                break;
            
            // 实际上不会发生,因为键名缓冲区足够大
            if (error == ERROR_MORE_DATA) {
                printf(" (Key name too long)\n");
                continue;
            }

            if (error == ERROR_SUCCESS)
                printf(" %-50ws Modified: %ws\n", name, (PCWSTR)CTime(modified).Format(L"%c"));

            if (recurse) {
                HKEY hSubKey;
                if (ERROR_SUCCESS == ::RegOpenKeyEx(hKey, name, 0, KEY_READ, &hSubKey)) {
                    printf("--------\n");
                    printf("Subkey: %ws\n", name);
                    DumpKey(hSubKey, dumpKeys, dumpValues, recurse);
                    ::RegCloseKey(hSubKey);
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

枚举给定键内的值的方式与此类似,使用RegEnumValue函数:

LSTATUS RegEnumValue(
    _In_ HKEY hKey,
    _In_ DWORD dwIndex,
    _Out_ LPTSTR lpValueName,
    _Inout_ LPDWORD lpcchValueName,
    _Reserved_ LPDWORD lpReserved,
    _Out_opt_ LPDWORD lpType,
    _Out_opt_ LPBYTE lpData,
    _Inout_opt_ LPDWORD lpcbData);
1
2
3
4
5
6
7
8
9

该函数的工作方式与RegEnumKeyEx类似,dwIndex应从0开始,并在循环中递增,直到函数返回ERROR_NO_MORE_ITEMS。hKey必须具有KEY_QUERY_VALUE访问掩码(也是KEY_READ的一部分),否则函数返回ERROR_ACCESS_DENIED。唯一必须返回的结果是值的名称,由lpValueName缓冲区及其大小(lpcchValueName)指定。这里的规则与RegEnumKeyEx相同:如果值名缓冲区不够大,则不会返回名称。这比RegEnumKeyEx的情况更棘手,因为值名的最大长度为16383个字符。解决此问题的最简单方法是分配一个大小为16384个字符的缓冲区(加上NULL终止符),但这可能被认为效率不高。另一种处理方法是在枚举开始前调用RegQueryInfoKey,它可以返回给定键内的最大值名长度:

LSTATUS RegQueryInfoKey(
    _In_ HKEY hKey,
    _Out_opt_ LPWSTR lpClass,
    _Inout_opt_ LPDWORD lpcchClass,
    _Reserved_ LPDWORD lpReserved,
    _Out_opt_ LPDWORD lpcSubKeys,          // 子键数量
    _Out_opt_ LPDWORD lpcbMaxSubKeyLen,    // 最大子键长度
    _Out_opt_ LPDWORD lpcbMaxClassLen,
    _Out_opt_ LPDWORD lpcValues,           // 值的数量
    _Out_opt_ LPDWORD lpcbMaxValueNameLen, // 最大值名长度
    _Out_opt_ LPDWORD lpcbMaxValueLen,     // 最大值大小
    _Out_opt_ LPDWORD lpcbSecurityDescriptor,
    _Out_opt_ PFILETIME lpftLastWriteTime);
1
2
3
4
5
6
7
8
9
10
11
12
13

就我的喜好而言,这个函数的参数太多了,将所有这些值放在一个结构中会更好。尽管如此,它还是相当容易使用的。大多数参数是可选的,允许调用者仅检索其关心的信息。它提供了最大的值名长度,使调用者能够为枚举中的任何值名分配足够大的缓冲区,这个缓冲区很可能小于16384个字符。

回到RegEnumValue函数——该函数还可以选择性地返回值的类型(lpType)和值本身(lpData)。如果需要值(lpData不为NULL),则值的缓冲区必须足够大,以容纳整个值,否则函数将失败并返回ERROR_MORE_DATA,并且不会向缓冲区写入任何内容。

以下示例(取自DumpKey示例)枚举值并显示值的名称和值(针对最常见的类型):

void DumpKey(HKEY hKey, bool dumpKeys, bool dumpValues, bool recurse) {
    DWORD nsubkeys, nvalues;
    DWORD maxValueSize;
    DWORD maxValueNameLen;
    FILETIME modified;
    if (ERROR_SUCCESS != ::RegQueryInfoKey(hKey, nullptr, nullptr, nullptr, &nsubkeys, nullptr, nullptr, &nvalues, &maxValueNameLen, &maxValueSize, nullptr, &modified))
        return;
    printf("Subkeys: %u Values: %u\n", nsubkeys, nvalues);
    if (dumpValues) {
        DWORD type;
        auto value = std::make_unique<BYTE[]>(maxValueSize);
        auto name = std::make_unique<WCHAR[]>(maxValueNameLen + 1);
        printf("values:\n");
        for (DWORD i = 0; ; i++) {
            DWORD cname = maxValueNameLen + 1;
            DWORD size = maxValueSize;
            auto error = ::RegEnumValue(hKey, i, name.get(), &cname, nullptr, &type, value.get(), &size);
            if (error == ERROR_NO_MORE_ITEMS)
                break;
            auto display = GetValueAsString(value.get(), min(64, size), type);
            printf(" %-30ws %-12ws (%5u B) %ws\n", name.get(), (PCWSTR)display.first, size, (PCWSTR)display.second);
        }
    }
    //...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

GetValueAsString辅助函数返回一个std::pair<CString, CString>,其中包含最常见类型的文本形式的类型和值:

std::pair<CString, CString>
GetValueAsString(const BYTE* data, DWORD size, DWORD type) {
    CString value, stype;
    switch (type) {
    case REG_DWORD:
        stype = L"REG_DWORD";
        value.Format(L"%u (0x%X)", *(DWORD*)data, *(DWORD*)data);
        break;

    case REG_QWORD:
        stype = L"REG_QWORD";
        value.Format(L"%llu (0x%llX)", *(DWORD64*)data, *(DWORD64*)data);
        break;

    case REG_SZ:
        stype = L"REG_SZ";
        value = (PCWSTR)data; break;

    case REG_EXPAND_SZ:
        stype = L"REG_EXPAND_SZ"; value = (PCWSTR)data;
        break;

    case REG_BINARY:
        stype = L"REG_BINARY";
        for (DWORD i = 0; i < size; i++)
            value.Format(L"%s%02X ", value, data[i]);
        break;

    default:
        stype.Format(L"%u", type);
        value = L"(Unsupported)";
        break;
    }

    return { stype, value };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

以下是调用DumpKey函数的部分输出,键为HKEY_CURRENT_USER\Control Panel,所有参数都设置为true:

Subkeys: 15 Values: 1
values:
SettingsExtensionAppSnapshot   REG_BINARY   (    8 B) 00 00 00 00 00 00 00 00
Keys:
Accessibility                                  Modified: Tue Mar 10 12:47:14 2020
--------
Subkey: Accessibility
Subkeys: 13 Values: 4
values:
MessageDuration                REG_DWORD    (    4 B) 5 (0x5)
MinimumHitRadius               REG_DWORD    (    4 B) 0 (0x0)
Sound on Activation            REG_DWORD    (    4 B) 1 (0x1)
Warning Sounds                 REG_DWORD    (    4 B) 1 (0x1)
Keys:
AudioDescription                               Modified: Tue Mar 10 12:47:14 2020
--------
Subkey: AudioDescription
Subkeys: 0 Values: 2
values:
On                           REG_SZ      (    4 B) 0
Locale                       REG_SZ      (    4 B) 0
Keys:
Blind Access                                   Modified: Tue Mar 10 12:42:53 2020
--------
Subkey: Blind Access
Subkeys: 0 Values: 1
values:
On                           REG_SZ      (    4 B) 0
Keys:
HighContrast                                   Modified: Tue Mar 10 12:47:14 2020
--------
Subkey: HighContrast
Subkeys: 0 Values: 3
values:
Flags                          REG_SZ      (    8 B) 126
High Contrast Scheme           REG_SZ       (    2 B)
Previous High Contrast Scheme MUI Value REG_SZ       (    2 B)
Keys:
Keyboard Preference                            Modified: Tue Mar 10 12:42:53 2020
--------
Subkey: Keyboard Preference
Subkeys: 0 Values: 1
values:
On                           REG_SZ      (    4 B) 0
Keys:
Keyboard Response            Modified: Tue Mar 10 12:42:53 2020
Subkey: Keyboard Response
Subkeys: 0 Values: 9
values:
AutoRepeatDelay                REG_SZ      (    10 B) 1000
AutoRepeatRate                 REG_SZ      (    8 B) 500
BounceTime                     REG_SZ      (    4 B) 0
DelayBeforeAcceptance          REG_SZ      (    10 B) 1000
Flags                          REG_SZ      (    8 B) 126
Last BounceKey Setting         REG_DWORD    (    4 B) 0 (0x0)
Last Valid Delay               REG_DWORD    (    4 B) 0 (0x0)
Last Valid Repeat              REG_DWORD    (    4 B) 0 (0x0)
Last Valid Wait                REG_DWORD    (    4 B) 1000 (0x3E8)
Keys:
MouseKeys                      Modified: Tue Mar 10 12:42:53 2020
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 注册表通知

有些应用程序需要知晓注册表中特定变化的发生时机,并通过重新读取某些关注的值来更新自身行为。注册表应用程序编程接口(API)提供了RegNotifyChangeKeyValue函数,正是用于这一目的:

LSTATUS RegNotifyChangeKeyValue(
    _In_ HKEY hKey,
    _In_ BOOL bWatchSubtree,
    _In_ DWORD dwNotifyFilter,
    _In_opt_ HANDLE hEvent,
    _In_ BOOL fAsynchronous);
1
2
3
4
5
6

hKey是要监视的根键。它可通过正常调用RegCreateKeyEx或RegOpenKeyEx 并指定REG_NOTIFY访问掩码来获取,或者也可以使用5个主要的预定义键之一。bWatchSubtree用于指示是仅监视指定的键(FALSE),还是监视hKey下的整个键树的变化(TRUE)。

dwNotifyFilter用于指示哪些操作应触发通知。表17-4中所示的标志的任意组合都是有效的。

标志 描述
REG_NOTIFY_CHANGE_NAME (1) 子键被添加或删除
REG_NOTIFY_CHANGE_ATTRIBUTES (2) 键的任何属性发生变化
REG_NOTIFY_CHANGE_LAST_SET (4) 修改时间发生变化,表明键值被添加、更改或删除
REG_NOTIFY_CHANGE_SECURITY (8) 键的安全描述符发生变化
REG_NOTIFY_THREAD_AGNOSTIC (0x10000000)(Windows 8+) 通知注册与调用线程无关(详见正文了解更多细节)

hEvent参数是一个指向事件内核对象的可选句柄,当通知到达时,该对象会变为有信号状态。如果最后一个参数(fAsynchronous)被设置为TRUE,则此参数是必需的。如果fAsynchronous为FALSE,则调用在检测到变化之前不会返回。如果fAsynchronous为TRUE,则调用会立即返回,并且必须等待该事件以获取通知。REG_NOTIFY_THREAD_AGNOSTIC标志表明调用线程与注册无关,这样任何线程都可以等待该事件句柄。如果未指定此标志,并且调用线程终止,则注册将被取消。

使用RegNotifyChangeKeyValue相当简单。它的主要不足在于,它无法确切指明发生了何种变化,也不会提供发生变化的键和 / 或值的额外信息。这使得它适用于仅需监视单个键(非递归)的简单场景,所以当检测到变化时,检查键中的变化并不会耗费太多资源。

RegWatch示例应用程序展示了如何在同步模式下使用RegNotifyChangeKeyValue。以下是其中有趣的部分:

// root 是标准配置单元键之一
// path 是子键(可以为NULL)
HKEY hKey;
auto error = ::RegOpenKeyEx(root, path, 0, KEY_NOTIFY, &hKey);
if (error != ERROR_SUCCESS) {
    printf("Failed to open key (%u)\n", error);
    return 1;
}

// 监视键/值的添加/修改
DWORD notifyFlags = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET;

printf("Watching...\n");
while (ERROR_SUCCESS == ::RegNotifyChangeKeyValue(hKey, recurse, notifyFlags, nullptr, FALSE)) {
    // 没有更多信息
    printf("Changed occurred.\n");
}

::RegCloseKey(hKey);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

获取注册表变化更多详细信息的另一种方法是使用Windows事件跟踪(Event Tracing For Windows,ETW)。这与第7章和第9章中用于检测动态链接库(DLL)加载的机制相同。ETW内核提供程序会提供一系列与注册表相关的通知,这些通知可被处理。

RegWatch2示例应用程序使用ETW来展示注册表活动。它没有用于从特定键获取通知的内置筛选器,需要由使用者自行筛选其不关注的通知。所示示例未进行任何筛选,并且展示了与操作相关的键名(如果有的话)。

第7章和第9章中使用的TraceManager类被原样复制。唯一的改动在Start方法(TraceManager.cpp)中,该方法更改了事件标志以指示注册表事件(而非原始代码中的映像事件):

_properties->EnableFlags = EVENT_TRACE_FLAG_REGISTRY;
1

EventParser类(EventParser.h/cpp)也被原样复制(唯一的改动是添加了头文件,因为RegWatch2不使用预编译头文件)。以下是主函数:

TraceManager* g_pMgr;
HANDLE g_hEvent;

int main() {
    TraceManager mgr;
    if (!mgr.Start(OnEvent)) {
        printf("Failed to start trace. Are you running elevated?\n");
        return 1;
    }

    g_pMgr = &mgr;

    g_hEvent = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
    ::SetConsoleCtrlHandler([](auto type) {
        if (type == CTRL_C_EVENT) {
            g_pMgr->Stop();
            ::SetEvent(g_hEvent);
            return TRUE;
        }
        return FALSE;
    }, TRUE);

    ::WaitForSingleObject(g_hEvent, INFINITE);
    ::CloseHandle(g_hEvent);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

主函数创建一个TraceManager对象并调用Start(请记住,以这种方式使用事件跟踪会话(ETW)需要管理员权限)。通过按Ctrl+C组合键来停止会话。调用SetConsoleCtrlHandler函数是为了在检测到该组合键时得到通知。遗憾的是,SetConsoleCtrlHandler的函数指针没有提供传递上下文参数的方法,这就是TraceManager的指针也存储在全局变量中的原因。

此外,创建一个事件对象并将其保存在全局变量中,用于指示Stop函数已被调用,这样等待操作就会结束,程序也可以退出。

每个通知都会被发送到OnEvent函数,该函数在调用TraceManager::Start时被传入。以下是OnEvent函数的代码:

void OnEvent(PEVENT_RECORD rec) {
    EventParser parser(rec);

    auto ts = parser.GetEventHeader().TimeStamp.QuadPart;
    printf("Time: %ws PID: %u: ",
           (PCWSTR)CTime(*(FILETIME*)&ts).Format(L"%c"),
           parser.GetProcessId());

    switch (parser.GetEventHeader().EventDescriptor.Opcode) {
        case EVENT_TRACE_TYPE_REGCREATE:            printf("Create key"); break;
        case EVENT_TRACE_TYPE_REGOPEN:              printf("Open key"); break;
        case EVENT_TRACE_TYPE_REGDELETE:            printf("Delete key"); break;
        case EVENT_TRACE_TYPE_REGQUERY:             printf("Query key"); break;
        case EVENT_TRACE_TYPE_REGSETVALUE:          printf("Set value"); break;
        case EVENT_TRACE_TYPE_REGDELETEVALUE:       printf("Delete value"); break;
        case EVENT_TRACE_TYPE_REGQUERYVALUE:        printf("Query value"); break;
        case EVENT_TRACE_TYPE_REGENUMERATEKEY:      printf("Enum key"); break;
        case EVENT_TRACE_TYPE_REGENUMERATEVALUEKEY: printf("Enum values"); break;
        case EVENT_TRACE_TYPE_REGSETINFORMATION:    printf("Set key info"); break;
        case EVENT_TRACE_TYPE_REGCLOSE:             printf("Close key"); break;
        default:                                    printf("(Other)"); break;
    }

    auto prop = parser.GetProperty(L"KeyName");
    if (prop) {
        printf(" %ws", prop->GetUnicodeString());
    }
    printf("\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

这里只具体捕获了部分可能的通知,其他所有通知都显示为“(other)”。如果存在键名属性,也会一并显示。运行该应用程序时,你会感受到在任何给定时间发生的注册表操作数量之多。

每个事件还有其他信息。调用EventParser::GetProperties()可获取事件记录的所有自定义属性。

我们介绍的这两种通知选项都不允许拦截更改操作。只有使用内核API才能实现这种强大的功能。我的《Windows内核编程》一书介绍了如何实现这一点。

# 事务性注册表

在第9章(第1部分)中,我们了解到可以通过使用CreateFileTransacted等函数,将文件操作作为事务的一部分,并将其与打开的事务相关联。可以使用CreateTransaction创建这样的事务(具体细节请参考第9章)。注册表操作也可以作为事务进行,作为一组原子操作执行,还可以与文件事务操作结合使用。

要将注册表操作作为事务的一部分,必须使用通过RegCreateKeyTransacted创建或通过RegOpenKeyTransacted打开的键:

LSTATUS RegCreateKeyTransacted (
    _In_ HKEY hKey,
    _In_ LPCTSTR lpSubKey,
    _Reserved_ DWORD Reserved,
    _In_opt_ LPTSTR lpClass,
    _In_ DWORD dwOptions,
    _In_ REGSAM samDesired,
    _In_opt_ CONST LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _Out_ PHKEY phkResult,
    _Out_opt_ LPDWORD lpdwDisposition,
    _In_        HANDLE hTransaction,
    _Reserved_ PVOID  pExtendedParemeter);

LSTATUS RegOpenKeyTransacted (
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpSubKey,
    _In_opt_ DWORD ulOptions,
    _In_ REGSAM samDesired,
    _Out_ PHKEY phkResult,
    _In_        HANDLE hTransaction,
    _Reserved_ PVOID  pExtendedParemeter);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这些扩展函数与非事务版本的参数相同,除了最后两个参数用于指定要操作的事务(hTransaction)。使用返回的hKey进行的所有后续操作都属于该事务,最终要么全部成功,要么全部作为一个原子单元失败。

请参阅第9章中关于事务的讨论。

# 远程注册表

可以通过首先调用RegConnectRegistry连接到另一台计算机上的注册表,从而在远程计算机上打开或创建注册表项。要使其正常工作,远程计算机上必须运行“远程注册表”服务。默认情况下,该服务配置为手动启动,所以它不太可能处于运行状态。即使该服务正在运行,连接到远程注册表时也存在其他安全限制。详细信息请查看文档。以下是RegConnectRegistry函数:

LSTATUS RegConnectRegistry (
    _In_opt_ LPCTSTR lpMachineName,
    _In_ HKEY hKey,
    _Out_ PHKEY phkResult);
1
2
3
4

lpMachineName是要连接的计算机,其格式必须为\\computername。hKey必须是以下预定义键之一:HKEY_USERS、HKEY_LOCAL_MACHINE或HKEY_PERFORMANCE_DATA。最后一个参数(phkResult)是返回的句柄,供本地使用。

有了新的句柄后,就可以执行常规的注册表操作,包括读取值、打开子键、写入值等,所有操作都要经过安全检查。操作完成后,应使用RegCloseKey正常关闭句柄。

还有RegConnectRegistry的扩展版本RegConnectRegistryEx:

LSTATUS RegConnectRegistryEx (
    _In_opt_ LPCTSTR lpMachineName,
    _In_ HKEY hKey,
    _In_ ULONG Flags,
    _Out_ PHKEY phkResult);
1
2
3
4
5

它与RegConnectRegistry相同,但允许指定一些标志。目前唯一支持的标志是REG_SECURE_CONNECTION(值为1),它表示调用方希望与远程注册表建立安全连接,这会使发送到远程计算机的RPC调用被加密。

# 其他注册表函数

注册表API还包括其他一些杂项函数,本节简要介绍其中部分函数。

RegGetKeySecurity和RegSetKeySecurity函数用于检索和操作给定键的安全描述符。虽然更通用的函数GetSecurityInfo和SetSecurityInfo(在第16章中介绍)也可用于注册表项,但这些特定函数使用起来更简便。

LSTATUS RegGetKeySecurity(
    _In_ HKEY hKey,
    _In_    SECURITY_INFORMATION SecurityInformation,
    _Out_   PSECURITY_DESCRIPTOR pSecurityDescriptor,
    _Inout_ LPDWORD lpcbSecurityDescriptor);

LSTATUS RegSetKeySecurity(
    _In_ HKEY hKey,
    _In_ SECURITY_INFORMATION SecurityInformation,
    _In_ PSECURITY_DESCRIPTOR pSecurityDescriptor);
1
2
3
4
5
6
7
8
9
10

可以通过调用RegSaveKey将注册表项(及其所有值和子项)保存到文件中:

LSTATUS RegSaveKey (
    _In_ HKEY hKey,
    _In_ LPCTSTR lpFile,
    _In_opt_ CONST LPSECURITY_ATTRIBUTES lpSecurityAttributes);
1
2
3
4

这些函数将hKey及其子项的所有信息保存到由lpFile指定的文件中。hKey可以是使用标准函数打开的键,也可以是以下预定义键之一:HKEY_CLASSES_ROOT或HKEY_CURRENT_USER。在调用之前,lpFile指定的文件不能存在,否则调用失败。可选的安全属性参数可以为新文件提供安全描述符;如果为NULL,则使用默认安全描述符,通常从文件的父文件夹继承。

数据保存的格式称为“标准格式”,自Windows 2000起开始支持。这种格式是专有的,通常没有文档说明。具体来说,这不是在RegEdit.exe中选择“导出”菜单项备份注册表项时使用的.REG文件格式。.REG文件格式特定于RegEdit,注册表函数通常不识别这种格式。

通过调用扩展函数RegSaveKeyEx可以使用当前(最新)的保存键的格式:

LSTATUS RegSaveKeyEx(
    _In_ HKEY hKey,
    _In_ LPCTSTR lpFile,
    _In_opt_ CONST LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _In_ DWORD Flags);
1
2
3
4
5

该函数增加了一个Flags参数,它必须是以下值之一(且只能是其中一个):

  • REG_STANDARD_FORMAT(值为1)——原始格式,与RegSaveKey使用的格式相同。
  • REG_LATEST_FORMAT(值为2)——最新(更好)的格式。
  • REG_NO_COMPRESSION(值为4)——不压缩保存数据(原样保存)。仅适用于真正的配置单元(在RegEditX中用特殊图标标记)。完整的配置单元列表可以在注册表中查看,路径为HKLM\System\CurrentControlSet\Control\hivelist。

RegSaveKeyEx的一个缺点是不支持预定义键HKEY_CLASSES_ROOT。

与RegSaveKey和RegSaveKeyEx一样,调用方的令牌中必须具有SeBackupPrivilege权限。此权限通常授予管理员组的成员,但标准用户没有此权限。

有了保存的文件后,可以使用RegRestoreKey将信息恢复到注册表中:

LSTATUS RegRestoreKey(
    _In_ HKEY hKey,
    _In_ LPCTSTR lpFile,
    _In_ DWORD dwFlags);
1
2
3
4

hKey指定要恢复信息的键。它可以是任何打开的键,也可以是标准的5个配置单元键之一。lpFile是存储数据的文件。恢复操作会保留由hKey标识的根键名称,但会替换该键的所有其他属性,并替换文件中存储的所有子键/值。

dwFlags提供了几个选项,其中只有两个是官方有文档说明的:

  • REG_FORCE_RESTORE(值为8)——即使存在将被覆盖的子键的打开句柄,也强制进行恢复操作。
  • REG_WHOLE_HIVE_VOLATILE(值为1)——仅在内存中创建一个新的配置单元。在这种情况下,hKey必须是HKEY_USERS或HKEY_LOCAL_MACHINE。该配置单元会在下一次系统启动时被删除。 | 与RegSaveKey(Ex)类似,调用方的令牌中必须具有SeRestorePrivilege权限,通常授予管理员。 | | ------------------------------------------------------------ |

除了使用RegRestoreKey加载配置单元外,还可以使用RegLoadAppKey:

LSTATUS RegLoadAppKey(
    _In_ LPCTSTR lpFile,
    _Out_ PHKEY phkResult,
    _In_ REGSAM samDesired,
    _In_ DWORD dwOptions,
    _Reserved_ DWORD Reserved);
1
2
3
4
5
6

RegLoadAppkey将由lpFile指定的配置单元加载到一个不可枚举的不可见根中,因此它不属于标准注册表的一部分。访问该配置单元中任何内容的唯一方法是通过成功返回的根键phkResult。与RegRestoreKey相比,其优势在于调用方不需要SeRestorePrivilege权限,因此非管理员调用方也可以使用。

dwOptions中唯一可用的标志是REG_PROCESS_APPKEY,如果使用该标志,则在键句柄打开期间,可防止其他调用方加载相同的配置单元文件。

与使用REG_WHOLE_HIVE_VOLATILE的RegRestoreKey有些类似的操作是RegLoadKey,它可以从文件加载配置单元,并将其作为新的配置单元存储在HKEY_LOCAL_MACHINE或HKEY_USERS下。在离线查看注册表文件(例如从其他系统获取的文件)时,这很有用。RegEdit在其“文件”菜单的“加载配置单元…”选项中通过调用RegLoadKey提供了此功能。你会注意到,只有在树视图中选择的键是HKEY_LOCAL_MACHINE或HKEY_USERS时,此选项才可用(图17-6)。

img

图17-6:RegEdit中的“加载配置单元”选项

以下是RegLoadKey的定义:

LSTATUS RegLoadKey(
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpSubKey,
    _In_ LPCTSTR lpFile);
1
2
3
4

hKey可以是HKEY_LOCAL_MACHINE或HKEY_USERS(因为这些是真正的键,而不是各种链接)。lpSubKey是在其下从由lpFile指定的文件加载配置单元的子键名称。对加载的键(或其任何子键)所做的任何更改最终都会持久保存在文件中。

可以使用RegUnloadKey卸载配置单元:

LSTATUS RegUnLoadKey(
    _In_ HKEY hKey,
    _In_opt_ LPCTSTR lpSubKey);
1
2
3

这些参数与RegLoadKey中的对应参数类似。

# 总结

注册表是Windows系统使用的主要数据库,用于存储系统范围和用户特定的设置。注册表的某些部分在系统安全、内核操作等方面尤为重要。在本章中,我们介绍了注册表的概念、布局以及最常用的用于读取和以其他方式操作注册表的API函数。

上次更新: 2025/05/07, 21:40:50
第16章:安全性

← 第16章:安全性

最近更新
01
第一章 auto与类型推导
03-27
02
第二章 关键字static及其不同用法
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式