在筆者前一篇文章《內核枚舉registry注冊表回調》中實現了對注冊表的枚舉,本章將實現對注冊表的監控,不同于32位系統在64位系統中,微軟為我們提供了兩個針對注冊表的專用內核監控函數,通過這兩個函數可以在不劫持內核api的前提下實現對注冊表增加,刪除,創建等事件的有效監控,注冊表監視通常會通過cmregistercallback創建監控事件并傳入自己的回調函數,與該創建對應的是cmunregistercallback當注冊表監控結束后可用于注銷回調。
CmregisterCallback和CmUnRegisterCallback是Windows操作系統提供的兩個內核API函數,用于注冊和取消注冊注冊表回調函數。
注冊表回調函數是一種內核回調函數,它可以用于監視和攔截系統中的注冊表操作,例如鍵值的創建、修改和刪除等。當有相關操作發生時,操作系統會調用注冊的注冊表回調函數,并將操作相關的信息傳遞給回調函數。
CmRegisterCallback函數用于注冊注冊表回調函數,而CmUnRegisterCallback函數則用于取消注冊已經注冊的回調函數。開發者可以在注冊表回調函數中執行自定義的邏輯,例如記錄日志、過濾敏感數據、或者阻止某些操作。
需要注意的是,注冊表回調函數的注冊和取消注冊必須在內核模式下進行,并且需要開發者有一定的內核開發經驗。同時,注冊表回調函數也需要遵守一些約束條件,例如不能阻塞或掛起進程或線程的創建或訪問,不能調用一些內核API函數等。
內核監控Register注冊表回調在安全軟件、系統監控和調試工具等領域有著廣泛的應用。開發者可以利用這個機制來監視和攔截系統中的注冊表操作,以保護系統安全。
CmRegisterCallback 設置注冊表回調CmUnRegisterCallback 注銷注冊表回調
默認情況下CmRegisterCallback需傳入三個參數,參數一回調函數地址,參數二空余,參數三回調句柄,微軟定義如下。
代碼語言:JavaScript代碼運行次數:0運行復制
// 參數1:回調函數地址// 參數2:無作用// 參數3:回調句柄NTSTATUS CmRegisterCallback( [in] PEX_CALLBACK_FUNCTION Function, [in, optional] PVOID Context, [out] PLARGE_INTEGER Cookie);
自定義注冊表回調函數MyLySharkCallback需要保留三個參數,CallbackContext回調上下文,Argument1是操作類型,Argument2定義詳細結構體指針。
代碼語言:javascript代碼運行次數:0運行復制
NTSTATUS MyLySharkCallback(_In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2)
在自定義回調函數內Argument1則可獲取到操作類型,類型是一個REG_NOTIFY_CLASS枚舉結構,微軟對其的具體定義如下所示。
代碼語言:javascript代碼運行次數:0運行復制
typedef enum _REG_NOTIFY_CLASS { RegNtDeleteKey, RegNtPredeleteKey = RegNtDeleteKey, RegNtSetValueKey, RegNtPreSetValueKey = RegNtSetValueKey, RegNtDeleteValueKey, RegNtPreDeleteValueKey = RegNtDeleteValueKey, RegNtSetInformationKey, RegNtPreSetInformationKey = RegNtSetInformationKey, RegNtRenameKey, RegNtPreRenameKey = RegNtRenameKey, RegNtEnumerateKey, RegNtPreEnumerateKey = RegNtEnumerateKey, RegNtEnumerateValueKey, RegNtPreEnumerateValueKey = RegNtEnumerateValueKey, RegNtQueryKey, RegNtPreQueryKey = RegNtQueryKey, RegNtQueryValueKey, RegNtPreQueryValueKey = RegNtQueryValueKey, RegNtQueryMultipleValueKey, RegNtPreQueryMultipleValueKey = RegNtQueryMultipleValueKey, RegNtPreCreateKey, RegNtPostCreateKey, RegNtPreOpenKey, RegNtPostOpenKey, RegNtKeyHandleClose, RegNtPreKeyHandleClose = RegNtKeyHandleClose, // // .Net only // RegNtPostDeleteKey, RegNtPostSetValueKey, RegNtPostDeleteValueKey, RegNtPostSetInformationKey, RegNtPostRenameKey, RegNtPostEnumerateKey, RegNtPostEnumerateValueKey, RegNtPostQueryKey, RegNtPostQueryValueKey, RegNtPostQueryMultipleValueKey, RegNtPostKeyHandleClose, RegNtPreCreateKeyEx, RegNtPostCreateKeyEx, RegNtPreOpenKeyEx, RegNtPostOpenKeyEx, // // new to Windows Vista // RegNtPreFlushKey, RegNtPostFlushKey, RegNtPreLoadKey, RegNtPostLoadKey, RegNtPreUnLoadKey, RegNtPostUnLoadKey, RegNtPreQueryKeySecurity, RegNtPostQueryKeySecurity, RegNtPreSetKeySecurity, RegNtPostSetKeySecurity, // // per-object context cleanup // RegNtCallbackObjectContextCleanup, // // new in Vista SP2 // RegNtPreRestoreKey, RegNtPostRestoreKey, RegNtPreSaveKey, RegNtPostSaveKey, RegNtPreReplaceKey, RegNtPostReplaceKey, MaxRegNtNotifyClass //should always be the last enum} REG_NOTIFY_CLASS;
其中對于注冊表最常用的監控項為以下幾種類型,當然為了實現監控則我們必須要使用之前,如果使用之后則只能起到監視而無法做到監控的目的。
RegNtPreCreateKey 創建注冊表之前RegNtPreOpenKey 打開注冊表之前RegNtPreDeleteKey 刪除注冊表之前RegNtPreDeleteValueKey 刪除鍵值之前RegNtPreSetValueKey 修改注冊表之前
如果需要實現監視則,首先CmRegisterCallback注冊一個自定義回調,當有消息時則觸發MyLySharkCallback其內部獲取到lOperateType操作類型,并通過switch選擇不同的處理例程,每個處理例程都通過GetFullPath得到注冊表完整路徑,并打印出來,這段代碼實現如下。
代碼語言:javascript代碼運行次數:0運行復制
#include <ntifs.h>#include <windef.h>// 未導出函數聲明 pEProcess -> PIDPUCHAR PsGetProcessImageFileName(PEPROCESS pEProcess);NTSTATUS ObQueryNameString( _In_ PVOID Object, _Out_writes_bytes_opt_(Length) POBJECT_NAME_INFORMATION ObjectNameInfo, _In_ ULONG Length, _Out_ PULONG ReturnLength );// 注冊表回調CookieLARGE_INTEGER g_liRegCookie;// 獲取注冊表完整路徑BOOLEAN GetFullPath(PUNICODE_STRING pRegistryPath, PVOID pRegistryObject){ // 判斷數據地址是否有效 if ((FALSE == MmIsAddressValid(pRegistryObject)) || (NULL == pRegistryObject)) { return FALSE; } // 申請內存 ULONG ulSize = 512; PVOID lpObjectNameInfo = ExAllocatePool(NonPagedPool, ulSize); if (NULL == lpObjectNameInfo) { return FALSE; } // 獲取注冊表路徑 ULONG ulRetLen = 0; NTSTATUS status = ObQueryNameString(pRegistryObject, (POBJECT_NAME_INFORMATION)lpObjectNameInfo, ulSize, &ulRetLen); if (!NT_SUCCESS(status)) { ExFreePool(lpObjectNameInfo); return FALSE; } // 復制 RtlCopyUnicodeString(pRegistryPath, (PUNICODE_STRING)lpObjectNameInfo); // 釋放內存 ExFreePool(lpObjectNameInfo); return TRUE;}// 注冊表回調函數NTSTATUS MyLySharkCallback(_In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2){ NTSTATUS status = STATUS_SUCCESS; UNICODE_STRING ustrRegPath; // 獲取操作類型 LONG lOperateType = (REG_NOTIFY_CLASS)Argument1; // 申請內存 ustrRegPath.Length = 0; ustrRegPath.MaximumLength = 1024 * sizeof(WCHAR); ustrRegPath.Buffer = ExAllocatePool(NonPagedPool, ustrRegPath.MaximumLength); if (NULL == ustrRegPath.Buffer) { return status; } RtlZeroMemory(ustrRegPath.Buffer, ustrRegPath.MaximumLength); // 判斷操作 switch (lOperateType) { // 創建注冊表之前 case RegNtPreCreateKey: { // 獲取注冊表路徑 GetFullPath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject); DbgPrint("[創建注冊表][%wZ][%wZ] ", &ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->CompleteName); break; } // 打開注冊表之前 case RegNtPreOpenKey: { // 獲取注冊表路徑 GetFullPath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject); DbgPrint("[打開注冊表][%wZ][%wZ] ", &ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->CompleteName); break; } // 刪除鍵之前 case RegNtPreDeleteKey: { // 獲取注冊表路徑 GetFullPath(&ustrRegPath, ((PREG_DELETE_KEY_INFORMATION)Argument2)->Object); DbgPrint("[刪除鍵][%wZ] ", &ustrRegPath); break; } // 刪除鍵值之前 case RegNtPreDeleteValueKey: { // 獲取注冊表路徑 GetFullPath(&ustrRegPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->Object); DbgPrint("[刪除鍵值][%wZ][%wZ] ", &ustrRegPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->ValueName); // 獲取當前進程, 即操作注冊表的進程 PEPROCESS pEProcess = PsGetCurrentProcess(); if (NULL != pEProcess) { UCHAR *lpszProcessName = PsGetProcessImageFileName(pEProcess); if (NULL != lpszProcessName) { DbgPrint("進程 [%s] 刪除了鍵值對 ", lpszProcessName); } } break; } // 修改鍵值之前 case RegNtPreSetValueKey: { // 獲取注冊表路徑 GetFullPath(&ustrRegPath, ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->Object); DbgPrint("[修改鍵值][%wZ][%wZ] ", &ustrRegPath, ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->ValueName); break; } default: break; } // 釋放內存 if (NULL != ustrRegPath.Buffer) { ExFreePool(ustrRegPath.Buffer); ustrRegPath.Buffer = NULL; } return status;}VOID UnDriver(PDRIVER_OBJECT driver){ DbgPrint(("Uninstall Driver Is OK ")); // 注銷當前注冊表回調 if (0 DriverUnload = UnDriver; return STATUS_SUCCESS;}</windef.h></ntifs.h>
運行驅動程序,則會輸出當前系統中所有針對注冊表的操作,如下圖所示。

如上的代碼只能實現注冊表項的監視,而如果需要監控則需要在回調函數MyLySharkCallback判斷,如果指定注冊表項是需要保護的則直接返回status = STATUS_access_DENIED;從而達到保護注冊表的目的,核心代碼如下所示。
代碼語言:javascript代碼運行次數:0運行復制
// 反注冊表刪除回調NTSTATUS MyLySharkCallback(_In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2){ NTSTATUS status = STATUS_SUCCESS; UNICODE_STRING ustrRegPath; // 獲取操作類型 LONG lOperateType = (REG_NOTIFY_CLASS)Argument1; ustrRegPath.Length = 0; ustrRegPath.MaximumLength = 1024 * sizeof(WCHAR); ustrRegPath.Buffer = ExAllocatePool(NonPagedPool, ustrRegPath.MaximumLength); if (NULL == ustrRegPath.Buffer) { return status; } RtlZeroMemory(ustrRegPath.Buffer, ustrRegPath.MaximumLength); // 判斷操作 switch (lOperateType) { // 刪除鍵值之前 case RegNtPreDeleteValueKey: { // 獲取注冊表路徑 GetFullPath(&ustrRegPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->Object); DbgPrint("[刪除鍵值][%wZ][%wZ] ", &ustrRegPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->ValueName); // 如果要刪除指定注冊表項則拒絕 PWCH pszRegister = L"REGISTRYMACHINESOFTWARElyshark"; if (wcscmp(ustrRegPath.Buffer, pszRegister) == 0) { DbgPrint("[lyshark] 注冊表項刪除操作已被攔截! "); // 拒絕操作 status = STATUS_ACCESS_DENIED; } break; } default: break; } // 釋放內存 if (NULL != ustrRegPath.Buffer) { ExFreePool(ustrRegPath.Buffer); ustrRegPath.Buffer = NULL; } return status;}
運行驅動程序,然后我們嘗試刪除LySharkHKEY_LOCAL_MACHINESOFTWARElyshark里面的子項,則會提示如下信息。

當然這里的RegNtPreDeleteValueKey是指的刪除操作,如果將其替換成RegNtPreSetValueKey,那么只有當注冊表被創建才會攔截,此時就會變成攔截創建。
代碼語言:javascript代碼運行次數:0運行復制
// 攔截創建操作NTSTATUS MyLySharkCallback(_In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2){ NTSTATUS status = STATUS_SUCCESS; UNICODE_STRING ustrRegPath; // 獲取操作類型 LONG lOperateType = (REG_NOTIFY_CLASS)Argument1; // 申請內存 ustrRegPath.Length = 0; ustrRegPath.MaximumLength = 1024 * sizeof(WCHAR); ustrRegPath.Buffer = ExAllocatePool(NonPagedPool, ustrRegPath.MaximumLength); if (NULL == ustrRegPath.Buffer) { return status; } RtlZeroMemory(ustrRegPath.Buffer, ustrRegPath.MaximumLength); // 判斷操作 switch (lOperateType) { // 修改鍵值之前 case RegNtPreSetValueKey: { // 獲取注冊表路徑 GetFullPath(&ustrRegPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->Object); // 攔截創建 PWCH pszRegister = L"REGISTRYMACHINESOFTWARElyshark"; if (wcscmp(ustrRegPath.Buffer, pszRegister) == 0) { DbgPrint("[lyshark] 注冊表項創建操作已被攔截! "); status = STATUS_ACCESS_DENIED; } break; } default: break; } // 釋放內存 if (NULL != ustrRegPath.Buffer) { ExFreePool(ustrRegPath.Buffer); ustrRegPath.Buffer = NULL; } return status;}
加載驅動保護,然后我們嘗試在LySharkHKEY_LOCAL_MACHINESOFTWARElyshark里面創建一個子項,則會提示創建失敗。
