-
-
[原创]通过PsSetLoadImageNotifyRoutine学习模块监控与反模块监控
-
2021-10-23 18:36 21542
-
一.实验环境
本文要实现的是对模块加载的监控,并卸载掉目标模块。实验是在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的函数指针,该函数在文档中的定义如下
typedef VOID (*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 <ntifs.h> #include <ntimage.h> #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\n"); status = PsSetLoadImageNotifyRoutine(LoadImageNotifyRoutine); if (!NT_SUCCESS(status)) { DbgPrint("模块监控设置失败 0x%X\r\n", status); } else { DbgPrint("模块监控设置成功\r\n"); } 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\n"); DbgPrint("检测到新加载的模块,模块信息如下:\r\n"); DbgPrint("加载该模块的进程ID:%d 模块完整名:%wZ 模块基址:0x%X 模块大小:0x%X", ProcessId, FullImageName, ImageInfo->ImageBase, ImageInfo->ImageSize); DbgPrint("===========================================================================\r\n"); */ // 是否是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\n", 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\n"); } 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\n", 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\n", status); } else { DbgPrint("模块监控删除成功\r\n"); } DbgPrint("驱动卸载完成\r\n"); }
2.实验结果
对于DLL加载来说,当没有开起监控程序卸载的时候,在加载DLL文件以后可以在监控软件中查找到对应的DLL
而当开起监控完成DLL卸载以后,就没办法找到相应的DLL了
对于驱动来说,当没有开起监控的时候,可以正常的加载与卸载
而开起监控以后,驱动的加载就会被拦截
三.反模块监控
1.逆向分析PsSetLoadImageNotifyRoutine
PsSetLoadImageNotifyRoutine在IDA中的反汇编代码如下
PAGE:005709B3 ; NTSTATUS __stdcall PsSetLoadImageNotifyRoutine(PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine) PAGE:005709B3 public PsSetLoadImageNotifyRoutine PAGE:005709B3 PsSetLoadImageNotifyRoutine proc near ; CODE XREF: sub_580241+215↓p PAGE:005709B3 PAGE:005709B3 NotifyRoutine = dword ptr 8 PAGE:005709B3 PAGE:005709B3 mov edi, edi PAGE:005709B5 push ebp PAGE:005709B6 mov ebp, esp PAGE:005709B8 push ebx PAGE:005709B9 push esi PAGE:005709BA push edi PAGE:005709BB xor edi, edi ; edi清0 PAGE:005709BD push edi ; 将0入栈 PAGE:005709BE push [ebp+NotifyRoutine] ; 将函数地址入栈 PAGE:005709C1 call AllocateAssign ; 分配12字节的内存,低4位和高4位清0,中间4位存放函数地址 PAGE:005709C6 mov ebx, eax ; 申请到的内存地址赋给ebx PAGE:005709C8 cmp ebx, edi ; 判断是否为0,也就是是否分配失败 PAGE:005709CA jz short loc_5709F1 ; 如果内存分配失败则退出 PAGE:005709CC mov esi, offset LoadImageFuncArray ; 将一个数组地址赋给esi PAGE:005709D1 PAGE:005709D1 loc_5709D1: ; CODE XREF: PsSetLoadImageNotifyRoutine+36↓j PAGE:005709D1 push 0 ; 将0入栈 PAGE:005709D3 mov ecx, ebx ; 将分配的地址赋给ecx PAGE:005709D5 mov eax, esi ; 将数组地址赋给eax PAGE:005709D7 call SetArray PAGE:005709DC test al, al PAGE:005709DE jnz short loc_5709FD ; 判断返回值是否为0,不为0则执行成功,跳转 PAGE:005709E0 add edi, 4 ; edi加4 PAGE: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 ; Buffer PAGE:005709EC call FreeAllocate ; 否则函数执行失败,释放申请到的内存 PAGE:005709F1 PAGE:005709F1 loc_5709F1: ; CODE XREF: PsSetLoadImageNotifyRoutine+17↑j PAGE:005709F1 mov eax, STATUS_INSUFFICIENT_RESOURCES ; 赋值失败的返回值 PAGE:005709F6 PAGE:005709F6 loc_5709F6: ; CODE XREF: PsSetLoadImageNotifyRoutine+6B↓j PAGE:005709F6 pop edi PAGE:005709F7 pop esi PAGE:005709F8 pop ebx PAGE:005709F9 pop ebp PAGE:005709FA retn 4 PAGE:005709FD ; --------------------------------------------------------------------------- PAGE:005709FD PAGE:005709FD loc_5709FD: ; CODE XREF: PsSetLoadImageNotifyRoutine+2B↑j PAGE:005709FD xor ecx, ecx PAGE:005709FF mov eax, offset unk_542BA0 PAGE:00570A04 inc ecx PAGE:00570A05 lock xadd [eax], ecx PAGE:00570A09 mov eax, dword_542B78 PAGE:00570A0E test al, 1 PAGE:00570A10 jnz short loc_570A1C PAGE:00570A12 mov eax, offset dword_542B78 PAGE:00570A17 lock bts dword ptr [eax], 0 PAGE:00570A1C PAGE:00570A1C loc_570A1C: ; CODE XREF: PsSetLoadImageNotifyRoutine+5D↑j PAGE:00570A1C xor eax, eax PAGE:00570A1E jmp short loc_5709F6 PAGE:00570A1E PsSetLoadImageNotifyRoutine endp
基本上和这篇一样通过对PsSetCreateProcessNotifyRoutineEx的逆向分析得出的结果来实现反进程监控,那这篇就不再详细说,唯一的区别就是这里的数组的大小是8。
2.反模块监控
接下来就根据下图选择0x7425BE作为硬编码来查找模块数组
得出如下的反模块监控代码
#include <ntifs.h> #include <ntimage.h> 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\n", *pFuncAddr); } } } } exit: driverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; } VOID DriverUnload(IN PDRIVER_OBJECT driverObject) { DbgPrint("驱动卸载完成\r\n"); } 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.运行截图
首先在驱动卸载前,可以看到所有模块的加载都会监控到
接下来运行反监控代码可以看到模块的加载就不再被监控到
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界