通過PsSetLoadImageNotifyRoutine學習模塊監控與反模塊監控
本文要實現的是對模塊加載的監控,并卸載掉目標模塊。實驗是在WIN 7 X86系統上進行,需要使用的文件是InjectDll.dll和TestDriver.sys。這兩個文件的內容非常簡單,InjectDll.dll在加載進進程中以后只會彈出加載的對話框,而TestDriver.sys只是在驅動加載和卸載的時候打印字符提醒。
模塊監控
1、實現原理
在內核中可以通過PsSetLoadImageNotifyRoutine來設置模塊監控,監控系統中各個應用程序加載的DLL以及系統加載的驅動。在函數在中文檔的定義如下:
NTSTATUS PsSetLoadImageNotifyRoutine( IN PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine);
其中NotifyRoutine是一個LOAD_IMAGE_NOTIFY_ROUTINE的函數指針,該函數在文檔中的定義如下:
typedefVOID(*PLOAD_IMAGE_NOTIFY_ROUTINE)( __in PUNICODE_STRING FullImageName, __in HANDLE ProcessId, // pid into which image is being mapped __in PIMAGE_INFO ImageInfo );
參數 說明 FullImageName 指向模塊的完整路徑 ProcessId 加載模塊的進程PID。如果是驅動文件,該值為0 ImageInfo 指向IMAGE_INFO結構體,包含了模塊信息 |
IMAGE_INFO在文檔中的定義如下:
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;
最關鍵的兩個是ImageBase和ImageSize,分別代表了模塊的基地址和模塊的大小。
而要卸載模塊監控,則需要使用PsRemoveLoadImageNotifyRoutine,該函數在文檔中的定義如下,參數含義和設置模塊監控一樣。
NTSTATUS PsRemoveLoadImageNotifyRoutine( IN PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine );
驅動的卸載和DLL的卸載是不一樣的。
對于驅動的卸載,只需要找到驅動入口點,并且直接返回STATUS_ACCESS_DENIED(0xC0000022)錯誤碼就好。這樣,已經加載的驅動就會在執行的時候出錯,導致驅動啟動失敗。
對于DLL的卸載則需要使用未到出函數MmUnmapViewOfSection用來進行卸載。當由于加載進程模塊的時候,系統會有一個內部鎖,為了避免死鎖。要通過創建線程進程延遲等待,等待DLL加載完成以后在調用函數進行卸載。
具體代碼如下:
#include #include #define DRIVER_NAME L"TestDriver.sys" //要攔截的驅動名#define DLL_NAME L"InjectDll.dll" //要攔截的DLL名 typedef struct _DLL_INFO{ HANDLE ProcessId; PVOID pImageBase;}DLL_INFO, *PDLL_INFO; //Dll的信息,用來作為線程的參數傳遞 VOID DriverUnload(IN PDRIVER_OBJECT driverObject);VOID LoadImageNotifyRoutine(PUNICODE_STRING FullImageName, HANDLE ProcessId, PIMAGE_INFO ImageInfo); //模塊監控回調函數BOOLEAN DenyLoadDriver(PVOID pLoadImageBase); //對驅動的加載進行攔截BOOLEAN DenyLoadDll(HANDLE ProcessId, PVOID pImageBase); //對DLL的加載進行攔截NTSTATUS MmUnmapViewOfSection(PEPROCESS Process, PVOID BaseAddr); //未導出函數聲明VOID ThreadProc(PVOID StartContext); //運行的線程函數 NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath){ NTSTATUS status = STATUS_SUCCESS; DbgPrint("驅動加載完成\r"); status = PsSetLoadImageNotifyRoutine(LoadImageNotifyRoutine); if (!NT_SUCCESS(status)) { DbgPrint("模塊監控設置失敗 0x%X\r", status); } else { DbgPrint("模塊監控設置成功\r"); }exit: driverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS;} VOID LoadImageNotifyRoutine(PUNICODE_STRING FullImageName, HANDLE ProcessId, PIMAGE_INFO ImageInfo){ PDLL_INFO pDllInfo = NULL; HANDLE hThread = NULL; /* DbgPrint("===========================================================================\r"); DbgPrint("檢測到新加載的模塊,模塊信息如下:\r"); DbgPrint("加載該模塊的進程ID:%d 模塊完整名:%wZ 模塊基址:0x%X 模塊大小:0x%X", ProcessId, FullImageName, ImageInfo->ImageBase, ImageInfo->ImageSize); DbgPrint("===========================================================================\r"); */ // 是否是exe或者dll文件 if (ProcessId) { if (wcsstr(FullImageName->Buffer, DLL_NAME) != NULL) { pDllInfo = (PDLL_INFO)ExAllocatePool(NonPagedPool, sizeof(DLL_INFO)); if (!pDllInfo) { DbgPrint("ExAllocatePool Error"); } else { pDllInfo->ProcessId = ProcessId; pDllInfo->pImageBase = ImageInfo->ImageBase; PsCreateSystemThread(&hThread, 0, NULL, NtCurrentProcess(), NULL, ThreadProc, pDllInfo); if (hThread) ZwClose(hThread); } } } else { //加載的是驅動,判斷是否是要攔截的驅動 if (wcsstr(FullImageName->Buffer, DRIVER_NAME) != NULL ) { if (DenyLoadDriver(ImageInfo->ImageBase)) { DbgPrint("成功攔截驅動%wZ的加載\r", FullImageName); } } }} VOID ThreadProc(PVOID StartContext){ PDLL_INFO pDllInfo = (PDLL_INFO)StartContext; LARGE_INTEGER liTime = { 0 }; //延時5秒 liTime.QuadPart = -50 * 1000 * 1000; KeDelayExecutionThread(KernelMode, FALSE, &liTime); //卸載DLL if (DenyLoadDll(pDllInfo->ProcessId, pDllInfo->pImageBase)) { DbgPrint("Dll卸載完成\r"); } if (pDllInfo) ExFreePool(pDllInfo);} BOOLEAN DenyLoadDll(HANDLE ProcessId, PVOID pImageBase){ BOOLEAN bRet = TRUE; NTSTATUS status = STATUS_SUCCESS; PEPROCESS pEprocess = NULL; //保存加載DLL的進程的EPROCESS //根據進程PID獲取EPROCESS status = PsLookupProcessByProcessId(ProcessId, &pEprocess); if (!NT_SUCCESS(status)) { DbgPrint("PsLookupProcessByProcessId Error 0x%X", status); bRet = FALSE; goto exit; } //卸載模塊 status = MmUnmapViewOfSection(pEprocess, pImageBase); if (!NT_SUCCESS(status)) { DbgPrint("MmUnmapViewOfSection Error 0x%X\r", status); bRet = FALSE; goto exit; }exit: return bRet;} BOOLEAN DenyLoadDriver(PVOID pLoadImageBase){ BOOLEAN bRet = TRUE; NTSTATUS status = STATUS_SUCCESS; PVOID pVoid = NULL; PIMAGE_DOS_HEADER pDosHead = NULL; PIMAGE_NT_HEADERS pNtHeader = NULL; PVOID pDriverEntry = NULL; PMDL pMdl = NULL; // 要寫入的ShellCode,硬編碼的意思是 // mov eax, 0xC0000022 // ret UCHAR szShellCode[6] = { 0xB8, 0x22, 0x00, 0x00, 0xC0, 0xC3}; ULONG uShellCodeLength = 6; pDosHead = (PIMAGE_DOS_HEADER)pLoadImageBase; pNtHeader = (PIMAGE_NT_HEADERS)((ULONG)pLoadImageBase + pDosHead->e_lfanew); pDriverEntry = (PVOID)((ULONG)pDosHead + pNtHeader->OptionalHeader.AddressOfEntryPoint); //獲取驅動入口點位置 //創建MDL并為內存屬性添加可寫屬性 pMdl = MmCreateMdl(NULL, pDriverEntry, uShellCodeLength); if (pMdl == NULL) { bRet = FALSE; goto exit; } //建立內存頁的MDL描述 MmBuildMdlForNonPagedPool(pMdl); //改變MDL的標記為可寫 pMdl->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA; //映射MDL空間 pVoid = MmMapLockedPages(pMdl, KernelMode); //將shellcode拷到目標地址 RtlCopyMemory(pVoid, szShellCode, uShellCodeLength); //釋放MDL MmUnmapLockedPages(pVoid, pMdl); IoFreeMdl(pMdl); pMdl = NULL;exit: return bRet;} VOID DriverUnload(IN PDRIVER_OBJECT driverObject){ NTSTATUS status = STATUS_SUCCESS; status = PsRemoveLoadImageNotifyRoutine(LoadImageNotifyRoutine); if (!NT_SUCCESS(status)) { DbgPrint("模塊監控刪除失敗 0x%X\r", status); } else { DbgPrint("模塊監控刪除成功\r"); } DbgPrint("驅動卸載完成\r");}
2、實驗結果
對于DLL加載來說,當沒有開起監控程序卸載的時候,在加載DLL文件以后可以在監控軟件中查找到對應的DLL。


而當開起監控完成DLL卸載以后,就沒辦法找到相應的DLL了。


對于驅動來說,當沒有開起監控的時候,可以正常的加載與卸載。

而開起監控以后,驅動的加載就會被攔截。

反模塊監控
1、逆向分析PsSetLoadImageNotifyRoutine
PsSetLoadImageNotifyRoutine在IDA中的反匯編代碼如下:
PAGE:005709B3 ; NTSTATUS __stdcall PsSetLoadImageNotifyRoutine(PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine)PAGE:005709B3 public PsSetLoadImageNotifyRoutinePAGE:005709B3 PsSetLoadImageNotifyRoutine proc near ; CODE XREF: sub_580241+215↓pPAGE:005709B3PAGE:005709B3 NotifyRoutine = dword ptr 8PAGE:005709B3PAGE:005709B3 mov edi, ediPAGE:005709B5 push ebpPAGE:005709B6 mov ebp, espPAGE:005709B8 push ebxPAGE:005709B9 push esiPAGE:005709BA push ediPAGE:005709BB xor edi, edi ; edi清0PAGE:005709BD push edi ; 將0入棧PAGE:005709BE push [ebp+NotifyRoutine] ; 將函數地址入棧PAGE:005709C1 call AllocateAssign ; 分配12字節的內存,低4位和高4位清0,中間4位存放函數地址PAGE:005709C6 mov ebx, eax ; 申請到的內存地址賦給ebxPAGE:005709C8 cmp ebx, edi ; 判斷是否為0,也就是是否分配失敗PAGE:005709CA jz short loc_5709F1 ; 如果內存分配失敗則退出PAGE:005709CC mov esi, offset LoadImageFuncArray ; 將一個數組地址賦給esiPAGE:005709D1PAGE:005709D1 loc_5709D1: ; CODE XREF: PsSetLoadImageNotifyRoutine+36↓jPAGE:005709D1 push 0 ; 將0入棧PAGE:005709D3 mov ecx, ebx ; 將分配的地址賦給ecxPAGE:005709D5 mov eax, esi ; 將數組地址賦給eaxPAGE:005709D7 call SetArrayPAGE:005709DC test al, alPAGE:005709DE jnz short loc_5709FD ; 判斷返回值是否為0,不為0則執行成功,跳轉PAGE:005709E0 add edi, 4 ; edi加4PAGE:005709E3 add esi, 4 ; esi,也就是數組地址+4,所以這是在取數組的下一個元素PAGE:005709E6 cmp edi, 20h ; 判斷edi是否小于0x20,小于則跳轉過去執行函數PAGE:005709E9 jb short loc_5709D1 ; 根據前面的清0操作知道這里是為了保證循環調用8次,所以可以判斷這個數組中最多保存7個地址PAGE:005709EB push ebx ; BufferPAGE:005709EC call FreeAllocate ; 否則函數執行失敗,釋放申請到的內存PAGE:005709F1PAGE:005709F1 loc_5709F1: ; CODE XREF: PsSetLoadImageNotifyRoutine+17↑jPAGE:005709F1 mov eax, STATUS_INSUFFICIENT_RESOURCES ; 賦值失敗的返回值PAGE:005709F6PAGE:005709F6 loc_5709F6: ; CODE XREF: PsSetLoadImageNotifyRoutine+6B↓jPAGE:005709F6 pop ediPAGE:005709F7 pop esiPAGE:005709F8 pop ebxPAGE:005709F9 pop ebpPAGE:005709FA retn 4PAGE:005709FD ; ---------------------------------------------------------------------------PAGE:005709FDPAGE:005709FD loc_5709FD: ; CODE XREF: PsSetLoadImageNotifyRoutine+2B↑jPAGE:005709FD xor ecx, ecxPAGE:005709FF mov eax, offset unk_542BA0PAGE:00570A04 inc ecxPAGE:00570A05 lock xadd [eax], ecxPAGE:00570A09 mov eax, dword_542B78PAGE:00570A0E test al, 1PAGE:00570A10 jnz short loc_570A1CPAGE:00570A12 mov eax, offset dword_542B78PAGE:00570A17 lock bts dword ptr [eax], 0PAGE:00570A1CPAGE:00570A1C loc_570A1C: ; CODE XREF: PsSetLoadImageNotifyRoutine+5D↑jPAGE:00570A1C xor eax, eaxPAGE:00570A1E jmp short loc_5709F6PAGE:00570A1E PsSetLoadImageNotifyRoutine endp
基本上和這篇一樣:
通過對PsSetCreateProcessNotifyRoutineEx的逆向分析得出的結果來實現反進程監控(https://bbs.pediy.com/thread-269831.htm)
那這篇就不再詳細說,唯一的區別就是這里的數組的大小是8。
2、反模塊監控
接下來就根據下圖選擇0x7425BE作為硬編碼來查找模塊數組:

得出如下的反模塊監控代碼:
#include #include VOID DriverUnload(IN PDRIVER_OBJECT driverObject);PULONG GetImageArray(); NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath){ NTSTATUS status = STATUS_SUCCESS; PULONG pImageArray = NULL; PULONG pFuncAddr = NULL; ULONG i = 0; pImageArray = GetImageArray(); if (pImageArray) { for (i = 0; i < 8; i++) { if (pImageArray[i] & ~0x7) { pFuncAddr = (PULONG)(pImageArray[i] & ~0x7 + 4); status = PsRemoveLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)*pFuncAddr); if (NT_SUCCESS(status)) { DbgPrint("模塊監控刪除成功 模塊地址:0x%X\r", *pFuncAddr); } } } } exit: driverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS;} VOID DriverUnload(IN PDRIVER_OBJECT driverObject){ DbgPrint("驅動卸載完成\r");} PULONG GetImageArray(){ PULONG pImageArray = NULL; //要獲取的函數地址的函數名 UNICODE_STRING uStrFuncName = RTL_CONSTANT_STRING(L"PsSetLoadImageNotifyRoutine"); PUCHAR pPsSetCreateThreadNotifyRoutine = NULL; //獲取函數地址 pPsSetCreateThreadNotifyRoutine = (PUCHAR)MmGetSystemRoutineAddress(&uStrFuncName); while (*pPsSetCreateThreadNotifyRoutine != 0xC2) { if (*pPsSetCreateThreadNotifyRoutine == 0x74 && *(pPsSetCreateThreadNotifyRoutine + 1) == 0x25 && *(pPsSetCreateThreadNotifyRoutine + 2) == 0xBE) { pImageArray = (PULONG)*(PULONG)(pPsSetCreateThreadNotifyRoutine + 3); break; } pPsSetCreateThreadNotifyRoutine++; } return pImageArray;}
3、運行截圖
首先在驅動卸載前,可以看到所有模塊的加載都會監控到:

接下來運行反監控代碼可以看到模塊的加載就不再被監控到: