12、注册表操作:打开与创建键、读写值、枚举子键、注册表回调监控
注册表操作,是驱动开发里绕不开的活儿。我刚开始做驱动时,总觉得注册表是应用层的事,后来被现实狠狠教育了一回——驱动里很多配置信息、启动参数、设备状态,都得靠注册表来存。说白了,注册表就是Windows的“神经中枢”,驱动和它打交道是家常便饭。
这一章,咱们就聊聊驱动里怎么操作注册表。包括打开键、创建键、读写值、枚举子键,还有那个比较高级的注册表回调监控。嗯,内容不少,但都是实战干货。
12.1 注册表操作的基础概念
在驱动开发中,注册表操作和用户态不太一样。用户态用RegOpenKeyEx、RegQueryValueEx这些API,驱动里则用Zw系列函数。为什么?因为驱动运行在内核态,需要直接和内核对象管理器打交道。
我个人习惯把注册表操作分为两类:
- 自用配置:驱动自己的参数,比如日志级别、功能开关
- 系统信息:读取硬件信息、系统设置等
不管哪一类,核心API就那么几个。咱们一个一个来。
12.2 打开与创建注册表键
先看怎么打开一个已有的键。最常用的函数是ZwOpenKey。它的原型长这样:
NTSTATUS ZwOpenKey(
PHANDLE KeyHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes
);
你看,它需要一个OBJECT_ATTRIBUTES结构体。这个结构体里要指定键的路径。路径怎么写?用InitializeObjectAttributes宏来初始化。
举个例子,打开\Registry\Machine\SYSTEM\CurrentControlSet\Services:
HANDLE hKey = NULL;
OBJECT_ATTRIBUTES oa;
UNICODE_STRING usPath;
RtlInitUnicodeString(&usPath, L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Services");
InitializeObjectAttributes(&oa, &usPath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
NTSTATUS status = ZwOpenKey(&hKey, KEY_READ, &oa);
if (NT_SUCCESS(status)) {
// 操作成功,hKey就是打开的句柄
// 用完记得 ZwClose(hKey);
} else {
// 处理错误
}
这里有个坑,我踩过。路径前缀必须是\Registry\Machine\(对应HKEY_LOCAL_MACHINE)或\Registry\User\(对应HKEY_USERS)。别写成HKEY_LOCAL_MACHINE\...,那是用户态的写法。
如果要创建新键,用ZwCreateKey。它比ZwOpenKey多了几个参数,可以指定键的创建方式:
NTSTATUS ZwCreateKey(
PHANDLE KeyHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
ULONG TitleIndex,
PUNICODE_STRING Class,
ULONG CreateOptions,
PULONG Disposition
);
CreateOptions可以填REG_OPTION_NON_VOLATILE(持久化键)或REG_OPTION_VOLATILE(内存键,重启消失)。Disposition会返回操作结果,告诉你键是新建的还是已存在的。
ULONG disposition;
status = ZwCreateKey(&hKey, KEY_WRITE, &oa, 0, NULL,
REG_OPTION_NON_VOLATILE, &disposition);
if (NT_SUCCESS(status)) {
if (disposition == REG_CREATED_NEW_KEY) {
DbgPrint("新键创建成功\n");
} else if (disposition == REG_OPENED_EXISTING_KEY) {
DbgPrint("键已存在,直接打开\n");
}
}
12.3 读写注册表值
打开键之后,就该读写值了。读值用ZwQueryValueKey,写值用ZwSetValueKey。
先看读值。这个函数有点特殊,它需要你先查询一次获取数据大小,再查询一次获取实际数据。为什么?因为驱动里不能假设数据长度,得动态分配内存。
NTSTATUS ReadRegistryValue(HANDLE hKey, PCWSTR ValueName, PULONG pData) {
UNICODE_STRING usValueName;
RtlInitUnicodeString(&usValueName, ValueName);
// 第一步:查询数据大小
KEY_VALUE_PARTIAL_INFORMATION kvpi;
ULONG resultLength = 0;
NTSTATUS status = ZwQueryValueKey(hKey, &usValueName, KeyValuePartialInformation,
&kvpi, sizeof(kvpi), &resultLength);
if (status == STATUS_BUFFER_OVERFLOW || status == STATUS_BUFFER_TOO_SMALL) {
// 分配足够大的缓冲区
ULONG bufferSize = resultLength;
PKEY_VALUE_PARTIAL_INFORMATION pKvpi = (PKEY_VALUE_PARTIAL_INFORMATION)ExAllocatePoolWithTag(
NonPagedPool, bufferSize, 'gReT');
if (!pKvpi) return STATUS_INSUFFICIENT_RESOURCES;
// 第二步:实际读取
status = ZwQueryValueKey(hKey, &usValueName, KeyValuePartialInformation,
pKvpi, bufferSize, &resultLength);
if (NT_SUCCESS(status) && pKvpi->Type == REG_DWORD) {
*pData = *(PULONG)pKvpi->Data;
}
ExFreePoolWithTag(pKvpi, 'gReT');
}
return status;
}
写值就简单多了:
ULONG data = 0x12345678;
UNICODE_STRING usValueName;
RtlInitUnicodeString(&usValueName, L"MyValue");
status = ZwSetValueKey(hKey, &usValueName, 0, REG_DWORD, &data, sizeof(data));
if (!NT_SUCCESS(status)) {
DbgPrint("写值失败,状态码: 0x%X\n", status);
}
12.4 枚举子键
枚举子键,说白了就是遍历一个键下面所有的子键。这个在驱动里用得不多,但偶尔会用到,比如遍历某个服务下的所有参数键。
枚举用ZwEnumerateKey。它按索引号来获取子键信息,从0开始,直到返回STATUS_NO_MORE_ENTRIES为止。
void EnumerateSubKeys(HANDLE hKey) {
ULONG index = 0;
NTSTATUS status;
do {
// 先查子键名长度
KEY_BASIC_INFORMATION kbi;
ULONG resultLength = 0;
status = ZwEnumerateKey(hKey, index, KeyBasicInformation,
&kbi, sizeof(kbi), &resultLength);
if (status == STATUS_BUFFER_OVERFLOW || status == STATUS_BUFFER_TOO_SMALL) {
ULONG bufferSize = sizeof(KEY_BASIC_INFORMATION) + kbi.NameLength;
PKEY_BASIC_INFORMATION pKbi = (PKEY_BASIC_INFORMATION)ExAllocatePoolWithTag(
NonPagedPool, bufferSize, 'mReT');
if (!pKbi) break;
status = ZwEnumerateKey(hKey, index, KeyBasicInformation,
pKbi, bufferSize, &resultLength);
if (NT_SUCCESS(status)) {
// 子键名在 pKbi->Name 中,长度是 pKbi->NameLength(字节)
DbgPrint("子键[%d]: %wZ\n", index, &pKbi->Name);
}
ExFreePoolWithTag(pKbi, 'mReT');
index++;
} else {
break;
}
} while (NT_SUCCESS(status));
}
枚举值项用ZwEnumerateValueKey,用法和枚举子键几乎一样,只是信息类型换成KeyValueBasicInformation。我就不重复贴代码了,你照着上面的思路改一下就行。
12.5 注册表回调监控
这个功能比较高级,也是我个人觉得最有意思的部分。注册表回调监控,就是当有进程对注册表进行增删改查操作时,驱动能收到通知,甚至可以拦截或修改操作。
Windows提供了CmRegisterCallbackEx来注册回调函数。这个函数在Vista之后引入,比老版的CmRegisterCallback更灵活。
先看怎么注册:
LARGE_INTEGER altitude;
RtlInitUnicodeString(&Altitude, L"320000"); // 海拔高度,决定回调顺序
NTSTATUS status = CmRegisterCallbackEx(
RegistryCallback, // 回调函数
&Altitude, // 海拔
NULL, // 驱动对象
NULL, // 上下文
&g_Cookie, // 返回的Cookie,注销时用
NULL // 保留
);
回调函数的原型:
NTSTATUS RegistryCallback(
PVOID CallbackContext,
PVOID Argument1, // 操作类型
PVOID Argument2 // 操作相关数据
) {
REG_NOTIFY_CLASS notifyClass = (REG_NOTIFY_CLASS)Argument1;
switch (notifyClass) {
case RegNtPreCreateKeyEx:
case RegNtPreOpenKeyEx: {
// 可以在这里检查或阻止键的创建/打开
PREG_CREATE_KEY_INFORMATION pInfo = (PREG_CREATE_KEY_INFORMATION)Argument2;
// 检查路径,决定是否允许
break;
}
case RegNtPreSetValueKey: {
// 写值前的回调
PREG_SET_VALUE_KEY_INFORMATION pInfo = (PREG_SET_VALUE_KEY_INFORMATION)Argument2;
// 可以修改值数据,或者直接拒绝
break;
}
case RegNtPostCreateKeyEx:
case RegNtPostOpenKeyEx: {
// 操作完成后的通知,不能阻止,只能观察
break;
}
}
return STATUS_SUCCESS; // 返回STATUS_ACCESS_DENIED可以阻止操作
}
注销回调用CmUnRegisterCallback:
if (g_Cookie) {
CmUnRegisterCallback(g_Cookie);
g_Cookie = NULL;
}
12.6 实战:注册表操作流程图
说了这么多,我画个图帮你理清思路。这张图展示了驱动操作注册表的完整流程:
12.7 避坑指南
最后,我把自己踩过的坑总结一下,你写代码时多留个心眼:
- 路径格式:驱动里路径用
\Registry\Machine\,不是HKEY_LOCAL_MACHINE\。这个我一开始就搞错过。 - 内存泄漏:ZwQueryValueKey和ZwEnumerateKey需要先查大小再分配内存,用完一定要ExFreePool。我曾经漏过一次,结果驱动卸载后内存还在涨。
- 回调海拔:注册回调时海拔高度不能随便填。微软有规定的海拔范围,不同功能用不同区间。乱填可能导致和其他驱动冲突。
- Pre回调别睡:Pre类回调里不要做耗时操作,比如等待事件、分配大内存。这些回调在同步路径上,阻塞会影响系统性能。
- 句柄类型:InitializeObjectAttributes时加上OBJ_KERNEL_HANDLE标志,这样句柄只在内核态有效,更安全。
嗯,注册表操作这块就聊到这儿。内容不少,但核心就是那几个Zw函数,加上回调监控这个进阶玩法。你写代码时多练练,很快就能上手。
一句话总结:驱动操作注册表,记住ZwOpenKey/ZwCreateKey打开键,ZwQueryValueKey/ZwSetValueKey读写值,ZwEnumerateKey枚举子键,CmRegisterCallbackEx做监控。路径用内核格式,内存记得释放,回调别干重活。
公众号:蓝海资料掘金营,微信deep3321