剖析Windows Defender驱动程序:WdFilter(Part 4)
导语:剖析Windows Defender驱动程序——WdFilter
剖析Windows Defender驱动程序:WdFilter(Part 1)
剖析Windows Defender驱动程序:WdFilter(Part 2)
剖析Windows Defender驱动程序:WdFilter(Part 3)
在前两篇文章中,我们已经介绍了WdFilter如何通过ImageLoad回调例程处理内存中的映像加载。另外我们还介绍了如何在两个不同的线程创建回调例程中检查新线程,以及如何将消息同步和异步发送到MsMpEng。现在,我们将重点关注以下内容:
1.进程和桌面处理操作回调;
2.驱动程序信息和验证
进程和桌面处理回调
首先,如何完成对象回调的初始化。该进程在MpObInitialize内部启动,此函数将动态获取两个函数的地址:
1.ObRegisterCallbacks(指针保存在MpData-> pObRegisterCallbacks中);
2.ObUnRegisterCallbacks(指针保存在MpData-> pObUnRegisterCallbacks中);
如果两个函数指针都被检索到,它将继续调用MpObAddCallback。 MpObAddCallback的主要工作是实际注册回调并在out参数中返回注册句柄,然后将其保存在MpData-> ObRegistrationHandle中。
为了注册对象回调,必须向ObRegisterCallbacks提供OB_CALLBACK_REGISTRATION结构。除其他事项外,此结构还包含一个OB_OPERATION_REGISTRATION数组,它将以以下这种方式初始化:
OB_CALLBACK_REGISTRATION ObCbRegistration = {} OB_OPERATION_REGISTRATION OperationRegistration[2] = {} OperationRegistration[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; OperationRegistration[0].ObjectType = PsProcessType; OperationRegistration[0].PreOperation = MpObPreOperationCallback; if (MpData->OsVersionMask & OsVersionWin10) { OperationRegistration[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; OperationRegistration[0].ObjectType = ExDesktopObjectType; OperationRegistration[0].PreOperation = MpObPreOperationCallback; ObCbRegistration.OperationRegistrationCount = 2; } else { ObCbRegistration.OperationRegistrationCount = 1; } ObCbRegistration.OperationRegistration = OperationRegistration;
在前面的伪代码中,我们可以看到如何将两个条目(在Windows 10中)添加到数组中,这两个操作都注册相同的PreOperation而没有PostOperation,也都注册以处理创建和复制。现在,我们将专注于PreOperation函数MpObPreOperationCallback。
要了解更多关于所有这些结构和其他内容,我强烈推荐你阅读这篇文章。
MpObPreOperationCallback
正如我们在此回调之前所看到的,PsProcessType和ExDesktopObjectType都注册了回调,因此很明显,例程需要一种方法来区分哪个对象触发了回调。由于这是PreOperation例程,因此必须将其原型定义为POB_PRE_OPERATION_CALLBACK,这意味着它的第二个参数将是OB_PRE_OPERATION_INFORMATION结构,该结构包含具有ObjectType的字段,该值可用于了解触发回调的对象的类型。因此,此函数的唯一工作就是将OperationInformation重定向到每种对象类型的正确函数。
MpObHandleOpenDesktopCallback
这是处理有关桌面对象的操作的函数,主要是充当通知员的作用。它将接收OB_PRE_OPERATION_INFORMATION作为参数,它要做的第一件事是从当前进程获取ProcessCtx。此时,ProcessCtx->ProcessRules将被检查将被检查,以查看是否设置了我创建的DoNotNotifyDesktopHandlesOp(0x8)值。如果未设置该值,则将从OB_PRE_OPERATION_INFORMATION中检索目标对象。使用此对象,代码将继续在函数ObQueryNameString中获取其名称。然后,根据对象名称的长度分配AsyncMessageData结构,并相应地填充它。union TypeOfMessage将包含以下结构:
typedef struct _ObDesktopHandleMessage { AuxPidCreationTime Process; INT ThreadId; INT SessionId; BYTE Operation; BYTE KernelHandleFlag; INT DesiredAccess; // https://docs.microsoft.com/en-us/windows/win32/winstation/desktop-security-and-access-rights WCHAR *ObjectName; } ObDesktopHandleMessage, *PObDesktopHandleMessage;
填满整个AsyncMessageData后,将使用MpAsyncSendNotification发送通知。这差不多就是关于这个回调的全部内容了,正如我在这一节开始说的它主要作为一个通知工具。
MpObHandleOpenProcessCallback
这个函数将处理关于进程句柄的操作,为了让这个回调启动它的操作,千万不要设置标记MpData->UnsetObAndRegCallback,这样当前进程就不会是MsMpEng,并且句柄不能是KernelHandle。如果这些条件都得到满足,则代码将继续为当前进程和目标进程获取ProcessCtx,当前进程是试图获取目标进程句柄的进程。从现在开始,应用不同的规则这就要取决于进程所请求的访问类型。如果该进程正在请求以下任何一种访问:
1.PROCESS_VM_WRITE:写入目标进程的地址空间;
2.PROCESS_VM_OPERATION:修改目标进程的地址空间;
3.PROCESS_CREATE_THREAD:在目标进程的上下文中创建一个新线程;
然后,回调将继续检查是否允许将代码从当前进程注入目标进程,为此,它使用了两个函数。BOOLEAN MpAllowCodeInjection(PProcessCtx CurrentProcess,PProcessCtx TargetProcess)此函数将检查CurrentProcess的ProcessFlags是否与以下任何值匹配:
ExcludedProcess - 0x1 MpServiceSidProcess - 0x10 FriendlyProcess - 0x20 SvchostProcess - 0x100
如果没有标志匹配,则它将从TargetProcess获得值ProcessCtx-> CodeInjectionTargetMask,并从CurrentProcess获得值ProcessCtx-> CodeInjectionRequestMask,它将继续处理这两个值,以确定是否允许注入。下面的伪代码展示了这个函数的行为。
processFlags = CurrentProcess->ProcessFlags; if (processFlags & ExcludedProcess || processFlags & MpServiceSidProcess || processFlags & FriendlyProcess || processFlags & SvchostProcess) { return TRUE; } targetMask = TargetProcess->CodeInjectionTargetMask; requestMask = CurrentProcess->CodeInjectionRequestMask; if (!targetMask || requestMask == -1 || targetMask & requestMask) { return TRUE } MpLogPrintfW(L"[Mini-filter] Injection into process %u from process %u is BLOCKED.", TargetProcess->ProcessId, CurrentProcess->ProcessId); return FALSE;
如果此函数返回FALSE,则将修改DesiredAccess,以删除触发此检查的权限。
如果我们运行的是Windows 10 build 16000或更高版本,则将基于Windows Defender主机入侵和防御系统(HIPS)规则进行另一项检查。
老实说,我对Windows Defender HIPS知之甚少,并且在互联网上找不到关于它的太多信息。因此,我不知道是否有添加或检查系统上正在运行的HIPS规则的方法。
在MpAllowAccessBasedOnHipsRule中检查用于句柄创建的HIPS规则,除此之外,此函数将接受以下参数:
HipsRule - @r8 ProcessRule - @r9 TargetRule - @rsp+20
并且该函数将基本上检查作为参数提供的规则是否与相应的ProcessRules相对照,对HipsRule根据当前的进程规则进行相对照,实际行为可以在以下伪代码中看到:
allowedFlag = FALSE; allowedOrBlocked = L"BLOCKED"; targetRules = TargetProcess->ProcessRules; processRules = CurrentProcess->ProcessRules; if (!(processRules & HipsRule) || targetRules & TargetHipsRule || processRules & ProcessRule) { allowedOrBlocked = L"ALLOWED"; status = TRUE; } MpLogPrintfW( L"[Mini-filter] Applying HipsRule 0x%x: Access from process %u to target %u is '%ls'", HipsRule, CurrentProcess->ProcessId, TargetProcess->ProcessId, allowedOrBlocked); return allowedFlag;
对于PROCESS_VM_WRITE,PROCESS_VM_OPERATION和PROCESS_CREATE_THREAD,将以下值作为参数传递给此函数:
HipsRule => AllowCodeInjectionHIPSRule-0x8000 ProcessRule => AllowedToInjectCode-0x10000 TargetRule => AllowIncomingCodeInjection-0x80000
再次进入回调,还有另一组访问权限将触发检查,可以在以下列表中看到它们:
SYNCHRONIZE:使用wait函数来等待进程终止;
PROCESS_TERMINATE:使用TerminateProcess终止进程;
PROCESS_SUSPEND_RESUME:暂停或恢复进程;
PROCESS_QUERY_LIMITED_INFORMATION:检索有关流程的某些信息;
在本文示例中,回调将检查当前进程的值ProcessCtx->ProcessProtection,以检查类型是否为PsProtectedTypeNone,签名者是否小于PsProtectedSignerAntimalware。与前一个例子一样,在这个例子中也有HIPS规则,在这个例子中,传递给函数MpAllowAccessBasedOnHipsRule的参数如下:
HipsRule => QuerySuspendResumeHIPSRule-0x800000 ProcessRule => AllowedToQuerySuspendResume-0x1000000 TargetRule => AllowQuerySuspendResume-0x2000000
如果这些检查失败,那么DesiredAccess将相应地进行调整。
最后,此回调将继续发送异步通知。该通知将在MpObSendOpenProcessBMNotification内部创建并发送,我们已经在上面看到了几个类似的函数,它将创建一个AsyncMessageData,其中TypeOfMessage将变为ObProcessHandleMessage,该结构具有以下定义:
typedef struct _ObProcessHandleMessage { AuxPidCreationTime Process; AuxPidCreationTime TargetProcess; INT SessionId; INT FinalDesiredAccess; INT FileNameLen; INT FileNameOffset; INT TargeFileNameLen; INT TargeFileNameOffset; BYTE CodeInjectionHIPS[16]; // Needs investigation BYTE QuerySuspendResumeHIPSRule[16]; // Needs investigation INT Unk; MP_OB_NOTIFICATION_REASON NotificationReason; } ObProcessHandleMessage, * PObProcessHandleMessage; typedef enum MP_OB_NOTIFICATION_REASON { // Default notification set to 0x0 DesiredAccessModified = 0x1, AllowCodeInjectionHIPSTrigger = 0x2, QuerySuspendResumeHIPSTrigger = 0x4, SameDesiredAccesAndAllowCodeInjectionHIPSTrigger = 0x8, SameDesiredAccessAndQuerySuspendResumeHIPSTrigger = 0x10, }
一旦这个结构被分配和填充,通知将添加到AsyncNotificationsList中,以供工作线程处理。
回到主函数中,在发送通知之后,在完成该函数之前需要进行最后一项检查,如果目标进程是MpServiceSidProcess,而当前进程既不是MpServiceSidProcess也不是FriendlyProcess,,然后下面的访问权限将被删除。
if (!(TargetProcess->ProcessFlags & MpServiceSidProcess)) { if (!(CurrentProcess->ProcessFlags & MpServiceSidProcess) && !(CurrentProcess->ProcessFlags & FriendlyProcess)) { ObOpParameters->CreateHandleInformation.DesiredAccess &= ~(PROCESS_VM_WRITE|PROCESS_VM_OPERATION|PROCESS_CREATE_THREAD|PROCESS_TERMINATE); } goto ReleaseProcessCtx; }
有关流程安全性和访问权限的更多信息,请点此参阅。
驱动程序信息和验证
此进程中涉及的第一个函数是MpInitializeDriverInfo,它是在DriverEntry内部进行调用的。我们已经在第一篇文章中提到了此函数,现在我们将看到有关此函数以及与驱动程序信息和验证相关的其他函数的更多详细信息。进入实际函数,主要是它将分配以下结构:
typedef struct _MP_DRIVERS_INFO { INT Status; BYTE Reserved[8]; INT ElamSignaturesMajorVer; INT ElamSignatureMinorVer; LIST_ENTRY LoadedDriversList; PSLIST_ENTRY ElamRegistryEntries; LIST_ENTRY BootProcessList; PCALLBACK_OBJECT CallbackObject; PVOID BootDriverCallbackRegistration; FAST_MUTEX DriversInfoFastMutex; INT TotalDriverEntriesLenght; NTSTATUS (__fastcall *pSeRegisterImageVerificationCallback)(SE_IMAGE_TYPE, SE_IMAGE_VERIFICATION_CALLBACK_TYPE, PSE_IMAGE_VERIFICATION_CALLBACK_FUNCTION, PVOID, SE_IMAGE_VERIFICATION_CALLBACK_TOKEN, PVOID *); VOID (__fastcall *pSeUnregisterImageVerificationCallback)(PVOID); PVOID ImageVerificationCbHandle; INT RuntimeDriversCount; INT RuntimeDriversArrayLenght; PVOID RuntimeDriversArray; LIST_ENTRY RuntimeDriversList; INT64 field_C8; } MP_DRIVERS_INFO, *PMP_DRIVERS_INFO
在初始化结构的某些字段之后,该函数将获得回调对象\ Callback \ WdEbNotificationCallback的句柄,然后它将继续将MpBootDriverCallback注册为回调函数,将注册句柄保存在结构成员BootDriverCallbackRegistration中。
MpAddDriverInfo和MpAddBootProcessEntry
在启动驱动程序回调之前,我想解释一下列表项LoadedDriversList和BootProcessList是如何被填充的。在LoadedDriversList中,数据将链接到在Image-Load回调中执行的MpAddDriverInfo中,而BootProcessList则被填充在MpAddBootProcessEntry中,MpAddBootProcessEntry是在Process-Creation回调中调用的。
正如我刚才所说,只要加载的映像是SystemModeImage,就将从Image-Load回调中调用MpAddDriverInfo。该函数将接收IMAGE_INFO和完整映像名称作为参数,并且主要它将为结构MP_DRIVER_INFO分配内存
typedef struct _MP_DRIVER_INFO { LIST_ENTRY DriverInfoList; UNICODE_STRING ImageName; UNICODE_STRING DriverRegistryPath; UNICODE_STRING CertPublisher; UNICODE_STRING CertIssuer; PVOID ImageHash; INT ImageHashAlgorithm; INT ImageHashLength; PVOID CertThumbprint; INT ThumbprintHashAlgorithm; INT CertificateThumbprintLength; PVOID ImageBase; INT64 ImageSize; INT ImageFlags; INT DriverClassification; INT ModuleEntryEnd; } MP_DRIVER_INFO, *PMP_DRIVER_INFO;
它将填充ImageSize、ImageBase和ImageName,然后将这个新条目链接到MP_DRIVERS_INFO->LoadedDriversList。
对于MpAddBootProcessEntry而言,此函数是在进程创建进程中调用的,,它执行前50个加载的进程(Counter保留在全局变量BootProcessCounter中)。函数原型如下所示:
VOID MpAddBootProcessEntry( HANDLE ProcessId, HANDLE ParentProcessId, PCUNICODE_STRING ImageFileName, PCUNICODE_STRING CmdLine )
同样,如前所述,该函数的主要目标是分配和初始化该结构,在本文的示例中,该结构为MP_BOOT_PROCESS,其定义如下:
typedef struct _MP_BOOT_PROCESS { LIST_ENTRY BootProcessList; HANDLE ProcessId; HANDLE ParentProcessId; UNICODE_STRING ImageFileName; UNICODE_STRING CommandLine; INT SomeFlag; // Set to 3 } MP_BOOT_PROCESS, *PMP_BOOT_PROCESS;
初始化条目后,将其链接到列表MP_DRIVERS_INFO->BootProcessList中。
MpBootDriverCallback
既然知道了如何填充这两个列表项,我们将开始研究初始化期间注册的回调函数。正如我在有关Windows Defender ELAM的文章中已经解释的那样,当在ELAM驱动程序的启动驱动程序回调函数中将BDCB_CALLBACK_TYPE设置为BdCbStatusUpdate并且映像信息的BDCB_CLASSIFICATION设置为BdCbClassificationKnownBadImage时,将通知该回调。如果满足了这些条件,则将以Argument1作为指向主要WdBoot结构MP_EB_GLOBALS的指针并将Argument2设置为常数0x28的指针来通知回调。
因此,进入MpBootDriverCallback,首先要做的是对Argument1和Argument2进行完整性检查。对于Argument2,它将检查是否等于0x28,而对于Argument1,它将检查结构的Magic是否为0xEB01。如果两项检查都正确,则它将继续遍历WdBoot创建的驱动程序列表,并且对于每个驱动程序,它将调用MpCopyDriverEntry,它会将驱动程序条目数据复制到MP_DRIVER_INFO结构中,然后将其链接到MP_DRIVERS_INFO-> LoadedDriversList。
接下来,我会描述一个我无法触发的路径/用例,但其中会有一些猜测的成分,对此我表示歉意。之所以会这样,我认为这与ELAM驱动程序可以使用注册表回调或引导驱动程序回调来监控和验证配置数据有关,至少在我的研究中,我只看到了第二种情况。更多信息,请点此。
复制完每个条目后,将遍历SLIST_ENTRY ElamRegistryEntries,对于每个条目,它将调用MpCopyElamRegistryEntry。此函数基本上会将条目复制到名为MP_ELAM_REGISTRY_ENTRY的结构中,并将此结构插入到单链接列表MP_DRIVERS_INFO-> ElamRegistryEntries中,这可能与ELAM注册表配置单元条目有关,MP_ELAM_REGISTRY_ENTRY sizeof为0x40,其中包含两个UNICODE_STRINGS,但我无法提供更多有关此的信息。
最后,将另外两个值从MP_EB_GLOBALS复制到MP_DRIVERS_INFO。这些值为ElamSignaturesMajorVer和ElamSignatureMinorVer。以上就是这个函数要做的全部工作,总而言之,它主要将来自MP_EB_GLOBALS的信息(即由ELAM驱动程序获得的信息)复制到MP_DRIVERS_INFO结构中。
MpSetImageVerificationCallback
这是在初始化期间执行的最后一个函数,此函数的目标主要是注册一个映像验证回调,为此,它将动态检索两个函数指针:
SeRegisterImageVerificationCallback SeUnregisterImageVerificationCallback
这两个函数都驻留在内核中,并且它们基本上按照其名称进行操作。 SeRegisterImageVerificationCallback有一些检查,可以在以下伪代码上看到:
NTSTATUS SeRegisterImageVerificationCallback( SE_IMAGE_TYPE ImageType, SE_IMAGE_VERIFICATION_CALLBACK_TYPE CallbackType, PSE_IMAGE_VERIFICATION_CALLBACK_FUNCTION CallbackFunction, PVOID CallbackContext, SE_IMAGE_VERIFICATION_CALLBACK_TOKEN CallbackToken, PVOID * RegistrationHandle ) { if (ImageType != SeImageTypeDriver || CallbackType || CallbackToken) { // CallbackType should be SeImageVerificationCallbackInformational which is 0 return STATUS_INVALID_PARAMETER; } PVOID registrationHandle = ExRegisterCallback(ExCbSeImageVerificationDriverInfo, CallbackFunction, CallbackContext); if (registrationHandle) { *RegistrationHandle = registrationHandle; } }
返回MpSetImageVerificationCallback,除了将函数MpImageVerificationCallback注册为映像验证回调例程外,它还将分配大小为0x800的池,这样RuntimeDriversArray将驻留在该池中。
MpImageVerificationCallback
这个回调将从内核内的SepImageVerificationCallbackWorker中得到通知,执行此回调时的调用堆栈如下所示:
通知回调后,首先要做的就是调用MpAllocateDriverInfoEx来分配和初始化一个MP_DRIVER_INFO_EX。
typedef struct _MP_DRIVER_INFO_EX { USHORT Magic; // Set to 0xDA18 USHORT Size; // Sizeof 0xB0 _QWORD WdFilterFlag; PVOID SameIndexList; _QWORD IndexHash; MP_DRIVER_INFO DriverInfo; } MP_DRIVER_INFO_EX, *PMP_DRIVER_INFO_EX;
如果你还记得我写的有关WdBoot的内容,那这就是我在那篇文章中命名为MODULE_ENTRY的结构。同样,该结构似乎是MP_DRIVER_INFO的扩展版本,这让人想起IMAGE_INFO_EX和IMAGE_INFO,不过在本文的示例中可不是一个指针,而是包含的整个结构。
现在,代码的作用将与我在关于WdBoot的文章中所解释的作用基本相同。将使用相同算法计算IndexHash:
WCHAR upper; _QWORD IndexHash = 0x4CB2F; while(*DriverInfo.ImageName.Buffer) { upper = RtlUpcaseUnicodeChar(*DriverInfo.ImageName.Buffer); IndexHash = HIBYTE(upper) + 0x25 * (upper + 0x25 * IndexHash); DriverInfo.ImageName.Buffer++; }
然后使用这个IndexHash,使用以下算法在RuntimeDriversArray中获得一个索引:
DWROD size = (MpDriversInfo.RuntimeDriversArray >> 5) - 1; _QWORD tmp = IndexHash & (-1 << (MpDriversInfo.RuntimeDriversArray & 0x1F)) _QWORD idx = (0x25 * (BYTE6(tmp) + 0x25 * (BYTE5(tmp) + 0x25 * (BYTE4(tmp) + 0x25 * (BYTE3(tmp) + 0x25 * (BYTE2(tmp) + 0x25 * (BYTE1(tmp) + 0x25 * (BYTE(tmp) + 0xB15DCB))))))) + HIBYTE(tmp)) & size;
详细过程,请参阅WdBoot这篇文章,以更好地了解所有这些疯狂行为的执行过程。
最后,驱动程序将链接到MpDriversInfo.RuntimeDriversList。
我们所看到的有关驱动程序信息的所有内容都将在一个函数中发挥作用,该作用将在另一篇名为MpQueryLoadedDrivers的文章中看到。这个函数可以由MsMpEng触发,以获得MP_DRIVERS_INFO数据的副本。
发表评论