6.7 Windows驱动开发:内核枚举LoadImage映像回调

2023-12-02 07:01

本文主要是介绍6.7 Windows驱动开发:内核枚举LoadImage映像回调,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在笔者之前的文章《内核特征码搜索函数封装》中我们封装实现了特征码定位功能,本章将继续使用该功能,本次我们需要枚举内核LoadImage映像回调,在Win64环境下我们可以设置一个LoadImage映像加载通告回调,当有新驱动或者DLL被加载时,回调函数就会被调用从而执行我们自己的回调例程,映像回调也存储在数组里,枚举时从数组中读取值之后,需要进行位运算解密得到地址。

LoadImage映像回调是Windows操作系统提供的一种机制,它允许开发者在加载映像文件(如DLL、EXE等)时拦截并修改映像的加载过程。LoadImage映像回调是通过操作系统提供的ImageLoad事件机制来实现的。

当操作系统加载映像文件时,它会调用LoadImage函数。在LoadImage函数内部,操作系统会触发ImageLoad事件,然后在ImageLoad事件中调用注册的LoadImage映像回调函数。开发者可以在LoadImage映像回调函数中执行自定义的逻辑,例如修改映像文件的内容,或者阻止映像文件的加载。

LoadImage映像回调可以通过Win32 API函数SetImageLoadCallback或者操作系统提供的驱动程序回调函数PsSetLoadImageNotifyRoutine来进行注册。同时,LoadImage映像回调函数需要遵守一定的约束条件,例如必须是非分页代码,不能调用一些内核API函数等。

我们来看一款闭源ARK工具是如何实现的:

如上所述,如果我们需要拿到回调数组那么首先要得到该数组,数组的符号名是PspLoadImageNotifyRoutine我们可以在PsSetLoadImageNotifyRoutineEx中找到。

第一步使用WinDBG输入uf PsSetLoadImageNotifyRoutineEx首先定位到,能够找到PsSetLoadImageNotifyRoutineEx这里的两个位置都可以被引用,当然了这个函数可以直接通过PsSetLoadImageNotifyRoutineEx函数动态拿到此处不需要我们动态定位。

我们通过获取到PsSetLoadImageNotifyRoutineEx函数的内存首地址,然后向下匹配特征码搜索找到488d0d88e8dbff并取出PspLoadImageNotifyRoutine内存地址,该内存地址就是LoadImage映像模块的基址。

如果使用代码去定位这段空间,则你可以这样写,这样即可得到具体特征地址。

#include <ntddk.h>
#include <windef.h>// 指定内存区域的特征码扫描
PVOID SearchMemory(PVOID pStartAddress, PVOID pEndAddress, PUCHAR pMemoryData, ULONG ulMemoryDataSize)
{PVOID pAddress = NULL;PUCHAR i = NULL;ULONG m = 0;// 扫描内存for (i = (PUCHAR)pStartAddress; i < (PUCHAR)pEndAddress; i++){// 判断特征码for (m = 0; m < ulMemoryDataSize; m++){if (*(PUCHAR)(i + m) != pMemoryData[m]){break;}}// 判断是否找到符合特征码的地址if (m >= ulMemoryDataSize){// 找到特征码位置, 获取紧接着特征码的下一地址pAddress = (PVOID)(i + ulMemoryDataSize);break;}}return pAddress;
}// 根据特征码获取 PspLoadImageNotifyRoutine 数组地址
PVOID SearchPspLoadImageNotifyRoutine(PUCHAR pSpecialData, ULONG ulSpecialDataSize)
{UNICODE_STRING ustrFuncName;PVOID pAddress = NULL;LONG lOffset = 0;PVOID pPsSetLoadImageNotifyRoutine = NULL;PVOID pPspLoadImageNotifyRoutine = NULL;// 先获取 PsSetLoadImageNotifyRoutineEx 函数地址RtlInitUnicodeString(&ustrFuncName, L"PsSetLoadImageNotifyRoutineEx");pPsSetLoadImageNotifyRoutine = MmGetSystemRoutineAddress(&ustrFuncName);if (NULL == pPsSetLoadImageNotifyRoutine){return pPspLoadImageNotifyRoutine;}// 查找 PspLoadImageNotifyRoutine  函数地址pAddress = SearchMemory(pPsSetLoadImageNotifyRoutine, (PVOID)((PUCHAR)pPsSetLoadImageNotifyRoutine + 0xFF), pSpecialData, ulSpecialDataSize);if (NULL == pAddress){return pPspLoadImageNotifyRoutine;}// 先获取偏移, 再计算地址lOffset = *(PLONG)pAddress;pPspLoadImageNotifyRoutine = (PVOID)((PUCHAR)pAddress + sizeof(LONG) + lOffset);return pPspLoadImageNotifyRoutine;
}VOID UnDriver(PDRIVER_OBJECT Driver)
{
}NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint("hello lyshark \n");PVOID pPspLoadImageNotifyRoutineAddress = NULL;RTL_OSVERSIONINFOW osInfo = { 0 };UCHAR pSpecialData[50] = { 0 };ULONG ulSpecialDataSize = 0;// 获取系统版本信息, 判断系统版本RtlGetVersion(&osInfo);if (10 == osInfo.dwMajorVersion){// 48 8d 0d 88 e8 db ff// 查找指令 lea rcx,[nt!PspLoadImageNotifyRoutine (fffff804`44313ce0)]/*nt!PsSetLoadImageNotifyRoutineEx+0x41:fffff801`80748a81 488d0dd8d3dbff  lea     rcx,[nt!PspLoadImageNotifyRoutine (fffff801`80505e60)]fffff801`80748a88 4533c0          xor     r8d,r8dfffff801`80748a8b 488d0cd9        lea     rcx,[rcx+rbx*8]fffff801`80748a8f 488bd7          mov     rdx,rdifffff801`80748a92 e80584a3ff      call    nt!ExCompareExchangeCallBack (fffff801`80180e9c)fffff801`80748a97 84c0            test    al,alfffff801`80748a99 0f849f000000    je      nt!PsSetLoadImageNotifyRoutineEx+0xfe (fffff801`80748b3e)  Branch*/pSpecialData[0] = 0x48;pSpecialData[1] = 0x8D;pSpecialData[2] = 0x0D;ulSpecialDataSize = 3;}// 根据特征码获取地址 获取 PspLoadImageNotifyRoutine 数组地址pPspLoadImageNotifyRoutineAddress = SearchPspLoadImageNotifyRoutine(pSpecialData, ulSpecialDataSize);DbgPrint("[LyShark] PspLoadImageNotifyRoutine = 0x%p \n", pPspLoadImageNotifyRoutineAddress);Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}

将这个驱动拖入到虚拟机中并运行,输出结果如下:

有了数组地址接下来就是要对数组进行解密,如何解密?

  • 1.首先拿到数组指针pPspLoadImageNotifyRoutineAddress + sizeof(PVOID) * i此处的i也就是下标。
  • 2.得到的新地址在与pNotifyRoutineAddress & 0xfffffffffffffff8进行与运算。
  • 3.最后*(PVOID *)pNotifyRoutineAddress取出里面的参数。

增加解密代码以后,这段程序的完整代码也就可以被写出来了,如下所示。

#include <ntddk.h>
#include <windef.h>// 指定内存区域的特征码扫描
PVOID SearchMemory(PVOID pStartAddress, PVOID pEndAddress, PUCHAR pMemoryData, ULONG ulMemoryDataSize)
{PVOID pAddress = NULL;PUCHAR i = NULL;ULONG m = 0;// 扫描内存for (i = (PUCHAR)pStartAddress; i < (PUCHAR)pEndAddress; i++){// 判断特征码for (m = 0; m < ulMemoryDataSize; m++){if (*(PUCHAR)(i + m) != pMemoryData[m]){break;}}// 判断是否找到符合特征码的地址if (m >= ulMemoryDataSize){// 找到特征码位置, 获取紧接着特征码的下一地址pAddress = (PVOID)(i + ulMemoryDataSize);break;}}return pAddress;
}// 根据特征码获取 PspLoadImageNotifyRoutine 数组地址
PVOID SearchPspLoadImageNotifyRoutine(PUCHAR pSpecialData, ULONG ulSpecialDataSize)
{UNICODE_STRING ustrFuncName;PVOID pAddress = NULL;LONG lOffset = 0;PVOID pPsSetLoadImageNotifyRoutine = NULL;PVOID pPspLoadImageNotifyRoutine = NULL;// 先获取 PsSetLoadImageNotifyRoutineEx 函数地址RtlInitUnicodeString(&ustrFuncName, L"PsSetLoadImageNotifyRoutineEx");pPsSetLoadImageNotifyRoutine = MmGetSystemRoutineAddress(&ustrFuncName);if (NULL == pPsSetLoadImageNotifyRoutine){return pPspLoadImageNotifyRoutine;}// 查找 PspLoadImageNotifyRoutine  函数地址pAddress = SearchMemory(pPsSetLoadImageNotifyRoutine, (PVOID)((PUCHAR)pPsSetLoadImageNotifyRoutine + 0xFF), pSpecialData, ulSpecialDataSize);if (NULL == pAddress){return pPspLoadImageNotifyRoutine;}// 先获取偏移, 再计算地址lOffset = *(PLONG)pAddress;pPspLoadImageNotifyRoutine = (PVOID)((PUCHAR)pAddress + sizeof(LONG) + lOffset);return pPspLoadImageNotifyRoutine;
}// 移除回调
NTSTATUS RemoveNotifyRoutine(PVOID pNotifyRoutineAddress)
{NTSTATUS status = PsRemoveLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)pNotifyRoutineAddress);return status;
}VOID UnDriver(PDRIVER_OBJECT Driver)
{
}NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint("hello lyshark \n");PVOID pPspLoadImageNotifyRoutineAddress = NULL;RTL_OSVERSIONINFOW osInfo = { 0 };UCHAR pSpecialData[50] = { 0 };ULONG ulSpecialDataSize = 0;// 获取系统版本信息, 判断系统版本RtlGetVersion(&osInfo);if (10 == osInfo.dwMajorVersion){// 48 8d 0d 88 e8 db ff// 查找指令 lea rcx,[nt!PspLoadImageNotifyRoutine (fffff804`44313ce0)]/*nt!PsSetLoadImageNotifyRoutineEx+0x41:fffff801`80748a81 488d0dd8d3dbff  lea     rcx,[nt!PspLoadImageNotifyRoutine (fffff801`80505e60)]fffff801`80748a88 4533c0          xor     r8d,r8dfffff801`80748a8b 488d0cd9        lea     rcx,[rcx+rbx*8]fffff801`80748a8f 488bd7          mov     rdx,rdifffff801`80748a92 e80584a3ff      call    nt!ExCompareExchangeCallBack (fffff801`80180e9c)fffff801`80748a97 84c0            test    al,alfffff801`80748a99 0f849f000000    je      nt!PsSetLoadImageNotifyRoutineEx+0xfe (fffff801`80748b3e)  Branch*/pSpecialData[0] = 0x48;pSpecialData[1] = 0x8D;pSpecialData[2] = 0x0D;ulSpecialDataSize = 3;}// 根据特征码获取地址 获取 PspLoadImageNotifyRoutine 数组地址pPspLoadImageNotifyRoutineAddress = SearchPspLoadImageNotifyRoutine(pSpecialData, ulSpecialDataSize);DbgPrint("[LyShark] PspLoadImageNotifyRoutine = 0x%p \n", pPspLoadImageNotifyRoutineAddress);// 遍历回调ULONG i = 0;PVOID pNotifyRoutineAddress = NULL;// 获取 PspLoadImageNotifyRoutine 数组地址if (NULL == pPspLoadImageNotifyRoutineAddress){return FALSE;}// 获取回调地址并解密for (i = 0; i < 64; i++){pNotifyRoutineAddress = *(PVOID *)((PUCHAR)pPspLoadImageNotifyRoutineAddress + sizeof(PVOID) * i);pNotifyRoutineAddress = (PVOID)((ULONG64)pNotifyRoutineAddress & 0xfffffffffffffff8);if (MmIsAddressValid(pNotifyRoutineAddress)){pNotifyRoutineAddress = *(PVOID *)pNotifyRoutineAddress;DbgPrint("[LyShark] 序号: %d | 回调地址: 0x%p \n", i, pNotifyRoutineAddress);}}Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}

运行这段完整的程序代码,输出如下效果:

目前系统中只有两个回调,所以枚举出来的只有两条,打开ARK验证一下会发现完全正确,忽略pyark这是后期打开的。

这篇关于6.7 Windows驱动开发:内核枚举LoadImage映像回调的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/444390

相关文章

Java 枚举的基本使用方法及实际使用场景

《Java枚举的基本使用方法及实际使用场景》枚举是Java中一种特殊的类,用于定义一组固定的常量,枚举类型提供了更好的类型安全性和可读性,适用于需要定义一组有限且固定的值的场景,本文给大家介绍Jav... 目录一、什么是枚举?二、枚举的基本使用方法定义枚举三、实际使用场景代替常量状态机四、更多用法1.实现接

基于Python开发一个有趣的工作时长计算器

《基于Python开发一个有趣的工作时长计算器》随着远程办公和弹性工作制的兴起,个人及团队对于工作时长的准确统计需求日益增长,本文将使用Python和PyQt5打造一个工作时长计算器,感兴趣的小伙伴可... 目录概述功能介绍界面展示php软件使用步骤说明代码详解1.窗口初始化与布局2.工作时长计算核心逻辑3

Windows 系统下 Nginx 的配置步骤详解

《Windows系统下Nginx的配置步骤详解》Nginx是一款功能强大的软件,在互联网领域有广泛应用,简单来说,它就像一个聪明的交通指挥员,能让网站运行得更高效、更稳定,:本文主要介绍W... 目录一、为什么要用 Nginx二、Windows 系统下 Nginx 的配置步骤1. 下载 Nginx2. 解压

windows系统上如何进行maven安装和配置方式

《windows系统上如何进行maven安装和配置方式》:本文主要介绍windows系统上如何进行maven安装和配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录1. Maven 简介2. maven的下载与安装2.1 下载 Maven2.2 Maven安装2.

使用Python实现Windows系统垃圾清理

《使用Python实现Windows系统垃圾清理》Windows自带的磁盘清理工具功能有限,无法深度清理各类垃圾文件,所以本文为大家介绍了如何使用Python+PyQt5开发一个Windows系统垃圾... 目录一、开发背景与工具概述1.1 为什么需要专业清理工具1.2 工具设计理念二、工具核心功能解析2.

python web 开发之Flask中间件与请求处理钩子的最佳实践

《pythonweb开发之Flask中间件与请求处理钩子的最佳实践》Flask作为轻量级Web框架,提供了灵活的请求处理机制,中间件和请求钩子允许开发者在请求处理的不同阶段插入自定义逻辑,实现诸如... 目录Flask中间件与请求处理钩子完全指南1. 引言2. 请求处理生命周期概述3. 请求钩子详解3.1

如何基于Python开发一个微信自动化工具

《如何基于Python开发一个微信自动化工具》在当今数字化办公场景中,自动化工具已成为提升工作效率的利器,本文将深入剖析一个基于Python的微信自动化工具开发全过程,有需要的小伙伴可以了解下... 目录概述功能全景1. 核心功能模块2. 特色功能效果展示1. 主界面概览2. 定时任务配置3. 操作日志演示

Windows Server 2025 搭建NPS-Radius服务器的步骤

《WindowsServer2025搭建NPS-Radius服务器的步骤》本文主要介绍了通过微软的NPS角色实现一个Radius服务器,身份验证和证书使用微软ADCS、ADDS,具有一定的参考价... 目录简介示意图什么是 802.1X?核心作用802.1X的组成角色工作流程简述802.1X常见应用802.

JavaScript实战:智能密码生成器开发指南

本文通过JavaScript实战开发智能密码生成器,详解如何运用crypto.getRandomValues实现加密级随机密码生成,包含多字符组合、安全强度可视化、易混淆字符排除等企业级功能。学习密码强度检测算法与信息熵计算原理,获取可直接嵌入项目的完整代码,提升Web应用的安全开发能力 目录

windows和Linux安装Jmeter与简单使用方式

《windows和Linux安装Jmeter与简单使用方式》:本文主要介绍windows和Linux安装Jmeter与简单使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录Windows和linux安装Jmeter与简单使用一、下载安装包二、JDK安装1.windows设