第13章 注册表
# 第13章 注册表
注册表是Windows系统中最具辨识度的组成部分之一。它是一个分层数据库(hierarchical database),用于存储系统和用户信息。本章将介绍注册表的结构,并详细探讨用于操作注册表的原生API(native APIs)。
许多注册表相关的原生API在Windows驱动程序工具包(Windows Driver Kit,WDK)中有文档说明,这些API在WDK中通常以Zw为前缀(而非Nt)。
本章内容包括:
- 注册表结构(Registry Structure)
- 创建和打开键(Creating and Opening Keys)
- 键和值的操作(Working with Keys and Values)
- 键信息(Key Information)
- 其他注册表函数(Other Registry Functions)
- 键持久性(Key Persistence)
- 注册表通知(Registry Notifications)
- 注册表事务(Registry Transactions)
- 高层注册表辅助函数(Higher Level Registry Helpers)
# 13.1 注册表结构
查看注册表通常使用系统内置的图形界面工具RegEdit.exe。图13-1展示了RegEdit的截图,其中显示了五个固定的“配置单元(hives)”。这种注册表视图在使用Windows注册表API时非常实用,因为该API使用图中所示的标识符来表示根配置单元,例如HKEY_LOCAL_MACHINE和HKEY_CURRENT_USER。

图13-1:RegEdit中的配置单元
以下是各配置单元及其用途的简要说明:
- HKEY_LOCAL_MACHINE:存储计算机范围内的系统信息。
- HKEY_CURRENT_USER:存储运行RegEdit的当前用户相关信息。它实际上是指向HKEY_USERS{UserSid}的链接键(link key)。
- HKEY_USERS:存储计算机上所有拥有用户配置文件的用户信息,包括所有曾经交互式登录系统的用户,以及通常用于运行服务的三个用户账户:LocalSYSTEM(安全标识符SID为“S-1-5-18”)、NetworkService(SID为“S-1-5-20”)和LocalService(SID为“S-1-5-19”)。
- HKEY_CLASSES_ROOT:合并了两个键的内容:HKEY_CURRENT_USER\Software\Classes和HKEY_LOCAL_MACHINE\Software\Classes。若存在冲突的键,HKEY_CURRENT_USER中的键将优先生效。
- HKEY_CURRENT_CONFIG:指向HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current的链接键(在当今系统中已基本无用)。
上述注册表视图仅仅是一种“抽象视图”,并非注册表的“真实”结构。通过笔者开发的TotalRegistry工具,可以查看注册表的真实结构,该工具同时展示了注册表的“真实视图”和Windows API使用的抽象视图(如图13-2所示)。

图13-2:TotalRegistry中的“真实”注册表
注册表的根节点名为“Registry”,包含以下子键:
- MACHINE:计算机配置单元,对应抽象视图中的HKEY_LOCAL_MACHINE。
- USER:存储计算机上所有用户的配置文件列表,对应抽象视图中的HKEY_USERS。
- A:该配置单元供通用Windows平台(UWP)进程存储私有数据,其他任何进程均无法访问。并非所有系统中都存在此键。
- WC:可选键,用于存储Windows容器(Windows Containers,因此缩写为“WC”)相关信息,也称为服务器隔离舱(Server Silos)。
使用原生注册表API时,必须按照“真实”注册表结构指定注册表路径。例如,抽象视图中的HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services,在原生API中必须指定为\REGISTRY\MACHINE\System\CurrentControlSet\Services。
# 13.2 创建和打开键
可以通过NtOpenKey或NtOpenKeyEx函数打开已存在的注册表键:
NTSTATUS NtOpenKey(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
NTSTATUS NtOpenKeyEx(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ ULONG OpenOptions);
2
3
4
5
6
7
8
9
10
KeyHandle用于存储函数执行成功后返回的键句柄。DesiredAccess是请求的键访问权限,通常为KEY_READ和/或KEY_WRITE。键路径本身存储在ObjectAttributes结构中,这与我们之前多次遇到的用法一致。
以下示例展示了如何打开\Registry\Machine\System\CurrentControlSet\Services的只读句柄:
HANDLE hKey;
UNICODE_STRING keyName;
RtlInitUnicodeString(&keyName,
L"\\Registry\\Machine\\System\\CurrentControlSet\\Services");
OBJECT_ATTRIBUTES keyAttr;
InitializeObjectAttributes(&keyAttr, &keyName, 0 , nullptr , nullptr);
auto status = NtOpenKey(&hKey, KEY_READ, &keyAttr);
2
3
4
5
6
7
注意,无论OBJECT_ATTRIBUTES中是否设置OBJ_CASE_INSENSITIVE属性,键路径均不区分大小写。若指定的键不存在,函数调用将失败。
支持通过设置OBJECT_ATTRIBUTES的RootDirectory成员并使用相对路径来打开子键。以下示例展示了如何使用相对路径打开上述键的ACPI子键:
keyAttr.RootDirectory = hKey;
HANDLE hSubKey;
RtlInitUnicodeString(&keyName, L"ACPI");
status = NtOpenKey(&hSubKey, KEY_READ, &keyAttr);
2
3
4
通过常规的NtClose API关闭键句柄。
NtOpenKeyEx的OpenOptions参数支持以下选项:
- REG_OPTION_BACKUP_RESTORE:如果调用者的令牌中已启用SeBackupPrivilege权限,则支持以读权限打开键;如果已启用SeRestorePrivilege权限,则支持以写权限打开键。在此情况下,DesiredAccess参数将被忽略。
- REG_OPTION_OPEN_LINK:如果目标键是链接键,则不跟随链接,而是直接打开该链接键本身。使用此选项时,还需在OBJECT_ATTRIBUTES结构的属性中设置OBJ_OPENLINK。
- REG_OPTION_DONT_VIRTUALIZE:指定键路径不应被虚拟化。这与用户账户控制(UAC)虚拟化相关——在UAC虚拟化机制下,虚拟化进程向本地计算机配置单元写入数据时,实际上是写入用户专属位置,从而使写入操作“看似成功”。有关更多信息,可在网上搜索“UAC virtualization”。
RegOpenKey函数会调用RegOpenKeyEx,并将OpenOptions参数设置为0。
编写一个函数,使用NtOpenKey打开“标准”键HKEY_CURRENT_USER。
# 13.2.1 创建键
要创建注册表键(若键已存在则可选打开),调用NtCreateKey函数:
NTSTATUS NtCreateKey(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Reserved_ ULONG TitleIndex, // 未使用
_In_opt_ PUNICODE_STRING Class,
_In_ ULONG CreateOptions,
_Out_opt_ PULONG Disposition);
2
3
4
5
6
7
8
NtCreateKey根据ObjectAttributes中存储的路径创建或打开键。前三个参数实际上与NtOpenKey完全相同。Class是可选字符串,作为一种“元数据”附加到创建的键上——它没有其他实际用途,应用程序可将其用作某种隐藏的字符串值。该值后续可通过NtQueryKey函数获取(详见本章后续“键信息”部分)。
CreateOptions参数可以是0,也可以是多个标志的组合——其中三个标志已在上一节中描述,其他附加标志如下:
- REG_OPTION_NON_VOLATILE:指定键为非易失性(non-volatile),即键值会在系统重启后保留。这是默认选项,该标志的值为0。
- REG_OPTION_VOLATILE:指定键为易失性(volatile),即系统关闭时该键将被删除。
- REG_OPTION_CREATE_LINK:创建链接键(link key),而非普通键。创建此类键时,必须设置一个名为“SymbolicLinkName”的特殊值,其值为目标键的完整路径。
最后,Disposition是可选的返回值,用于指示键是新建的(REG_CREATED_NEW_KEY),还是因已存在而被打开的(REG_OPENED_EXISTING_KEY)。
# 13.3 键和值的操作
创建或打开键句柄后,可以通过NtQueryValueKey和NtSetValueKey函数读取或写入键的值:
NTSTATUS NtQueryValueKey(
_In_ HANDLE KeyHandle,
_In_ PUNICODE_STRING ValueName,
_In_ KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
_Out_writes_bytes_opt_ (Length) PVOID KeyValueInformation,
_In_ ULONG Length,
_Out_ PULONG ResultLength);
NTSTATUS NtSetValueKey(
_In_ HANDLE KeyHandle,
_In_ PUNICODE_STRING ValueName,
_In_opt_ ULONG TitleIndex, // 未使用
_In_ ULONG Type,
_In_reads_bytes_opt_(DataSize) PVOID Data,
_In_ ULONG DataSize);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NtSetValueKey的使用非常直接。KeyHandle必须具有KEY_SET_VALUE访问权限(该权限已包含在更常用的KEY_WRITE权限中)。ValueName是要设置的键值名称,若该名称已存在,则会被覆盖。Type是键值的数据类型,为注册表支持的多种数据类型之一,例如REG_SZ、REG_DWORD等,完整列表可参考Windows SDK文档。
最后,Data是指向数据的指针,DataSize为数据的字节大小。注意,字符串类型(REG_SZ、REG_EXPAND_SZ和REG_MULTI_SZ)均为以NULL结尾的UTF-16字符串(而非UNICODE_STRING结构)。
NtQueryValueKey的使用相对复杂。它接受键句柄(至少需要KEY_QUERY_VALUE访问权限,该权限包含在更常用的KEY_READ权限中)、键值名称(ValueName),以及一个枚举类型(KeyValueInformationClass)——该枚举用于指定请求获取的信息类型:
typedef enum _KEY_VALUE_INFORMATION_CLASS
{
KeyValueBasicInformation, // KEY_VALUE_BASIC_INFORMATION
KeyValueFullInformation, // KEY_VALUE_FULL_INFORMATION
KeyValuePartialInformation, // KEY_VALUE_PARTIAL_INFORMATION
KeyValueFullInformationAlign64,
KeyValuePartialInformationAlign64, // KEY_VALUE_PARTIAL_INFORMATION_ALIGN64
KeyValueLayerInformation, // KEY_VALUE_LAYER_INFORMATION
MaxKeyValueInfoClass
} KEY_VALUE_INFORMATION_CLASS;
2
3
4
5
6
7
8
9
10
NtQueryValueKey支持以下三种有效的信息类型:KeyValueBasicInformation、KeyValueFullInformation和KeyValueFullInformationAlign64(指定需要8字节对齐),对应的结构定义如下:
typedef struct _KEY_VALUE_BASIC_INFORMATION
{
ULONG TitleIndex;
ULONG Type;
ULONG NameLength;
WCHAR Name[1];
} KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION;
typedef struct _KEY_VALUE_FULL_INFORMATION
{
ULONG TitleIndex;
ULONG Type;
ULONG DataOffset;
ULONG DataLength;
ULONG NameLength;
WCHAR Name[1];
// 数据紧随其后
} KEY_VALUE_FULL_INFORMATION, *PKEY_VALUE_FULL_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可以看到,这些是变长结构(variable-length structures),因为它们需要存储名称信息,而KEY_VALUE_FULL_INFORMATION还需要存储数据。这意味着调用者必须提供足够大的缓冲区来容纳请求的信息。如果缓冲区过小,函数将返回STATUS_BUFFER_OVERFLOW错误状态,此时*ResultLength会返回所需的缓冲区字节大小。
函数调用成功后,信息将存储在KeyValueInformation中。KEY_VALUE_BASIC_INFORMATION/KEY_VALUE_FULL_INFORMATION中的Name字段紧跟在名称长度(字节数)之后,且不以NULL结尾。对于KEY_VALUE_FULL_INFORMATION,数据紧随名称之后,但应使用DataOffset字段获取从结构起始位置到实际数据的偏移量。DataLength是数据的字节大小,Type是数据类型(如REG_DWORD、REG_SZ等)。
以下示例查询了两个键值并显示其内容:
HANDLE hKey;
UNICODE_STRING keyName;
RtlInitUnicodeString(&keyName,
L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\ACPI");
OBJECT_ATTRIBUTES keyAttr;
InitializeObjectAttributes(&keyAttr, &keyName, 0 , nullptr , nullptr);
auto status = NtOpenKey(&hKey, KEY_READ, &keyAttr);
if (NT_SUCCESS(status))
{
UNICODE_STRING valueName;
RtlInitUnicodeString(&valueName, L"ImagePath");
ULONG len = 0;
//
// 调用NtQueryValueKey两次。第一次调用用于获取所需缓冲区长度
//
NtQueryValueKey(hKey, &valueName, KeyValueFullInformation, nullptr , 0 , &len);
if (len)
{
//
// 分配缓冲区
//
auto info = (KEY_VALUE_FULL_INFORMATION*)RtlAllocateHeap(
NtCurrentPeb()->ProcessHeap, 0, len);
if (NT_SUCCESS(NtQueryValueKey(hKey, &valueName,
KeyValueFullInformation, info, len, &len)))
{
_ASSERTE(info->Type == REG_SZ || info->Type == REG_EXPAND_SZ);
printf("ImagePath: %ws\n " , (PCWSTR)((PBYTE)info + info->DataOffset));
}
RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, info);
}
RtlInitUnicodeString(&valueName, L"Type");
len = 0;
NtQueryValueKey(hKey, &valueName, KeyValueFullInformation, nullptr , 0 , &len);
if (len)
{
auto info = (KEY_VALUE_FULL_INFORMATION*)RtlAllocateHeap(
NtCurrentPeb()->ProcessHeap, 0, len);
if (NT_SUCCESS(NtQueryValueKey(hKey, &valueName,
KeyValueFullInformation, info, len, &len)))
{
_ASSERTE(info->Type == REG_DWORD);
printf("Type: %u\n " , *(DWORD*)((PBYTE)info + info->DataOffset));
}
RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, info);
}
NtClose(hKey);
}
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
在笔者的系统中,输出结果如下:
ImagePath: System32\drivers\ACPI.sys Type: 1
有时需要查询同一个键中的多个值。虽然可以通过多次调用NtQueryValueKey实现,但单次调用NtQueryMultipleValueKey函数会更高效:
typedef struct _KEY_VALUE_ENTRY
{
PUNICODE_STRING ValueName;
ULONG DataLength;
ULONG DataOffset;
ULONG Type;
} KEY_VALUE_ENTRY, *PKEY_VALUE_ENTRY;
NTSTATUS NtQueryMultipleValueKey(
_In_ HANDLE KeyHandle,
_Inout_updates_(EntryCount) PKEY_VALUE_ENTRY ValueEntries,
_In_ ULONG EntryCount,
_Out_writes_bytes_ (*BufferLength) PVOID ValueBuffer,
_Inout_ PULONG BufferLength,
_Out_opt_ PULONG RequiredBufferLength);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NtQueryMultipleValueKey接受一个KEY_VALUE_ENTRY数组,每个元素指定一个要读取的键值。函数执行成功后,结果数据和数据类型将存储在同一个结构中。与NtQueryValueKey类似,调用者提供的缓冲区必须足够大以容纳所有结果。
以下示例读取与前一个代码示例相同的两个键值,但使用了更高效的NtQueryMultipleValueKey(省略了部分错误处理):
// 假设hKey是目标键的句柄
// 初始化键值条目数组
UNICODE_STRING value1Name, value2Name;
RtlInitUnicodeString(&value1Name, L"ImagePath");
RtlInitUnicodeString(&value2Name, L"Type");
KEY_VALUE_ENTRY entry[] =
{
{ &value1Name },
{ &value2Name },
};
// 必须先进行初始分配。若缓冲区过小,需按需重新分配
ULONG len = 32, needed;
auto info = (BYTE*)RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, 0, len);
status = NtQueryMultipleValueKey(hKey, entry, _ARRAYSIZE(entry),
info, &len, &needed);
if ( !NT_SUCCESS(status))
{
// 分配的缓冲区过小,重新分配
info = (BYTE*)RtlReAllocateHeap(NtCurrentPeb()->ProcessHeap, 0, info, needed);
NtQueryMultipleValueKey(hKey, entry, _ARRAYSIZE(entry),
info, &needed, nullptr);
}
// 显示结果
_ASSERTE(entry[0].Type == REG_SZ || entry[0].Type == REG_EXPAND_SZ);
_ASSERTE(entry[1].Type == REG_DWORD);
printf("ImagePath: %ws\n " , (PCWSTR)(info + entry[0].DataOffset));
printf("Type: %u\n " , *(DWORD*)(info + entry[1].DataOffset));
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
# 13.3.1 枚举键和值
可以通过调用 NtEnumerateKey(枚举键的原生API)来枚举特定键下的子键:
NTSTATUS NtEnumerateKey(
_In_ HANDLE KeyHandle,
_In_ ULONG Index,
_In_ KEY_INFORMATION_CLASS KeyInformationClass,
_Out_writes_bytes_opt_ (Length) PVOID KeyInformation,
_In_ ULONG Length,
_Out_ PULONG ResultLength);
2
3
4
5
6
7
KeyHandle 必须具备 KEY_ENUMERATE_SUB_KEY 访问掩码(该掩码是更常用的 KEY_READ 访问掩码的一部分)。Index 表示子键的索引。对于标准枚举,应从 0 开始,每次将 Index 递增 1,直到函数返回 STATUS_NO_MORE_ENTRIES(无更多条目状态码)。对于每个键,返回的信息类型由 KeyInformationClass(键信息类别)决定,数据本身会存入 KeyInformation 指向的缓冲区。Length 指定了 KeyInformation 缓冲区的大小。如果缓冲区过小,函数会返回 STATUS_BUFFER_OVERFLOW(缓冲区溢出状态码),且 *ResultLength 会指示所需的缓冲区大小。
KEY_INFORMATION_CLASS(键信息类别)枚举类型的定义如下:
typedef enum _KEY_INFORMATION_CLASS {
KeyBasicInformation, // KEY_BASIC_INFORMATION(键基本信息)
KeyNodeInformation, // KEY_NODE_INFORMATION(键节点信息)
KeyFullInformation, // KEY_FULL_INFORMATION(键完整信息)
KeyNameInformation, // KEY_NAME_INFORMATION(键名称信息)
KeyCachedInformation, // KEY_CACHED_INFORMATION(键缓存信息)
KeyFlagsInformation, // KEY_FLAGS_INFORMATION(键标志信息)
KeyVirtualizationInformation, // KEY_VIRTUALIZATION_INFORMATION(键虚拟化信息)
KeyHandleTagsInformation, // KEY_HANDLE_TAGS_INFORMATION(键句柄标签信息)
KeyTrustInformation, // KEY_TRUST_INFORMATION(键信任信息)
KeyLayerInformation, // KEY_LAYER_INFORMATION(键层级信息)
MaxKeyInfoClass
} KEY_INFORMATION_CLASS;
2
3
4
5
6
7
8
9
10
11
12
13
我们将在下一节详细介绍所有这些信息类型。
以下代码示例通过使用 KeyBasicInformation 枚举值及其关联的 KEY_BASIC_INFORMATION(键基本信息)结构,枚举指定的键并显示每个子键的名称:
typedef struct _KEY_BASIC_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG NameLength;
WCHAR Name[1];
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
2
3
4
5
6
NTSTATUS EnumerateKey(HANDLE hKey)
{
// 分配足够大的缓冲区,用于存储键名称和写入时间
BYTE buffer[1024];
auto info = (KEY_BASIC_INFORMATION*)buffer;
ULONG len;
for (ULONG i = 0; ; i++)
{
auto status = NtEnumerateKey(hKey, i, KeyBasicInformation,
buffer, sizeof(buffer), &len);
if (status == STATUS_NO_MORE_ENTRIES)
break;
if (!NT_SUCCESS(status))
return status;
// 名称字符串不一定以 NULL 结尾
printf("Name: %ws (Last written: %s)\n " ,
std::wstring(info->Name, info->NameLength / sizeof(WCHAR)) .c_str(),
FormatTime(info->LastWriteTime) .c_str());
}
return STATUS_SUCCESS;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FormatTime 是一个辅助函数,用于将 64 位日期/时间值转换为人类可读的格式:
std::string FormatTime(LARGE_INTEGER& dt)
{
TIME_FIELDS tf;
RtlTimeToTimeFields(&dt, &tf);
// 使用 C++ 20 中的 std::format 函数
return std::format("{:04}/{:02}/{:02} {:02}:{:02}:{:02} . {:03}" ,
tf.Year, tf.Month, tf.Day,
tf.Hour, tf.Minute, tf.Second, tf.Milliseconds);
}
2
3
4
5
6
7
8
9
对于指定的键,可以使用 NtEnumerateValueKey(枚举键值的原生API)来枚举该键下的所有值:
NTSTATUS NtEnumerateValueKey(
_In_ HANDLE KeyHandle,
_In_ ULONG Index,
_In_ KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
_Out_writes_bytes_opt_ (Length) PVOID KeyValueInformation,
_In_ ULONG Length,
_Out_ PULONG ResultLength);
2
3
4
5
6
7
其核心思想与键枚举类似。传入的 Index 应从 0 开始,只要返回的状态码不是 STATUS_NO_MORE_ENTRIES,就持续递增。获取的信息基于 KEY_VALUE_INFORMATION_CLASS(键值信息类别)及其关联的结构。
以下示例枚举指定键的所有值,并显示每个值的名称和数据:
NTSTATUS EnumerateKeyValues(HANDLE hKey)
{
ULONG len;
for (ULONG i = 0; ; i++)
{
// 第一次调用以获取所需缓冲区大小
auto status = NtEnumerateValueKey(hKey, i,
KeyValueFullInformation, nullptr , 0 , &len);
if (status == STATUS_NO_MORE_ENTRIES)
break;
auto buffer = std::make_unique<BYTE[]>(len);
// 执行实际的枚举调用
status = NtEnumerateValueKey(hKey, i,
KeyValueFullInformation, buffer .get(), len, &len);
if (!NT_SUCCESS(status))
continue;
auto info = (KEY_VALUE_FULL_INFORMATION*)buffer .get();
printf( "Name: %ws Type: %s (%u) Data Size: %u bytes Data: " ,
std::wstring(info->Name, info->NameLength / sizeof(WCHAR)) .c_str(),
RegistryTypeToString(info->Type), info->Type,
info->DataLength);
DisplayData(info);
}
return STATUS_SUCCESS;
}
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
RegistryTypeToString 函数返回注册表数据类型的字符串表示形式,DisplayData 函数则根据数据类型显示数据本身:
void DisplayData(KEY_VALUE_FULL_INFORMATION const* info)
{
auto p = (PBYTE)info + info->DataOffset;
switch (info->Type)
{
case REG_SZ:
case REG_EXPAND_SZ:
printf("%ws\n " , (PCWSTR)p);
break;
case REG_MULTI_SZ:
{
auto s = (PCWSTR)p;
while (*s)
{
printf( "%ws " , s);
s += wcslen(s) + 1;
}
printf( "\n " );
break;
}
case REG_DWORD:
printf("%u (0x%X)\n " , *(DWORD*)p, *(DWORD*)p);
break;
case REG_QWORD:
printf("%llu (0x%llX)\n " , *(ULONGLONG*)p, *(ULONGLONG*)p);
break;
case REG_BINARY:
case REG_FULL_RESOURCE_DESCRIPTOR:
case REG_RESOURCE_LIST:
case REG_RESOURCE_REQUIREMENTS_LIST:
auto len = min(64, info->DataLength);
for (DWORD i = 0; i < len; i++)
{
printf( "%02X " , p[i]);
}
printf( "\n " );
break;
}
}
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
完整的示例代码位于 EnumKeyValues 项目中,该项目的命令行接受一个键路径,并显示该键下的所有值。以下是部分示例输出:
c:\>EnumKeyValues \registry\machine\system\currentcontrolset\services\acpi
Name: ImagePath Type: REG_EXPAND_SZ (2) Data Size: 52 bytes Data: System32\drivers\ACPI.sys
Name: Type Type: REG_DWORD (4) Data Size: 4 bytes Data: 1 (0x1)
Name: Start Type: REG_DWORD (4) Data Size: 4 bytes Data: 0 (0x0)
Name: ErrorControl Type: REG_DWORD (4) Data Size: 4 bytes Data: 3 (0x3)
Name: DisplayName Type: REG_SZ (1) Data Size: 94 bytes Data: @acpi.inf,%ACPI.SvcDesc%;Microsoft ACPI Driver
Name: Owners Type: REG_MULTI_SZ (7) Data Size: 20 bytes Data: acpi.inf
Name: Tag Type: REG_DWORD (4) Data Size: 4 bytes Data: 2 (0x2)
Name: Group Type: REG_SZ (1) Data Size: 10 bytes Data: Core
2
3
4
5
6
7
8
9
C:\>EnumKeyValues "\registry\machine\software\microsoft\windows nt\currentversion"
Name: SystemRoot Type: REG_SZ (1) Data Size: 22 bytes Data: C:\Windows
Name: BaseBuildRevisionNumber Type: REG_DWORD (4) Data Size: 4 bytes Data: 1 (0x1)
Name: BuildBranch Type: REG_SZ (1) Data Size: 22 bytes Data: vb_release
Name: BuildGUID Type: REG_SZ (1) Data Size: 74 bytes Data: ffffffff-ffff-ffff-ffff-ffffffffffff
Name: BuildLab Type: REG_SZ (1) Data Size: 58 bytes Data: 19041.vb_release.191206-1406
Name: BuildLabEx Type: REG_SZ (1) Data Size: 80 bytes Data: 19041.1.amd64fre.vb_release.191206-1406
Name: CompositionEditionID Type: REG_SZ (1) Data Size: 22 bytes Data: Enterprise
Name: CurrentBuild Type: REG_SZ (1) Data Size: 12 bytes Data: 19045 ...
Name: DigitalProductId Type: REG_BINARY (3) Data Size: 164 bytes Data: A4 00 00 00 03 00 00 00 30 30 33 33 30 2D 35 30 30 30 30 2D 30 30 30 30 30 2D . . .
Name: DigitalProductId4 Type: REG_BINARY (3) Data Size: 1272 bytes Data: F8 04 00 00 04 00 00 00 30 00 33 00 36 00 31 00 32 00 2D 00 30 00 33 00 33 00 ...
Name: InstallTime Type: REG_QDWORD (11) Data Size: 8 bytes Data: 132760139614416808 (0x1D7A8A4C20C6FA8)
Name: DisplayVersion Type: REG_SZ (1) Data Size: 10 bytes Data: 22H2
Name: RegisteredOwner Type: REG_SZ (1) Data Size: 12 bytes Data: Pavel
Name: WinREVersion Type: REG_SZ (1) Data Size: 32 bytes Data: 10.0.19041.3920
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 13.4 键信息
可以通过 NtQueryKey(查询键信息的原生API)获取注册表键的详细信息,也可以通过 NtSetInformationKey(设置键信息的原生API)设置键的某些属性。
# 13.4.1 查询键信息
NtQueryKey 的声明如下:
NTSTATUS NtQueryKey(
_In_ HANDLE KeyHandle,
_In_ KEY_INFORMATION_CLASS KeyInformationClass,
_Out_writes_bytes_opt_ (Length) PVOID KeyInformation,
_In_ ULONG Length,
_Out_ PULONG ResultLength);
2
3
4
5
6
KeyHandle 是常规的键句柄:对于 KeyNameInformation(键名称信息)和 KeyHandleTagsInformation(键句柄标签信息),其访问掩码可以是任意非零值;对于其他所有信息类别,访问掩码需为 KEY_QUERY_VALUE(查询键值权限)。KeyInformation 是用于接收数据的指针,Length 是调用者期望的最大缓冲区大小。实际数据大小会通过 *ResultLength 返回。如果事先不知道数据大小,可以将 KeyInformation 设为 NULL、Length 设为 0,此时 *ResultLength 会返回所需的缓冲区大小。
KeyBasicInformation(键基本信息,枚举值为 0)是最简单的信息类别,对应我们之前接触过的 KEY_BASIC_INFORMATION 结构:
typedef struct _KEY_BASIC_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG NameLength;
WCHAR Name[1];
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
2
3
4
5
6
它提供了键的最后写入时间(以 1601 年 1 月 1 日为基准的 100 纳秒单位),以及存储在 Name 字段中的键名称;TitleIndex(标题索引)字段即使被显式设置为其他值,实际返回值似乎也始终为 0。
KeyNodeInformation(键节点信息,枚举值为 1)在上述基础上增加了为键设置的“类”(class)信息:
typedef struct _KEY_NODE_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG ClassOffset; // 相对于结构起始位置的偏移量
ULONG ClassLength;
ULONG NameLength;
WCHAR Name[1];
} KEY_NODE_INFORMATION, *PKEY_NODE_INFORMATION;
2
3
4
5
6
7
8
KeyFullInformation(键完整信息,枚举值为 2)对应 KEY_FULL_INFORMATION 结构:
typedef struct _KEY_FULL_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG ClassOffset;
ULONG ClassLength;
ULONG SubKeys;
ULONG MaxNameLen;
ULONG MaxClassLen;
ULONG Values;
ULONG MaxValueNameLen;
ULONG MaxValueDataLen;
WCHAR Class[1];
} KEY_FULL_INFORMATION, *PKEY_FULL_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
它提供的信息包括:类名称、该键下的最大名称长度、该键下的最大类长度、子键数量(SubKeys)、值的数量(Values)、该键下的最大值名称长度,以及该键下的最大值数据长度。
KeyInfo 示例项目会根据传入的键路径,利用上述信息类别显示键的相关信息。
以下是根据给定句柄显示信息的函数:
void DisplayInfo(HANDLE hKey)
{
BYTE buffer[1024];
ULONG len;
if (NT_SUCCESS(NtQueryKey(hKey, KeyBasicInformation,
buffer, sizeof(buffer), &len)))
{
auto info = (KEY_BASIC_INFORMATION*)buffer;
printf("名称(Name): %ws\n ",
std::wstring(info->Name, info->NameLength / sizeof(WCHAR)).c_str());
printf("写入时间(Write time): %s\n ", FormatTime(info->LastWriteTime).c_str());
}
if (NT_SUCCESS(NtQueryKey(hKey, KeyFullInformation,
buffer, sizeof(buffer), &len)))
{
auto info = (KEY_FULL_INFORMATION*)buffer;
if (info->ClassLength)
{
printf("类(Class): %ws\n ", std::wstring(
info->Class, info->ClassLength / sizeof(WCHAR)).c_str());
}
printf("子键(Subkeys): %u\n ", info->SubKeys);
printf("值项(Values): %u\n ", info->Values);
printf("最大类长度(Max class length): %u\n ", info->MaxClassLen);
printf("最大名称长度(Max name length): %u\n ", info->MaxNameLen);
printf("最大值项名称长度(Max value name length): %u\n ", info->MaxValueNameLen);
printf("最大数据长度(Max data length): %u\n ", info->MaxValueDataLen);
}
}
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
以下是一些输出示例:
c:\>keyinfo "\registry\machine\software\microsoft\windows nt\currentversion"
名称(Name): CurrentVersion
写入时间(Write time): 2024/06/07 22:19:21.716
子键(Subkeys): 100
值项(Values): 31
最大类长度(Max class length): 0
最大名称长度(Max name length): 68
最大值项名称长度(Max value name length): 50
最大数据长度(Max data length): 1272
2
3
4
5
6
7
8
9
c:\>keyinfo \registry\machine\system\currentcontrolset\services
名称(Name): Services
写入时间(Write time): 2024/06/09 23:16:27.020
子键(Subkeys): 937
值项(Values): 0
最大类长度(Max class length): 0
最大名称长度(Max name length): 108
最大值项名称长度(Max value name length): 0
最大数据长度(Max data length): 0
2
3
4
5
6
7
8
9
c:\>keyinfo \registry\machine\system\currentcontrolset\services\acpi
名称(Name): ACPI
写入时间(Write time): 2024/06/07 21:53:13.006
子键(Subkeys): 2
值项(Values): 8
最大类长度(Max class length): 0
最大名称长度(Max name length): 20
最大值项名称长度(Max value name length): 24
最大数据长度(Max data length): 94
2
3
4
5
6
7
8
9
KeyNameInformation(3)信息类需要搭配 KEY_NAME_INFORMATION 结构体使用:
typedef struct _KEY_NAME_INFORMATION
{
ULONG NameLength;
WCHAR Name[1];
} KEY_NAME_INFORMATION, *PKEY_NAME_INFORMATION;
2
3
4
5
该结构体仅返回键名(Key Name)。注意,返回的名称是完整键名(full key name)。这与 KEY_BASIC_INFORMATION 不同,后者返回的名称仅为键的最后一部分(final part)。
接下来是 KeyCachedInformation(4)。此信息类仅适用于预定义键(如 "\Registry\Machine"),并返回 KEY_CACHED_INFORMATION 结构体:
typedef struct _KEY_CACHED_INFORMATION
{
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG SubKeys;
ULONG MaxNameLen;
ULONG Values;
ULONG MaxValueNameLen;
ULONG MaxValueDataLen;
ULONG NameLength;
WCHAR Name[1];
} KEY_CACHED_INFORMATION, *PKEY_CACHED_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
该结构体包含的信息与 KEY_FULL_INFORMATION 类似,但缺少类名信息(class name information)和键名本身(name itself)。
KeyFlagsInformation(5)返回 KEY_FLAGS_INFORMATION 结构体:
typedef struct _KEY_FLAGS_INFORMATION
{
ULONG Wow64Flags;
ULONG KeyFlags;
ULONG ControlFlags;
} KEY_FLAGS_INFORMATION, *PKEY_FLAGS_INFORMATION;
2
3
4
5
6
KeyFlags 可以是 0,也可以是以下标志的组合:REG_FLAG_VOLATILE(=1,易失性键)和 REG_FLAG_LINK(=2,链接键)。ControlFlags 可以是 0,也可以是以下值的组合:REG_KEY_DONT_VIRTUALIZE(=2,不对键进行虚拟化)、REG_KEY_DONT_SILENT_FAIL(=4,强制访问检查失败,不执行真实检查)、REG_KEY_RECURSE_FLAG(=8,从父键获取属性)。Wow64Flags 用于用户标志,因此任何人都可以将这些标志用于任何用途。
KeyVirtualizationInformation(6)信息类返回虚拟化相关信息,对应 KEY_VIRTUALIZATION_INFORMATION 结构体:
typedef struct _KEY_VIRTUALIZATION_INFORMATION
{
// 键属于虚拟化命名空间范围(目前仅适用于 HKLM\Software)
ULONG VirtualizationCandidate: 1;
// 此键已启用虚拟化。仅当上述标志为 1 时,此标志才能为 1
ULONG VirtualizationEnabled: 1;
// 键是虚拟键。仅当上述两个标志均为 0 时,此标志才能为 1。仅对虚拟存储键句柄有效
ULONG VirtualTarget: 1;
// 键属于虚拟存储路径的一部分
ULONG VirtualStore: 1;
// 键曾被虚拟化过。仅当 VirtualizationCandidate 为 1 时,此标志才能为 1
ULONG VirtualSource: 1;
ULONG Reserved : 27;
} KEY_VIRTUALIZATION_INFORMATION, *PKEY_VIRTUALIZATION_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这些标志在 WDK(Windows 驱动程序工具包)的 ZwQueryKey 文档中有详细说明。上述注释总结了相关信息。
KeyHandleTagsInformation(7)信息类返回以下简单结构体:
typedef struct _KEY_HANDLE_TAGS_INFORMATION
{
ULONG HandleTags;
} KEY_HANDLE_TAGS_INFORMATION, *PKEY_HANDLE_TAGS_INFORMATION;
2
3
4
这个“标签(tags)”值用于存储键的额外信息,例如 KEY_WOW64_32KEY 标志。
KeyTrustInformation(8)信息类填充 KEY_TRUST_INFORMATION 结构体:
typedef struct _KEY_TRUST_INFORMATION
{
ULONG TrustedKey : 1;
ULONG Reserved : 31;
} KEY_TRUST_INFORMATION, *PKEY_TRUST_INFORMATION;
2
3
4
5
其中唯一的关键信息是 TrustedKey:对于计算机配置单元(machine hive)及其子键,该值似乎为 1;对于 \Registry\User 下的键,该值为 0。
最后,KeyLayerInformation(9)信息类填充 KEY_LAYER_INFORMATION 结构体:
typedef struct _KEY_LAYER_INFORMATION
{
ULONG IsTombstone : 1; // 不可修改的键(unchangeable key)
ULONG IsSupersedeLocal : 1; // 本地安全描述符替代父键(local Security descriptor supersedes parent)
ULONG IsSupersedeTree : 1; // 父键替代此键(parent supersedes this key)
ULONG ClassIsInherited : 1; // 类由子键继承(class inheirted by sub keys)
ULONG Reserved : 28;
} KEY_LAYER_INFORMATION, *PKEY_LAYER_INFORMATION;
2
3
4
5
6
7
8
# 13.4.2 设置键信息(Setting Key Information)
可以使用 NtSetInformationKey 函数设置键的特定信息:
typedef enum _KEY_SET_INFORMATION_CLASS
{
KeyWriteTimeInformation, // KEY_WRITE_TIME_INFORMATION
KeyWow64FlagsInformation, // KEY_WOW64_FLAGS_INFORMATION
KeyControlFlagsInformation, // KEY_CONTROL_FLAGS_INFORMATION
KeySetVirtualizationInformation, // KEY_SET_VIRTUALIZATION_INFORMATION
KeySetDebugInformation, // KEY_CONTROL_FLAGS_INFORMATION
KeySetHandleTagsInformation, // KEY_HANDLE_TAGS_INFORMATION
KeySetLayerInformation, // KEY_SET_LAYER_INFORMATION
MaxKeySetInfoClass
} KEY_SET_INFORMATION_CLASS;
NTSTATUS NtSetInformationKey(
_In_ HANDLE KeyHandle,
_In_ KEY_SET_INFORMATION_CLASS KeySetInformationClass,
_In_reads_bytes_(KeySetInformationLength) PVOID KeySetInformation,
_In_ ULONG KeySetInformationLength);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
KeyHandle 是要修改的键的句柄:对于 KeySetHandleTagsInformation,该句柄需要具备任意非零访问权限;对于其他信息类,需要具备 KEY_SET_VALUE 访问权限。下面我们逐一分析各个信息类。
KeyWriteTimeInformation(0)允许通过传入 KEY_WRITE_TIME_INFORMATION 结构体显式设置键的写入时间,该结构体本质上是一个 64 位值:
typedef struct _KEY_WRITE_TIME_INFORMATION
{
LARGE_INTEGER LastWriteTime;
} KEY_WRITE_TIME_INFORMATION, *PKEY_WRITE_TIME_INFORMATION;
2
3
4
KeyWow64FlagsInformation(1)信息类允许通过传入 32 位值来修改与键关联的“用户标志(user flags)”:
typedef struct _KEY_WOW64_FLAGS_INFORMATION
{
ULONG UserFlags;
} KEY_WOW64_FLAGS_INFORMATION, *PKEY_WOW64_FLAGS_INFORMATION;
2
3
4
KeyControlFlagsInformation(2)和 KeySetDebugInformation(4)信息类接受另一组单独存储在键信息中的标志:
typedef struct _KEY_CONTROL_FLAGS_INFORMATION
{
ULONG ControlFlags;
} KEY_CONTROL_FLAGS_INFORMATION, *PKEY_CONTROL_FLAGS_INFORMATION;
2
3
4
KeySetVirtualizationInformation(3)信息类用于存储键的虚拟化标志:
typedef struct _KEY_SET_VIRTUALIZATION_INFORMATION
{
ULONG VirtualTarget : 1;
ULONG VirtualStore : 1;
ULONG VirtualSource : 1; // 如果键至少被虚拟化过一次,则为 true
ULONG Reserved : 29;
} KEY_SET_VIRTUALIZATION_INFORMATION, *PKEY_SET_VIRTUALIZATION_INFORMATION;
2
3
4
5
6
7
KeySetHandleTagsInformation(5)需要传入另一个 32 位值,用于设置键的“标签(tags)”。内核内部会使用此值。支持的有效标志包括:
- WOW64_HANDLE_REFLECTED(1)- Windows 7 之前的反射键
- WOW64_HANDLE_64KEY(0x100,与 KEY_WOW64_64KEY 相同)- 键以 64 位模式打开
- WOW64_HANDLE_32KEY(0x200,与 KEY_WOW64_32KEY 相同)- 键以 32 位模式打开
- WOW64_HANDLE_FINAL(0x400)- 子键不会被重定向
- WOW64_HANDLE_WOW6432NODE(0x800)- 键被显式打开为 Wow6432Node
最后,KeySetLayerInformation 信息类允许设置 KEY_SET_LAYER_INFORMATION 结构体中的标志(参见 NtQueryKey 的相关讨论)。
# 13.5 其他注册表函数(Other Registry Functions)
本节将介绍其他原生注册表 API(native Registry APIs)。
可以使用 NtRenameKey 函数重命名现有键:
NTSTATUS NtRenameKey(
_In_ HANDLE KeyHandle,
_In_ PUNICODE_STRING NewName);
2
3
要使调用成功,KeyHandle 必须具备 KEY_WRITE 访问掩码。新名称(NewName)必须是简单名称——即不包含路径分隔符(反斜杠)。
使用 NtDeleteValueKey 函数可以删除键中的值项:
NTSTATUS NtDeleteValueKey(
_In_ HANDLE KeyHandle,
_In_ PUNICODE_STRING ValueName);
2
3
KeyHandle 必须具备 KEY_SET_VALUE 访问掩码。如果 ValueName 对应的 value 存在,则会被删除。要删除整个键,请调用 NtDeleteKey 函数:
NTSTATUS NtDeleteKey(_In_ HANDLE KeyHandle);
KeyHandle 必须具备 DELETE 访问掩码。只有当该键的最后一个句柄被关闭后,该键才会被完全删除。NtFlushKey 函数会强制将键的信息和值项写入磁盘:
NTSTATUS NtFlushKey(_In_ HANDLE KeyHandle);
NtCompactKeys 函数会将指定的键在内部放入同一个存储桶:
NTSTATUS NtCompactKeys(
_In_ ULONG Count,
_In_reads_(Count) HANDLE KeyArray[]);
2
3
这些句柄必须具备 KEY_WRITE 访问掩码,且必须属于同一个配置单元(hive)(例如计算机配置单元)。在实际应用中,这个 API 似乎没有太多用途。
NtCompressKey 函数尝试压缩指定的键(该键必须是配置单元根键):
NTSTATUS NtCompressKey(_In_ HANDLE Key);
# 13.6 键持久化(Key Persistence)
可以使用 NtSaveKey 或 NtSaveKeyEx 函数将键持久化(保存)到文件中:
NTSTATUS NtSaveKey(
_In_ HANDLE KeyHandle,
_In_ HANDLE FileHandle);
NTSTATUS NtSaveKeyEx(
_In_ HANDLE KeyHandle,
_In_ HANDLE FileHandle,
_In_ ULONG Format);
2
3
4
5
6
7
8
调用者必须在其令牌(token)中启用 SeBackupPrivilege 权限。默认情况下,管理员拥有此权限。
KeyHandle 是要写入文件的键——该键本身及其所有子键都会被包含在内。FileHandle 必须是具备 FILE_WRITE_DATA 访问掩码的文件句柄。NtSaveKey 函数会调用 NtSaveKeyEx 函数,并将 Format 参数设置为 REG_STANDARD_FORMAT(1)。其他 Format 值包括 REG_LATEST_FORMAT(2)和 REG_NO_COMPRESSION(4)。有关更多信息,请参阅 Windows API RegSaveKeyEx 的文档。
注册表编辑器(RegEdit)和 TotalRegistry 支持的 REG 文件格式并非真正受支持的格式。该格式是这些工具私下实现的。NtSaveKeyEx 支持的格式是二进制格式(binary formats),而非文本格式(textual),目的是使数据尽可能小。
可以使用 NtRestoreKey 函数恢复已保存的键:
NTSTATUS NtRestoreKey(
_In_ HANDLE KeyHandle,
_In_ HANDLE FileHandle,
_In_ ULONG Flags);
2
3
4
调用者必须在其令牌中启用 SeRestorePrivilege 权限。KeyHandle 对应的键会被文件(FileHandle 指定)中的信息覆盖。有关更多信息,请参阅 Windows API RegRestoreKey 的文档。
有多个函数允许从文件加载注册表数据,并将其附加到目标键:
NTSTATUS NtLoadKey(
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_ POBJECT_ATTRIBUTES SourceFile);
NTSTATUS NtLoadKey2(
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_ POBJECT_ATTRIBUTES SourceFile,
_In_ ULONG Flags);
NTSTATUS NtLoadKeyEx(
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_ POBJECT_ATTRIBUTES SourceFile,
_In_ ULONG Flags,
_In_opt_ HANDLE TrustClassKey, // Win10+
_In_opt_ HANDLE Event,
_In_opt_ ACCESS_MASK DesiredAccess,
_Out_opt_ PHANDLE RootHandle,
_Out_opt_ PIO_STATUS_BLOCK IoStatus);
NTSTATUS NtLoadKey3( // Windows 20H1+
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_ POBJECT_ATTRIBUTES SourceFile,
_In_ ULONG Flags,
_In_reads_(ExtParamCount) PCM_EXTENDED_PARAMETER ExtendedParameters,
_In_ ULONG ExtParamCount,
_In_opt_ ACCESS_MASK DesiredAccess,
_Out_opt_ PHANDLE RootHandle,
_Out_opt_ PIO_STATUS_BLOCK IoStatus);
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
这些 API 的调用者需要具备 SeRestorePrivilege 权限。
NtLoadKey* 系列 API 与 NtRestoreKey 的核心区别在于:NtRestoreKey 从文件读取数据并将其集成到注册表中,之后该文件不再被使用或需要;而 NtLoadKey* 函数将提供的文件用作键数据的底层存储(underlying backing store)。
Windows API RegLoadKey 会调用 NtLoadKey 函数。
NtLoadKey函数会调用NtLoadKeyEx函数,传入提供的目标键(TargetKey)和源文件(SourceFile),其余参数均设为零/空(zero/NULL)。
目标键(TargetKey)必须是 \Registry\Machine 或 \Registry\User。源文件(SourceFile)是之前通过NtSaveKey(或NtSaveKeyEx)保存的已存储键。使用NtLoadKey时,只需提供这些信息即可。
随附的示例项目SaveKey和LoadKey提供了使用NtSaveKey和NtLoadKey的标准示例。
扩展的NtLoadKey*系列函数提供了更多选项。标志(Flags)参数可以是零,也可以是多个标志的组合,部分标志描述如下:
- REG_NO_LAZY_FLUSH(值为4)——不延迟刷新此配置单元(hive)。
- REG_APP_HIVE(值为0x10)——加载配置单元后,仅调用进程(calling process)可见。
- REG_PROCESS_PRIVATE(值为0x20)——该配置单元被某个进程使用期间,其他任何进程都无法挂载(mount)它。
- REG_APP_HIVE_OPEN_READ_ONLY(值为0x2000)——以只读模式(read only mode)打开配置单元。
- REG_NO_IMPERSONATION_FALLBACK(值为0x8000)——如果文件访问检查(file access check)失败,不尝试回退到模拟调用者(impersonating the caller)。
Windows API中的RegLoadAppKey函数会调用NtLoadKeyEx,传入标志REG_APP_HIVE,还可选择性传入REG_PROCESS_PRIVATE。它使用
\Registry\A子键(subkey)并搭配随机全局唯一标识符(GUID)作为加载根目录(root of the load)。
扩展函数中的其他参数描述如下:
- TrustClassKey:可选的键句柄(key handle),从该句柄获取配置单元键(hive key)的可信/不可信属性(trusted/not trusted property)。
- Event:可选的事件句柄(event handle),配置单元卸载时会发出信号(signaled)。
- DesiredAccess:如果在下一个参数中指定,可为返回的键设置可选访问权限(optional access)。
- RootHandle:新配置单元键的可选返回句柄(optional returned handle)。
- IoStatus:目前未使用。
- ExtendedParameters和ExtParamCount表示更多自定义选项,未来可能会扩展。目前它们支持NtLoadKeyEx可指定的参数:
typedef enum CM_EXTENDED_PARAMETER_TYPE {
CmExtendedParameterInvalidType,
CmExtendedParameterTrustClassKey,
CmExtendedParameterEvent,
CmExtendedParameterFileAccessToken,
CmExtendedParameterMax
} CM_EXTENDED_PARAMETER_TYPE, *PCM_EXTENDED_PARAMETER_TYPE;
#define CM_EXTENDED_PARAMETER_TYPE_BITS 8
typedef struct DECLSPEC_ALIGN(8) CM_EXTENDED_PARAMETER {
struct {
ULONG64 Type : CM_EXTENDED_PARAMETER_TYPE_BITS;
ULONG64 Reserved : 64 - CM_EXTENDED_PARAMETER_TYPE_BITS;
};
union {
ULONG64 ULong64;
PVOID Pointer;
SIZE_T Size;
HANDLE Handle;
ULONG ULong;
ACCESS_MASK AccessMask;
};
} CM_EXTENDED_PARAMETER, *PCM_EXTENDED_PARAMETER;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
键加载后,可通过以下函数之一卸载:
NTSTATUS NtUnloadKey(_In_ POBJECT_ATTRIBUTES TargetKey);
NTSTATUS NtUnloadKey2(
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_ ULONG Flags);
NTSTATUS NtUnloadKeyEx(
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_opt_ HANDLE Event);
2
3
4
5
6
7
TargetKey是存储在OBJECT_ATTRIBUTES中的键路径(key path)。如果配置单元当前正被某个应用程序(app)使用,NtUnloadKey会失败。NtUnloadKey2允许将标志(Flags)设为REG_FORCE_UNLOAD(值为1),即使配置单元当前在使用中也会强制卸载。NtUnloadKeyEx允许指定事件对象句柄(event object handle),卸载完成时该句柄会发出信号。NtUnloadKeyEx还会设置“延迟卸载(late unload)”,即如果当前无法完成卸载,会在未来配置单元不再被使用时完成卸载。
# 13.7 注册表通知(Registry Notifications)
注册表(Registry)支持通过NtNotifyChangeKey和NtNotifyChangeMultipleKeys函数,针对某个(或多个)键的各类更改发送通知:
NTSTATUS NtNotifyChangeKey(
_In_ HANDLE KeyHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG CompletionFilter,
_In_ BOOLEAN WatchTree,
_Out_writes_bytes_opt_(BufferSize) PVOID Buffer,
_In_ ULONG BufferSize,
_In_ BOOLEAN Asynchronous);
NTSTATUS NtNotifyChangeMultipleKeys(
_In_ HANDLE MasterKeyHandle,
_In_opt_ ULONG Count,
_In_reads_opt_(Count) OBJECT_ATTRIBUTES SubordinateObjects[],
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG CompletionFilter,
_In_ BOOLEAN WatchTree,
_Out_writes_bytes_opt_(BufferSize) PVOID Buffer,
_In_ ULONG BufferSize,
_In_ BOOLEAN Asynchronous);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这些API由Windows API中的RegNotifyChangeKeyValue函数调用。NtNotifyChangeKey会传入单个键句柄(single key handle)调用NtNotifyChangeMultipleKeys。
KeyHandle(以及MasterKeyHandle)是要监控的根键(root key),必须具备REG_NOTIFY访问掩码(access mask)。Event是可选的事件句柄,当有通知可用时会发出信号。这仅适用于异步调用(Asynchronous设为TRUE)。ApcRoutine是可选的异步过程调用(APC,Asynchronous Procedure Call),当有通知可用时会排入调用线程(calling thread)的队列。如果指定了该参数,ApcContext可设为任何值,并会传递给APC函数。IoStatusBlock是输出结构(output structure),仅存储调用状态(status of the call)。实际上它并无太多实用价值(同步调用时会被忽略,即Synchronous设为TRUE),但必须提供。
完成筛选器(CompletionFilter)指定要监控的通知类型。可能的值包括REG_NOTIFY_CHANGE_NAME(子键添加/删除,subkeys added/deleted)、REG_NOTIFY_CHANGE_ATTRIBUTES(属性更改)、REG_NOTIFY_CHANGE_LAST_SET(值添加/修改/删除,value added/modified/deleted)、REG_NOTIFY_CHANGE_SECURITY(安全描述符更改,security descriptor changed)以及REG_NOTIFY_THREAD_AGNOSTIC(表示任何线程都可等待提供的事件,而非仅调用线程)。更多信息请参阅Windows API的RegNotifyChangeKeyValue文档。
WatchTree指定仅监控提供的键(FALSE)还是整个子键树(TRUE,entire subkey)。Buffer和BufferSize看似表明通知可用时会返回详细信息(REG_NOTIFY_INFORMATION结构),但在实现中从未使用该缓冲区。Windows API的RegNotifyChangeKeyValue函数没有此类缓冲区这一事实也暗示了这一点。最终结果是,调用者需要自行确定哪个键/值/属性发生了更改。
通过SubordinateObjects,可以添加更多非MasterKeyHandle后代(descendants)的键。但目前实现仅支持一个此类对象,即Count最多为1。如果Count为0,NtNotifyChangeMultipleKeys与NtNotifyChangeKey功能相同。
实际上,这些通知的实用性有限,因为它们不提供更改的详细信息。要从用户模式(user mode)获取注册表通知,更推荐使用Windows事件跟踪(ETW,Event Tracing for Windows)及相关提供程序(providers),例如传统内核提供程序(classic kernel provider)。(ETW超出本书范围。)
# 13.8 注册表事务(Registry Transactions)
注册表支持符合经典ACID属性(原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability)的事务性操作(transactional operations)。这些操作甚至可与NTFS事务配合使用。事务中的操作保证要么全部成功,要么全部失败。
要开始使用事务,可在事务范围内打开或创建键:
NTSTATUS NtCreateKeyTransacted(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Reserved_ ULONG TitleIndex,
_In_opt_ PUNICODE_STRING Class,
_In_ ULONG CreateOptions,
_In_ HANDLE TransactionHandle,
_Out_opt_ PULONG Disposition);
NTSTATUS NtOpenKeyTransacted(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE TransactionHandle);
NTSTATUS NtOpenKeyTransactedEx(
_Out_ PHANDLE KeyHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ ULONG OpenOptions,
_In_ HANDLE TransactionHandle);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这些API是本章前文介绍的函数的扩展版本,唯一区别是增加了事务对象句柄(transaction object handle)。获取事务有两种方式。第一种是通过NtCreateTransaction创建“通用”事务:
NTSTATUS NtCreateTransaction(
_Out_ PHANDLE TransactionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ LPGUID Uow,
_In_opt_ HANDLE TmHandle,
_In_opt_ ULONG CreateOptions,
_In_opt_ ULONG IsolationLevel,
_In_opt_ ULONG IsolationFlags,
_In_opt_ PLARGE_INTEGER Timeout,
_In_opt_ PUNICODE_STRING Description);
2
3
4
5
6
7
8
9
10
11
事务相关API超出本章范围,但Windows API的CreateTransaction函数会调用NtCreateTransaction——更多细节请参阅该函数文档。
同理,可调用NtOpenTransaction根据事务名称(为GUID)获取现有事务:
NTSTATUS NtOpenTransaction(
_Out_ PHANDLE TransactionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ LPGUID Uow,
_In_opt_ HANDLE TmHandle);
2
3
4
5
6
更多细节请参阅OpenTransaction函数文档。
第二种获取专用于注册表操作的事务的方式,在Windows 10 1607版本及后续版本中可用,相关API如下:
NTSTATUS NtCreateRegistryTransaction(
_Out_ HANDLE *RegistryTransactionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjAttributes,
_Reserved_ ULONG CreateOptions);
NTSTATUS NtOpenRegistryTransaction(
_Out_ HANDLE *RegistryTransactionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjAttributes);
2
3
4
5
6
7
8
9
NtCreateRegistryTransaction创建新的注册表事务对象(可通过常规的ObjectAttributes命名),并返回该新事务的句柄。NtOpenRegistryTransaction打开指定名称的现有注册表事务对象的句柄。
注册表事务(Registry Transaction)是一种内核对象类型(在工具中显示为“RegistryTransaction”类型)。
获取注册表事务后,可将该事务句柄传入NtOpenKeyTransacted及上述其他API进行调用。
对事务中使用的键执行各种更改后,必须提交(committed)或中止(aborted,即回滚rolled back)事务:
NTSTATUS NtCommitRegistryTransaction(
_In_ HANDLE RegistryTransactionHandle,
_Reserved_ ULONG Flags);
NTSTATUS NtRollbackRegistryTransaction(
_In_ HANDLE RegistryTransactionHandle,
_Reserved_ ULONG Flags);
2
3
4
5
6
如果在提交事务前关闭事务句柄,事务会被中止。
以下示例展示了如何使用这些API(省略错误处理):
//
// 初始化键名称和属性
//
UNICODE_STRING keyName;
RtlInitUnicodeString(&keyName, // 示例键
L"\\REGISTRY\\USER\\S-1-5-21-3456612-33973779-3838822-1001\\Zebra");
OBJECT_ATTRIBUTES keyAttr;
InitializeObjectAttributes(&keyAttr, &keyName, 0, nullptr, nullptr);
//
// 创建命名事务(本示例中可不指定名称)
//
HANDLE hTrans;
OBJECT_ATTRIBUTES txAttr;
UNICODE_STRING txName;
RtlInitUnicodeString(&txName, L"\\BaseNamedObjects\\MyTransaction");
InitializeObjectAttributes(&txAttr, &txName, OBJ_OPENIF, nullptr, nullptr);
NtCreateRegistryTransaction(&hTrans, TRANSACTION_ALL_ACCESS, &txAttr, 0);
//
// 在事务中创建新键
//
HANDLE hKey;
NtCreateKeyTransacted(&hKey, KEY_WRITE, &keyAttr, 0, nullptr, 0, hTrans, nullptr);
//
// 在新键中设置值
//
UNICODE_STRING valueName;
RtlInitUnicodeString(&valueName, L"SecretToUniverse");
int data = 42; // 示例值
NtSetValueKey(hKey, &valueName, 0, REG_DWORD, &data, sizeof(data));
//
// 提交事务
//
NtCommitRegistryTransaction(hTrans, 0);
NtClose(hKey);
NtClose(hTrans);
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
# 13.9 杂项函数(Miscellaneous Functions)
本节将介绍其他一些注册表原生 API(Registry native APIs)。
NtQueryOpenSubKeys 函数返回指定配置单元(hive)下已打开的键(keys)的句柄(handle)数量:
NTSTATUS NtQueryOpenSubKeys(
_In_ POBJECT_ATTRIBUTES TargetKey,
_Out_ PULONG HandleCount);
2
3
TargetKey 是要查询其配置单元的键,该键无需是根配置单元键(root hive key)。例如,\Registry\Machine\Software 是根配置单元键,但 \Registry\Machine\Software\Microsoft 不是,不过它仍属于同一个配置单元。
要查看注册表的配置单元,可查看键 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\hivelist。TotalRegistry 工具在 Locations\Hive List 菜单项中提供了该键的快速访问方式。
调用者必须能够以 KEY_READ 访问权限(access)打开该键,函数才能正常工作。如果调用成功,*HandleCount 将返回该配置单元内所有键的已打开句柄数量。
若要获取这些键的更多信息,可使用扩展函数:
NTSTATUS NtQueryOpenSubKeysEx(
_In_ POBJECT_ATTRIBUTES TargetKey,
_In_ ULONG BufferLength,
_Out_writes_bytes_opt_ (BufferLength) PVOID Buffer,
_Out_ PULONG RequiredSize);
2
3
4
5
调用此函数要求调用者的令牌(token)中已启用 SeBackupPrivilege 权限(privilege)。返回的信息通过以下结构体(structures)提供:
typedef struct _KEY_PID_ARRAY
{
HANDLE ProcessId;
UNICODE_STRING KeyName;
} KEY_PID_ARRAY, *PKEY_PID_ARRAY;
typedef struct _KEY_OPEN_SUBKEYS_INFORMATION
{
ULONG Count;
KEY_PID_ARRAY KeyArray[1];
} KEY_OPEN_SUBKEYS_INFORMATION, *PKEY_OPEN_SUBKEYS_INFORMATION;
2
3
4
5
6
7
8
9
10
11
对于每个句柄,我们会获取到进程 ID(Process ID)和完整的键名(full key name)。典型用法是调用两次函数:第一次获取所需的缓冲区大小(needed size),第二次获取实际数据(actual data)。
KeyHandles 示例项目展示了如何使用这些 API。首先,我们获取键名:
int wmain(int argc, const wchar_t* argv[])
{
if (argc < 2)
{
printf("Usage: KeyHandles <key>\n " );
return 0;
}
UNICODE_STRING keyName;
RtlInitUnicodeString(&keyName, argv[1]);
OBJECT_ATTRIBUTES keyAttr;
InitializeObjectAttributes(&keyAttr, &keyName, 0 , nullptr , nullptr);
2
3
4
5
6
7
8
9
10
11
12
13
接下来,我们先使用基础函数,因为它不需要特殊权限:
ULONG count;
auto status = NtQueryOpenSubKeys(&keyAttr, &count);
if (NT_SUCCESS(status))
{
printf("Handle count: %u\n " , count);
2
3
4
5
如果基础函数调用成功,我们可以尝试使用扩展函数。首先启用所需权限:
BOOLEAN wasEnabled;
status = RtlAdjustPrivilege(SE_RESTORE_PRIVILEGE, TRUE, FALSE, &wasEnabled);
if ( !NT_SUCCESS(status))
{
printf("Failed to enable Restore privilege . Launching with admin rights may help\ .\n " );
return status;
}
2
3
4
5
6
7
接下来进行第一次调用。缓冲区的最小大小必须为 sizeof(KEY_OPEN_SUBKEYS_INFORMATION),函数才能调用成功。这意味着即使缓冲区不足以容纳所有信息,也会始终返回句柄计数(count):
ULONG needed;
KEY_OPEN_SUBKEYS_INFORMATION dummy;
status = NtQueryOpenSubKeysEx(&keyAttr,
sizeof(dummy), &dummy, &needed);
// 分配缓冲区(适当增大,以防在此期间有新句柄创建)
auto buffer = std::make_unique<BYTE[]>(needed += 1024);
2
3
4
5
6
最后,我们可以执行实际调用并显示结果:
status = NtQueryOpenSubKeysEx(&keyAttr, needed, buffer .get(), &needed);
if (NT_SUCCESS(status))
{
auto info = (KEY_OPEN_SUBKEYS_INFORMATION*)buffer .get();
for (ULONG i = 0; i < info->Count; i++)
{
printf("PID: %7u Key: %wZ\n " ,
HandleToULong(info->KeyArray[i].ProcessId),
&info->KeyArray[i].KeyName);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
NtFreezeRegistry 函数能够在指定的秒数内阻止对注册表的任何修改:
NTSTATUS NtFreezeRegistry(_In_ ULONG TimeOutInSeconds);
超时时间(time out)不能超过 900 秒(15 分钟)。调用者必须在其令牌中启用 SeBackupPrivilege 权限。若要在超时前解冻注册表,可调用 NtThawRegistry 函数:
NTSTATUS NtThawRegistry();
此外,还有 NtLockRegistryKey 函数,它可以阻止对指定键的修改,但该 API 仅在 kernel 模式(kernel mode)下调用时有效。
# 13.10 高级注册表辅助函数(Higher Level Registry Helpers)
Ntdll(和内核,kernel)提供了一些高级函数,用于处理常见的注册表操作。通常,使用原生注册表 API 编写的代码较为繁琐,而这些辅助函数可以简化代码并缩短代码长度。其中许多函数在 WDK(Windows Driver Kit)中有文档说明。
创建注册表键可通过 RtlCreateRegistryKey 函数实现:
NTSTATUS RtlCreateRegistryKey( _In_ ULONG RelativeTo,
_In_ PWSTR Path);
2
RelativeTo 是一组值之一,用于指示 Path 应“相对于什么”进行解释:
#define RTL_REGISTRY_ABSOLUTE 0
// \Registry\Machine\System\CurrentControlSet\Services
#define RTL_REGISTRY_SERVICES 1
// \Registry\Machine\System\CurrentControlSet\Control
#define RTL_REGISTRY_CONTROL 2
// \Registry\Machine\Software\Microsoft\Windows NT\CurrentVersion
#define RTL_REGISTRY_WINDOWS_NT 3
// \Registry\Machine\Hardware\DeviceMap
#define RTL_REGISTRY_DEVICEMAP 4
// \Registry\User\CurrentUser
#define RTL_REGISTRY_USER 5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
该函数创建键后,若调用成功,不会返回该键的任何已打开句柄。
RtlCheckRegistryKey 函数用于检查键是否存在:
NTSTATUS RtlCheckRegistryKey(
_In_ ULONG RelativeTo,
_In_ PWSTR Path);
2
3
如果键存在,函数返回 STATUS_SUCCESS。
以下是其他一些辅助函数的列表,由于它们在 WDK 中有详细文档说明,本节不再赘述。
查询多个值可能比较繁琐,RtlQueryRegistryValues 函数有助于简化这一操作,尤其是在需要查询大量键/值时:
NTSTATUS RtlQueryRegistryValues( _In_ ULONG RelativeTo,
_In_ PCWSTR Path,
_In_ PRTL_QUERY_REGISTRY_TABLE QueryTable,
_In_ PVOID Context,
_In_opt_ PVOID Environment);
2
3
4
5
使用 RtlWriteRegistryValue 函数可以快速写入值,无需获取键句柄、构建值名称(value name)等操作:
NTSTATUS RtlWriteRegistryValue( _In_ ULONG RelativeTo,
_In_ PCWSTR Path,
_In_ PCWSTR ValueName,
_In_ ULONG ValueType,
_In_ PVOID ValueData,
_In_ ULONG ValueLength);
2
3
4
5
6
其他值得一提的函数包括 RtlDeleteRegistryValue 和 RtlOpenCurrentUser(无需查询用户的 SID 即可打开当前用户的配置单元键)。
# 13.11 总结
许多应用程序(applications)和服务(services)都需要与注册表交互。原生 API 为此提供了强大的函数,涵盖了从创建和打开键、枚举键和值(keys and values enumeration)到事务(transactions)等各类操作。