基于PsSetLoadImageNotifyRoutine实现监控模块加载并卸载已加载模块

大葱

发布日期: 2019-02-17 10:18:06 浏览量: 1732
评分:
star star star star star star star star star_border star_border
*转载请注明来自write-bug.com

背景

对于内核层实现监控模块的加载,包括加载DLL模块、内核模块等。你也许会想到 HOOK 各种内核函数来实现。确定,在内核层中的 HOOK 已经给人留下太多深刻的印象了,有 SSDT HOOK、Inline HOOK、IRP HOOK、过滤驱动等等。

但是,Windows 其实给我们提供现成的内核函数接口,方便我们在内核下监控用户层上模块加载的情况,即 PsSetLoadImageNotifyRoutine 内核函数,可以设置一个回调函数,来监控监控模块加载。

现在,本文就使用 PsSetLoadImageNotifyRoutine 实现监控模块加载以及卸载加载模块的实现过程和原理进行整理,形成文档,分享给大家。

函数介绍

PsSetLoadImageNotifyRoutine 函数

设置模块加载回调函数,只要有模块加载完成就会通知回调函数。

函数声明

  1. NTSTATUS PsSetLoadImageNotifyRoutine(
  2. _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
  3. );

参数

  • NotifyRoutine [in]
    指向回调函数 PLOAD_IMAGE_NOTIFY_ROUTINE 的指针。

返回值

  • 成功,则返回 STATUS_SUCCESS;否则,返回其它失败错误码 NTSTATUS。

备注

  • 可通过调用 PsRemoveLoadImageNotifyRoutine 函数来删除回调。

PLOAD_IMAGE_NOTIFY_ROUTINE 回调函数

函数声明

  1. PLOAD_IMAGE_NOTIFY_ROUTINE SetLoadImageNotifyRoutine;
  2. void SetLoadImageNotifyRoutine(
  3. _In_opt_ PUNICODE_STRING FullImageName,
  4. _In_ HANDLE ProcessId,
  5. _In_ PIMAGE_INFO ImageInfo
  6. )
  7. { ... }

参数

FullImageName [in,可选]
指向缓冲的Unicode字符串的指针,用于标识可执行映像文件。 (在程序创建时操作系统无法获取图像的全名的情况下,FullImageName参数可以为NULL。)

ProcessId [in]
加载模块所属的进程ID,但如果新加载的映像是驱动程序,则该句柄为 0。

ImageInfo [in]
指向包含图像信息的 IMAGE_INFO 结构的指针。 见备注。

返回值

  • 无返回值。

IMAGE_INFO 结构体

  1. typedef struct _IMAGE_INFO {
  2. union {
  3. ULONG Properties;
  4. struct {
  5. ULONG ImageAddressingMode : 8;
  6. ULONG SystemModeImage : 1;
  7. ULONG ImageMappedToAllPids : 1;
  8. ULONG ExtendedInfoPresent : 1;
  9. ULONG MachineTypeMismatch : 1;
  10. ULONG ImageSignatureLevel : 4;
  11. ULONG ImageSignatureType : 3;
  12. ULONG Reserved : 13;
  13. };
  14. };
  15. PVOID ImageBase;
  16. ULONG ImageSelector;
  17. SIZE_T ImageSize;
  18. ULONG ImageSectionNumber;
  19. } IMAGE_INFO, *PIMAGE_INFO;

成员

  • Properties
    ImageAddressingMode
    始终设置为IMAGE_ADDRESSING_MODE_32BIT。
  • SystemModeImage
    设置为一个用于新加载的内核模式组件(如驱动程序),或者对于映射到用户空间的映像设置为 0。
  • ImageMappedToAllPids
    始终设置为0。
  • ExtendedInfoPresent
    如果设置了ExtendedInfoPresent标志,则IMAGE_INFO结构是图像信息结构的较大扩展版本的一部分(请参阅IMAGE_INFO_EX)。在Windows Vista中添加。有关详细信息,请参阅本备注部分的“扩展版本的图像信息结构”。
  • MachineTypeMismatch
    始终设置为 0。在Windows 8 / Windows Server 2012中添加。
  • ImageSignatureLevel
    代码完整性标记为映像的签名级别。该值是ntddk.h中的#define SESIGNING_LEVEL *常量之一。在Windows 8.1 / Windows Server 2012 R2中添加。
  • ImageSignatureType
    代码完整性标记为映像的签名类型。该值是在ntddk.h中定义的SE_IMAGE_SIGNATURE_TYPE枚举值。在Windows 8.1 / Windows Server 2012 R2中添加。
  • ImagePartialMap
    如果调用的映像视图是不映射整个映像的部分视图,则该值不为零; 0如果视图映射整个图像。在Windows 10 / Windows Server 2016中添加。
  • Reserved
    始终设置为 0。
  • ImageBase
    设置为映像的虚拟基地址。
  • ImageSelector
    始终设置为 0。
  • ImageSize
    映像的虚拟大小(以字节为单位)。
  • ImageSectionNumber
    始终设置为 0。

实现原理

我们根据上面的函数介绍,大概知道实现的流程了吧。对于设置回调函数,直接调用 PsSetLoadImageNotifyRoutine 函数来设置就好。传入设置的回调函数名称,这样,就可以成功设置进程监控的回调函数了。

那么,我们的回调函数也并不复杂,它的函数声明为:

  1. void SetLoadImageNotifyRoutine(
  2. _In_opt_ PUNICODE_STRING FullImageName,
  3. _In_ HANDLE ProcessId,
  4. _In_ PIMAGE_INFO ImageInfo
  5. );

回调函数的名称可以任意,但是返回值类型以及函数参数类型必须是固定的,不能变更。回调函数的第一个参数 FullImageName 表示加载模块的路径;第二个参数 ProcessId 表示加载模块所属的进程PID,如果为0,则表示该模块是一个驱动模块;第三个参数 ImageInfo 存储着模块的加载信息,存储在 IMAGE_INFO 结构体中。

我们可以从 IMAGE_INFO 中模块的加载内存大小、加载基址等加载信息。

当我们要删除会调设置的时候,只需要调用 PsRemoveLoadImageNotifyRoutine 函数,传入要删除的回调函数名称,这样,就可以成功删除设置的回调函数了。

要清楚一个问题就是,当我们的回调函数接收到模块加载信息的时候,模块已经加载完成,所以,我们需要通过其他方法来实现卸载已加载的模块。本文只讨论卸载驱动模块以及 DLL 模块。接下来,分别给出二者的实现思路:

卸载驱动模块

对于卸载驱动模块,我们的实现思路就是在驱动模块的入口点 DriverEntry 函数中,直接返回 NTSTATUS 错误码,如 STATUS_ACCESS_DENIED (0xC0000022)。那么,我们就需要先定位处 DriverEntry 的内存地址。

好在回调函数的第三个参数 ImageInfo 给我们提供了模块在内存中的加载基址,而且,驱动文件 .sys 也是一个可执行文件。所以,我们可以根据 PE 结构,获取 IMAGE_NT_HEADERS 头的 IMAGE_OPTIONAL_HEADRE 的 AddressOfEntryPoint 字段,再加上加载基址,就可以计算出驱动程序 DriverEntry 函数的地址了。

获得 DriverEntry 函数后,我们直接将入口函数的前几个字节数据修改为 :

  1. B8 22 00 00 C0 C3

对应的汇编代码为:

  1. mov eax, 0xC0000022
  2. ret

由于在 32 位程序和 64 位程序下,NTSTATUS 数据类型都是无符号 4 字节整型数据,所以,机器码都是不变的,在 32 位程序和 64 位程序下都是通用的。

卸载 DLL 模块

由于模块已经加载完成,我们不能通过类似卸载驱动模块那样直接在入口点返回拒绝加载信息,因为 DLL 的入口点函数 DllMain 的返回值并不能决定 DLL 是否成功加载,所以,这样达不到卸载 DLL 的效果。

要想卸载 DLL ,换个思路想,也就是即使 DLL 加载了,但也不能那个让 DLL 正常工作。如果这个问题换成了让加载的 DLL 不能正常工作,事情一下子就好办了。我们可以直接将 PE 头数据全部值为 0,破坏 PE 头的数据,这样,DLL 在往后就不能正常去获取导出函数等工作,从而间接实现卸载 DLL 的功能。

所以,我们实现的思路就是将 DLL 加载基址的前 0x200 字节数据全部置为 0,破环 PE 头结构。

编码实现

设置回调

  1. // 设置回调函数
  2. NTSTATUS SetLoadImageNotify()
  3. {
  4. NTSTATUS status = PsSetLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyRoutine);
  5. if (!NT_SUCCESS(status))
  6. {
  7. ShowError("PsSetLoadImageNotifyRoutine", status);
  8. }
  9. return status;
  10. }

删除回调

  1. // 删除设置的回调函数
  2. NTSTATUS RemoveLoadImageNotify()
  3. {
  4. NTSTATUS status = PsRemoveLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyRoutine);
  5. if (!NT_SUCCESS(status))
  6. {
  7. ShowError("PsRemoveLoadImageNotifyRoutine", status);
  8. }
  9. return status;
  10. }

回调函数

  1. // 回调函数
  2. VOID LoadImageNotifyRoutine(
  3. _In_ PUNICODE_STRING FullImageName,
  4. // pid into which image is being mapped
  5. _In_ HANDLE ProcessId,
  6. _In_ PIMAGE_INFO ImageInfo
  7. )
  8. {
  9. // 显示加载模块信息
  10. DbgPrint("[%d][%wZ][%d][0x%p]\n", ProcessId, FullImageName, ImageInfo->ImageSize, ImageInfo->ImageBase);
  11. // 拒绝加载指定模块
  12. if (NULL != wcsstr(FullImageName->Buffer, L"DriverTest.sys") ||
  13. NULL != wcsstr(FullImageName->Buffer, L"Test.dll"))
  14. {
  15. // Driver
  16. if (0 == ProcessId)
  17. {
  18. DbgPrint("Deny Load Driver\n");
  19. DenyLoadDriver(ImageInfo->ImageBase);
  20. }
  21. // Dll
  22. else
  23. {
  24. DbgPrint("Deny Load DLL\n");
  25. DenyLoadDll(ImageInfo->ImageBase);
  26. }
  27. }
  28. }

拒绝驱动模块的加载

  1. // 拒绝加载驱动模块
  2. BOOLEAN DenyLoadDriver(PVOID pLoadImageBase)
  3. {
  4. // 拒绝加载驱动
  5. // 即根据BaseImage,找到IMAGE_NT_HEADERS中的IMAGE_OPTIONAL_HEADRE中的AddressOfEntryPoint,
  6. // 写入mov eax, STATUS_ACCESS_DENIED ret,意思是返回 NTSTATUS 码为 STATUS_ACCESS_DENIED 拒绝访问
  7. // 即mov eax, 0xC0000022 ret 机器码是:b8220000C0 C3
  8. // 无论64位驱动还是32位驱动, 都是写入以下ShellCode, 拒绝加载. 因为 NTSTATUS 在32位和64位下都是 4 字节.
  9. // 在入口地址处, 写入代码:
  10. // mov eax, 0xC0000022
  11. // ret
  12. // 机器码为:
  13. // B8 22 00 00 C0 C3
  14. // 根据加载基址, 获取入口地址
  15. PIMAGE_DOS_HEADER pDosHeader = pLoadImageBase;
  16. PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PCHAR)pDosHeader + pDosHeader->e_lfanew);
  17. PVOID pAddressOfEntryPoint = (PVOID)((PCHAR)pDosHeader + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
  18. DbgPrint("----------- pAddressOfEntryPoint=0x%p\n", pAddressOfEntryPoint);
  19. // 使用 MDL 方式写入SHELLCODE
  20. ULONG ulShellCodeSize = 6;
  21. UCHAR pShellCode[6] = { 0xB8, 0x22, 0x00, 0x00, 0xC0, 0xC3 };
  22. PMDL pMdl = MmCreateMdl(NULL, pAddressOfEntryPoint, ulShellCodeSize);
  23. if (NULL == pMdl)
  24. {
  25. ShowError("MmCreateMdl", 0);
  26. return FALSE;
  27. }
  28. MmBuildMdlForNonPagedPool(pMdl);
  29. PVOID pVoid = MmMapLockedPages(pMdl, KernelMode);
  30. if (NULL == pVoid)
  31. {
  32. IoFreeMdl(pMdl);
  33. ShowError("MmMapLockedPages", 0);
  34. return FALSE;
  35. }
  36. // 写入数据
  37. RtlCopyMemory(pVoid, pShellCode, ulShellCodeSize);
  38. // 释放 MDL
  39. MmUnmapLockedPages(pVoid, pMdl);
  40. IoFreeMdl(pMdl);
  41. return TRUE;
  42. }

拒绝 DLL 模块的加载

  1. // 拒绝加载 DLL 模块
  2. BOOLEAN DenyLoadDll(PVOID pLoadImageBase)
  3. {
  4. // DLL拒绝加载, 不能类似驱动那样直接在入口点返回拒绝加载信息. 这样达不到卸载DLL的效果.
  5. // 将文件头 前0x200 字节数据置零
  6. ULONG ulDataSize = 0x200;
  7. // 创建 MDL 方式修改内存
  8. PMDL pMdl = MmCreateMdl(NULL, pLoadImageBase, ulDataSize);
  9. if (NULL == pMdl)
  10. {
  11. ShowError("MmCreateMdl", 0);
  12. return FALSE;
  13. }
  14. MmBuildMdlForNonPagedPool(pMdl);
  15. PVOID pVoid = MmMapLockedPages(pMdl, KernelMode);
  16. if (NULL == pVoid)
  17. {
  18. IoFreeMdl(pMdl);
  19. ShowError("MmMapLockedPages", 0);
  20. return FALSE;
  21. }
  22. // 置零
  23. RtlZeroMemory(pVoid, ulDataSize);
  24. // 释放 MDL
  25. MmUnmapLockedPages(pVoid, pMdl);
  26. IoFreeMdl(pMdl);
  27. return TRUE;
  28. }

程序测试

在 Win7 32 位系统下,驱动程序正常执行:

在 Win10 64 位系统下,驱动程序正常执行:

总结

这个程序实现起来并不复杂,关键是对 PsSetLoadImageNotifyRoutine 函数要理解透彻。

其中,我们在根据加载基址卸载加载模块的时候,更改加载模块内存数据的时候,建议通过 MDL 方式来修改内存,这样会比较安全和保险。

参考

参考自《Windows黑客编程技术详解》一书

上传的附件 cloud_download PsSetLoadImageNotifyRoutine_Test.7z ( 10.42kb, 34次下载 )

发送私信

这一切都不是我的,但总有一天,会是我的

77
文章数
68
评论数
最近文章
eject