首页
社区
课程
招聘
[原创]通过PsSetLoadImageNotifyRoutine学习模块监控与反模块监控
2021-10-23 18:36 21542

[原创]通过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世界

最后于 2021-11-30 10:00 被1900编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回