探索黑客技术攻防,实战研究与安全创新

导航菜单

在Win64上无HOOK实现监控驱动加载

在32位系统上监控驱动加载,常用的手段是HookNtLoadDriver,不过有不少方法加载驱动无需经过NtLoadDriver,比如用ZwSetSystemInformation,或者使用一些未公开的0Day方法,可见HookNtLoadDriver是极其表层并不可信的方法。后来黑防上刊登过一篇HookMmCheckSystemImage来拦截驱动加载的方法,可惜此方法在Vista以后的操作系统上并不适用。其实大家都多虑了,微软早就帮我们想好了一种标准方法来监控驱动的加载,下面我就详细地介绍一下。

此函数名称为PsSetLoadImageNotifyRoutine,可以设置一个“映像加载通告例程”,来通知你的驱动当前系统在加载什么DLL或者驱动。有人可能认为这个标准方法的检控非常表层,其实恰恰相反,这个方法非常底层,大部分加载驱动的方法都可以绕过NtLoadDriver,但是无法绕过“映像加载通告例程”,所以用此方法监控驱动加载是最合适的了。

首先看看此函数的原型:


NTSTATUSPsSetLoadImageNotifyRoutine(PLOAD_IMAGE_NOTIFY_ROUTINENotifyRoutine);


其中NotifyRoutine是一个函数指针,此回调函数的原型是:


VOID(*PLOAD_IMAGE_NOTIFY_ROUTINE)
(
__in_optPUNICODE_STRINGFullImageName,
__inHANDLEProcessId,
__inPIMAGE_INFOImageInfo
);


回调函数的前两个参数显而易见,分别是映像的路径和加载此映像的进程ID,第三个参数包含了更加详细的信息。


typedefstruct_IMAGE_INFO{
union{
ULONGProperties;
struct{
ULONGImageAddressingMode:8;//codeaddressingmode
ULONGSystemModeImage
ULONGImageMappedToAllPids:1;//mappedinallprocesses
ULONGReserved:22;
:1;//systemmodeimage
};
};
PVOIDImageBase;
ULONGImageSelector;
ULONGImageSize;
ULONGImageSectionNumber;
}IMAGE_INFO,*PIMAGE_INFO;
不过此结构体到了Vista之后,发生了一点变化。
typedefstruct_IMAGE_INFO{
union{
ULONGProperties;
struct{
ULONGImageAddressingMode:8;//Codeaddressingmode
ULONGSystemModeImage:1;//Systemmodeimage
ULONGImageMappedToAllPids:1;//Imagemappedintoallprocesses
ULONGExtendedInfoPresent:1;//IMAGE_INFO_EXavailable
ULONGReserved
:21;
};
};
PVOIDImageBase;
ULONGImageSelector;
SIZE_TImageSize;
ULONGImageSectionNumber;
}IMAGE_INFO,*PIMAGE_INFO;
当ExtendedInfoPresent标志非零时,IMAGE_INFO结构体被包含在了另外一个更大的结构体里。
typedefstruct_IMAGE_INFO_EX{
SIZE_T
Size;
IMAGE_INFO
ImageInfo;
struct_FILE_OBJECT*FileObject;
}IMAGE_INFO_EX,*PIMAGE_INFO_EX;
不过这个变动与实现监控驱动加载的关系不大,我们只需要IMAGE_INFO的信息即可实现监控驱动加载。下面先讲解如何添加和删除“映像加载通告例程”。
//添加
PsSetLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyRout
ine);
//删除
PsRemoveLoadImageNotifyRoutine((PLOAD_IMAGE_NOTIFY_ROUTINE)LoadImageNotifyR
outine);


接下来讲如何获得加载驱动的信息。之前说过,这个通告例程不仅负责处理加载驱动,连进程加载DLL也负责,那我们怎么判断到底是加载驱动还是加载DLL呢?根据后缀名判断,很明显是一个很不好的方法。我的方法是,根据回调函数LoadImageNotifyRoutine的第二个参数判断,如果PID是0,则表示加载驱动,如果PID位非零,则表示加载DLL。原因很简单,之前说过这个函数很底层,到了一定的深度之后,就无法判断到底是谁主动引发的行为了,一切都是系统的行为。当然,也可以认为这是通过回调来监控驱动加载的缺点。判断是驱动后,就通过ImageInfo->ImageBase来获取驱动的映像基址。如果不想让这个驱动加载,就通过ImageBase来获得DriverEntry的地址,写入如下汇编的机器码:


Moveax,c0000022h
Ret
B8220000C0
C3
实现代码如下:
PVOIDGetDriverEntryByImageBase(PVOIDImageBase)
{
PIMAGE_DOS_HEADERpDOSHeader;
PIMAGE_NT_HEADERS64pNTHeader;
PVOIDpEntryPoint;
pDOSHeader=(PIMAGE_DOS_HEADER)ImageBase;
pNTHeader=(PIMAGE_NT_HEADERS64)((ULONG64)ImageBase+pDOSHeader->e_lfanew);
pEntryPoint
=
(PVOID)((ULONG64)ImageBase
+
pNTHeader->OptionalHeader.AddressOfEntryPoint);
returnpEntryPoint;
}
voidDenyLoadDriver(PVOIDDriverEntry)
{
UCHARfuck[]=\xB8\x22\x00\x00\xC0\xC3;
VxkCopyMemory(DriverEntry,fuck,sizeof(fuck));
}
VOIDLoadImageNotifyRoutine
(
__in_optPUNICODE_STRINGFullImageName,
__inHANDLEProcessId,
__inPIMAGE_INFOImageInfo
)
{
PVOIDpDrvEntry;
charszFullImageName[260]={0};
if(FullImageName!=NULLMmIsAddressValid(FullImageName))
{
if(ProcessId==0)
{
DbgPrint([LoadImageNotifyX64]%wZ\n,FullImageName);
pDrvEntry=GetDriverEntryByImageBase(ImageInfo->ImageBase);
DbgPrint([LoadImageNotifyX64]DriverEntry:%p\n,pDrvEntry);
UnicodeToChar(FullImageName,szFullImageName);
if(strstr(_strlwr(szFullImageName),win64ast.sys))
{
DbgPrint(Denyload[WIN64AST.SYS]);
//禁止加载win64ast.sys
DenyLoadDriver(pDrvEntry);
}
}
}
}


有些读者心中可能想问,为什么拒绝加载驱动处仍然是“moveax,c000022h”而不“movrax,c000022h”?这是因为NTSTATUS其实就是long的马甲,而long的长度在Win64系统下依然是4字节而不是8字节,所以用“moveax”足矣。如果对通过ImageBase获得DriverEntry不理解,可以参考我以前的拙文《初步探索PE32+格式文件》。最后实现的效果如图1所示,效果为监视所有的驱动加载并拒绝名为win64ast.sys的驱动加载。

019.png