在x86的體系結構中,我們常用hook關鍵的系統調用來達到對系統的監控,但是對於x64的結構,因為有PatchGuard的存在,對於一些系統關鍵點進行hook是很不穩定的,在很大幾率上會導致藍屏的發生,而且在Vista之后的操作系統中,還提供了ObRegisterCallbacks()函數注冊自定義的回調對特定的對象進行監控。本文就是對在ring0經常使用的幾種回調進行一個小結。
進程創建回調
要監控系統進程的創建,我們可以hook NtCreateProcess或者是更為底層的PspCreateProcess。但是最好的方法是利用系統提供的回調,這樣可以增強程序的兼容性和健壯性,首先我們要注冊一個回調函數,使用WDK提供的API接口函數PsSetCreateProcessNotifyRoutine
NTSTATUS
PsSetCreateProcessNotifyRoutine(
IN PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
IN BOOLEAN Remove
);
NotifyRoutine是個函數指針,函數原型如下:
Remove表示是增加一個回調還是刪除一個回調,TRUE表示刪除,FALSE表示增加
VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE) ( IN HANDLE ParentId, IN HANDLE ProcessId, IN BOOLEAN Create );
下面是使用這個回調的一個小例子:
#include <ntifs.h> VOID ProcessCallBack(IN HANDLE ParentId,IN HANDLE ProcessId,IN BOOLEAN bCreate); VOID UnloadDriver(PDRIVER_OBJECT DriverObject); NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegisterPath) { NTSTATUS Status = STATUS_SUCCESS; DbgPrint("驅動加載\r\n"); DriverObject->DriverUnload = UnloadDriver; Status = PsSetCreateProcessNotifyRoutine((PCREATE_PROCESS_NOTIFY_ROUTINE)ProcessCallBack,FALSE); return STATUS_SUCCESS; } VOID ProcessCallBack(IN HANDLE ParentId,IN HANDLE ProcessId,IN BOOLEAN bCreate) { if (bCreate==TRUE) { DbgPrint("%d進程被創建\r\n",ProcessId); } else { DbgPrint("%d進程被銷毀\r\n",ProcessId); } } VOID UnloadDriver(PDRIVER_OBJECT DriverObject) { PsSetCreateProcessNotifyRoutine((PCREATE_PROCESS_NOTIFY_ROUTINE)ProcessCallBack,TRUE); DbgPrint("驅動卸載\r\n"); }
利用PsSetCreateProcessNotifyRoutine我們可以知道哪些進程被創建,哪些被銷毀,但是對於一些目標進程不能進行攔截,比如說,如果在我們hook NtCreateProcess的情況下,是可以防止calc.exe創建的,而PsSetCreateProcessNotifyRoutine只能讓我們知道calc.exe創建了,卻不能阻止它,所以我們要用到另一個API:PsSetCreateProcessNotifyRoutineEx,也就是PsSetCreateProcessNotifyRoutine的“升級版”,可以用來對進程的創建進行攔截,先看函數的聲明:
NTSTATUS
PsSetCreateProcessNotifyRoutineEx(
IN PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
IN BOOLEAN Remove
);
對比與PsSetCreateProcessNotifyRoutine第一參數,也就是回調的函數指針類型發生了變化:
//回調函數 VOID CreateProcessNotifyEx( __inout PEPROCESS Process, //要創建的進程的進程體 __in HANDLE ProcessId, //進程的ID __in_opt PPS_CREATE_NOTIFY_INFO CreateInfo //新進程的信息 );
CreateInfo包含了進程成創建的主要信息:
typedef struct _PS_CREATE_NOTIFY_INFO { __in SIZE_T Size; //_PS_CREATE_NOTIFY_INFO結構體的大小 union { __in ULONG Flags; struct { __in ULONG FileOpenNameAvailable : 1; __in ULONG Reserved : 31; }; }; __in HANDLE ParentProcessId; //新進程的父進程ID __in CLIENT_ID CreatingThreadId; //結構體中包含進程ID和線程ID __inout struct _FILE_OBJECT *FileObject; //新進程的exe文件的文件對象 __in PCUNICODE_STRING ImageFileName; //exe文件名稱 __in_opt PCUNICODE_STRING CommandLine; __inout NTSTATUS CreationStatus; //進程創建的狀態 } PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
而我們要阻止一個新進程的創建,就是要改變CreateInfo中的CreationStatus的值,下面的小例子就是阻止calc.exe創建:
GetProcessPathBySectionObject()自封裝的函數,來自 進程完整路徑獲得
#include <ntifs.h> NTSTATUS RegisterProcessFilter(); VOID ProcessCallBackEx(PEPROCESS EProcess,HANDLE ProcessId,PPS_CREATE_NOTIFY_INFO CreateInfo); VOID UnloadDriver(PDRIVER_OBJECT DriverObject); NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegisterPath) { PDEVICE_OBJECT DeviceObject; NTSTATUS Status; ULONG i; DriverObject->DriverUnload = UnloadDriver; // RegisterProcessFilter(); return STATUS_SUCCESS; } NTSTATUS RegisterProcessFilter() { NTSTATUS Status; Status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)ProcessCallBackEx,FALSE); //添加一個 進程 創建的回調Notity if (!NT_SUCCESS(Status)) { return Status; } Status; } VOID ProcessCallBackEx(PEPROCESS EProcess,HANDLE ProcessId,PPS_CREATE_NOTIFY_INFO CreateInfo) { NTSTATUS Status; WCHAR wzProcessPath[512] = {0}; if (CreateInfo) { if (GetProcessPathBySectionObject(EProcess,wzProcessPath)==TRUE) { if (wcscmp(wzProcessPath,L"C:\\Windows\\System32\\calc.exe")==0) { CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL; } } } else { //這里是一個進程退出的請求 } } VOID UnloadDriver(PDRIVER_OBJECT DriverObject) { ULONG i = 0; PDEVICE_OBJECT NextObject = NULL; PDEVICE_OBJECT CurrentObject = NULL; CurrentObject = DriverObject->DeviceObject; while (CurrentObject != NULL) { NextObject = CurrentObject->NextDevice; IoDeleteDevice(CurrentObject); CurrentObject = NextObject; } PsSetCreateProcessNotifyRoutineEx(ProcessCallBackEx,TRUE); return; }
映像加載回調
當以個可執行文件被加載或者是映射進內存的時候,我們注冊的回調函數被調用。這樣就可以做很多事了,比如對於當目標進程被創建映射exe文件時,我們可以處理目標進程的導入表來達到注入的目的,因為當有新進程創建時,我們的回調函數是處於新進程的Context之中;也可以對我們自己的進程進行保護來防止注入,例如當有可執行文件被映射時,我們可以判斷ProcessId是否為我們自己的進程,如果是我們自己的進程,在得到可執行文件被映射的基地址之后,調用ZwUnmapViewOfSection()來取消映射,達到反注入的目的。
VOID (*PLOAD_IMAGE_NOTIFY_ROUTINE) ( IN PUNICODE_STRING FullImageName, //被映射的可執行文件的名稱 IN HANDLE ProcessId, // where image is mapped //映射的進程,如果是.sys,就為0 IN PIMAGE_INFO ImageInfo //映射信息的結構體 );
typedef struct _IMAGE_INFO { union { ULONG Properties; struct { ULONG ImageAddressingMode : 8; //code addressing mode ULONG SystemModeImage : 1; //system mode image ULONG ImageMappedToAllPids : 1; //mapped in all processes ULONG Reserved : 22; }; }; PVOID ImageBase; //映射的虛擬地址 ULONG ImageSelector; ULONG ImageSize; //映射的大小 ULONG ImageSectionNumber; } IMAGE_INFO, *PIMAGE_INFO;
我們可以通過ImageInfo中的信息得到映像被映射的基地址,來反注入。而IMAGE_INFO結構在Vista 之后又有擴展:
//Vista 之后的定義 typedef struct _IMAGE_INFO { union { ULONG Properties; struct { ULONG ImageAddressingMode : 8; // Code addressing mode ULONG SystemModeImage : 1; // System mode image ULONG ImageMappedToAllPids : 1; // Image mapped into all processes ULONG ExtendedInfoPresent : 1; // IMAGE_INFO_EX available ULONG Reserved : 21; }; }; PVOID ImageBase; ULONG ImageSelector; SIZE_T ImageSize; ULONG ImageSectionNumber; } IMAGE_INFO, *PIMAGE_INFO;
主要的變化就是增加了ExtendedInfoPresent位,如果ExtendedInfoPresent置1的話,則IMAGE_INFO只是IMAGE_INFO_EX的一部分,可以通過CONTAINING_RECORD來獲得整個IMAGE_INFO_EX結構。
#define CONTAINING_RECORD(addr,type,field) ((type*)((unsigned char*)addr - (unsigned long)&((type*)0)->field)) //addr: 結構體中某個成員變量的地址 //type: 結構體的原型 //field: 結構體的某個成員(與前面相同)
typedef struct _IMAGE_INFO_EX { SIZE_T Size; //IMAGE_INFO_EX結構體的大小 IMAGE_INFO ImageInfo; struct _FILE_OBJECT *FileObject; //映像文件的文件對象 } IMAGE_INFO_EX, *PIMAGE_INFO_EX;
下面是一個簡單的使用LoadImageNotify來監控驅動加載的例子:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegisterPath) { DbgPrint("驅動加載\r\n"); DriverObject->DriverUnload = UnloadDriver; PsSetLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyRoutine); return STATUS_SUCCESS; } VOID UnloadDriver(PDRIVER_OBJECT DriverObject) { PsRemoveLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyRoutine); DbgPrint("驅動卸載\r\n"); } VOID LoadImageNotifyRoutine(PUNICODE_STRING FullImageName,HANDLE ProcessId,PIMAGE_INFO ImageInfor) { PVOID DriverEntryAddress = NULL; char szFullImageName[260]={0}; if(!ProcessId && FullImageName!=NULL && MmIsAddressValid(FullImageName)) { DbgPrint("%wZ 驅動加載\r\n",FullImageName); } }
我們之前提到過,可以在進程創建映射.exe文件時利用LoadImageNotify來改變它的導入表,我們這里小結了兩種回調,就又產生了新的問題:在進程創建時,是CreateProcessNotify先執行,還是LoadImageNotify先執行?
我們用一個小例子來試驗,就以“calc.exe”為例:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryString) { DriverObject->DriverUnload = DriverUnload; PsSetLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyRoutine); PsSetCreateProcessNotifyRoutine((PCREATE_PROCESS_NOTIFY_ROUTINE)ProcessCallBack,FALSE); return STATUS_SUCCESS; } VOID DriverUnload(IN PDRIVER_OBJECT DriverObject) { PsRemoveLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyRoutine); PsSetCreateProcessNotifyRoutine((PCREATE_PROCESS_NOTIFY_ROUTINE)ProcessCallBack,TRUE); DbgPrint("驅動卸載\r\n"); } VOID LoadImageNotifyRoutine(PUNICODE_STRING FullImageName,HANDLE ProcessId,PIMAGE_INFO ImageInfor) { PVOID DriverEntryAddress = NULL; char szFullImageName[260]={0}; NTSTATUS Status = STATUS_UNSUCCESSFUL; PEPROCESS EProcess; if (ProcessId) { UnicodeToChar(FullImageName,szFullImageName); // DbgPrint("FullImageName:%s\r\n",szFullImageName); if(strstr(szFullImageName, "calc.exe")) { DbgPrint("calc.exeLoadImage\r\n"); } } } VOID ProcessCallBack(IN HANDLE ParentId,IN HANDLE ProcessId,IN BOOLEAN bCreate) { if (bCreate==TRUE) { DbgPrint("%d CreateProcessNotify\r\n",ProcessId); } else { DbgPrint("%d ExitProcessNotify\r\n",ProcessId); } } VOID UnicodeToChar(PUNICODE_STRING uniSource, CHAR *szDest) { ANSI_STRING ansiTemp; RtlUnicodeStringToAnsiString(&ansiTemp,uniSource,TRUE); strcpy(szDest,ansiTemp.Buffer); RtlFreeAnsiString(&ansiTemp); }
測試的結果就是CreateProcessNotifyRoutine先執行!
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
今天做dll加載檢測的時候又遇到的一個問題:當dll被加載,執行我們的回調函數時,被獲得dll的全路徑這個問題卡住了,因為如果是Windows自己的dll文件在FullImageName中是有盤符的,一般是/SystemRoot/../ , 而我們自己的dll文件是沒有盤符的,只有一個目錄的路徑,在Vista之后的EX結構中是存在文件對象,我們可以獲得完整路徑,但是XP下沒有,問題就來了,怎么才能獲得完整路徑呢?嘿嘿,萬萬沒想到啊,這個FullImageName的指針指向的就是文件對象中的FullImageName,我們可以直接通過FullImageName直接獲得文件對象,然后再或完整路徑,哈哈!
下一篇總結注冊表回調,線程創建回調和手動注冊回調監控特定對象。