查看: 990|回复: 0
收起左侧

[技术探讨] 【本人手动渣翻系列·其八】MDAV WdFilter分析(四)

[复制链接]
ANY.LNK
发表于 2024-11-16 15:44:10 | 显示全部楼层 |阅读模式
本帖最后由 ANY.LNK 于 2024-11-16 15:44 编辑

WdFilter的第四篇了,之前的文章可以看目录https://bbs.kafan.cn/thread-2275641-1-1.html

在本篇文章中,我们关注一件事:注册表操作。
那么话不多说进入正文!

MpRegInitialize

此函数负责初始化包含追踪注册表操作所需的所有字段的结构。它由DriverEntry调用,首个操作为获取如下函数的指针:

·CmCallbackGetKeyObjectIDEx
·CmCallbackReleaseKeyObjectIDEx

拥有这两个函数的指针后,它将分配一个标签为MPrD,大小为0x500的池并在其中继续初始化结构MP_REG_DATA。指向此结构的指针包含在全局变量MpRegData中

  1. typedef struct _MP_REG_DATA
  2. {
  3.    USHORT Magic;     // 设置为 0xDA09
  4.    USHORT Size;      // 大小为 0x500
  5.    ULONG_PTR RegDataPushLock;
  6.    PMP_REG_USER_DATA MonitoredKeys;
  7.    ULONG MonitoredRegKeyRules;
  8.    NTSTATUS (__fastcall *pCmCallbackGetKeyObjectIDEx)(PLARGE_INTEGER Cookie, PVOID Object, PULONG_PTR ObjectID, PCUNICODE_STRING *ObjectName, ULONG Flags);
  9.    void (__fastcall *pCmCallbackReleaseKeyObjectIDEx)(PCUNICODE_STRING ObjectName);
  10.    LARGE_INTEGER CmCallbackGetKeyCookie;
  11.    INT64 field_38;
  12.    PAGED_LOOKASIDE_LIST NotificationsLookaside;
  13.    FAST_MUTEX CmUnregisterFastMutex;
  14.    LARGE_INTEGER CmRegisterCallbackCookie;
  15.    INT OpenConnectionPortsCount;
  16.    UNICODE_STRING LoadAppInitString;
  17.    LIST_ENTRY ServiceKeyHardeningList;
  18.    FAST_MUTEX CallCtxFastMutex;
  19.    LIST_ENTRY CallCtxList;
  20.    INT64 Unk;
  21.    INT64 Unk1;
  22.    PAGED_LOOKASIDE_LIST CreateKeyCtxLookaside;
  23.    PAGED_LOOKASIDE_LIST SetValueKeyCtxLookaside;
  24.    PAGED_LOOKASIDE_LIST DeleteValueKeyCtxLookaside;
  25.    PAGED_LOOKASIDE_LIST DeleteKeyCtxLookaside;
  26.    PAGED_LOOKASIDE_LIST RegDataEntry;
  27.    PAGED_LOOKASIDE_LIST KeyNamesLookaside;
  28.    PAGED_LOOKASIDE_LIST RenameKeyCtxLookaside;
  29. } MP_REG_DATA, *PMP_REG_DATA;
复制代码


初始化事件和查找列表后,若我们运行的是MpFilter或Win8.1及更早之前的系统版本,函数将注册一个用于增强加固键值的注册表回调。我们很快就会看到该回调MpRegHardeningCallback的行为。

最终将在MpRegCreateHardeningList中创建增强加固键值列表。函数会获取CurrentControlSet的句柄,接着遍历一个硬编码服务键值数组(这实际上是一个拥有键名和一个决定关联到是MpFilter还是WdFilter的标志)。对于匹配到定义的键值,他们的完整键值名将被链入MpRegData->ServiceKeyHardeningList。在一台WdFilter的机器上将匹配到如下的名称。



MpRegHardeningCallback
在先前的一节中,初始化历程为增强加固键值注册了注册表回调。同所有注册表键值一样,此函数的原型是EX_CALLBACK_FUNCTION,在这种情况下,不会向此例程传入任何上下文。

此函数只会关注Argument1为RegNtQueryValueKey的情况,这意味着Argument2包含REG_QUERY_VALUE_KEY_INFORMATION结构。若Argument1符合预期值,将会调用MpRegPreQueryValueKey。这个函数很简单,伪代码如下:



我仍然没有头绪为何拥有MpServiceSid的进程会被拒绝访问。回调只会在运行NT内核版本低于6.3的Windows上注册,或许于此有什么关系吧……如果有人能指出这样做的原因,我会很感激的!

MpRegCallback

最终我们进入了处理注册表操作的主例程,此例程在MpRegisterRegCallback中注册,这是最后在DriverEntry中调用的函数。同先前的注册表例程一样,MpRegCallback函数的原型也是EX_CALLBACK_FUNCTION,再一次的,将不会为此函数注册任何上下文。
此例程使用CmRegisterCallback而非CmRegisterCallbackEx注册,可能是未修改更新的旧代码,因为此函数自Windows Vista起已经过时

进入实际的注册表回调例程,此函数由检查Argument1(其中保存了枚举REG_NOTIFY_CLASS和识别注册表操作类型的值)是否包含被监控的前置操作值之一开始,随后从Argument2包含的结构中获取数据。(如MSDN所述,Argument2包含一个指向一个结构的指针,该结构包含每种注册表操作类型特异的信息。)为决定获取的数据,随后的位掩码使用0x220000000017。为节约时间,我在下面直接列出了匹配此位掩码的REG_NOTIFY_CLASS值:
·RegNtDeleteKey = 0
·RegNtSetValueKey = 1
·RegNtDeleteValueKey = 2
·RegNtRenameKey = 4
·RegNtPostRestoreKey = 2Ah
·RegNtPreReplaceKey = 2Dh

对绝大部分值而言,函数会获取结构中的注册表键对象Object。唯一不同的是RegNtRenameKey,NewName也会保存在本地。

即使未用位掩码检查RegNtPreCreateKeyEx,但实际上函数已经在首次检查Argument1是否包含值RegNtPreCreateKeyEx (0x1A)时完成。且此时REG_CREATE_KEY_INFORMATION_V1已经复制到本地

一旦函数拥有了必要值,它将继续检查触发了回调的进程是否符合如下:

·System
·MsMpEng
·FriendlyProcess (ProcessCtx->ProcessFlags & 0x20)
·MpServicesSidProcess (ProcessCtx->ProcessFlags & 0x10)

若进程符合先前的任意检查,将不会调用MpRegHardeningIsMatch。我们稍后会谈到此函数,不过我们需要先检查一些在检查不匹配情况下的预处理。可使用如下三条路径:

·路径一:键值创建,主要检查要创建的键值是否已经存在,调用MpRegpCheckExistingKey。此函数尝试用RootObject获得对象的句柄接着尝试打开此键值,并返回CompleteName的一个副本。
·路径二:适用于RegKeyNewName。此函数首先使用MpRegpGetKeyName获取ObjectName,(MpRegpGetKeyName这个函数会在Win8和更高版本上调用CmCallbackGetKeyObjectIDEx,其余条件会从MpRegData->KeyNamesLookaside弹出一个条目并使用ObQueryNameString获取对象名称。)并使用函数名称和NewName创建一个将在后续调用MpRegHardeningIsMatch的新Unicode字符串。

·路径三:适用于其他所有之前看到的注册表操作。很简单,调用MpRegpGetKeyName获取对象名称以便于后续调用MpRegHardeningIsMatch。

令人好奇的是,他们使用了ObQueryNameString而非CmCallbackGetKeyObjectID,后者自Windows Vista起均可用。这让我怀疑这是否是来自Windows XP时代的老代码。或许他们在Windows 8上进行了更新并使用了CmCallbackGetKeyObjectIDEx……唔……只是随便想想

执行对操作的实际处理之前的最后一步是检查传入的键值是否匹配任意的加固增强键值匹配。我们已经提到了此过程在MpRegHardeningIsMatch中完成,此函数接收一个Unicode字符串作为唯一参数并遍历列表MpRegData->ServiceKeyHardeningList以检查作为参数传入的注册表键值是否匹配列表中的任意一项(RtlPrefixUnicodeString)若匹配则返回TRUE。若此函数返回TRUE,则无论进行的操作类型如何,注册表回调都会返回STATUS_ACCESS_DENIED。

最终我们进入了对不同注册表操作的实际处理流程。此流程的第一步是检查两个位掩码。第一个为0x66000C0B8017,包含所有回调要检查的注册表操作;第二个为0x4400080B8000,包含所有的已检查操作,并将用于了解是否可以获取一个CallCtx(我们将会在后面进一步了解它)

和之前一样,我会为你减少手动检查这些位掩码的时间(我将省略之前检查的值,但它们已包含在包含所有操作的位掩码中):

·RegNtPostDeleteKey = 0Fh
·RegNtPostSetValueKey = 10h
·RegNtPostDeleteValueKey = 11h
·RegNtPostRenameKey = 13h
·RegNtPostCreateKeyEx = 1Bh
·RegNtPostRestoreKey = 2Ah
·RegNtPostReplaceKey = 2Eh

代码在检查后将进入一个switch语句,它将结束对特定处理注册表操作的子函数调用。我将把这些子函数划分为不同的部分,其中包含对每种操作的不同前置操作,接着在另一部分讨论所有的后置操作(它们非常相似)

声明:各个子函数区别不太,但我们至少要看看函数的原型结构和每个包含的结构/枚举。同时,可以从函数的名称推断出它们对应的操作类型

MpRegPreCreateKeyEx


函数将始于对CreateKeyInfo和CreateKeyCtx的正确性检查,若一切验证通过它将检查MonitoredKeysRules中的CreateKeyOperation(0x1)是否被激活;这样之后若未提供KeyName,则调用MpRegpCheckExistingKey以获取KeyName并检查KeyExisy(键值是否存在)。若找到键值函数将返回,若未找到则调用MpRegMatchData

MpRegMatchData可能是有关注册表操作的最重要的函数了,我们会在文章的后半部分对其进行讨论。现在我们先设想此函数依据一个键值列表检查键名KeyName,若找到则返回适用该键名的规则

如果MpRegMatchData没有找到匹配的键值则函数将返回,若找到匹配的键值代码则检查该键值的CreateDenied(0x10000)是否被激活,若已激活它将继续检查试图创建键值的进程是否匹配如下需求:

·ProcessCtx->ProcessRules有AllowAllRegistryOperations (0x400) 被设置
·进程是排除进程(ExcludedProcess)(ProcessCtx->ProcessFlags & 0x1)
·进程是已知的白名单进程(FriendlyProcess) (ProcessCtx->ProcessFlags & 0x20)
·进程是MpServiceSidProcess (ProcessCtx->ProcessFlags & 0x10)

若匹配任何的上述需求,将不会拒绝访问。但若不匹配,则参数AccessDenied将被设置为TRUE并会发送MpRegpSendNotification通知。

另一种情况,键值规则未设置CreateDenied,但设置了CreateKeyOperation。此时将创建CallCtx。此CallCtx组合体因对应的操作类型不同而异,对于密钥创建,如下:



最终这个创建的CallCtx将被复制入外参CreateKeyCtx,此后就可以通过调用MpRegpInsertCallContext插入MpRegData->CallCtxList。



这是第一个函数所以会有更多细节,后续会更加简略。

MpRegPreRestoreKey



同样始于检查正确性和MonitoredKeysRules中的RestoreKeyOperation (0x4000)是否被激活。若无KeyName则通过调用MpRegpGetKeyName获取。拥有KeyName后将调用MpRegMatchData,和前文一样,有三类情况。第一种,若无匹配,函数返回;第二种,找到匹配的数据且设置了RestoreDenied (0x200000),函数会继续检查进程需求,若不满足需求条件,设置AccessDenied为TRUE并发送通知;第三种也是最后一种,匹配到数据但只设置了RestoreKeyOperation,创建如下CallCtx。此操作使用的CallCtx为用于键值重命名的CallCtx,通过MpRegpAllocRenameKeyContext分配



再次此上下文保存在将在稍后添加到CallCtx表中的参数RestoreKeyCtx。

MpRegPreSetValueKey



此函数也有同样的行为但在调用MpRegMatchData时也会提供将被添加的ValueName。所以KeyName和ValueName必须匹配。再一次若找到匹配将检查以下2值:

·SetValueKeyOperation (0x100)
·SetValueDenied (0x80000)

此操作还有一个小细节,匹配的规则或许拥有值SetValueRetrieveKeyValueInfo (0x400)设置。这种情况将调用MpRegpQueryValueKeyByPointer以获取值的键值部分信息(key partial information)。

最终,若访问未被拒绝但设置了SetValueKeyOperation将创建如下的CallCtx。



示例如下:



MpRegPreDeleteValueKey



此函数的行为和MpRegPreSetValueKey类似。同样传递ValueName到MpRegMatchData并在如下匹配后检查

·DeleteValueKeyOperation (0x800)
·DeleteDenied (0x40000)

再一次我们有了一个必须获取的键值部分信息,DeleteValueRetrieveKeyValueInfo (0x2000)。最终创建一个类似下面的CallCtx:



MpRegPreRenameKey



此函数的行为略有不同,因为有两个要匹配的KeyName。一个是重命名前的KeyName,一个是重命名后的KeyName。因此MpRegMatchData会被调用两次,若两个键名都匹配,则规则会被OR运算。其余规则方式同前文一样,检查值如下:
·RenameKeyOperation (0x4)
·RenameDenied (0x20000)

若创建了CallCtx,上下文类型则为我们之前在MpRegPreRestoreKey中见到的



MpRegPreDeleteKey



此前置操作的行为和前文创建键值的前置操作相同,检查如下值:

·DeleteKeyOperation (0x10)
·DeleteDenied (0x40000)

若创建CallCtx,上下文类型和创建键值操作相同,唯一的不同在于CallCtx->Magic在此种情况设置为了0xDA11

一个我忘记介绍了的细节是RegKey的规则内可以设置一个特殊的被我称为TamperProtectionActive (0x400000)的值,若设置了此值,则不论任何进程尝试对目标键值执行操作都会被拒绝

后置操作(Post-operations)

首先的首先,如前所示,有一个用于后置操作的位掩码且当RegNotifyClass匹配了任何这些值则会调用MpRegpFetchCallContext。正如其名,获取CallCtx。伪代码如下:



如你所见,基本上它就是遍历CallCtx列表条目并尝试找到一个匹配了同样在前置操作中上下文创建时设立的CurrentThread的上下文。

这么做是安全的,如MSDN所写的那样:“注册表回调在 IRQL = PASSIVE_LEVEL 时在进行注册表操作的线程上下文中执行。”我们基本可以认为前置操作和后置操作在同一线程中执行。下面的图片展示了在MpRegPreCreateKeyEx节中展示的创建前置操作的后置操作,两个操作的线程对象是一样的(

于是当我们有了预计的CallCtx后,代码再次进入switch语句以进入对应的后置操作函数。所有的后置操作函数功能类似,先进行正确性检查CallCtx是否作为参数传入,接着检查CallCtx->Magic是否匹配预计的操作类型之一。若正确性检查正确,函数将继续创建RegNotification并通过MpRegpSendNotification发送。我们将在文章的后面看到两个通知结构。

MpRegMatchData


最终我们进入了实际检查特定类型的操作是否被允许的函数。此函数首先从MpRegData->MonitoredKeys获取保存了被监控键值的数据的指针。

如果你好奇的话,这些数据来自用户空间——在函数MpRegUpdateData中解析——更具体一点,来自MpRtp.dll,Windows Defender的实时保护模块。本研究关注的是内核,因而并未深究数据是如何获得的,不过确实是个研究的好项目(即使是C++,或许在将来……

回到MpRegData->MonitoredKeys,此组件包含指向一个MP_REG_USER_DATA的指针,如下:



拥有此数据后函数就可以开始搜索键名KeyName是否包含在被监控的键值名中。为理解这是如何完成的,我们首先需要知道数据是如何排布的。我们先理解我命名为MP_KEY_ENTRY的结构的定义



如你可见,结构保存了一个指向SubKey、NextKey(均为MP_KEY_ENTRY类型)和一个KeyName的指针。因此,可能你已经猜到了数据是树形排列的。遍历此数据的伪代码如下:


所以你可以看到这个算法并不难。会解析整个KeyName,首个名称会同实际条目进行比较。若匹配则转到SubKey并保存NextKey到未访问的堆栈。若不匹配则算法会转到NextKey(如果存在的话),如果没有,将从堆栈中弹出一个条目并重复上述过程。

所以现在进入最后一步,你或许已经注意到了算法中央的一个奇怪函数,MpRegpMatchEntry。此函数填充MP_REG_MATCH_INFO结构,而这个结构之后会返回到预操作函数并包含适用于被操作键值的规则。

此函数只会在键路径检查完全匹配的时候执行。此时会有两种可能路径。第一条是适用于除SetValueKeyOperation和DeleteValueKeyOperation以外的所有操作的路径。此时会获取MP_KEY_ENTRY->ClientList,此组件包含如下结构:



第二条路经是适用于SetValueKey和DeleteValueKey的情况。此时会获取MP_KEY_ENTRY->ValuesList,组件包含如下结构:



为让函数获取MP_KEY_VALUE->ClientValue,操作想设置或删除的ValueName必须匹配结构中的其一

函数的主要工作方式是对比MP_CLIENT_VALUE->KeyRules和调用MpRegMatchData时作为参数传入的标志。

此标志内置于所有的预操作函数里,保存了所有我们在每个预操作中看到的值。例如,在创建键值时此标志为CreateDenied | CreateKeyOperation (0x100001)

若标志与KeyRules的对比返回为真(TRUE),则会分配一个MP_REG_MATCH_INFO结构,KeyRules和ValueHash将被复制。



ValueHash不会在WdFilter中使用,而是发送到MsMpEng并在那里使用,我猜测。同时,我不是很确定hash反映了什么,因为它们来自MpRtp而我不知道什么数据被计算哈希了(元数据?译者猜测)

最后,由于这个痛苦的调试过程,我决定编写如下脚本,它有两个选项:

·创建一个MP_KEY_ENTRY、MP_KEY_VALUE或MP_CLIENT_VALUE的实例:

  1. > dx Debugger.Utility.Analysis.WdFilterExtension.CreateInstance(typeName, addrObj)
复制代码


·列出所有被监控的键值

  1. > !mpRegData
  2. > dx Debugger.Utility.Analysis.WdFilterExtension.RegUserData()
复制代码


该脚本并非完美,我只是让调试对我而言更容易一些。它有很多缺陷,比如不能显示整个密钥路径以到达特定的MP_KEY_ENTRY。但如果我搭配LINQ语法搜索特定的KeyName和ValueName。比如我们可以运行下列查询搜索包含KeyName为MsMpEng.exe的条目,并检查应用到此键值上的规则
  1. > dx -r1 @$mpRegUserData().MpRegUserData.MonitoredKeysTree.Select(p => new {
  2.     Name = p.KeyName,
  3.     Client = p.ClientList.Select(n => new { Hash = n.ValueHash, KeyRules = n.KeyRules })
  4.   }).Where(p => p.Name != 0x0 && p.Name.ToDisplayString("su").ToLower().Contains("msmpeng.exe"))
复制代码

下图展示了此查询和对在Windows Defender键值中的受监控值的查询。



MpRegpSendNotification
如本文所述,注册表回调有两种可能的方式发送通知。其一,操作被拒绝;其二,前置操作创建了CallCtx,则后置操作将检索上下文并发送通知。负责准备通知的函数是MpRegpSendNotification,此函数很简单,但由于帖子很长在此不过多叙述。此函数主要接收一个指向被我命名为RegNotification结构的指针,如下:



你或许注意到了,此结构包含了每种可能操作的区域,这意味着它们为每种注册表操作使用了同样的结构,并且只填充适用于每种操作的字段。

拥有这个信息后MpRegpSendNotification将计算为通知分配的所必需缓冲区大小,并调用MpAsyncCreateNotification分配。此AsyncMessageData缓冲区在TypeOfMessage中包含如下的数据类型:



有一个帮助函数,负责将数据从RegNotification复制到RegOperationMessage,如果你在意的话,这里提供函数的名称MpRegpCopyVariableNotificationData


最终,通知将同步发送(若ProcessCtx->ProcessRules拥有被激活的NotifyRegistryOperationSync (0x200000))或异步发送(其他情况)

对同步消息而言,操作类型将被设置为RegistryEventSync (0x2)



总结

而这就是WdFilter分析的全部了!我再次为过长的文章抱歉,但我认为这是整篇文章中最酷的部分所以我想深入写。下一篇中我们将看看与minifilter功能无关的内容,例如MsMpEng向WdFilter发送的消息用于触发不同的操作如添加进程到排除项或创建引导扇区扫描

(然鹅……作者似乎把这部分给鸽了)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?快速注册

x

评分

参与人数 2技术 +1 经验 +100 魅力 +2 人气 +3 收起 理由
白露为霜 + 1 + 100 + 2 累计奖励
驭龙 + 3 版区有你更精彩: )

查看全部评分

您需要登录后才可以回帖 登录 | 快速注册

本版积分规则

手机版|杀毒软件|软件论坛| 卡饭论坛

Copyright © KaFan  KaFan.cn All Rights Reserved.

Powered by Discuz! X3.4( 沪ICP备2020031077号-2 ) GMT+8, 2024-12-22 00:54 , Processed in 0.147485 second(s), 19 queries .

卡饭网所发布的一切软件、样本、工具、文章等仅限用于学习和研究,不得将上述内容用于商业或者其他非法用途,否则产生的一切后果自负,本站信息来自网络,版权争议问题与本站无关,您必须在下载后的24小时之内从您的电脑中彻底删除上述信息,如有问题请通过邮件与我们联系。

快速回复 客服 返回顶部 返回列表