剖析Windows Defender驱动程序:WdFilter(Part 1)
剖析Windows Defender驱动程序:WdFilter(Part 2)
前两篇文章,我们介绍了回调中用到的主要函数。本文,我们会接着介绍回调的具体过程和方法。
MpLoadImageNotifyRoutine
mploadimagenotify例程是一个回调例程,每当一个映像被加载或映射到内存时,它都会被触发。为了注册这个回调,驱动程序使用函数PsSetLoadImageNotifyRoutine。
进入实际的回调代码,首先要检查要加载的映像是否要映射到用户空间或内核空间,并检查IMAGE_INFO中的Properties.SystemModeImage位。如果它是内核模式组件,则映像信息将被添加到一个DRIVER_INFO结构中,然后链接到“加载的驱动程序”列表条目中,这类似于将启动进程添加到启动进程列表的进程创建,该过程是在MpAddDriverInfo内部完成。
完成此检查后,将获取ProcessCtx,并检查ProcessCtx-> ProcessRules以查看是否设置了NotifyWow64cpuLoad(0x800)。如果设置了规则,则函数将继续将FullImageName字节与字符串\Windows\System32\Wow64cpu.dll进行比较。如果它们匹配,则ProcessCtx-> ProcessFlags将与ImageWow64cpuLoaded(0x200)进行或运算,并将ImageBase写入ProcessCtx-> Wow64CpuImageBase,如上所述,则将该字段命名为ImageBase,并且仅在整个代码中在此设置此字段,这就是我重命名它的原因。
此时,即使ImageName不匹配或未设置NotifyWow64cpuLoad,也体现了其主要功能,这部分代码将首先检查IMAGE_INFO是否具有如果将ExtendedInfoPresent位置1,则将IMAGE_INFO包含在IMAGE_INFO_EX中,该IMAGE_INFO_EX保留指向FileObject的指针,该指针将用于检索StreamContext(MpGetStreamContextFromFileObject),这基本上是由与Stream对象关联的微型过滤器定义的结构,关于过滤的工作方式,我们将进行更多讨论–使用StreamCtx和ProcessCtx进行以下检查:
1. 如果StreamCtx->StreamCtxRules已激活了NotifyImageLoadRule (0x8000),则设置NotifyImageLoadPerStreamFlag。
2. 如果ProcessCtx->ProcessRules已经激活了NotifyImageLoadRule (0x8000000),那么就设置了NotifyImageLoadPerProcessFlag。
3. 如果ProcessCtx-> ProcessRules的活动状态为0x200(尚未计算出该值),如果未设置,则激活AsyncNotificationFlag。
如果设置了AsyncNotificationFlag,则该函数将创建一个AsyncMessageData结构,其中联合TypeOfMessage将采用ImageLoadAndProcessNotify结构,我们已经在上一篇文章中看到了此结构,主要区别在于AsyncMessageData-> TypeOfOperation将被设置为LoadImage(0x3)。最后,将通过调用MpAsyncSendNotification发送通知。
对于其他两种情况,通知将同步发送,并且两种情况下发送的数据将相同。唯一不同的是OperationType和Rule,我们将在研究同步消息的发送方式时讨论此参数:
1. NotifyImageLoadPerProcessFlag-> OperationType = NewImageLoadPerProcess(0x5)和Rule = ProcessCtx-> ProcessRules;
2. NotifyImageLoadPerStreamFlag-> OperationType = NewImageLoadPerStream(0x1)和Rule = StreamCtx-> StreamCtxRules。
最后,使用参数Data作为UNICODE_STRING调用函数MpSendSyncMonitorNotification,并使用已加载映像的标准化名称FullImageName。
当在ProcessCtx->ProcessFlags上设置ImageWow64cpuLoaded时,代码流有一点不同。如果发生这种情况,则分配大小为0x30的AsyncMessageData结构,并且TypeOfMessage将包含以下结构:
typedef struct _Wow64CpuLoadMessage { INT ProcessId; INT ThreadId; PVOID Wow64CpuImageBase; } Wow64CpuLoadMessage, *PWow64CpuLoadMessage;
最后,使用AsyncMessageData填充例程,该例程将调用FltSendMessage,有趣的是,当结构的实际大小为0x30时,即使将FltSendMessage的参数SenderBufferLength设置为0x30,AsyncMessageData-> SizeOfData也会设置为0x70,这可能会导致某些潜在的错误MsMpEng使用AsyncMessageData-> SizeOfData。
因此,稍微解释一下该流程,一旦Wow64cpu.dll被加载,此回调将在ProcessCtx-> ProcessFlags中设置ImageWow64cpuLoaded,并将继续通过主路径执行。下次此过程加载映像时,由于先前已设置ImageWow64cpuLoaded,因此代码将在采用主路径之前遵循此路径。
同步通知
NTSTATUS MpSendSyncMonitorNotification( MP_SYNC_NOTIFICATION OperationType, PAuxPidCreationTime ProcessIdAndCreationTime, PVOID Data, PMP_IO_PRIORITY MpIoPriority, PULONG Rule ); typedef enum _MP_SYNC_NOTIFICATION_OPERATION { NewImageLoadPerStream = 0x1, RegistryEventSync = 0x2, NewThreadDifferentProcess = 0x3, NewImageLoadPerProcess = 0x5, NewThreadSameProcess = 0x6, NewThreadProcessCmdLine = 0x7, } MP_SYNC_NOTIFICATION_OPERATION;
MpSendSyncMonitorNotification是负责通过MicrosoftMalwareProtectionPort发送同步消息的人,为了使此函数通过MP_DATA执行标志SyncMonitorNotificationFlag,必须进行设置。完成此检查后,代码将检查OperationType是否在MP_SYNC_NOTIFICATION枚举的范围内,还将检查有没有其他任何参数是NULL。
如果完成所有检查,代码将继续获取参数数据的大小,如上所述,每种操作类型在此参数中提供的数据都不同。为此,代码使用函数MpConstructSyncMonitorVariableData。
ULONG MpConstructSyncMonitorVariableData( INT OperationType, PVOID Data, PVOID *__shifted(SyncMessageData,0x30) DataToSend, ULONG SizeOfData )
这个函数有两种使用方法:
1. 获取要发送的数据大小(DataToSend == NULL);
2. 使用参数Data中的数据填充要发送的缓冲区。
在第一种情况下,伪代码看起来如下所示:
if (!DataToSend) { switch (OperationType) { case NewImageLoadPerStream: case NewImageLoadPerProcess: case NewThreadAndCmdLine: return (UNICODE_STRING *) Data->Length + 0xA; case RegistryEventSync: return (RegistryNotifySyncMessage) Data->RegDataLength; case NewThreadDifferentProcess: return sizeof(AuxPidCreationTime); case NewThreadSameProcess: return sizeof(ThreadNotifySyncMessage); } }
回到主函数,第一次调用MpConstructSyncMonitorVariableData之后,代码将获取要发送的数据的大小,此大小将添加到消息头的大小(0x30)中,并且整个大小将是一个池,被分配并相应地填充。消息头具有以下定义:
typedef struct _SyncMessageData { SHORT Magic; // Set to 0x5D SHORT SizeHeader; // Sizeof 0x30 ULONG TotalSize; MP_IO_PRIORITY MpIoPriority; INT TypeOfOperation; AuxPidCreationTime CurrentProcess; INT SizeOfData; union SyncVariableData { WCHAR * NewThreadAndCmdLine; WCHAR * NewImageLoadPerStream; WCHAR * NewImageLoadPerProcess; RegistryNotifySyncMessage RegistryEventSync; AuxPidCreationTime NewThreadDifferentProcess; ThreadNotifySyncMessage NewThreadSameProcess; }; } SyncMessageData, *PSyncMessageData;
最后,在发送消息之前,必须将变量数据复制到SyncMessageData结构中,以再次执行此操作MpConstructSyncMonitorVariableData,但这一次参数DataToSend指向结构SyncMessageData偏移了0x30(指向变量数据),在这种情况下该函数只会将数据从缓冲区数据复制到缓冲区DataToSend –如果缓冲区数据是UNICODE_STRING,则UNICODE_STRING。将使用memcpy_s复制缓冲区。
至此,一切准备就绪,可以将数据发送到MsMpEng了,只需在MpAcquireSendingSyncMonitorNotification内部执行另一项检查,该检查将基本上检查MpData-> SendSyncNotificationFlag是否处于活动状态,此后该函数将使用FltCancellableWaitForSingleObject等待MpData-> SendingSyncSemaphore,用于此等待的超时来自变量MpData-> SyncMonitorNotificationTimeout,如果等待返回除STATUS_SUCCESS以外的任何值,则主函数将不发送任何消息,并且将递增并相应地设置以下两个变量:
1. MpData-> ErrorSyncNotificationsCount [OperationType];
2. MpData-> ErrorSyncNotificationsStatus [OperationType]。
如果等待成功,则将调用FltSendMessage,并根据返回的状态填充不同的变量。第一个变量是一个结构,用于保留通知及其总时间戳(针对每个OperationType)的计数器。结构数组可以在变量MpData-> SyncNotifications [OperationType]中找到,其定义如下所示:
typedef struct _MP_SYNC_NOTIFICATIONS { INT64 Timestamp; INT NotificationsCount; } MP_SYNC_NOTIFICATIONS, *PMP_SYNC_NOTIFICATIONS;
万一FltSendMessage返回错误,则会更新以下变量:
1. MpData-> ErrorSyncNotificationsCount [OperationType];
2. MpData-> ErrorSyncNotificationsStatus [OperationType];
3. MpData->SyncNotificationsIoTimeoutCount[OperationType] ->以防FltSendMessage返回的状态STATUS_TIMEOUT递增。
如果FltSendMessage返回STATUS_SUCCESS,则该函数将继续检查应答缓冲区。如果是这种情况,此缓冲区应在偏移量0x8中包含相同的OperationType,然后它将继续重置触发此特定通知的ProcessCtx-> ProcessRules或StreamCtx-> StreamCtxRule,使用参数Rule,在下面可以看到该映像:
最后一步中还有两个变量MpData-> SyncNotificationRecvCount [OperationType]和MpData-> SyncNotificationsRecvErrorCount [OperationType]。如果ReplyBuffer检查不匹配,则后者会增加,反之则前者会增加。
异步通知
在本节中,我将说明驱动程序如何处理发送异步通知,有两个函数负责执行此操作。 MpAsyncSendNotification负责将消息添加到异步消息队列中,而MpAsyncpWorkerThread是用于检查异步消息队列并发送消息(如果有)的工作线程。
MpAsyncpWorkerThread
如上所述,我们已经提到了这个工作线程。我们看到它在MpAsyncInitialize内部连同异步结构一起被初始化。此函数使用PsCreateSystemThread创建工作线程,将MpAsyncpWorkerThread设置为StartRoutine,没有StartContext被传递到这个新线程中。
该线程主要与MP_ASYNC结构一起使用,该结构具有以下定义(无论我如何尝试交叉引用这个结构,我现在无法获得更多的字段,这就是为什么我缺少许多字段的主要原因字段):
typedef struct _MP_ASYNC { SHORT Magic; // Set to 0xDA07 SHORT StructSize; // Sizeof 0x180 LIST_ENTRY HighPriorityNotificationsList; LIST_ENTRY NotificationsList; PETHREAD WorkerThread; KEVENT AsyncNotificationEvent; KSEMAPHORE AsyncSemaphore; FAST_MUTEX AsyncFastMutex; INT NotificationsCount; INT64 field_A8; INT64 field_B0; INT64 field_B8; PAGED_LOOKASIDE_LIST NotificationsLookaside; INT64 TotalSizeNotificationsSent; INT64 TotalSizeRemainingNotifications; INT FailedNotifications; INT64 field_158; INT64 field_160; INT64 field_168; INT64 field_170; INT64 field_178; } MP_ASYNC, *PMP_ASYNC;
一旦工作线程开始执行,它将进入无限循环,等待两个同步对象MpAsync-> AsyncSemaphore和MpAsync-> AsyncNotificationEvent。为了做到这一点,它使用KeWaitForMultipleObjects。
我想停止此调用以及如何使用它,因为这可以让我看到WaitType被设置为WaitAny,这意味着它将等待,直到有对象达到信号状态。同样使用WaitAny意味着如果函数返回STATUS_SUCCESS,它将实际返回对象的零索引作为NTSTATUS。考虑到这一点,由于每当发出事件信号时,事件便被设置为对象数组的第一个元素,因此返回值将为STATUS_WAIT_0,它对应于0x0。如上图所示,这将使for循环为FALSE,从而使循环停止,并且线程将通过调用PsTerminateSystemThread终止。
在信号量是信号对象的情况下,线程将继续获取必须发送到MsMpEng的数据。为此,首先将值MpConfig.AsyncStarvationLimit与全局变量AsyncStarvationLimit进行比较,如果它们相同,则全局AsyncStarvationLimit将设置为0x0,如果它们不匹配,则将在MpAsync->上搜索数据HighPriorityNotificationsList,如果在LIST_ENTRY中找到任何条目,则AsyncStarvationLimit将增加1。如果未找到任何条目,那么MpAsync->NotificationsList将被检查,并且如果找到有关条目,则清除AsyncStarvationLimit。如果达到极限,将以相反顺序检查列表条目,首先是正常优先级,然后是较高优先级。以下伪代码显示了这一点:
if (MpConfig.AsyncStarvationLimit == _InterlockedCompareExchange( &AsyncStarvationLimit, 0, MpConfig.AsyncStarvationLimit)) { if (&MpAsync->NotificationsList != MpAsync->NotificationsList.Flink) goto SendMessage; if (&MpAsync->HighPriorityNotificationsList != MpAsync->HighPriorityNotificationsList.Flink) { IncrementLimit: _InterlockedAdd(&AsyncStarvationLimit, 1); goto SendMessage } } if (&MpAsync->HighPriorityNotificationsList != MpAsync->HighPriorityNotificationsList.Flink) goto IncrementLimit if (&MpAsync->NotificationsList != MpAsync->NotificationsList.Flink) { _InterlockedCompareExchange(&AsyncStarvationLimit, 0, AsyncStarvationLimit); // Atomic set to 0 goto SendMessage; } A
如你所见,来自MpAsync-> HighPriorityNotificationsList的消息具有更高的优先级,因为除非达到限制,否则将首先检查此LIST_ENTRY。
接下来的部分非常简单,如果在两个列表条目中找到一个条目,则需要执行以下步骤:
1. 递减MpAsync-> NotificationsCount;
2. 从MpAsync-> TotalSizeRemainingNotifications中减去数据大小;
3. 将MP_ASYNC_NOTIFICATION的Magic 和Size(稍后将看到此结构)设置为0xBABAFAFA
推送或释放,将MP_ASYNC_NOTIFICATION条目添加到后备MpAsync-> AsyncNotificationsLookaside
4. 使用FltSendMessage发送实际消息;
5. 以防错误增加MpAsync-> FailedNotifications;
6. 将数据大小添加到MpAsync-> TotalSizeNotificationsSent。
之后,线程将再次遍历for循环,等待这两个对象中的任何一个发出信号。
只是为了完成该工作线程的完整循环,函数MpAsyncpShutdownWorkerThreads是使用MpAsync-> AsyncNotificationEvent作为要发出信号的事件来调用KeSetEvent函数,正如我们之前所看到的,它将结束循环并终止线程。这个函数是从MpAsyncShutdown中调用的,它负责清除所有与异步通知相关的内容。
MpAsyncSendNotification
NTSTATUS MpAsyncSendNotification( PVOID *__shifted(AsyncMessageData,8) AsyncMessageBuffer, ULONG SizeOfBuffer, INT PriorityFlag, PProcessCtx ProcessCtx );
我们已经看到了一些例子,其中代码将创建一个AsyncMessageData结构,并使用随后将要发送到MsMpEng的数据填充该结构。我们刚刚看到了这些数据的发送方式,现在我们将了解如何将这些数据添加到以前看到的列表条目中。
负责此功能的函数是MpAsyncSendNotification,它将首先对AsyncMessageBuffer和SizeOfBuffer进行完整性检查。如果检查完成,函数将测试SenderBuffer-> TypeOfOperation是否小于0xA,如果小于0xA,则MpData-> AsyncNotificationCount值将递增并分配给SenderBuffer-> NotificationNumber,TypeOfOperation的可能值如下:
typedef enum _MP_ASYNC_NOTIFICATION_OPERATION { CreateProcess = 0x0, RegistryEvent = 0x1, SendFile = 0x2, LoadImage = 0x3, OpenProcess = 0x4, RawVolumeWrite = 0x5, // High-Priority CreateThread = 0x6, DocOpen = 0x7, PostMount = 0x8, // High-Priority OpenDesktop = 0x9, PanicMode = 0xB, CheckJournal = 0xC, // High-Priority TrustedOrUntrustedProcess = 0xD, // High-Priority LogPrint = 0xE, Wow64cpuLoad = 0xF, OpenWithoutRead = 0x10, FolderGuardEvents = 0x11, DlpOnFileObjectClose = 0x13, } MP_ASYNC_NOTIFICATION_OPERATION;
下一步是增加ProcessCtx-> NotificationsSent,如果存在ProcessCtx ,则完成此操作后,将弹出或分配来自MpAsync-> AsyncNotificationsLookaside的条目,并将在该缓冲区中初始化以下结构:
typedef struct _MP_ASYNC_NOTIFICATION { SHORT Magic; // Set to 0xDA08 SHORT StructSize; // Sizeof 0x18 - Header Size LIST_ENTRY AsyncNotificationsList; PVOID *__shifted(AsyncMessageData,8) pMessageBuffer; INT MessageBufferSize; } MP_ASYNC_NOTIFICATION, *PMP_ASYNC_NOTIFICATION;
初始化此结构后,将有两个可能的路径,如果MpAsync-> NotificationsCount小于MpConfig.MaxAsyncNotificationCount,则为第一个路径。在这种情况下,将基于PriorityFlag将初始化的结构插入MpAsync-> HighPriorityNotificationsList或MpAsync-> NotificationsList的末尾,如果已设置,则链接到前者,在另一种情况下,链接到后者,然后是MpAsync-> NotificationsCount递增,MessageBufferSize被添加到MpAsync-> TotalSizeRemainingNotifications中,最后发出信号量:KeReleaseSemaphore。
当MpAsync-> NotificationsCount大于或等于MpConfig.MaxAsyncNotificationCount时,采用第二条路径,如果发生这种情况,那么MpAsync->HighPriorityNotificationsList或MpAsync->NotificationsList(同样基于PriorityFlag)的第一个条目将从LIST_ENTRY中被释放,并且新创建的条目将插入到它的末尾。在结束之前,正如我们在工作线程中看到的,该函数将增加MpAsync->AsyncMessagesFailed,并将未链接的条目推入或释放到后备列表,或者从后备列表释放(将第一个字节设置为0xBABAFAFA之后)。
这是为了确保在必须创建新通知的情况下有足够的资源从后备列表中分配池,另外也是确保较新的通知是保留在LIST_ENTRY上的通知,以防工作线程无法获取有足够的执行时间来释放通知列表。
在下一篇文章中,我们将研究对象(PsProcessType和ExDesktopObjectType)的注册回调,还将研究如何保存驱动程序信息以及如何进行验证。
本文翻译自:https://n4r1b.netlify.com/posts/2020/02/dissecting-the-windows-defender-driver-wdfilter-part-2/如若转载,请注明原文地址: