7、注册表操作:打开与创建键、读写值、枚举子键、注册表回调、配置管理
注册表操作,在驱动开发里是个绕不开的活。说实话,很多应用层程序员觉得注册表就是 RegOpenKeyEx 那套 API,但在内核里,玩法完全不一样。我早期做驱动时,就因为没搞懂内核注册表操作的差异,踩过不少坑。
今天咱们就把这块彻底捋清楚。从最基本的打开创建键,到读写值、枚举子键,再到注册表回调与配置管理,一条线讲下来。
7.1 打开与创建注册表键
内核里操作注册表,核心函数是 ZwCreateKey 和 ZwOpenKey。你可能会问,为什么不用 RegCreateKeyEx?嗯,那是应用层的 API,内核里用不了。内核有自己的一套 ZwXxx 系列函数。
先看一个打开键的例子:
HANDLE hKey = NULL;
OBJECT_ATTRIBUTES objAttr;
UNICODE_STRING ustrKeyPath;
NTSTATUS status;
RtlInitUnicodeString(&ustrKeyPath, L"\\Registry\\Machine\\SOFTWARE\\MyDriver");
InitializeObjectAttributes(&objAttr, &ustrKeyPath, OBJ_KERNEL_HANDLE, NULL, NULL);
status = ZwOpenKey(&hKey, KEY_READ, &objAttr);
if (!NT_SUCCESS(status)) {
// 处理错误
DbgPrint("打开注册表键失败: 0x%X\n", status);
}
注意路径写法。内核里注册表路径必须以 \Registry\Machine 开头,对应应用层的 HKEY_LOCAL_MACHINE。如果是 HKEY_CURRENT_USER,对应的是 \Registry\User。这个映射关系,我建议你记牢了,写错路径是新手最常见的错误。
创建键也很类似,只是多了一个 CreateOptions 参数:
status = ZwCreateKey(&hKey, KEY_WRITE, &objAttr, 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");
}
}
disposition 这个参数挺有用。它能告诉你这个键是新建的还是已有的。我在项目中就用它来判断是否是第一次加载驱动,从而决定是否写入默认配置。
OBJ_KERNEL_HANDLE 标志。我个人习惯是统一用内核句柄,需要时再通过 ObReferenceObjectByHandle 转换。
7.2 读写注册表值
读写值用的是 ZwQueryValueKey 和 ZwSetValueKey。这里有个小技巧——查询值时,你通常不知道值有多大,所以需要先查询大小,再分配缓冲区。
看代码:
// 先查询值的大小
KEY_VALUE_PARTIAL_INFORMATION kvpi;
ULONG ulSize = 0;
status = ZwQueryValueKey(hKey, &ustrValueName, KeyValuePartialInformation,
&kvpi, sizeof(kvpi), &ulSize);
if (status == STATUS_BUFFER_OVERFLOW || status == STATUS_BUFFER_TOO_SMALL) {
// 分配足够大的缓冲区
PVOID pBuffer = ExAllocatePoolWithTag(PagedPool, ulSize, 'gReT');
if (pBuffer) {
status = ZwQueryValueKey(hKey, &ustrValueName, KeyValuePartialInformation,
pBuffer, ulSize, &ulSize);
if (NT_SUCCESS(status)) {
PKEY_VALUE_PARTIAL_INFORMATION pKvpi = (PKEY_VALUE_PARTIAL_INFORMATION)pBuffer;
// 根据 pKvpi->Type 判断数据类型,读取 pKvpi->Data
}
ExFreePoolWithTag(pBuffer, 'gReT');
}
}
写值相对简单:
ULONG ulData = 12345;
status = ZwSetValueKey(hKey, &ustrValueName, 0, REG_DWORD, &ulData, sizeof(ulData));
这里有个坑——ZwQueryValueKey 返回的状态码。我第一次写的时候以为 STATUS_SUCCESS 才是成功,结果发现当缓冲区刚好够用时,它返回的是 STATUS_BUFFER_OVERFLOW。嗯,微软的 API 设计有时候就是这么「巧妙」。
7.3 枚举子键
枚举子键用 ZwEnumerateKey。这个函数需要你循环调用,每次传入一个索引值,从 0 开始递增,直到返回 STATUS_NO_MORE_ENTRIES。
核心逻辑是这样的:
for (ULONG i = 0; ; i++) {
// 先查询子键名称的长度
KEY_BASIC_INFORMATION kbi;
ULONG ulResultLen = 0;
status = ZwEnumerateKey(hKey, i, KeyBasicInformation, &kbi, sizeof(kbi), &ulResultLen);
if (status == STATUS_NO_MORE_ENTRIES) {
break;
}
if (status == STATUS_BUFFER_TOO_SMALL) {
// 分配缓冲区,重新查询
PKEY_BASIC_INFORMATION pKbi = (PKEY_BASIC_INFORMATION)
ExAllocatePoolWithTag(PagedPool, ulResultLen, 'mune');
if (pKbi) {
status = ZwEnumerateKey(hKey, i, KeyBasicInformation, pKbi, ulResultLen, &ulResultLen);
if (NT_SUCCESS(status)) {
// pKbi->Name 就是子键名称
DbgPrint("子键: %wZ\n", &pKbi->Name);
}
ExFreePoolWithTag(pKbi, 'mune');
}
}
}
枚举值项用 ZwEnumerateValueKey,用法完全一样。你想想看,这两个函数的设计思路其实很一致——先查大小,再分配,再读取。内核编程里这种模式随处可见。
7.4 注册表回调
注册表回调是驱动开发中比较高级的功能。通过 CmRegisterCallbackEx 注册一个回调函数,你就能监控甚至拦截注册表操作。
注册回调的步骤:
LARGE_INTEGER liAltitude;
RtlInitUnicodeString(&ustrAltitude, L"320000"); // 海拔高度,决定回调顺序
NTSTATUS status = CmRegisterCallbackEx(
RegistryCallback, // 回调函数
&ustrAltitude, // 海拔
NULL, // 驱动对象
NULL, // 上下文
&hCookie, // 返回的Cookie,用于卸载
NULL // 保留
);
回调函数的原型:
NTSTATUS RegistryCallback(
PVOID CallbackContext,
PVOID Argument1, // 操作类型
PVOID Argument2 // 操作相关数据
) {
REG_NOTIFY_CLASS NotifyClass = (REG_NOTIFY_CLASS)(ULONG_PTR)Argument1;
switch (NotifyClass) {
case RegNtPreCreateKeyEx:
// 创建键前触发
break;
case RegNtPreSetValueKey:
// 设置值前触发
break;
case RegNtPostDeleteKey:
// 删除键后触发
break;
}
return STATUS_SUCCESS;
}
我曾经用这个功能做过一个安全驱动,监控注册表启动项的修改。当检测到有进程试图修改 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run 时,直接拦截并记录日志。效果还不错。
ZwOpenKey 等注册表 API,否则可能造成死锁。我见过有人在这里递归调用把自己搞崩了。
7.5 配置管理
配置管理说白了就是如何组织你的驱动配置信息。我一般遵循几个原则:
- 路径统一:所有配置放在
HKLM\SYSTEM\CurrentControlSet\Services\YourDriver\Parameters下 - 版本兼容:配置项增加时,要能兼容旧版本(没有新配置项时使用默认值)
- 热加载:支持运行时重新加载配置,不需要重启驱动
这里给一个配置管理的结构体示例:
typedef struct _DRIVER_CONFIG {
ULONG ulLogLevel; // 日志级别
ULONG ulTimeout; // 超时时间(ms)
BOOLEAN bEnableFilter; // 是否启用过滤
WCHAR wszLogPath[256]; // 日志路径
} DRIVER_CONFIG, *PDRIVER_CONFIG;
// 从注册表加载配置
NTSTATUS LoadDriverConfig(PDRIVER_CONFIG pConfig) {
// 先设置默认值
pConfig->ulLogLevel = 2;
pConfig->ulTimeout = 5000;
pConfig->bEnableFilter = TRUE;
wcscpy_s(pConfig->wszLogPath, 256, L"\\SystemRoot\\Logs\\MyDriver.log");
// 再从注册表读取,覆盖默认值
// ... 读取逻辑
}
为什么要有默认值?因为用户可能没配置某些项。我在项目中遇到过,升级驱动后新增了一个配置项,结果旧版本没写这个值,驱动加载时直接蓝屏了。从那以后,我所有配置读取都先设默认值,再覆盖。
7.6 知识体系总览
下面这张图把注册表操作的核心知识点串起来了:
从这张图能看出来,注册表操作其实是个层层递进的过程。先掌握基础的打开、读写、枚举,再学习回调机制,最后用配置管理把整个体系串起来。
嗯,关于注册表操作,今天就讲这么多。这些内容都是我实际项目中反复用到的,希望能帮你少走弯路。记住一点:内核编程没有小事,每个 API 的返回值都要检查,每个句柄都要记得关闭。
核心总结:
- 使用
ZwXxx系列函数操作注册表,路径以\Registry\Machine开头 - 读写值前先查询大小,避免缓冲区溢出
- 枚举时循环调用,直到返回
STATUS_NO_MORE_ENTRIES - 注册表回调中不要调用注册表 API,防止死锁
- 配置管理始终设置默认值,保证兼容性
公众号:蓝海资料掘金营,微信deep3321