模块篡改保护是一种缓解措施,可防止对进程主映像的早期修改,例如 IAT 挂钩或进程空心化。它一共使用了三个 API:NtQueryVirtualMemory、NtQueryInformationProcess 和 NtMapViewOfSection。如果启用,加载程序将在调用入口点之前检查主图像标头和 IAT 页面中的更改。它通过使用信息类 MemoryWorkingSetExInformation 调用 NtQueryVirtualMemory 来做到这一点。返回的结构包含有关页面共享状态的信息,以及是否从其原始视图修改。如果标头或 IAT 已从其原始映射修改。例如,如果主图像已被取消映射,并且已在其位置映射了另一个图像,则加载器将使用类 ProcessImageSection 调用 NtQueryInformationProcess 以获取主图像部分,然后将使用 NtMapViewOfSection 重新映射它。这样,新部分将被使用,篡改的图像副本将被忽略。
此缓解从 RS3 开始可用,并且可以使用 PROCESS_CREATION_MITIGATION_POLICY2_MODULE_TAMPERING_PROTECTION_MASK 在进程创建时启用。
如果微软从未宣布或记录某些缓解措施,那人们如何才能发现这些缓解措施?因此,一个值得关注的好地方是 EPROCESS 结构中的各种 MitigationFlags 字段。目前存在三个 MitigationFlags 字段(MitigationFlags、MitigationFlags2、MitigationsFlags3),每个字段包含 32 位。在前两个中,整个32位已经被使用,所以最近添加了MitigationFlags3,目前包含三个缓解措施,我相信很快会添加更多。这些标志代表进程中启用的缓解措施。例如,我们可以使用 WinDbg 为当前进程打印 EPROCESS.MitigationFlags:
最后,在位 28 和 29 中,我们可以看到值 EnableModuleTamperingProtection 和 EnableModuleTamperingProtectionNoInherit。不幸的是,搜索这些名称并没有得到任何好的结果。有几个网站只显示结构而没有解释,一个模糊的堆栈溢出答案简要提到了 EnableModuleTamperingProtectionNoInherit 而没有添加细节,还有这条推文:
不出所料,最详细的解释是 Alex Ionescu 2017 年发布的一条推文。这虽并不是完整的文档,但它是一个开始。如果你已经了解并理解构成此缓解措施的概念,那么这一系列的推文可能会非常清楚地解释有关该特性的所有内容。
开始搜索进程缓解实现的第一个地方通常是内核:ntoskrnl.exe。然而,这是一个巨大的二进制文件,不容易搜索。似乎没有与此缓解措施完全相关的函数名称,所以没有明显的地方可以开始。
相反,你可以尝试不同的方法并尝试找到对 EPROCESS 的 MitigationFlags 字段的引用,并可以访问这两个标志中的一个。但除非你可以访问 Windows 源代码,否则没有简单的方法可以做到这一点。但是,你可以做的是利用 EPROCESS 是一个大型结构并且 MitigationFlags 存在于它的末尾,偏移量 0x9D0 的事实。一种非常粗暴但有效的方法是使用 IDA 搜索功能并搜索所有对 9D0h 的引用:
这会很慢,因为它是一个很大的二进制文件,并且一些结果与 EPROCESS 结构无关,因此你必须手动搜索结果。此外,仅查找对该字段的引用是不够的,MitigationFlags 包含 32 位,其中只有两个与当前上下文相关。所以,你必须搜索所有的结果,找出以下情况:
0x9D0被用作EPROCESS结构的偏移量——因为无法保证知道每种情况使用的结构类型,尽管对于较大的偏移量,只有少数选项可以是相关的,它主要可以通过函数名称和上下文来猜测。
比较或设置MitigationFlags字段为0x10000000 (EnableModuleTamperingProtection)或0x20000000 (EnableModuleTamperingProtectionNoInherit)。或者通过诸如bt或bts之类的汇编指令,按位数测试或设置位28或位29。
运行搜索后,结果看起来像这样:
你现在可以浏览结果并了解内核使用了哪些缓解标志以及在哪些情况下使用。然后我会告诉你,这个努力完全没有用,因为 EnableModuleTamperingProtection 在内核中的一个地方被引用:PspApplyMitigationOptions,当创建一个新进程时调用:
因此,内核会跟踪是否启用了此缓解措施,但从不对其进行测试。这意味着缓解措施本身在其他地方实现。这种搜索可能对这种特定的缓解措施毫无用处,但它是找出缓解措施实现位置的几种方法之一,并且对其他流程缓解措施很有用,所以我想提一下它。
现在让我们回到模块篡改保护,有时会实现进程缓解的第二个位置是 ntdll.dll,它是每个进程中要加载的第一个用户模式映像。此 DLL 包含所有进程所需的加载程序、系统调用存根和许多其他基本组件。在这里实现这种缓解是有意义的,因为顾名思义它与模块加载有关,这通过 ntdll.dll 中的加载器发生。此外,这是一个包含 Alex 在他的推文中提到的功能的模块。
即使我们没有这条推文,只要打开 ntdll 并搜索“tampering”,我们就能很快找到一个结果:函数 LdrpCheckPagesForTampering。寻找这个函数的调用者,我们看到它是从LdrpGetImportDescriptorForSnap调用的:
在截图的第一行,我们可以看到两个检查:第一个验证当前正在处理的条目是主图像,因此该模块被加载到主图像模块中。第二个检查是 LdrSystemSllInitBlock.MitigationOptionsMap.Map中的两个位。我们可以看到这里检查的确切字段只是因为我对 LdrSystemDllInitBlock 应用了正确的类型,如果你在没有应用正确类型的情况下查看这个函数,你会看到一些随机的、未命名的内存地址被引用。LdrSystemDllInitBlock 是一个数据结构,包含加载程序所需的所有全局信息,例如进程缓解选项。它没有记录,但具有符号中可用的 PS_SYSTEM_DLL_INIT_BLOCK 类型,因此我们可以在此处使用它。请注意,虽然此结构在 NTDLL 符号中不可用,而是你可以在 ole32.dll 和 combase.dll 的符号中找到它。MitigationOptionsMap 字段只是三个 ULONG64 的数组,其中包含标记为此过程设置的缓解选项的位。我们可以在 WinBase.h 中找到所有缓解标志的值。以下是模块篡改保护的值:
这些值与 Map 顶部的 DWORD 相关,因此模块篡改保护位实际上位于 Map的第 44 位,在 Hex Rays 屏幕截图中以及在 PspApplyMitigationOptions 中检查的是相同位。
现在我们知道该缓解措施在哪里应用了检查,因此我们可以开始查看实现并了解该缓解措施的作用。
再次查看 LdrpGetImportDescriptorForSnap:在我们已经看到的两次检查之后,该函数获取主图像的 NT 标头并调用 LdrpCheckPagesForTampering 两次。第一次发送的地址是 imageNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT],图像的导入表,大小为 8 个字节。第二次使用 NT 标头本身的地址和大小调用该函数。如果这些页面中的一个被认为被篡改了,LdrpMapCleanModuleView将被调用(根据名称判断)映射主图像模块的一个干净的视图。
让我们看看 LdrpCheckPagesForTampering 内部,看看 NTDLL 如何判断一个页面是否被篡改:
首先,此函数计算请求的字节范围内的页数(在本文的两个示例中,该数字都是 1)。然后它分配内存并使用 MemoryInformationClass == 4 (MemoryWorkingSetExInformation) 调用 ZwQueryVirtualMemory。这个系统调用和信息类是安全人员可能不太经常看到的,工作集是一种基于物理内存页面当前状态来管理和优先级排序的方法,因此大多数安全人员通常不感兴趣。然而,工作集确实包含一些我们感兴趣的属性。具体来说,是“共享”标志。
我不会在这里详细介绍映射和共享内存,因为它们在很多其他地方都有解释。但简而言之,系统尽量不复制内存,因为这意味着物理内存将很快被复制的页面填满,主要是那些属于图像和 DLL 的页面, 像 ntdll.dll 或 kernel32.dll 的系统 DLL被映射到大多数系统中的进程,因此在物理内存中为每个进程单独创建一个副本只是浪费。因此,这些图像页面在所有进程之间共享。也就是说,除非以任何方式修改图像。图像页面使用一种称为“写时复制(copy-on-write)”的特殊保护,它允许页面可写,但如果页面被写入,则会在物理内存中创建一个新副本。这意味着对 DLL 的本地映射所做的任何更改(例如,用户模式挂钩的写入或任何数据更改)都只会影响当前进程中的 DLL。
这些设置保存为可以通过 NtQueryVirtualMemory 查询的标志,这里使用的信息类:MemoryWorkingSetExInformation。它将在 MEMORY_WORKING_SET_EX_INFORMATION 结构中返回有关查询页面的数据:
这个结构为你提供了被查询的虚拟地址,以及包含页面状态信息的位,例如:它的有效性、保护以及它的共享状态。有几个不同的位与页面的共享状态相关:
共享——页面是可共享的吗?这并不一定意味着该页当前与任何其他进程共享,但是,例如,除非进程特别请求,否则私有内存不会被共享。
ShareCount——此字段告诉你此页面存在多少映射。对于当前未与任何其他进程共享的页面,这将是 1。对于与其他进程共享的页面,这通常会更高。
SharedOriginal ——该标志会告诉你该页面是否存在映射。因此,如果一个页面被修改,导致在物理内存中创建一个新副本,这将被设置为零,因为这不是页面的原始映射。
此 SharedOriginal 位是由 LdrpCheckPagesForTampering 检查的,以判断此页面是原始副本还是由于更改而创建的新副本。如果这不是原始副本,这意味着该页面以某种方式被篡改,因此该函数将返回 TRUE。LdrpCheckPagesForTampering 对正在查询的每个页面运行此检查,如果其中任何一个被篡改,则返回 TRUE。
如果函数对任何检查范围返回 TRUE,则调用 LdrpMapCleanModuleView:
这个函数简短而简单:它使用 InformationClass == 89 (ProcessImageSection) 调用 NtQueryInformationProcess 来获取主图像的部分句柄,然后使用 NtMapViewOfSection 重新映射它并关闭句柄。它将新部分的地址写入 DataTableEntry->SwitchBackContect,以代替原来的篡改映射。
这是因为这两个地方经常会成为试图虚化进程的攻击者的目标。如果主图像未映射并被恶意图像替换,则 NT 标头将不同并被视为已篡改。进程空心化(Process hollowing)还可以篡改导入表,以指向与进程预期不同的函数。所以,这主要是一个反空心化的功能,目标是发现主图像中的篡改企图,并用一个没有被篡改的新图像副本替换它。
不幸的是,此功能相对有限。你可以启用或禁用它,仅此而已。实现缓解的函数是内部调用,不能在外部调用。因此,例如,除非你自己编写代码(并手动映射模块,因为这些部分的句柄不方便地存储在任何地方),否则不可能将缓解扩展到其他模块。此外,此缓解不包含日志记录或 ETW 事件。当缓解通知在主图像中被篡改时,它会静默映射并使用新副本,并且不会留下任何痕迹供安全产品或团队查找。唯一的提示是 NtMapViewOfSection 将再次为主图像调用并生成 ETW 事件和内核回调。但这很可能会被忽视,因为它并不一定意味着发生了不好的事情,并且可能不会导致任何警报或对可能是真正的攻击的重大调查。
从好的方面来说,这种缓解非常简单和有用,如果你想实现它,则很容易模仿,例如检测放置在你的进程上的钩子并映射一个新的、未挂钩的页面副本以供使用。你可以这样做,而不是使用直接系统调用!
在 WinDbg 中运行查询,我没有发现任何启用模块篡改保护的进程的结果。经过一番探索,我设法找到了一个启用此功能的进程:SystemSettingsAdminFlows.exe。此过程在你打开 Windows 设置菜单中的应用程序->可选功能时执行。我不知道为什么这个特定的进程会使用这种缓解措施,或者为什么它是唯一一个这样做的,但这是迄今为止我设法找到的唯一一个启用模块篡改保护的过程。
参考及来源:https://windows-internals.com/understanding-a-new-mitigation-module-tampering-protection/