CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Windows Native API编程 专栏说明
  • 第1章 原生API(Native API)开发入门
  • 第2章 原生API(Native API)基础
  • 第3章 原生应用程序(Native Applications)
  • 第4章:系统信息
  • 第5章:进程
  • 第6章:线程
  • 第7章:对象与句柄
  • 第 8 章:内存(第一部分)
  • 第9章:I/O
  • 第10章:ALPC
  • 第11章 安全性(Security)
  • 第12章 内存(第二部分)
  • 第13章 注册表
    • 13.1 注册表结构
    • 13.2 创建和打开键
      • 13.2.1 创建键
    • 13.3 键和值的操作
      • 13.3.1 枚举键和值
    • 13.4 键信息
      • 13.4.1 查询键信息
      • 13.4.2 设置键信息(Setting Key Information)
    • 13.5 其他注册表函数(Other Registry Functions)
    • 13.6 键持久化(Key Persistence)
    • 13.7 注册表通知(Registry Notifications)
    • 13.8 注册表事务(Registry Transactions)
    • 13.9 杂项函数(Miscellaneous Functions)
    • 13.10 高级注册表辅助函数(Higher Level Registry Helpers)
    • 13.11 总结
目录

第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);
1
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);
1
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);
1
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);
1
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);
1
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;
1
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;
1
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);
}
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

在笔者的系统中,输出结果如下:

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);
1
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));
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

# 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);
1
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;
1
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;
1
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;
}
1
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);
}
1
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);
1
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;
}
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

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;
    }
}
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

完整的示例代码位于 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
1
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
1
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);
1
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;
1
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;
1
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;
1
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);
    }
}
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

以下是一些输出示例:

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
1
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
1
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
1
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;
1
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;
1
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;
1
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;
1
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;
1
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;
1
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;
1
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);
1
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;
1
2
3
4

KeyWow64FlagsInformation(1)信息类允许通过传入 32 位值来修改与键关联的“用户标志(user flags)”:

typedef struct _KEY_WOW64_FLAGS_INFORMATION
{
    ULONG UserFlags;
} KEY_WOW64_FLAGS_INFORMATION, *PKEY_WOW64_FLAGS_INFORMATION;
1
2
3
4

KeyControlFlagsInformation(2)和 KeySetDebugInformation(4)信息类接受另一组单独存储在键信息中的标志:

typedef struct _KEY_CONTROL_FLAGS_INFORMATION
{
    ULONG ControlFlags;
} KEY_CONTROL_FLAGS_INFORMATION, *PKEY_CONTROL_FLAGS_INFORMATION;
1
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;
1
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);
1
2
3

要使调用成功,KeyHandle 必须具备 KEY_WRITE 访问掩码。新名称(NewName)必须是简单名称——即不包含路径分隔符(反斜杠)。

使用 NtDeleteValueKey 函数可以删除键中的值项:

NTSTATUS NtDeleteValueKey(
    _In_ HANDLE KeyHandle,
    _In_ PUNICODE_STRING ValueName);
1
2
3

KeyHandle 必须具备 KEY_SET_VALUE 访问掩码。如果 ValueName 对应的 value 存在,则会被删除。要删除整个键,请调用 NtDeleteKey 函数:

NTSTATUS NtDeleteKey(_In_ HANDLE KeyHandle);
1

KeyHandle 必须具备 DELETE 访问掩码。只有当该键的最后一个句柄被关闭后,该键才会被完全删除。NtFlushKey 函数会强制将键的信息和值项写入磁盘:

NTSTATUS NtFlushKey(_In_ HANDLE KeyHandle);
1

NtCompactKeys 函数会将指定的键在内部放入同一个存储桶:

NTSTATUS NtCompactKeys(
    _In_ ULONG Count,
    _In_reads_(Count) HANDLE KeyArray[]);
1
2
3

这些句柄必须具备 KEY_WRITE 访问掩码,且必须属于同一个配置单元(hive)(例如计算机配置单元)。在实际应用中,这个 API 似乎没有太多用途。

NtCompressKey 函数尝试压缩指定的键(该键必须是配置单元根键):

NTSTATUS NtCompressKey(_In_ HANDLE Key);
1

# 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);
1
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);
1
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);
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

这些 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;
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

键加载后,可通过以下函数之一卸载:

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);
1
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);
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

这些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);
1
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);
1
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);
1
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);
1
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);
1
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);
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

# 13.9 杂项函数(Miscellaneous Functions)

本节将介绍其他一些注册表原生 API(Registry native APIs)。

NtQueryOpenSubKeys 函数返回指定配置单元(hive)下已打开的键(keys)的句柄(handle)数量:

NTSTATUS  NtQueryOpenSubKeys(
_In_  POBJECT_ATTRIBUTES  TargetKey,
_Out_  PULONG  HandleCount);
1
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);
1
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;
1
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);
1
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);
1
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;
        }
1
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);
1
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);
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12

NtFreezeRegistry 函数能够在指定的秒数内阻止对注册表的任何修改:

NTSTATUS  NtFreezeRegistry(_In_  ULONG  TimeOutInSeconds);
1

超时时间(time out)不能超过 900 秒(15 分钟)。调用者必须在其令牌中启用 SeBackupPrivilege 权限。若要在超时前解冻注册表,可调用 NtThawRegistry 函数:

NTSTATUS  NtThawRegistry();
1

此外,还有 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);
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

该函数创建键后,若调用成功,不会返回该键的任何已打开句柄。

RtlCheckRegistryKey 函数用于检查键是否存在:

NTSTATUS  RtlCheckRegistryKey(
_In_  ULONG  RelativeTo,
_In_  PWSTR  Path);
1
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);
1
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);
1
2
3
4
5
6

其他值得一提的函数包括 RtlDeleteRegistryValue 和 RtlOpenCurrentUser(无需查询用户的 SID 即可打开当前用户的配置单元键)。

# 13.11 总结

许多应用程序(applications)和服务(services)都需要与注册表交互。原生 API 为此提供了强大的函数,涵盖了从创建和打开键、枚举键和值(keys and values enumeration)到事务(transactions)等各类操作。

第12章 内存(第二部分)

← 第12章 内存(第二部分)

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