Windows内核安全

12、监控注册表

1、课程目标

  1. 掌握注册表回调的注册和使用
  2. 实现注册表操作监控
  3. 学会拦截和阻止注册表修改
  4. 理解注册表监控在安全软件中的应用

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、课后作业

  1. 实现自启动项保护功能
  2. 添加注册表值内容过滤
  3. 实现注册表操作的实时告警
  4. 结合进程监控实现行为分析

7、扩展阅读

  • Windows注册表内部结构
  • CmRegisterCallbackEx详解
  • 注册表虚拟化和重定向