1、课程目标
- 掌握注册表回调的注册和使用
- 实现注册表操作监控
- 学会拦截和阻止注册表修改
- 理解注册表监控在安全软件中的应用
2、名词解释
| 术语 |
解释 |
| CmRegisterCallback |
注册注册表回调(旧版) |
| CmRegisterCallbackEx |
注册注册表回调(新版) |
| REG_NOTIFY_CLASS |
注册表操作类型枚举 |
| REG_XXX_KEY_INFORMATION |
各操作的信息结构 |
3、使用工具
| 工具 |
用途 |
| Regmon/Process Monitor |
监控注册表操作 |
| Regedit |
注册表编辑器 |
| WinDbg |
调试回调 |
4、技术原理
4.1、注册表回调机制
┌─────────────────────────────────────────────────────────────┐
│ 注册表回调操作类型 │
│ │
│ Pre-Operation(可拦截) Post-Operation(只能监控) │
│ ────────────────────── ───────────────────────── │
│ RegNtPreCreateKey RegNtPostCreateKey │
│ RegNtPreOpenKey RegNtPostOpenKey │
│ RegNtPreDeleteKey RegNtPostDeleteKey │
│ RegNtPreSetValueKey RegNtPostSetValueKey │
│ RegNtPreDeleteValueKey RegNtPostDeleteValueKey │
│ RegNtPreQueryValueKey RegNtPostQueryValueKey │
│ RegNtPreQueryKey RegNtPostQueryKey │
│ ... ... │
│ │
│ Pre返回值: │
│ - STATUS_SUCCESS → 允许操作继续 │
│ - STATUS_CALLBACK_BYPASS → 允许但跳过其他回调 │
│ - 其他错误码 → 拒绝操作 │
└─────────────────────────────────────────────────────────────┘
5、代码实现
5.1、示例1:基础注册表监控
// RegistryMonitor.c - 基础注册表监控
#include <ntddk.h>
LARGE_INTEGER g_Cookie = {0};
// 注册表回调函数
NTSTATUS RegistryCallback(
PVOID CallbackContext,
PVOID Argument1,
PVOID Argument2
) {
UNREFERENCED_PARAMETER(CallbackContext);
REG_NOTIFY_CLASS notifyClass = (REG_NOTIFY_CLASS)(ULONG_PTR)Argument1;
NTSTATUS status = STATUS_SUCCESS;
switch (notifyClass) {
case RegNtPreCreateKeyEx: {
PREG_CREATE_KEY_INFORMATION info = (PREG_CREATE_KEY_INFORMATION)Argument2;
if (info->CompleteName) {
DbgPrint("[Reg] CreateKey: %wZ\n", info->CompleteName);
}
break;
}
case RegNtPreOpenKeyEx: {
PREG_OPEN_KEY_INFORMATION info = (PREG_OPEN_KEY_INFORMATION)Argument2;
if (info->CompleteName) {
DbgPrint("[Reg] OpenKey: %wZ\n", info->CompleteName);
}
break;
}
case RegNtPreDeleteKey: {
PREG_DELETE_KEY_INFORMATION info = (PREG_DELETE_KEY_INFORMATION)Argument2;
PCUNICODE_STRING keyName = NULL;
CmCallbackGetKeyObjectIDEx(&g_Cookie, info->Object, NULL, &keyName, 0);
if (keyName) {
DbgPrint("[Reg] DeleteKey: %wZ\n", keyName);
CmCallbackReleaseKeyObjectIDEx(keyName);
}
break;
}
case RegNtPreSetValueKey: {
PREG_SET_VALUE_KEY_INFORMATION info = (PREG_SET_VALUE_KEY_INFORMATION)Argument2;
PCUNICODE_STRING keyName = NULL;
CmCallbackGetKeyObjectIDEx(&g_Cookie, info->Object, NULL, &keyName, 0);
if (keyName && info->ValueName) {
DbgPrint("[Reg] SetValue: %wZ\\%wZ\n", keyName, info->ValueName);
CmCallbackReleaseKeyObjectIDEx(keyName);
}
break;
}
case RegNtPreDeleteValueKey: {
PREG_DELETE_VALUE_KEY_INFORMATION info = (PREG_DELETE_VALUE_KEY_INFORMATION)Argument2;
PCUNICODE_STRING keyName = NULL;
CmCallbackGetKeyObjectIDEx(&g_Cookie, info->Object, NULL, &keyName, 0);
if (keyName && info->ValueName) {
DbgPrint("[Reg] DeleteValue: %wZ\\%wZ\n", keyName, info->ValueName);
CmCallbackReleaseKeyObjectIDEx(keyName);
}
break;
}
case RegNtPreQueryValueKey: {
// 查询操作通常不记录(太频繁)
break;
}
default:
break;
}
return status;
}
// 注册回调
NTSTATUS RegisterRegistryCallback() {
UNICODE_STRING altitude;
RtlInitUnicodeString(&altitude, L"320000");
return CmRegisterCallbackEx(RegistryCallback, &altitude,
NULL, NULL, &g_Cookie, NULL);
}
// 注销回调
VOID UnregisterRegistryCallback() {
if (g_Cookie.QuadPart) {
CmUnRegisterCallback(g_Cookie);
g_Cookie.QuadPart = 0;
}
}
5.2、示例2:注册表保护
// RegistryProtect.c - 注册表保护
#include <ntddk.h>
// 保护的注册表路径
WCHAR* g_ProtectedPaths[] = {
L"\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",
L"\\REGISTRY\\MACHINE\\SYSTEM\\CurrentControlSet\\Services",
L"\\REGISTRY\\USER\\*\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"
};
#define PROTECTED_PATH_COUNT (sizeof(g_ProtectedPaths) / sizeof(g_ProtectedPaths[0]))
LARGE_INTEGER g_Cookie = {0};
BOOLEAN g_EnableProtect = TRUE;
// 检查路径是否受保护
BOOLEAN IsPathProtected(PCUNICODE_STRING Path) {
if (!Path || !Path->Buffer) return FALSE;
for (ULONG i = 0; i < PROTECTED_PATH_COUNT; i++) {
UNICODE_STRING protPath;
RtlInitUnicodeString(&protPath, g_ProtectedPaths[i]);
// 简单前缀匹配(生产环境需要更复杂的通配符匹配)
if (RtlPrefixUnicodeString(&protPath, Path, TRUE)) {
return TRUE;
}
}
return FALSE;
}
// 检查是否是可信进程
BOOLEAN IsTrustedProcess() {
PEPROCESS process = PsGetCurrentProcess();
PUCHAR imageName = PsGetProcessImageFileName(process);
// 允许的进程列表
const char* trustedProcesses[] = {
"regedit.exe",
"services.exe",
"svchost.exe"
};
for (int i = 0; i < sizeof(trustedProcesses) / sizeof(trustedProcesses[0]); i++) {
if (_stricmp((char*)imageName, trustedProcesses[i]) == 0) {
return TRUE;
}
}
return FALSE;
}
// 保护回调
NTSTATUS ProtectRegistryCallback(
PVOID CallbackContext,
PVOID Argument1,
PVOID Argument2
) {
UNREFERENCED_PARAMETER(CallbackContext);
REG_NOTIFY_CLASS notifyClass = (REG_NOTIFY_CLASS)(ULONG_PTR)Argument1;
NTSTATUS status = STATUS_SUCCESS;
if (!g_EnableProtect) return STATUS_SUCCESS;
switch (notifyClass) {
case RegNtPreDeleteKey: {
PREG_DELETE_KEY_INFORMATION info = (PREG_DELETE_KEY_INFORMATION)Argument2;
PCUNICODE_STRING keyName = NULL;
CmCallbackGetKeyObjectIDEx(&g_Cookie, info->Object, NULL, &keyName, 0);
if (keyName && IsPathProtected(keyName)) {
if (!IsTrustedProcess()) {
DbgPrint("[Protect] BLOCKED DeleteKey: %wZ\n", keyName);
status = STATUS_ACCESS_DENIED;
}
}
if (keyName) CmCallbackReleaseKeyObjectIDEx(keyName);
break;
}
case RegNtPreSetValueKey: {
PREG_SET_VALUE_KEY_INFORMATION info = (PREG_SET_VALUE_KEY_INFORMATION)Argument2;
PCUNICODE_STRING keyName = NULL;
CmCallbackGetKeyObjectIDEx(&g_Cookie, info->Object, NULL, &keyName, 0);
if (keyName && IsPathProtected(keyName)) {
if (!IsTrustedProcess()) {
DbgPrint("[Protect] BLOCKED SetValue: %wZ\\%wZ\n",
keyName, info->ValueName);
status = STATUS_ACCESS_DENIED;
}
}
if (keyName) CmCallbackReleaseKeyObjectIDEx(keyName);
break;
}
case RegNtPreDeleteValueKey: {
PREG_DELETE_VALUE_KEY_INFORMATION info = (PREG_DELETE_VALUE_KEY_INFORMATION)Argument2;
PCUNICODE_STRING keyName = NULL;
CmCallbackGetKeyObjectIDEx(&g_Cookie, info->Object, NULL, &keyName, 0);
if (keyName && IsPathProtected(keyName)) {
if (!IsTrustedProcess()) {
DbgPrint("[Protect] BLOCKED DeleteValue: %wZ\\%wZ\n",
keyName, info->ValueName);
status = STATUS_ACCESS_DENIED;
}
}
if (keyName) CmCallbackReleaseKeyObjectIDEx(keyName);
break;
}
default:
break;
}
return status;
}
5.3、示例3:注册表审计日志
// RegistryAudit.c - 注册表审计
#include <ntddk.h>
typedef struct _REGISTRY_AUDIT_ENTRY {
LARGE_INTEGER Timestamp;
HANDLE ProcessId;
ULONG Operation;
NTSTATUS Result;
WCHAR KeyPath[256];
WCHAR ValueName[64];
ULONG DataType;
ULONG DataSize;
LIST_ENTRY ListEntry;
} REGISTRY_AUDIT_ENTRY, *PREGISTRY_AUDIT_ENTRY;
LIST_ENTRY g_AuditList;
KSPIN_LOCK g_AuditLock;
ULONG g_AuditCount = 0;
#define MAX_AUDIT_ENTRIES 10000
VOID InitRegistryAudit() {
InitializeListHead(&g_AuditList);
KeInitializeSpinLock(&g_AuditLock);
}
VOID AddAuditEntry(
ULONG Operation,
PCUNICODE_STRING KeyPath,
PCUNICODE_STRING ValueName,
ULONG DataType,
ULONG DataSize,
NTSTATUS Result
) {
PREGISTRY_AUDIT_ENTRY entry;
KIRQL oldIrql;
entry = (PREGISTRY_AUDIT_ENTRY)ExAllocatePoolWithTag(
NonPagedPool, sizeof(REGISTRY_AUDIT_ENTRY), 'geRA');
if (!entry) return;
RtlZeroMemory(entry, sizeof(REGISTRY_AUDIT_ENTRY));
KeQuerySystemTime(&entry->Timestamp);
entry->ProcessId = PsGetCurrentProcessId();
entry->Operation = Operation;
entry->Result = Result;
entry->DataType = DataType;
entry->DataSize = DataSize;
if (KeyPath && KeyPath->Buffer) {
ULONG copyLen = min(KeyPath->Length, sizeof(entry->KeyPath) - sizeof(WCHAR));
RtlCopyMemory(entry->KeyPath, KeyPath->Buffer, copyLen);
}
if (ValueName && ValueName->Buffer) {
ULONG copyLen = min(ValueName->Length, sizeof(entry->ValueName) - sizeof(WCHAR));
RtlCopyMemory(entry->ValueName, ValueName->Buffer, copyLen);
}
KeAcquireSpinLock(&g_AuditLock, &oldIrql);
// 限制条目数量
if (g_AuditCount >= MAX_AUDIT_ENTRIES) {
PLIST_ENTRY oldest = RemoveHeadList(&g_AuditList);
PREGISTRY_AUDIT_ENTRY oldEntry = CONTAINING_RECORD(oldest, REGISTRY_AUDIT_ENTRY, ListEntry);
ExFreePoolWithTag(oldEntry, 'geRA');
g_AuditCount--;
}
InsertTailList(&g_AuditList, &entry->ListEntry);
g_AuditCount++;
KeReleaseSpinLock(&g_AuditLock, oldIrql);
}
// Post操作回调记录结果
NTSTATUS AuditRegistryCallback(
PVOID CallbackContext,
PVOID Argument1,
PVOID Argument2
) {
UNREFERENCED_PARAMETER(CallbackContext);
REG_NOTIFY_CLASS notifyClass = (REG_NOTIFY_CLASS)(ULONG_PTR)Argument1;
// 只记录Post操作(包含结果)
switch (notifyClass) {
case RegNtPostSetValueKey: {
PREG_POST_OPERATION_INFORMATION info = (PREG_POST_OPERATION_INFORMATION)Argument2;
PREG_SET_VALUE_KEY_INFORMATION preInfo =
(PREG_SET_VALUE_KEY_INFORMATION)info->PreInformation;
if (preInfo) {
PCUNICODE_STRING keyName = NULL;
CmCallbackGetKeyObjectIDEx(&g_Cookie, info->Object, NULL, &keyName, 0);
AddAuditEntry(RegNtPostSetValueKey, keyName, preInfo->ValueName,
preInfo->Type, preInfo->DataSize, info->Status);
if (keyName) CmCallbackReleaseKeyObjectIDEx(keyName);
}
break;
}
case RegNtPostDeleteValueKey: {
PREG_POST_OPERATION_INFORMATION info = (PREG_POST_OPERATION_INFORMATION)Argument2;
PREG_DELETE_VALUE_KEY_INFORMATION preInfo =
(PREG_DELETE_VALUE_KEY_INFORMATION)info->PreInformation;
if (preInfo) {
PCUNICODE_STRING keyName = NULL;
CmCallbackGetKeyObjectIDEx(&g_Cookie, info->Object, NULL, &keyName, 0);
AddAuditEntry(RegNtPostDeleteValueKey, keyName, preInfo->ValueName,
0, 0, info->Status);
if (keyName) CmCallbackReleaseKeyObjectIDEx(keyName);
}
break;
}
case RegNtPostCreateKeyEx: {
PREG_POST_OPERATION_INFORMATION info = (PREG_POST_OPERATION_INFORMATION)Argument2;
PREG_CREATE_KEY_INFORMATION preInfo =
(PREG_CREATE_KEY_INFORMATION)info->PreInformation;
if (preInfo && preInfo->CompleteName) {
AddAuditEntry(RegNtPostCreateKeyEx, preInfo->CompleteName, NULL,
0, 0, info->Status);
}
break;
}
default:
break;
}
return STATUS_SUCCESS;
}
// 导出审计日志
NTSTATUS ExportAuditLog(PVOID Buffer, ULONG Size, PULONG BytesWritten) {
KIRQL oldIrql;
ULONG offset = 0;
PLIST_ENTRY entry;
KeAcquireSpinLock(&g_AuditLock, &oldIrql);
for (entry = g_AuditList.Flink; entry != &g_AuditList; entry = entry->Flink) {
if (offset + sizeof(REGISTRY_AUDIT_ENTRY) - sizeof(LIST_ENTRY) > Size) break;
PREGISTRY_AUDIT_ENTRY auditEntry = CONTAINING_RECORD(entry, REGISTRY_AUDIT_ENTRY, ListEntry);
RtlCopyMemory((PUCHAR)Buffer + offset, auditEntry,
sizeof(REGISTRY_AUDIT_ENTRY) - sizeof(LIST_ENTRY));
offset += sizeof(REGISTRY_AUDIT_ENTRY) - sizeof(LIST_ENTRY);
}
KeReleaseSpinLock(&g_AuditLock, oldIrql);
*BytesWritten = offset;
return STATUS_SUCCESS;
}
5.4、示例4:注册表隐藏检测
// RegistryHide.c - 注册表隐藏检测
#include <ntddk.h>
// 检测隐藏的注册表键
// 原理:对比枚举结果和直接打开结果
NTSTATUS CheckHiddenKeys(PUNICODE_STRING ParentKeyPath) {
HANDLE hKey;
OBJECT_ATTRIBUTES objAttr;
NTSTATUS status;
InitializeObjectAttributes(&objAttr, ParentKeyPath,
OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
status = ZwOpenKey(&hKey, KEY_READ, &objAttr);
if (!NT_SUCCESS(status)) return status;
// 枚举子键
ULONG index = 0;
ULONG resultLength;
PKEY_BASIC_INFORMATION keyInfo;
keyInfo = (PKEY_BASIC_INFORMATION)ExAllocatePoolWithTag(PagedPool, 4096, 'yeKR');
if (!keyInfo) {
ZwClose(hKey);
return STATUS_INSUFFICIENT_RESOURCES;
}
while (TRUE) {
status = ZwEnumerateKey(hKey, index, KeyBasicInformation,
keyInfo, 4096, &resultLength);
if (status == STATUS_NO_MORE_ENTRIES) break;
if (!NT_SUCCESS(status)) break;
// 尝试直接打开这个子键
UNICODE_STRING subKeyName;
subKeyName.Buffer = keyInfo->Name;
subKeyName.Length = (USHORT)keyInfo->NameLength;
subKeyName.MaximumLength = (USHORT)keyInfo->NameLength;
HANDLE hSubKey;
InitializeObjectAttributes(&objAttr, &subKeyName,
OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, hKey, NULL);
status = ZwOpenKey(&hSubKey, KEY_READ, &objAttr);
if (NT_SUCCESS(status)) {
ZwClose(hSubKey);
} else {
// 枚举能看到但无法打开 - 可能被隐藏或权限问题
DbgPrint("[RegHide] Suspicious key: %wZ\\%wZ\n", ParentKeyPath, &subKeyName);
}
index++;
}
ExFreePoolWithTag(keyInfo, 'yeKR');
ZwClose(hKey);
return STATUS_SUCCESS;
}
6、课后作业
- 实现自启动项保护功能
- 添加注册表值内容过滤
- 实现注册表操作的实时告警
- 结合进程监控实现行为分析
7、扩展阅读
- Windows注册表内部结构
- CmRegisterCallbackEx详解
- 注册表虚拟化和重定向