Windows NT和Windows 98把配置信息和其它重要信息都记录到注册表(registry)中。WDM驱动程序可以使用表3-9列出的函数访问注册表。如果你在用户模式编程中曾涉及过注册表访问,你可能会猜出如何在驱动程序中使用这些函数。然而这些内核模式中的支持函数确有些不同,我想有必要描述一下它们的用法。
表3-9. 注册表访问函数
服务函数 | 描述 |
---|---|
IoOpenDeviceRegistryKey | 打开PDO专用键 |
IoOpenDeviceInterfaceRegistryKey | 打开与注册设备接口相连的键 |
RtlDeleteRegistryValue | 删除一个注册表值 |
RtlQueryRegistryValues | 从注册表中读取多个值 |
RtlWriteRegistryValue | 向注册表写一个值 |
ZwClose | 关闭注册表键句柄 |
ZwCreateKey | 创建一个注册表键 |
ZwDeleteKey | 删除一个注册表键 |
ZwEnumerateKey | 枚举子键 |
ZwEnumerateValueKey | 枚举某注册表键中的值 |
ZwFlushKey | 把注册表更改提交到磁盘 |
ZwOpenKey | 打开一个注册表键 |
ZwQueryKey | 取关于某注册表键的信息 |
ZwQueryValueKey | 取某个注册表键中的值 |
ZwSetValueKey | 置某个注册表键中的值 |
在这一节,我将描述ZwXxx函数族和RtlDeleteRegistryValue函数,这些函数基本上可以满足大部分WDM驱动程序的需要。
打开注册表键
在读注册表的某个值之前,你需要先打开包含该值的键。用ZwOpenKey函数可以打开一个已存在的键。ZwCreateKey函数既可以打开已存在键又可以创建新键。这两个函数都需要事先用键名以及某些其它信息初始化一个OBJECT_ATTRIBUTES结构。OBJECT_ATTRIBUTES结构的声明如下:
typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor; PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES; |
除了手工初始化这个结构外,你还可以调用InitializeObjectAttributes宏来初始化它。
例如,假设我们想要打开驱动程序的服务键。它的键名我们可以从I/O管理器传递给DriverEntry函数的一个参数中得到。所以,我们的代码如下:
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { ... OBJECT_ATTRIBUTES oa; InitializeObjectAttributes(&oa, RegistryPath, 0, NULL, NULL); <--1 HANDLE hkey; status = ZwOpenKey(&hkey, KEY_READ, &oa); <--2 if (NT_SUCCESS(status)) { ... ZwClose(hkey); <--3 } ... } |
- 我们用I/O管理器提供的注册表路径名和为NULL的安全描述符来初始化OBJECT_ATTRIBUTES结构对象oa。ZwOpenKey总是忽略安全描述符,安全描述符仅在创建键时才被真正使用。
- ZwOpenKey以读方式打开该键,并把返回句柄保存到hkey变量中。
- ZwClose是关闭内核模式对象句柄的通用例程。在这里,我们用它关闭注册表键句柄。
尽管我们经常把注册表说成数据库,但它并不具备真正数据库的所有特征。例如,它不允许提交或回卷数据更改。另外,在打开键操作中指定的访问权限是用于安全检测而不是为了防止共享冲突。即两个不同的进程能以写方式打开同一个键。然而,系统仍能自动保护与破坏性写操作同时发生的读操作,也能保证使用中的键不被意外删除。
其它打开注册表键的方法
除了ZwOpenKey函数,Windows 2000又提供了两个打开注册表键的函数。
IoOpenDeviceRegistryKey打开某设备对象专用的注册表键:
HANDLE hkey; status = IoOpenDeviceRegistryKey(pdo, flag, access, &hkey); |
pdo是物理设备对象的地址,flag指出你要打开哪一个专用键(见表3-10),access是访问掩码,如KEY_READ。
表3-10. IoOpenDeviceRegistryKey函数使用的注册表键代码
Flag值 | 被选择的注册表键 |
---|---|
PLUGPLAY_REGKEY_DEVICE | Enum键中的硬件(或实例)子键 |
PLUGPLAY_REGKEY_DRIVER | 服务(或软件)键 |
IoOpenDeviceInterfaceRegistryKey打开与已注册设备接口关联的键:
HANDLE hkey; status = IoOpenDeviceInterfaceRegistryKey(linkname, access, &hkey); |
linkname是已注册接口的符号连接名,access是访问掩码,如KEY_READ。
接口键是HKLM\System\CurrentControlSet\Control\DeviceClasses下的一个子键,可以常驻注册表。我们可以在这里放入需要与用户模式程序共享的参数信息,用户模式程序可以调用SetupDiOpenDeviceInterfaceRegKey函数访问该键。
在第十二章中,我将描述如何用安装脚本向硬件和接口键插入值,以及应用程序如何访问这些值。
获取和设置注册表值
通常,打开一个注册表键是为了从注册表中提取某个值。使用ZwQueryValueKey函数可以达到这个目的。例如,为了从驱动程序的服务键中提取ImagePath值,你可以参考下面代码:
UNICODE_STRING valname; RtlInitUnicodeString(&valname, L"ImagePath"); size = 0; status = ZwQueryValueKey(hkey, &valname, KeyValuePartialInformation, NULL, 0, &size); if (status == STATUS_OBJECT_NAME_NOT_FOUND || size == 0) <handle error>; PKEY_VALUE_PARTIAL_INFORMATION vpip = (PKEY_VALUE_PARTIAL_INFORMATION) ExAllocatePool(PagedPool, size); if (!vpip) <handle error>; status = ZwQueryValueKey(hkey, &valname, KeyValuePartialInformation, vpip, size, &size); if (!NT_SUCCESS(status)) <handle error>; <do something with vpip->Data> ExFreePool(vpip); |
这里,我们两次调用了ZwQueryValueKey函数。第一次调用是为了获取KEY_VALUE_PARTIAL_INFORMATION结构的长度,然后为其分配空间。第二次调用接收其内容。最开始我认为第一次调用ZwQueryValueKey将返回STATUS_BUFFER_TOO_SMALL错误码,因为我传给它一个空缓冲区指针NULL,实际上该函数并没有这样做。最重要的错误代码是STATUS_OBJECT_NAME_NOT_FOUND,它指出注册表中不存在该值。如果这里还有其它错误使ZwQueryValueKey函数不工作,第二次调用时会被发现。
这个所谓的“partial”信息结构包含该注册表值的数据和数据类型描述:
typedef struct _KEY_VALUE_PARTIAL_INFORMATION { ULONG TitleIndex; ULONG Type; ULONG DataLength; UCHAR Data[1]; } KEY_VALUE_PARTIAL_INFORMATION, *PKEY_VALUE_PARTIAL_INFORMATION; |
Type是表3-11中列出的一个注册表数据类型。(可能还有其它数据类型,但设备驱动程序一般不会用到) DataLength是数据的长度,Data是数据本身。TitleIndex与驱动程序无关。下面事实可以帮助你了解各种数据类型:
- REG_DWORD是一个32位无符号整型,其格式根据平台而变(大结尾或小结尾)。
- REG_SZ是一个空结尾的Unicode字符串。这个NULL结束符被计入DataLength中。
- 扩展REG_EXPAND_SZ值需要使用环境变量替换%-escapes,你应该使用RtlQueryRegistryValues函数获取这种类型的数据。但访问环境变量的内部例程没有对驱动程序公开。
- RtlQueryRegistryValues也可以用于获取REG_MULTI_SZ类型值,如果含有多个串,该函数将调用你指定的回调函数来处理。
RtlQueryRegistryValues是一个复杂的函数,我没有为它写例子,DDK中有几个例子驱动程序用到了该函数。
表3-11. WDM驱动程序使用的注册表值类型
数据类型常量 | 描述 |
---|---|
REG_BINARY | 可变长二进制数据 |
REG_DWORD | 无符号长整型,格式依平台而变 |
REG_DWORD_BIG_ENDIAN | 无符号长整型,大结尾格式 |
REG_EXPAND_SZ | 空结尾的Unicode串,包含%-escapes环境变量 |
REG_MULTI_SZ | 一个或多个空结尾的Unicode串,最后有一个额外的null |
REG_SZ | 空结尾的Unicode串 |
为了设置注册表值,你必须对其父键拥有KEY_SET_VALUE访问权。我前面用过的KEY_READ并没有这样的权力。你可以使用KEY_WRITE或KEY_ALL_ACCESS,尽管它们提供的权力有点超出你的需求。然后调用ZwSetValueKey,例如:
RtlInitUnicodeString(&valname, L"TheAnswer"); ULONG value = 42; ZwSetValueKey(hkey, &valname, 0, REG_DWORD, &value, sizeof(value)); |
删除子键或键值
为了删除已打开键中的键值,可以使用RtlDeleteRegistryValue函数:
RtlDeleteRegistryValue(RTL_REGISTRY_HANDLE, (PCWSTR) hkey, L"TheAnswer"); |
RtlDeleteRegistryValue是一个通用的服务函数,它的第一个参数可以指向注册表中的几个特殊位置。当使用RTL_REGISTRY_HANDLE时,表示你要删除已打开键中的键值。第二参数指出键。第三个参数是一个空结尾的Unicode串,它是被删除键值的名称。在这里,你不必为描述该串创建一个UNICODE_STRING结构。
如果你对某注册表键有DELETE权限(用KEY_ALL_ACCESS打开),可以调用ZwDeleteKey删除该键:
ZwDeleteKey(hkey); |
被删除键要等到其所有句柄都关闭后才真正消失,所有后来访问或打开该键的请求都将失败,并返回STATUS_KEY_DELETED错误代码。键删除后,仍需要调用ZwClose函数关闭其句柄。(DDK文档中关于ZwDeleteKey函数的内容指出那个句柄将变为无效,但实际不是这样,你仍要调用ZwClose关闭它)
枚举子键或键值
枚举键中的元素(子键和键值)是一个较复杂的操作。为此,需要先调用ZwQueryKey以获得该键子键和键值的一些信息,如个数、最长名的长度,等等。ZwQueryKey有一个参数可以让你指出需要该键的哪种类型的信息。有三种类型:基本信息、节点信息,和全部信息。为了准备枚举操作,你最好指出需要全部信息:
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; |
该结构实际上是可变长的,因为Class[0]只是类名的第一个字符。通常,第一次调用获得要分配缓冲区的大小,第二次调用获得实际的数据,如下:
ULONG size; ZwQueryKey(hkey, KeyFullInformation, NULL, 0, &size); PKEY_FULL_INFORMATION fip = (PKEY_FULL_INFORMATION) ExAllocatePool(PagedPool, size); ZwQueryKey(hkey, 0, KeyFullInformation, bip, size, &size); |
用subkeys值做计数器,循环调用ZwEnumerateKey:
for (ULONG i = 0; i < fip->SubKeys; ++i) { ZwEnumerateKey(hkey, i, KeyBasicInformation, NULL, 0, &size); PKEY_BASIC_INFORMATION bip = (PKEY_BASIC_INFORMATION) ExAllocatePool(PagedPool, size); ZwEnumerateKey(hkey, i, KeyBasicInformation, bip, size, &size); <do something with bip->Name> ExFreePool(bip); } |
每个子键的关键信息就是它的名字,存在于KEY_BASIC_INFORMATION结构中:
typedef struct _KEY_BASIC_INFORMATION { LARGE_INTEGER LastWriteTime; ULONG Type; ULONG NameLength; WCHAR Name[1]; } KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION; |
这个名字并不是空结尾的字符串,因此你必须使用NameLength成员确定其长度,不要忘了长度的单位是字节。这个名字并不是完整的注册表路径,它就是这个子键的名称。这样更好,因为只要有子键名和其父键的句柄我们就可以打开这个子键了。
为了枚举键中的键值,可使用下面方法:
ULONG maxlen = fip->MaxValueNameLen + sizeof(KEY_VALUE_BASIC_INFORMATION); PKEY_VALUE_BASIC_INFORMATION vip = (PKEY_VALUE_BASIC_INFORMATION) ExAllocatePool(PagedPool, maxlen); for (ULONG i = 0; i < fip->Values; ++i) { ZwEnumerateValueKey(hkey, i, KeyValueBasicInformation, vip, maxlen, &size); <do something with vip->Name> } ExFreePool(vip); |
基于已获得的KEY_FULL_INFORMATION结构中的MaxValueNameLen成员,你可以为最大可能的KEY_VALUE_BASIC_INFORMATION结构分配空间。在循环中,你可以处理该键值的名称,这个名称存在于KEY_VALUE_BASIC_INFORMATION结构中:
typedef struct _KEY_VALUE_BASIC_INFORMATION { ULONG TitleIndex; ULONG Type; ULONG NameLength; WCHAR Name[1]; } KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION |