剖析Windows Defender驱动程序:WdFilter(Part 1)
导语:本文研究的对象是WdFilter版本4.18.1910.4,首先,让我们进入DriverEntry。
虽然我一直对杀毒软件的工作原理及其在内核非常感兴趣,但由于我无法访问任何源代码,所以我只能通过逆向工程来反向了解。
WdFilter是Windows Defender的主要内核组件,大致来说,此驱动程序充当加载顺序组“FSFilter Anti-Virus”中的Minifilter,这意味着它已附加到文件系统堆栈并在一些前/后回调中处理I/O操作。不仅如此,该驱动程序还实现了通过其他技术来获取系统中正在发生的事情的功能,本系列文章的目的是对其幕后工作原理 进行深入分析。
初始化
本文研究的对象是WdFilter版本4.18.1910.4,首先,让我们进入DriverEntry。正如我们在WdBoot上看到的,第一步是检查是否在SafeBootMode上运行并初始化WPP跟踪。在此之后,我们将讨论主结构MpData的分配。在我们正在研究的驱动程序版本中,它的大小为0xCC0字节,并在带有标签MPfd的NonPaged Pool中分配。一旦我们为结构分配了Pool,代码将继续调用MpInitializeGlobals,它将初始化MpData内部的某些结构(PagedLookasideLists,EResources,Timer等),该函数还将负责计算确定运行操作系统版本的掩码。可以在下图中看到, MpVerifyWindowsVersion接收MajorVersion,MinorVersion,ServicePack和BuildNumber,最后调用RtlVerifyVersionInfo来验证运行的操作系统版本是否更高。
同样在此函数内部,将获得一些指向该函数的指针,特别是在MpGetSystemRoutines内部,此函数将使用MmGetSystemRoutineAddress并将返回的地址保存到MpData中。 操作系统VersionMask字段在这里起作用,因为仅在某些操作系统版本中会获得一些指针,例如FltRequestFileInfoOnOnCompletion仅在运行Windows 10 build 17726或更高版本时才会被检索,回到初始化函数,它将做的最后一件事是要创建以下SID:
· MpServiceSID
· NriServiceSID
· TrustedInstallerSID
此后,即使仍然有很多成员要填充其他功能,MpData的初始化也完成了,在这里你可以看到这种巨大的结构仍然缺少许多字段。
typedef struct _MP_DATA { SHORT Magic; // Set to 0xDA00 SHORT StructSize; // Sizeof 0xCC0 PDRIVER_OBJECT pDriverObject; PFLT_FILTER MpFilter; NTSTATUS (__fastcall *pPsSetCreateProcessNotifyRoutineEx)(PCREATE_PROCESS_NOTIFY_ROUTINE_EX, BOOLEAN); NTSTATUS (__fastcall *pPsSetCreateProcessNotifyRoutineEx2)(PSCREATEPROCESSNOTIFYTYPE, PVOID, BOOLEAN); NTSTATUS (__fastcall *pPsSetCreateThreadNotifyRoutineEx)(PSCREATETHREADNOTIFYTYPE, PVOID); NTSTATUS (__fastcall *pObRegisterCallbacks)(POB_CALLBACK_REGISTRATION, PVOID *); void (__fastcall *pObUnRegisterCallbacks)(PVOID); NTSTATUS (__fastcall *pFltRegisterForDataScan)(const PFLT_INSTANCE); NTSTATUS (__fastcall *pFltCreateSectionForDataScan)(PFLT_INSTANCE Instance, PFILE_OBJECT FileObject, PFLT_CONTEXT SectionContext, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PLARGE_INTEGER MaximumSize, ULONG SectionPageProtection, ULONG AllocationAttributes, ULONG Flags, PHANDLE SectionHandle, PVOID *SectionObject, PLARGE_INTEGER SectionFileSize); NTSTATUS (__fastcall *pFltCloseSectionForDataScan)(PFLT_CONTEXT); NTSTATUS (__fastcall *pFltRequestFileInfoOnCreateCompletion)(PFLT_FILTER, PFLT_CALLBACK_DATA, ULONG); PVOID (__fastcall *pFltRetrieveFileInfoOnCreateCompletion)(PFLT_FILTER Filter, PFLT_CALLBACK_DATA Data, ULONG InfoClass, PULONG Size); NTSTATUS (__fastcall *pFsRtlQueryCachedVdl)(PFILE_OBJECT FileObject, PLONGLONG Vdl); PVOID pIoBoostThreadIo; PVOID pKeSetActualBasePriorityThread; PVOID pSeGetCachedSigningLevel; PIO_FOEXT_SILO_PARAMETERS (__fastcall *pIoGetSiloParameters)(const PFILE_OBJECT); BYTE field_90; BYTE PanicModeFlag; BYTE field_92; BYTE field_93; INT ScannedFilesCount; INT field_98; INT field_9C; PEPROCESS MsMpEngProcess; HANDLE MsMpEngProcessId; INT ConnectionPortCookieSet; PFLT_PORT FltProtectionControlPort; PFLT_PORT ProtectionControlPortServerCookie; PFLT_PORT FltProtectionPort; PFLT_PORT ProtectionPortServerCookie; PFLT_PORT FltProtectionVeryLowIoPort; PFLT_PORT ProtectionVeryLowIoServerCookie; PFLT_PORT FltProtectionRemoteIoPort; PFLT_PORT ProtectionRemoteIoServerCookie; PFLT_PORT FltProtectionAsyncPort; PFLT_PORT ProtectionAsyncServerCookie; INT SomeScanFileFlag; INT SendSyncNotificationFlag; KSEMAPHORE ScanFileSemaphore1; KSEMAPHORE ScanFileSempahore2; KSEMAPHORE SendingSyncSemaphore; PVOID pBootSectorCache; LIST_ENTRY FltInstanceCtxList; LIST_ENTRY FltStreamCtxList; PCWSTR RegistryParametersPath; BYTE DriverVerifiedFlag; BYTE field_1A1; BYTE field_1A2; BYTE field_1A3; INT VerifyDriverLevelValue; INT64 ResetTimer; INT FileScanConsecutiveTimeoutsCount; INT field_1B4; KDPC WdFilterDPC; KTIMER WdFilterTimer; ERESOURCE MpDataResource; INT64 AsyncNotificationCount; INT OsVersionMask; INT MonitorFlags; INT64 field_2B0; INT64 field_2B8; PAGED_LOOKASIDE_LIST CompletionContextLookaside; NPAGED_LOOKASIDE_LIST WriteContextLookaside; NPAGED_LOOKASIDE_LIST field_3C0; PAGED_LOOKASIDE_LIST InstanceContextLookaside; PAGED_LOOKASIDE_LIST FltInputMessagesLookaside; PAGED_LOOKASIDE_LIST FltOutputMessagesLookaside; ULONG MpFilterEcpSize; INT64 field_5C8; INT64 field_5D0; INT64 field_5D8; INT64 field_5E0; INT64 field_5E8; INT64 field_5F0; INT64 field_5F8; NPAGED_LOOKASIDE_LIST ExtraCreateParamsLookaside; PVOID ObRegistrationHandle; PSID MpServiceSID; PSID NriServiceSID; PSID TrustedInstallerSID; INT MaxLocalScanTimeout; INT MaxNetworkScanTimeout; INT field_6A8; INT field_6AC; BYTE RawVolumeWriteFlag; BYTE MpOrWdFlag; BYTE field_6B2; BYTE field_6B3; INT field_6B4; PVOID PowerSettingCbHandle; BYTE LowPowerEpochOn; BYTE field_6C1; BYTE field_6C2; BYTE field_6C3; int field_6C4; INT64 MachineUptime; MP_CSRSS_HOOK_DATA *pCsrssHookData; PCALLBACK_OBJECT pProcessNotificationCallback; PCALLBACK_OBJECT pNriNotificationCallback; INT64 NriNotificationCallbackHandle; INT64 field_6F0; INT64 field_6F8; LIST_ENTRY field_700; FAST_MUTEX MpDataFastMutex; INT64 field_748; INT64 field_750; INT64 field_758; INT64 field_760; INT64 field_768; INT64 field_770; INT64 field_778; PAGED_LOOKASIDE_LIST PagedLookasideMPbc; INT field_800; INT field_804; INT64 field_808; INT64 field_810; INT64 field_818; INT64 field_820; INT64 field_828; INT64 field_830; INT64 field_838; INT64 field_840; INT64 field_848; INT64 field_850; INT64 field_858; INT64 field_860; INT CsvFileStateCacheType; INT FileStateCachePolicy; INT64 field_870; INT field_878; INT field_87C; INT CounterFileSystemTypeCSVFS; INT field_884; INT field_888; INT RefsFileStateCacheType; INT FileStateCachePolicy1; INT64 field_898; INT field_8A0; INT field_8A4; INT CounterFileSystemTypeREFS; INT field_8AC; INT field_8B0; INT64 FltSendMessageTimeStamp; INT FltSendMessageCount; INT field_8C4; INT SomethingWithSettingProcessInfo; INT FltSendMessageError; INT FltSendMessageErrorCode; INT FltSendMessageStatusTimeout; INT FltSendMessageReplyBufferMismatch; INT AllowFilterManualDetach; LIST_ENTRY BootScanCtxList; ERESOURCE ExResource1; ERESOURCE ExResource2; INT field_9C0; INT field_9C4; PUNICODE_STRING SystemRootPath; INT field_9D0; INT field_9D4; BYTE OpenWithoutReadNotificationFlag; RTL_GENERIC_TABLE RtlGenericTable; FAST_MUTEX WdFilterGenericTableMutex; MP_SYNC_NOTIFICATIONS_STATUS SyncNotifications[8]; INT SyncNotificationRecvCount[8]; INT SyncNotificationsCount[8]; INT SyncNotificationsStatus[8]; INT SyncNotificationsIoTimeoutCount[8]; INT SyncNotificationsRecvErrorCount[8]; INT MonitorNotificationFlag; INT field_B84; INT64 SyncMonitorNotificationTimeout; INT64 RandNumber; BYTE MpEaString[256]; INT AsyncDirectoryNotificationFlag; BYTE DataLossPreventionFlag; BYTE field_C9D; BYTE field_C9E; BYTE field_C9F; INT64 field_CA0; INT64 field_CA8; INT64 field_CB0; INT64 field_CB8; } MP_DATA, *PMP_DATA;
下一步是设置驱动程序的参数/配置,这将在MpLoadRegistryParameters内部完成,此函数将通过迭代我创造的MP_CONFIG_PARAMS结构数组来设置RTL_REGISTRY_QUERY_TABLE:
typedef struct _MP_CONFIG_PARAMS { PCWSTR Name; PMP_CONFIG *pMpConfig; INT64 DefaultData; } MP_CONFIG_PARAMS, *PMP_CONFIG_PARAMS
下图显示了此数组的一些条目:
如你所见,该结构的第二个成员是结构MP_CONFIG内的一个指针,该地址就是在QueryTable中将其设置为EntryContext的地址。最后,该函数将在调用完成后调用RtlQueryRegistryValuesEx,其注册表路径为HKLM\System\CurrentControlSet\Services\WdFilter\Parameters,然后检查EntryContext中返回的值是否符合某些条件,如果不符合,它们将被设置为默认值。MP_CONFIG的定义如下:
// Sizeof 0x5C typedef struct _MP_CONFIG { INT ResetToUnknownTimer; INT MaxLocalScanTimeout; INT MaxNetworkScanTimeout; INT MaxProcessCreationMessageTimeout; INT MaxConsecutiveTimeoutsUntilPassThrough; INT StartScanningAgainTimer; INT DebugPassthroughEnabled; INT MaxAsyncNotificationCount; INT AsyncStarvationLimit; INT AsyncTimeout; INT AllowManualDetach; INT MaxCopyCacheSize; INT KnownBadHashSize; BYTE DirectionalScanningNonNTFS; BYTE DisableQueryNameNormalize; BYTE ThreadBoostingFlag; INT CsvFileStateCacheType; INT RefsFileStateCacheType; INT FileStateCachePolicy; INT DisableReadHooking; INT FolderGuardDispatchTimer; INT FolderGuardDispatchLimit; INT DisableTransactionCallback; } MP_CONFIG, *PMP_CONFIG;
填充MpConfig结构后,一些默认值将被复制到MpSetDefaultConfigs中的MpData中,然后MpSetBufferLimits函数将设置用于与用户空间进程MsMpEng.exe通信的输入和输出消息的不同限制。
从现在开始,代码将开始初始化许多不同的结构,每个结构都意味着不同的东西,我将提及所有这些结构,但是在本文中,我将仅关注其中的一些结构。第一个函数是MpInitializeProcessTable,顾名思义,该函数将初始化一个结构,该结构将跟踪系统中的进程,为此,它将分配一个大小为0x800的池,该池将包含LIST_ENTRY数组。每个列表项都是大小为0x10,因此数组中有0x80个条目。这个LIST_ENTRY实际上是一个移入指向名为ProcessCtx的结构的指针,该结构包含有关进程的信息。流程表的定义如下所示:
typedef struct _MP_PROCESS_TABLE { SHORT Magic; // Set to 0xDA13 SHORT Size; // Sizeof 0x1C0 ERESOURCE ProcessTableResource; PAGED_LOOKASIDE_LIST ProcessCtxLookaside; PAGED_LOOKASIDE_LIST ProcessCtxListLookaside; LIST_ENTRY *__shifted(ProcessCtx,8) (*ProcessCtxArray)[0x80]; KEVENT ProcessTableEvent; _DWORD BeingAccessed; INT TrustedProcessCtxCounter; INT UntrustedProcessCtxCounter; INT Unk; INT CreateThreadNotifyLock; } MP_PROCESS_TABLE, *PMP_PROCESS_TABLE;
之后,DriverEntry将调用MpInitBootSectorCache,后者将分配一个大小为0x64的池并标记MPgb,并将指针保存在MpData-> pBootSectorCache中。
然后,基于保存在MpConfig.MaxCopyCacheSize上的值,将分配另一个池,这一次指向该池的指针将保存在全局变量MpCopyCacheData中,MaxCopyCacheSize的值不能大于0x200,并且为了分配该池值左移6次,因此最大大小将为0x8000。完成此操作后,下一步是初始化以下结构和回调:
1. 进程排除结构,在大小为0x78的mpinitializeprocessexclude内初始化,标记MPps,并保存在全局MpProcessExclusion中。
2. 电源设置回调,此操作在MpPowerStatusInitialize内部完成,它接收MpData-> PowerSettingCbHandle的地址作为参数,此函数将使用PoRegisterPowerSettingCallback在电源设置GUID_LOW_POWER_EPOCH上设置回调,如果成功注册该回调,则句柄将保存在参数,我们将在本文结尾处看到实际的回调函数。
3. 事务NTFS结构,将在MpTxfInitialize中初始化,大小为0x140,标记MPtd,并保存在一个名为MpTxfData的全局变量中。
4. 异步工作线程与异步结构一起,将在MpAsyncInitialize中初始化,该结构将主要保留两个列表项中的消息,这些消息已排队由异步工作线程发送。该线程也在该函数内部初始化,并且函数MpAsyncpWorkerThread设置为它的StartRoutine。
5. 注册表数据结构,将在MpRegInitialize内部初始化,大小为0x500,标记为MPrD。这是另一个重要的结构,主要将在RegistryCallback中使用。
6. 文档规则结构,在MpInitializeDocOpenRules内部初始化,大小为0x100,标记为MPdo,并保存在全局MpBmDocOpenRules中。
7. 文件夹保护结构仅在运行Windows 10 build 16000或更高版本的系统上在MpFgInitialize中初始化,大小为0x240,标记MPFg并保存在全局MpFolderGuard中。该结构将保留指向RTL_AVL_TABLE表和RTL_GENERIC_TABLE的指针,并将主要用于允许或撤消对文件/文件夹的访问。
8. 最后,驱动程序信息结构在MpInitializeDriverInfo内部初始化,该结构与ELAM驱动程序绑定,并且将主要用于为回调\ Callback \ WdEbNotificationCallback注册的函数上。当我们了解如何使用此函数和结构时,我们将能够将关于WdBoot的文章与WdFilter处理该数据的内容联系在一起。
此时,我们会发现自己有大量的分配池和初始化的结构:
DriverEntry的下一步是初始化MpInitializeFltMgr内部的微型过滤器和MpCreateCommPorts内部的通信端口。前者将根据配置和操作系统VersionMask为FLT_REGISTRATION结构选择特定的OperationRegistration,使用此FilterRegistration它将注册微型过滤器(FltRegisterFilter)。后者将首先使用MpServiceSID设置安全描述符,并且此安全描述符将在ObjectAttributes中使用,该ObjectAttributes作为FltCreateCommunicationPort的参数。将创建四个不同的端口:
1. MicrosoftMalwareProtectionControlPort(这是唯一一个将注册MessageNotifyCallback的端口);
2. MicrosoftMalwareProtectionPort;
3. MicrosoftMalwareProtectionVeryLowIoPort;
4. MicrosoftMalwareProtectionRemoteIoPort。
DriverEntry将为以下事件注册回调:
· 流程创建
· 图像加载
· 线程创建
· 图像验证
· 对象操作(ProcessType和DesktopObjectType)
· 注册表操作
在设置了图像验证回调之后,驱动程序将开始过滤(FltStartFiltering),并且在注册最后两个回调之后,将完成驱动程序初始化。当然,如果上述任何步骤中的任何一个失败,驱动程序都会清理所有内容。
MpSetProcessNotifyRoutine
我们将要研究的第一个回调注册是流程创建,该回调在MpSetProcessNotifyRoutine中注册。该函数要做的第一件事是检查PsSetCreateProcessNotifyRoutineEx2是否可用,以防它将使用此函数注册回调,如果没有,则将最后检查PsSetCreateProcessNotifyRoutineEx。一个也不可用,那么它将在PsSetCreateProcessNotifyRoutine中求助。一旦注册了一个回调例程,则代码将继续创建两个回调对象\ Callback \ WdProcessNotificationCallback和\ Callback \ WdNriNotificationCallback。对于后者,代码还将注册一个回调函数:MpNriNotificationCallback。
MpCreateProcessNotifyRoutineEx - MpCreateProcessNotifyRoutine
如本节标题所示,可以有两个例程,第一个由..Ex2和..Ex注册,而第二个由PsSetCreateProcessNotifyRoutine注册。
..Ex和..Ex2函数之间的区别基本上是:后者允许提供PSCREATEPROCESSNOTIFYTYPE,即使此值只能设置为PsCreateProcessNotifySubsystems,也许将来它们会增加更多值,例如一个会仅获得通知来自WSL子系统。另一方面,这两个与PsSetCreateProcessNotifyRoutine的区别在于,后者的注册例程原型为CREATE_PROCESS_NOTIFY_ROUTINE,而其他两个例程的原型为CREATE_PROCESS_NOTIFY_ROUTINE_EX
这两个函数非常相似,而且它们共享许多代码。它们之间只有几个区别,主要区别是:
1. MpCreateProcessNotifyRoutineEx可以利用具有结构PS_CREATE_NOTIFY_INFO的优势,例如,如果设置了标志FileOpenNameAvailable,则它可以检索ImageFileName,而无需获取进程的句柄。
2. MpCreateProcessNotifyRoutineEx可以拒绝将CreationStatus值设置为错误的进程创建;
3. 最后一个区别是MpCreateProcessNotifyRoutineEx函数也能够通过调用MpAddBootProcessEntry将进程添加到启动进程列表条目中。
如上所述,进入实际代码时,如果我们没有标志FileOpenNameAvailable或没有PS_CREATE_NOTIFY_INFO,则代码将继续获取该进程的句柄(ZwOpenProcess),并使用该句柄调用MpGetProcessNameByHandle,该方法基本上调用ZwQueryInformationProcess,并将ProcessImageFileName作为ProcessInformationClass。一旦回调例程具有ImageFileName,它将继续获取标准化名称,为此它将调用函数MpGetImageNormalizedName,此函数将主要使用NameOptions FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT调用FltGetFileNameInformationUnsafe。最后,回调例程将最终调用MpHandleProcessNotification,这是此回调的主要函数。
本文着重介绍了初始化的过程,在下一篇文章中,我们会接着介绍回调中用到的主要函数。
发表评论