在笔者之前的文章《内核特征码搜索函数封装》
中我们封装实现了特征码定位功能,本章将继续使用该功能,本次我们需要枚举内核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; }
PVOID SearchPspLoadImageNotifyRoutine(PUCHAR pSpecialData, ULONG ulSpecialDataSize) { UNICODE_STRING ustrFuncName; PVOID pAddress = NULL; LONG lOffset = 0; PVOID pPsSetLoadImageNotifyRoutine = NULL; PVOID pPspLoadImageNotifyRoutine = NULL;
RtlInitUnicodeString(&ustrFuncName, L"PsSetLoadImageNotifyRoutineEx"); pPsSetLoadImageNotifyRoutine = MmGetSystemRoutineAddress(&ustrFuncName); if (NULL == pPsSetLoadImageNotifyRoutine) { return pPspLoadImageNotifyRoutine; }
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.com \n");
PVOID pPspLoadImageNotifyRoutineAddress = NULL; RTL_OSVERSIONINFOW osInfo = { 0 }; UCHAR pSpecialData[50] = { 0 }; ULONG ulSpecialDataSize = 0;
RtlGetVersion(&osInfo); if (10 == osInfo.dwMajorVersion) {
pSpecialData[0] = 0x48; pSpecialData[1] = 0x8D; pSpecialData[2] = 0x0D; ulSpecialDataSize = 3; }
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; }
PVOID SearchPspLoadImageNotifyRoutine(PUCHAR pSpecialData, ULONG ulSpecialDataSize) { UNICODE_STRING ustrFuncName; PVOID pAddress = NULL; LONG lOffset = 0; PVOID pPsSetLoadImageNotifyRoutine = NULL; PVOID pPspLoadImageNotifyRoutine = NULL;
RtlInitUnicodeString(&ustrFuncName, L"PsSetLoadImageNotifyRoutineEx"); pPsSetLoadImageNotifyRoutine = MmGetSystemRoutineAddress(&ustrFuncName); if (NULL == pPsSetLoadImageNotifyRoutine) { return pPspLoadImageNotifyRoutine; }
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.com \n");
PVOID pPspLoadImageNotifyRoutineAddress = NULL; RTL_OSVERSIONINFOW osInfo = { 0 }; UCHAR pSpecialData[50] = { 0 }; ULONG ulSpecialDataSize = 0;
RtlGetVersion(&osInfo); if (10 == osInfo.dwMajorVersion) {
pSpecialData[0] = 0x48; pSpecialData[1] = 0x8D; pSpecialData[2] = 0x0D; ulSpecialDataSize = 3; }
pPspLoadImageNotifyRoutineAddress = SearchPspLoadImageNotifyRoutine(pSpecialData, ulSpecialDataSize); DbgPrint("[LyShark] PspLoadImageNotifyRoutine = 0x%p \n", pPspLoadImageNotifyRoutineAddress);
ULONG i = 0; PVOID pNotifyRoutineAddress = NULL;
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
这是后期打开的。