剖析Windows Defender驱动程序:WdFilter(Part 2) - 嘶吼 RoarTalk – 回归最本质的信息安全,互联网安全新媒体,4hou.com

剖析Windows Defender驱动程序:WdFilter(Part 2)

xiaohui 系统安全 2020-05-18 09:01:24
收藏

导语:上一篇文章,我们着重介绍了初始化的过程,本文,我们会接着介绍回调中用到的主要函数。

上一篇文章,我们着重介绍了初始化的过程,本文,我们会接着介绍回调中用到的主要函数。

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执行此操作。

12.png

通知流程创建之前的最后一步是检查流程是否与某些异常匹配,并相应地设置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列表的能力。

14.png

此检查中有一种特殊情况,当进程为MsMpEng时在这种情况下,ProcessFlags将使用0x9进行匹配。

第二次检查将首先检查FinalComponent是否与mpcmdrun.exe或msmpeng.exe匹配,如果它确实使用先前创建的MpServiceSID,它将检查进程的访问令牌是否与该SID相匹配。如果这些进程名都不匹配,则它将检查nissrv.exe和NriServiceSID。如果成功匹配了其中任何一种情况,那么ProcessFlags将被0x10赋值。

如果我们运行的是MpFilter而不是WdFilter,则还有另一种可能的情况,在这种情况下,进程名称将再次与msseces.exe进行比较,如果与进程名称匹配,则将与0x80进行比较。

如果需要,将首先创建最后一个检查,以列出强化的排除进程的列表条目。此列表条目的值在WdFilter中以硬编码形式保留名称,该标志指示其适用于哪个系统,最后指示将应用于ProcessFlags的掩码值。

15.png

用这些值填充下面的结构:

// 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中的进程列表。

17.png

// 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) 
  } 
}

如果运行上述脚本,你应该会看到类似以下内容的信息:

27.png

到此为止,我们已经了解了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。

3.png

如果线程是在同一个进程中创建的,那么在调用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)。

5.png

最后,如果设置了NotifyNewThreadDifferentProcess,则回调将执行最后一步。此步骤将包括发送带有以下数据的异步通知:

typedef struct _ThreadNotifyMessage
{
  AuxPidCreationTime CurrentProcess;
  INT CurrentThreadId;
  AuxPidCreationTime CreatedThreadProcess;
  AuxTidCreationTime CreatedThread;
  WCHAR *ImageFileName;
} ThreadNotifyMessage, *PThreadNotifyMessage;

对于ImageFileName而言,此字段将从ProcessCtx中被检索到,在本例中,ProcessCtx对应于线程创建者进程中的一个字段,它可能与线程将要运行的那个字段不同。

7.png

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标志:

9.png

在规则NotifyProcessCmdLine的情况下,它是在设置过程信息时来自MsMpEng的。同样,我没有设法通过任何过程触发此规则,所以我真的不知道该规则适用于哪种过程,我对此表示歉意。因此,在流程创建的最后,如果设置了此规则,则MpProcessTable-> CreateThreadNotifyLock值将增加:

10.png

返回到实际函数,如果满足所有条件,那么首先要递减CreateThreadNotifyLock并从ProcessCtx中删除ThreadNotifyRoutineExSet,一旦完成,将获得流程对象的句柄(ObOpenObjectByPointer,ObjectType为PsProcessType)此句柄将用于检索MpGetProcessCommandLineByHandle内的Process CommandLine,此函数几乎使用ZwQueryInformationProcess,并将ProcessInformationClass设置为ProcessCommandLineInformation。该命令行将与ProcessCtx-> ProcessCmdLine内部的命令行进行比较,以防万一它们不匹配,则该函数将获得MP_IO_PRIORITY,AuxPidCreationTime,并且它将调用MpSendSyncMonitorNotification并将这两个命令行作为数据。

11.png

如上图所示,如果有人修改了来自PEB的命令行,这个回调将通知MsMpEng被篡改的命令行。不过前提是,设置了ProcessCtx的规则和标志。

本文,我们介绍了回调中用到的主要函数。下一篇文章,我们会接着介绍回调的具体过程和方法。

本文翻译自:https://n4r1b.netlify.com/posts/2020/01/dissecting-the-windows-defender-driver-wdfilter-part-1/ https://n4r1b.netlify.com/posts/2020/02/dissecting-the-windows-defender-driver-wdfilter-part-2/如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论