背景

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

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

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

函数介绍

PsSetLoadImageNotifyRoutine 函数

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

函数声明

NTSTATUS PsSetLoadImageNotifyRoutine(
_In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);

参数

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

返回值

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

备注

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

PLOAD_IMAGE_NOTIFY_ROUTINE 回调函数

函数声明

PLOAD_IMAGE_NOTIFY_ROUTINE SetLoadImageNotifyRoutine;void SetLoadImageNotifyRoutine(
_In_opt_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId,
_In_ PIMAGE_INFO ImageInfo
)
{ ... }

参数

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

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

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

返回值

  • 无返回值。

IMAGE_INFO 结构体

typedef struct _IMAGE_INFO {
union {
ULONG Properties;
struct {
ULONG ImageAddressingMode : 8;
ULONG SystemModeImage : 1;
ULONG ImageMappedToAllPids : 1;
ULONG ExtendedInfoPresent : 1;
ULONG MachineTypeMismatch : 1;
ULONG ImageSignatureLevel : 4;
ULONG ImageSignatureType : 3;
ULONG Reserved : 13;
};
};
PVOID ImageBase;
ULONG ImageSelector;
SIZE_T ImageSize;
ULONG ImageSectionNumber;
} 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 函数来设置就好。传入设置的回调函数名称,这样,就可以成功设置进程监控的回调函数了。

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


void SetLoadImageNotifyRoutine(
_In_opt_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId,
_In_ PIMAGE_INFO ImageInfo
);

回调函数的名称可以任意,但是返回值类型以及函数参数类型必须是固定的,不能变更。回调函数的第一个参数 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 头结构。

编码实现

设置回调


// 设置回调函数
NTSTATUS SetLoadImageNotify()
{
NTSTATUS status = PsSetLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyRoutine);
if (!NT_SUCCESS(status))
{
ShowError("PsSetLoadImageNotifyRoutine", status);
}
return status;
}

删除回调


// 删除设置的回调函数
NTSTATUS RemoveLoadImageNotify()
{
NTSTATUS status = PsRemoveLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyRoutine);
if (!NT_SUCCESS(status))
{
ShowError("PsRemoveLoadImageNotifyRoutine", status);
}
return status;
}

// 回调函数
VOID LoadImageNotifyRoutine(
_In_ PUNICODE_STRING FullImageName,
// pid into which image is being mapped
_In_ HANDLE ProcessId,
_In_ PIMAGE_INFO ImageInfo
)
{
// 显示加载模块信息DbgPrint("[%d][%wZ][%d][0x%p]\n", ProcessId, FullImageName, ImageInfo->ImageSize, ImageInfo->ImageBase);// 拒绝加载指定模块
if (NULL != wcsstr(FullImageName->Buffer, L"DriverTest.sys") ||
NULL != wcsstr(FullImageName->Buffer, L"Test.dll"))
{
// Driver
if (0 == ProcessId)
{
DbgPrint("Deny Load Driver\n");
DenyLoadDriver(ImageInfo->ImageBase);
}
// Dll
else
{
DbgPrint("Deny Load DLL\n");
DenyLoadDll(ImageInfo->ImageBase);
}
}
}

拒绝驱动模块的加载

// 拒绝加载驱动
NTSTATUS DenyLoadDriver(PVOID pImageBase)
{NTSTATUS status = STATUS_SUCCESS;PMDL pMdl = NULL;PVOID pVoid = NULL;ULONG ulShellcodeLength = 16;UCHAR pShellcode[16] = { 0xB8, 0x22, 0x00, 0x00, 0xC0, 0xC3, 0x90, 0x90,0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };PIMAGE_DOS_HEADER pDosHeader = pImageBase;PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);//找到DriverEntry的基址PVOID pDriverEntry = (PVOID)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.AddressOfEntryPoint);pMdl = MmCreateMdl(NULL, pDriverEntry, ulShellcodeLength);MmBuildMdlForNonPagedPool(pMdl);pVoid = MmMapLockedPages(pMdl, KernelMode);RtlCopyMemory(pVoid, pShellcode, ulShellcodeLength);MmUnmapLockedPages(pVoid, pMdl);IoFreeMdl(pMdl);return status;
}

拒绝 DLL 模块的加载


// 拒绝加载 DLL 模块
BOOLEAN DenyLoadDll(PVOID pLoadImageBase)
{
// DLL拒绝加载, 不能类似驱动那样直接在入口点返回拒绝加载信息. 这样达不到卸载DLL的效果.
// 将文件头 前0x200 字节数据置零ULONG ulDataSize = 0x200;
// 创建 MDL 方式修改内存
PMDL pMdl = MmCreateMdl(NULL, pLoadImageBase, ulDataSize);
if (NULL == pMdl)
{
ShowError("MmCreateMdl", 0);
return FALSE;
}
MmBuildMdlForNonPagedPool(pMdl);
PVOID pVoid = MmMapLockedPages(pMdl, KernelMode);
if (NULL == pVoid)
{
IoFreeMdl(pMdl);
ShowError("MmMapLockedPages", 0);
return FALSE;
}
// 置零
RtlZeroMemory(pVoid, ulDataSize);
// 释放 MDL
MmUnmapLockedPages(pVoid, pMdl);
IoFreeMdl(pMdl);return TRUE;
}

程序测试

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

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

总结

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

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

参考

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

基于PsSetLoadImageNotifyRoutine实现监控模块加载并卸载已加载模块(卸载DLL、EXE和sys等加载)相关推荐

  1. confluence 编辑器这次没有加载_喵的Unity游戏开发之路 - 多场景:场景加载

    如果丢失格式.图片或视频,请查看原文:喵的Unity游戏开发之路 - 多场景:场景加载 很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学.为此我们精选了一套国外优秀的Unity3D游戏 ...

  2. word出现无法加载加载项Please restart Word to load Mathtype addin properly或wps无法加载此加载项程序

    word出现无法加载加载项Please restart Word to load Mathtype addin properly或wps无法加载此加载项程序 因为电脑安装了mathtype,所以加载项 ...

  3. 模块xxxx.dll已加载,但对DllRegisterServer的调用失败,错误代码为 XXXXXXXXX

    WIN7.WIN8  注册 卸载dll  报错: 模块"xxxx.dll"已加载,但对DllRegisterServer的调用失败,错误代码为 XXXXXXXXX 解决方法: 若为 ...

  4. java加载pmml模型文件报错_PMML总结与思考PMML模型生成和加载示例

    在机器学习用于产品的时候,我们经常会遇到跨平台的问题.比如我们用Python基于一系列的机器学习库训练了一个模型,但是有时候其他的产品和项目想把这个模型集成进去,但是这些产品很多只支持某些特定的生产环 ...

  5. 未能加载文件或程序集“XXX”或它的一个依赖项,试图加载格式不正确的程序...

    问题描述: 未能加载文件或程序集"XXX"或它的一个依赖项,试图加载格式不正确的程序 解决方法: 方法一:在vs的配置管理器中,把活动解决方案平台改为Any CPU就可以了 方法二 ...

  6. R语言广义加性模型(GAMs:Generalized Additive Model)建模:数据加载、划分数据、并分别构建线性回归模型和广义线性加性模型GAMs、并比较线性模型和GAMs模型的性能

    R语言广义加性模型(GAMs:Generalized Additive Model)建模:数据加载.划分数据.并分别构建线性回归模型和广义线性加性模型GAMs.并比较线性模型和GAMs模型的性能 目录

  7. 未能加载文件或程序集“****”或它的某一个依赖项。试图加载格式不正确的程序。解决方案总结

    未能加载文件或程序集"****"或它的某一个依赖项.试图加载格式不正确的程序.解决方案总结 参考文章: (1)未能加载文件或程序集"****"或它的某一个依赖项 ...

  8. 模块XX.dll已加载,但对DllRegisterServer的调用失败

    为什么80%的码农都做不了架构师?>>> 模块"XX.dll"已加载,但对DllRegisterServer的调用失败,错误代码为0x80004005 一句话,权 ...

  9. 模块pdf2image.dll加载失败_Webpack 原理从前端模块化开始

    当前主流 JS 模块化方案 无模块化 CommonJS 规范,nodejs 实现的规范 AMD 规范,requirejs 实现的规范 CMD 规范,seajs 实现的规范, seajs 与 requi ...

最新文章

  1. CentOS7安装iptables防火墙
  2. 优化自定义函数_玩转reacthooks,自定义hooks设计模式及其实战
  3. 谈一下对绩效和自身技能发展的理解
  4. Boost Part III. 函数对象与高级编程 Library 10. Lambda 用法
  5. easyui treegrid获取父节点的id_超简单的分布式ID生成方案!美团开源框架介绍
  6. 计算机网络管理员中级理论知识试卷06,计算机网络管理员中级理论+技能完整题库及答案...
  7. python学习笔记三 pickle序列化
  8. DDR3布线的那些事儿(二)
  9. 各种对话框 Dialog
  10. Tensorflow——Dropout(解决过拟合问题)
  11. 嵌套的SQL另外一种写法
  12. android google GMS服务包安装
  13. mysql sql 分析工具下载_DB Query Analyzer下载
  14. 3d打开无法下载star.php,下载的3dmax模型打开失败的原因及解决方法
  15. 【搞笑】新闻联播熏陶下的小学生作文
  16. python实现RsaWithSHA256签名以及国密Sm3WithSm2签名
  17. simscape动力学仿真注意事项
  18. python实现简单的神经网络,python的神经网络编程
  19. usgs dem 导入matlab 程序,用GDAL打开从USGS下载的img影像文件
  20. chrome 插件 click 无效

热门文章

  1. VMWare/VMPlayer中“HGFS“的全称
  2. java后台(java后端开发)
  3. K、M、G、T、P、E、B,你知道都是10的多少次方吗?
  4. 二、JAVA调用海康威视SDK实现摄像头预览完整版
  5. 采用FPGA进行bayer插值算法的实现方法
  6. SLF4J-bridge
  7. macbook air m1中的office安装mathtype
  8. 使用java解析当前ip地址等信息
  9. 删除Microsoft Security Essentials
  10. 使用python的scapy库,提供一个发送nbns询问包的一个示例代码