在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的驱动加载。