剖析Windows Defender驱动程序:WdFilter(Part 2)
导语:上一篇文章,我们着重介绍了初始化的过程,本文,我们会接着介绍回调中用到的主要函数。
上一篇文章,我们着重介绍了初始化的过程,本文,我们会接着介绍回调中用到的主要函数。
MpHandleProcessNotification
void __fastcall MpHandleProcessNotification( _In_ PEPROCESS Process, _In_ HANDLE ParentId, _In_ HANDLE ProcessId, _In_ BOOLEAN Create, _In_ BOOLEAN IsTransacted, _In_ PUNICODE_STRING ImageFileName, _In_ PUNICODE_STRING CommandLine, _Out_ PBYTE AccessDenied );
该函数有两个非常清晰的代码路径,它们由Create标志定义。在创建流程的情况下,过滤器中的第一步(可能也是最重要的步骤之一)是创建ProcessContext结构,这是在MpCreateProcessContext内部完成的。
NTSTATUS __fastcall MpCreateProcessContext( _In_ HANDLE ProcessId, _In_ LONGLONG CreationTime, _In_ PUNICODE_STRING FileNameAndCmdLine[2], // This is probably a struct with two UNICODE_STRING _Out_ PProcessCtx *ProcessCtx )
此函数主要从Lookaside MpProcessTable-> ProcessCtxLookaside分配内存以容纳一个Process Context,大小为0xC0-Tag MPpX,分配内存后,它将开始填充Process Context结构的成员,此结构如下所示:
typedef struct _ProcessCtx { SHORT Magic; // Set to 0xDA0F SHORT StructSize; // Sizeof 0xC0 LIST_ENTRY ProcessCtxList ; HANDLE ProcessId; QWORD CreationTime; PUNICODE_STRING ProcessCmdLine; INT RefCount; DWORD ProcessFlags; DWORD ProcessRules; QWORD SthWithCodeInjection; // Requires further investigation QWORD SthWithCodeInjection1; // Both fields used in MpAllowCodeInjection PMP_DOC_RULE pDocRule; BOOLEAN (__fastcall *pCsrssPreScanHook)(PFLT_CALLBACK_DATA, FltStreamCtx *); INT field_60; INT NotificationsSent; INT field_68; INT field_6C; PVOID Wow64CpuImageBase; INT ProcessSubsystemInformation; PUNICODE_STRING ImageFileName; INT64 InfoSetFromUserSpace; // This requires further investigation too INT64 InfoSetFromUserSpace1; // This data is filled in the function INT64 InfoSetFromUserSpace2; // MpSetProcessInfoByContext which uses INT64 InfoSetFromUserSpace3; // data that comes from MsMpEng to populate INT64 InfoSetFromUserSpace4; // this fields INT64 InfoSetFromUserSpace5; _PS_PROTECTION ProcessProtection; INT StreamHandleCtxCount; } ProcessCtx, *PProcessCtx;
一旦检索或创建了流程上下文(从现在开始即为ProcessCtx),该函数将继续查看是否应将文档规则附加到此流程。这是在MpSetProcessDocOpenRule内部完成的,涉及两个结构。一个保存所有文档规则的列表,一个保存每个规则的列表。
typedef struct _MP_DOC_OPEN_RULES { SHORT Magic; // Set to 0xDA14 SHORT StructSize; // Sizeof 0x100 SINGLE_LIST_ENTRY *__shifted(MP_DOC_RULE,8) DocObjectsList; ERESOURCE DocRulesResource; struct _PAGED_LOOKASIDE_LIST DocObjectsLookasideList; } MP_DOC_OPEN_RULES, *PMP_DOC_OPEN_RULES; typedef struct _MP_DOC_RULE { SHORT Magic; // Set to 0xDA15 SHORT StructSize; // Sizeof 0x228 INT RefCount; SINGLE_LIST_ENTRY SingleListEntryDocRules; WCHAR DocProcessName[261]; PCWSTR RuleExtension; } MP_DOC_RULE, *PMP_DOC_RULE;
该代码基本上会迭代比较ImageFileName和DocProcessName的单个列表条目,如果有任何规则匹配,则MP_DOC_RULE结构的指针将保存在ProcessCtx-> pDocRule中。
下一步是检查是否已创建上下文的进程是csrss.exe – MpSetProcessPreScanHook,如果存在,则指向CsrssPreScanHook的指针将保存在ProcessCtx.pCsrssPreScanHook中,并设置标志MpData-> pCsrssHookData-> HookSetFlag,仅对csrss.exe的ProcessCtx执行此操作。
通知流程创建之前的最后一步是检查流程是否与某些异常匹配,并相应地设置ProcessCtx.ProcessFlags。为此,需要执行以下三个函数:
1. MpSetProcessExempt;
2. MpSetProcessHardening;
3. MpSetProcessHardeningExclusion。
第一个将遍历以下结构的单个列表条目,其中有很多结构。
// Sizeof 0x20 typedef struct _MP_PROCESS_EXCLUDED { SINGLE_LIST_ENTRY ExcludedProcessList; UNICODE_STRING ProcessPath; BYTE NoBackslashFlag; BYTE WildcardPathFlag; } MP_PROCESS_EXCLUDED, *PMP_PROCESS_EXCLUDED;
并且它将检查ImageFileName的FinalComponent是否为前缀或等于列表中的任何前缀,如果匹配,则将通过将OR设置为0x1来设置ProcessFlags。驱动程序具有根据从用户空间收到的消息将进程/路径添加到MP_PROCESS_EXCLUDED列表的能力。
此检查中有一种特殊情况,当进程为MsMpEng时在这种情况下,ProcessFlags将使用0x9进行匹配。
第二次检查将首先检查FinalComponent是否与mpcmdrun.exe或msmpeng.exe匹配,如果它确实使用先前创建的MpServiceSID,它将检查进程的访问令牌是否与该SID相匹配。如果这些进程名都不匹配,则它将检查nissrv.exe和NriServiceSID。如果成功匹配了其中任何一种情况,那么ProcessFlags将被0x10赋值。
如果我们运行的是MpFilter而不是WdFilter,则还有另一种可能的情况,在这种情况下,进程名称将再次与msseces.exe进行比较,如果与进程名称匹配,则将与0x80进行比较。
如果需要,将首先创建最后一个检查,以列出强化的排除进程的列表条目。此列表条目的值在WdFilter中以硬编码形式保留名称,该标志指示其适用于哪个系统,最后指示将应用于ProcessFlags的掩码值。
用这些值填充下面的结构:
// Sizeof 0x20 typedef struct _MP_PROCESS_HARDENING_EXCLUDED { LIST_ENTRY ProcessExcludedList; PUNICODE_STRING ProcessPath; INT ProcessHardeningExcludedMask; } MP_PROCESS_HARDENING_EXCLUDED, *PMP_PROCESS_HARDENING_EXCLUDED;
一旦结构被填充,检查过程就非常标准了,代码将比较名称,如果名称匹配,则将ProcessHardeningExcludedFlag应用于ProcessCtx.ProcessFlags。在下图中,我们可以看到我系统的MP_PROCESS_HARDENING_EXCLUDED中的进程列表。
// Sizeof 0x78 typedef struct _MP_PROCESS_EXCLUSION { ERESOURCE ProcessExclusionResource; MP_PROCESS_EXCLUDED *ProcessExclusionList; MP_PROCESS_HARDENING_EXCLUDED *ProcessHardenedExclusionList; } MP_PROCESS_EXCLUSION, *PMP_PROCESS_EXCLUSION;
完成所有这些操作后,“默认” ProcessCtx已准备就绪,现在是时候通知回调\Callback\WdProcessNotificationCallback。 Argument1将包含以下结构:
typdef struct _MP_PROCESS_CB_NOTIFY { HANDLE ProcessId; HANDLE ParentId; PUNICODE_STRING ImageFileName; INT OperationType; // ProcessCreation = 1; ProcessTermination = 2; SetProcessInfo = 3 BYTE ProcessFlags; } MP_PROCESS_CB_NOTIFY, *PMP_PROCESS_CB_NOTIFY;
通知回调之后,我们只需要最后一步即可完成ProcessNotification回调,此步骤是向用户空间进程发送一条消息,侦听端口ProtectionPortServerCookie。
在进入创建和发送消息的功能之前,我将快速解释一下未设置标志Create(表示创建过程正在退出)的情况。在这种情况下,ProcessCtx将由进程ID获得,并使用该ProcessCtx将填充结构MP_PROCESS_CB_NOTIFY并通知回调。此后,将调用MpSendProcessMessage来创建和发送消息。
最后一个细节是对MpCopyCacheProcessTerminate的调用,该调用将在MP_COPY_CACHE_ENTRY数组上进行迭代:
typedef struct _MP_COPY_CACHE_ENTRY { DWORD Flags; HANDLE ProcessId; HANDLE ThreadId; UNICODE_STRING FileName; QWORD FileSize; QWORD TimeStamp; INT64 qword38; } MP_COPY_CACHE_ENTRY, *PMP_COPY_CACHE_ENTRY;
MpSendProcessMessage
NTSTATUS __fastcall MpSendProcessMessage( _In_ BYTE CreateFlag, _In_ PEPROCESS Process, _In_ HANDLE ProcessId, _In_ BOOLEAN IsTransacted, _In_ HANDLE ParentId, _In_ PAuxPidCreationTime ParentPidAndCreationTime, _In_ PUNICODE_STRING ImageFileName, _In_ PProcessCtx ProcessCtx, _In_ PUNICODE_STRING CommandLine, _Out_ PBYTE AccessDenied )
在这个函数中,两个消息都将使用异步结构创建,但是如果参数CreateFlag是0x1,那么消息将同步发送(FltSendMessage),如果是0x0,它将被加入队列,工作线程将处理它。
在该调用之后,我们将得到一个需要用特定数据填充的缓冲区。同样,该缓冲区将8个字节移入名为AsyncMessageData的结构中。结构看起来如下所示:
typedef struct _AsyncMessageData { INT Magic; INT Size; INT64 NotificationNumber; DWORD SizeOfData; INT RefCount; INT TypeOfOperation; union { // This are the ones I have for now ImageLoadAndProcessNotifyMessage ImageLoadAndProcessNotify; TrustedOrUntrustedProcessMessage TrustedProcess; ThreadNotifyMessage ThreadNotify; CheckJournalMessage CheckJournal; }; } AsyncMessageData, *PAsyncMessageData;
正如我们所看到的,这个结构包含一个union,在这个union中,每种不同消息类型的特定数据都将启动。在这种情况下,我们将重点关注与ProcessNotify有关的数据,这个结构看起来像这样:
typedef struct _ImageLoadAndProcessNotifyMessage { AuxPidCreationTime ParentProcess; // ZwOpenProcess -> PsGetProcessCreateTimeQuadPart AuxPidCreationTime CurrentProcess; // ZwOpenProcess -> PsGetProcessCreateTimeQuadPart BYTE CreateFlag; BYTE ProcessFlags; BYTE UnkGap[10]; // Weird alignment :S DWORD FileNameLength; DWORD OffsetToImageFileName; DWORD SessionId; DWORD CommandLineLenght; DWORD OffsetToCommandLine; DWORD TokenElevationType; DWORD TokenElevation; DWORD TokenIntegrityLevel; DWORD Unk; AuxPidCreationTime CreatorProcess; // Parameter -> ParentPidAndCreationTime } ImageLoadAndProcessNotifyMessage, *PImageLoadAndProcessNotifyMessage;
发送消息后,在使用FltSendMessage的情况下,该函数将继续检查调用状态并相应地填充MpData的某些字段:
· FltSendMessageCount
· FltSendMessageError
· FltSendMessageStatusTimeout
如果一切顺利,代码将检查ReplyBuffer(第一个字节应为0x5D,第二个字应为0x60,即回复消息的大小)。此回复缓冲区可以包含的内容包括是否允许创建进程(字节0x48)。
最后,完成之前的最后一步是设置流程信息(主要使用从ReplyBuffer接收的信息),然后,它将测试ProcessFlags & 0x20 || ProcessFlags & 0x18,将进程添加到“受信任”或“不受信任”列表中,分别在MpSetTrustedProcess或MpSetUntrustedProcess内部完成。
MpPowerStatusCallback
在结束之前还有最后一件事,我之前说过,我将稍微介绍一下在初始化期间注册的power-setting回调例程。
NTSTATUS MpPowerStatusCallback( LPCGUID SettingGuid, PVOID Value, ULONG ValueLength, PVOID Context ) { if (Value && Value == 4 && IsEqualGUID(SettingGuid, GUID_LOW_POWER_EPOCH)) { if ( *(ULONG *) Value ) { if ( *(ULONG *) Value == 1 ) { MpData->LowPowerEpochOn = 1; MpData->MachineUptime = 0; } } else { MpData->MachineUptime = *(ULONG64 *) 0xFFFFF78000000014; } } return STATUS_SUCCESS; }
附注
这个小的windbg脚本使我们可以打印系统中所有ProcessCtx所需的任何数据,我们只需要WdFilter的符号并根据需要调整命令!list。
r @$t0 = poi(poi(WdFilter!MpProcessTable)+180); // Pointer to MpProcessTable->ProcessCtxArray .for (r $t1 = 0; @$t1 != 0x80; r $t1 = @$t1+1) // Array size 0x80 { r @$t2 = @$t0+10*@$t1; // Move pointer to next LIST_ENTRY .if ( @$t2 == poi(@$t2) ) { // Check if our pointer value is the same as Blink .continue } .else { // We walk the LIST_ENTRY and print whatever // member we want from ProcessCtx in this case // ProcessCtx.ProcessId and ProcessCtx.ProcessCmdLine !list -t nt!_LIST_ENTRY.Flink -x "dd @$extret+10 L1; dS /c100 poi(@$extret+20)" -a "L1" poi(@$t2) } }
如果运行上述脚本,你应该会看到类似以下内容的信息:
到此为止,我们已经了解了WdFilter是如何初始化的,以及它如何在整个过程创建回调中处理过程创建。另外,我们还了解了ProcessCtx结构,该结构将在整个驱动程序中使用,以跟踪系统上运行的不同进程。接下里,我们将重点了解以下内容:
1. 映像加载回调;
2. 线程创建回调;
3. 发送同步/异步通知。
不过请注意:我将在本文中解释的回调主要依赖于ProcessCtx.ProcessRules,尽管我尝试使用不同类型的进程(甚至是恶意软件),但我仍无法确定每种规则对应的进程类型(也许与Windows Defender配置有关)。
出于演示目的,我已强制代码遵循不同的路径。
MpCreateThreadNotifyRoutineEx-MpCreateThreadNotifyRoutine
我们将看到的前两个回调是MpCreateThreadNotifyRoutine和MpCreateThreadNotifyRoutineEx,它们在创建新线程或删除线程时都会收到通知。有两种不同的回调,因为第一个使用PsSetCreateThreadNotifyRoutine注册,而第二个使用PsSetCreateThreadNotifyRoutineEx注册,此函数从Windows 10开始可用,并且指向它的指针保存在MpData中,当然如果第二个回调的指针为NULL将不会被注册。
如PsSetCreateThreadNotifyRoutineEx文档的备注部分所述,这两个函数的不同之处在于执行回调的上下文引用了MS文档:“使用PsSetCreateThreadNotifyRoutine,回调在创建者线程上执行。使用PsSetCreateThreadNotifyRoutineEx,可以在新创建的线程上执行回调。”
MpCreateThreadNotifyRoutine
回调的代码差异可能超出你的预期,因此我们将对两者进行研究。从MpCreateThreadNotifyRoutine开始,请记住,此回调在创建者线程的上下文中执行,该回调将检查以下三件事来执行:
1. Create参数设置为TRUE;
2. ProcessId与0x4(系统)不同;
3. Curren线程不是一个系统线程!PsIsSystemThread。
如果满足这三个条件,则代码将继续设置一个标志,该标志指示当前进程是否与参数ProcessId中的进程相同。
一个进程可能正在另一个进程中创建线程,并且由于此回调在创建者线程的上下文中执行,因此当前进程将是创建者,而参数ProcessId将是线程将要执行的那个。
如果它们相同,则将根据规则NotifyNewThreadSameProcess(0x10000000)测试当前进程ProcessCtx.ProcessRules,并将相应地设置一个标志。如果当前进程不相同,则将根据规则NotifyNewThreadDifferentProcess(0x400000)测试ProcessRules并相应地设置其他标志。如果未设置这些标志,则回调将返回。下面的伪代码将显示这种行为,以防我的解释不够清楚。
BOOLEAN SameProcess = 1; BOOLEAN NotifyNewThreadSameProcFlag = 0; BOOLEAN NotifyNewThreadDiffProcFlag = 0; if ( Create && ProcessId != 4 && !PsIsSystemThread(KeGetCurrentThread()) ) { SameProcess = ProcessId == PsGetCurrentProcessId(); // Retrieve the ProcessCtx by the ProcessId MpGetProcessContextById(PsGetCurrentProcessId(), &CurrentProcessCtx); if ( SameProcess && CurrentProcessCtx->ProcessRules & NotifyNewThreadSameProcess ) NotifyNewThreadSameProcFlag = 1; if ( !SameProcess && CurrentProcessCtx->ProcessRules & NotifyNewThreadDifferentProcess ) NotifyNewThreadDiffProcFlag = 1; if ( !NotifyNewThreadSameProcFlag && !NotifyNewThreadDiffProcFlag ) goto Cleanup; }
如果设置了其中一个标志,则代码将继续获取我称为AuxPidCreationTime的结构,我们在第1部分中看到了,但其余部分包含de PID和进程的CreationTime,在这两个进程都具有此结构之后(即使是相同的过程也获得两次),代码将继续调用MpGetPriorityInfo,此函数将主要调用FltRetrieveIoPriorityInfo来获取当前线程的IO_PRIORITY_INFO并使用此数据填充我创建的MP_IO_PRIORITY结构:
typedef struct _MP_IO_PRIORITY { IO_PRIORITY_HINT IoPriority ULONG ThreadPriority ULONG PagePriority } MP_IO_PRIORITY, *PMP_IO_PRIORITY;
根据设置的标志,不同的消息将被发送到MsMpEng。对于NotifyNewThreadDifferentProcess,将以OperationType等于NewThreadDifferentProcess(0x3)的方式调用MpSendSyncMonitorNotification,并且数据将是执行线程的进程的AuxPidCreationTime。
如果线程是在同一个进程中创建的,那么在调用MpSendSyncMonitorNotification之前,将初始化要发送的数据,MpCreatePsThreadSyncMonitorData函数负责这一工作。这个函数将基本填充以下结构:
typedef struct _ThreadNotifySyncMessage { AuxTidCreationTime CreatedThread; AuxTidCreationTime CurrentThread; AuxPidCreationTime Process; INT64 Unk; PVOID ThreadStartAddress; } ThreadNotifySyncMessage, *PThreadNotifySyncMessage;
要获取ThreadStartAddress的值,它将打开以获取线程的句柄(PsLookupThreadByThreadId),然后使用这个句柄调用类ThreadQuerySetWin32StartAddress的ZwQueryInformationThread。一旦ThreadNotifySyncMessage被填充,函数MpSendSyncMonitorNotification将以这种结构被调用,因为Data和OperationType等于NewThreadSameProcess(0x6)。
最后,如果设置了NotifyNewThreadDifferentProcess,则回调将执行最后一步。此步骤将包括发送带有以下数据的异步通知:
typedef struct _ThreadNotifyMessage { AuxPidCreationTime CurrentProcess; INT CurrentThreadId; AuxPidCreationTime CreatedThreadProcess; AuxTidCreationTime CreatedThread; WCHAR *ImageFileName; } ThreadNotifyMessage, *PThreadNotifyMessage;
对于ImageFileName而言,此字段将从ProcessCtx中被检索到,在本例中,ProcessCtx对应于线程创建者进程中的一个字段,它可能与线程将要运行的那个字段不同。
MpCreateThreadNotifyRoutineEx
该例程比上一个例程简单得多,在本例中,该函数在新线程上执行,这基本上意味着当前进程将始终与参数ProcessId指示的进程匹配。首先,为了实际发送通知,必须满足以下条件:
1. MpProcessTable-> CreateThreadNotifyLock设置为不同于0的值(我知道,lock不是此字段的最佳名称,为零时被锁定);
2. 将参数设置为TRUE;
3. 除PsInitialSystemProcess以外的当前进程;
4. 在ProcessCtx.ProcessFlags中设置的ThreadNotifyRoutineExSet(0x400)标志;
5. 在ProcessCtx.ProcessRules中设置的规则NotifyProcessCmdLine(0x20000000)。
以下伪代码对此进行了更好的解释:
if ( _InterlockedCompareExchange(&MpProcessTable->CreateThreadNotifyLock, 0, 0) && IoGetCurrentProcess() != PsInitialSystemProcess && Create ) { // Retrieve the ProcessCtx using the Process Object, in the end it will use // the CreationTime (PsGetProcessCreateTimeQuadPart) and the ProcessId (PsGetProcessId) MpGetProcessContextByObject(IoGetCurrentProcess(), &ProcessCtx) // Same as ((ProcessCtx->ProcessFlags >> 10) & 1 && (ProcessCtx->ProcessRules >> 0x1D) & 1) if ((ProcessCtx->ProcessFlags & ThreadNotifyRoutineExSet) && (ProcessCtx->ProcessRules & NotifyProcessCmdLine)) { ..... } }
这里需要说明一下,如果MP_DATA中指向PsSetCreateThreadNotifyRoutineEx的指针不为NULL,则在每个ProcessCtx中设置ThreadNotifyRoutineExSet标志:
在规则NotifyProcessCmdLine的情况下,它是在设置过程信息时来自MsMpEng的。同样,我没有设法通过任何过程触发此规则,所以我真的不知道该规则适用于哪种过程,我对此表示歉意。因此,在流程创建的最后,如果设置了此规则,则MpProcessTable-> CreateThreadNotifyLock值将增加:
返回到实际函数,如果满足所有条件,那么首先要递减CreateThreadNotifyLock并从ProcessCtx中删除ThreadNotifyRoutineExSet,一旦完成,将获得流程对象的句柄(ObOpenObjectByPointer,ObjectType为PsProcessType)此句柄将用于检索MpGetProcessCommandLineByHandle内的Process CommandLine,此函数几乎使用ZwQueryInformationProcess,并将ProcessInformationClass设置为ProcessCommandLineInformation。该命令行将与ProcessCtx-> ProcessCmdLine内部的命令行进行比较,以防万一它们不匹配,则该函数将获得MP_IO_PRIORITY,AuxPidCreationTime,并且它将调用MpSendSyncMonitorNotification并将这两个命令行作为数据。
如上图所示,如果有人修改了来自PEB的命令行,这个回调将通知MsMpEng被篡改的命令行。不过前提是,设置了ProcessCtx的规则和标志。
本文,我们介绍了回调中用到的主要函数。下一篇文章,我们会接着介绍回调的具体过程和方法。
发表评论