查看原文
其他

通过PsSetLoadImageNotifyRoutine学习模块监控与反模块监控

1900 看雪学苑 2022-07-01


本文为看雪论坛优秀文章
看雪论坛作者ID:1900


本文要实现的是对模块加载的监控,并卸载掉目标模块。实验是在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 <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 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 <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、运行截图


首先在驱动卸载前,可以看到所有模块的加载都会监控到:


接下来运行反监控代码可以看到模块的加载就不再被监控到:


 


看雪ID:1900

https://bbs.pediy.com/user-home-835440.htm

*本文由看雪论坛 1900 原创,转载请注明来自看雪社区






# 往期推荐

1.Kernel从0开始

2.常见的几种DLL注入技术

3.侠盗猎车 — 玩转固定码

4.Java正则表达式笔记总结

5.Frida动静态结合分析Base64签名校验的四个变种

6.二进制漏洞挖掘学习——MS09-050





球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存