动态分析实战:深入解析Windows Defender导出地址过滤机制
分析PayloadRestrictions.dll的导出地址过滤功能
这篇文章与我往常的文章有些不同。它不会涵盖任何新的安全功能或技术,也不会分享任何新颖的安全研究成果。相反,它将通过Windows Defender Exploit Guard(前身为EMET)中的一个真实示例,指导您分析未知的防护机制的过程。因为这里的目的是展示一个逐步的、真实的研究过程,所以文章会有些杂乱,并遵循更有机和混乱的思路。
对Windows Defender Exploit Guard的简要解释:前身为EMET,这是一个按需注入的DLL,实现了多种安全防护措施,如导出地址过滤、导入地址过滤、堆栈完整性验证等。这些功能默认都是禁用的,需要在Windows安全设置中手动启用,可以针对特定进程或整个系统。自被微软收购以来,这些防护措施都在PayloadRestrictions.dll中实现,该文件位于C:\Windows\System32。
本文将跟踪其中一种名为导出地址过滤(或EAF)的防护措施。本教程将演示分析此防护措施的分步指南,使用WinDbg进行动态分析和IDA及Hex Rays进行静态分析。我将尝试突出在分析防护措施时应关注的重点,并展示即使只有部分信息,我们也能得出有用的结论并了解此功能。
首先,我们在Windows安全设置中为calc.exe启用EAF:
我们还不知道关于此防护措施的任何信息,除了安全设置中的那一行描述,因此我们首先在调试器下运行calc.exe,看看会发生什么。立即我们可以看到PayloadRestrictions.dll被加载到进程中:
几乎立刻我们就遇到了一个保护页违规:
这个神秘地址中有什么?为什么访问它会抛出保护页违规?
为了开始找到第一个问题的答案,我们可以运行!address以获取有关导致异常的地址的更多详细信息: !address 00007ffe`3da6416c
用法: 映像
基地址: 00007ffe3d8b9000
结束地址: 00007ffe
3da7a000
区域大小: 00000000001c1000(1.754 MB)
状态: 00001000 MEM_COMMIT
保护: 00000002 PAGE_READONLY
类型: 01000000 MEM_IMAGE
分配基址: 00007ffe
3d730000
分配保护: 00000080 PAGE_EXECUTE_WRITECOPY
映像路径: C:\WINDOWS\System32\kernelbase.dll
模块名称: kernelbase
加载的映像名称:
映射的映像名称:
更多信息: lmv m kernelbase
更多信息: !lmi kernelbase
更多信息: ln 0x7ffe3da6416c
更多信息: !dh 0x7ffe3d730000
内容来源:1(目标),长度:15e94
现在我们知道了这个地址位于KernelBase.dll内的一个只读页面中。但我们没有任何信息可以帮助我们理解这个页面是什么以及为什么它被保护。让我们遵循命令输出的建议,运行!dh以转储KernelBase.dll的头部以获取更多信息(这里显示部分输出,因为完整输出非常长): !dh 0x7ffe3d730000
文件类型:DLL 文件头值 8664 机器(X64) 7 节数 FE317FB0 时间戳 Sat Feb 21 05:53:36 2105
0 符号表文件指针 0 符号数 F0 可选头大小 2022 特征 可执行 应用程序可以处理>2gb地址 DLL
可选头值 20B 魔数 14.30 链接器版本 188000 代码大小 211000 初始化数据大小 0 未初始化数据大小 89FE0 入口点地址 1000 代码基址 —– 新 —– 00007ffe3d730000 映像基址 1000 节对齐 1000 文件对齐 3 子系统(Windows CUI) 10.00 操作系统版本 10.00 映像版本 10.00 子系统版本 39A000 映像大小 1000 头大小 3A8E61 校验和 0000000000040000 堆栈保留大小 0000000000001000 堆栈提交大小 0000000000100000 堆堆保留大小 0000000000001000 堆堆提交大小 4160 DLL特征 支持高熵VA 动态基址 NX兼容 保护 334150 [ F884] 导出目录的地址[大小] 3439D4 [ 50] 导入目录的地址[大小] 369000 [ 548] 资源目录的地址[大小] 34F000 [ 18828] 异常目录的地址[大小] 397000 [ 92D0] 安全目录的地址[大小] 36A000 [ 2F568] 基址重定位目录的地址[大小] 29B8C4 [ 70] 调试目录的地址[大小] 0 [ 0] 描述目录的地址[大小] 0 [ 0] 特殊目录的地址[大小] 255C20 [ 28] 线程存储目录的地址[大小] 1FB6D0 [ 140] 加载配置目录的地址[大小] 0 [ 0] 绑定导入目录的地址[大小] 2569D8 [ 16E0] 导入地址表目录的地址[大小] 331280 [ 620] 延迟导入目录的地址[大小] 0 [ 0] COR20头目录的地址[大小] 0 [ 0] 保留目录的地址[大小]
我们的故障地址是0x7ffe3da6416c,它在KernelBase.dll内的偏移量0x33416c处。在!dh的输出中寻找最接近的匹配,我们可以找到偏移量0x334150处的导出目录: 334150 [ F884] 导出目录的地址[大小]
因此,故障代码试图访问KernelBase导出表中的一个条目。在正常情况下这不应该发生——如果你调试另一个进程(一个没有启用EAF的进程),在访问导出表时你不会看到任何异常被抛出。所以我们可以猜测PayloadRestrictions.dll正在导致这种情况,我们很快就会看到它是如何以及为什么这样做的。
关于保护页违规需要注意的一点是,引用自这个MSDN页面: 如果程序试图访问保护页内的地址,系统会引发一个STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。系统还会清除PAGE_GUARD修饰符,移除内存页的保护页状态。系统不会因STATUS_GUARD_PAGE_VIOLATION异常而停止下一次访问内存页的尝试。
因此,这个保护页违规应该只发生一次,然后被移除,再也不会发生。然而,如果我们继续执行calc.exe,我们很快会在同一个地址看到另一个页保护违规:
这意味着保护页以某种方式回来了,并再次设置在KernelBase导出表上。
在这种情况下,最好的猜测可能是有人注册了一个异常处理程序,每次保护页违规发生时都会被调用,并立即再次设置PAGE_GUARD标志,以便下次任何东西访问导出表时发生相同的异常。不幸的是,在WinDbg中没有好的方法来查看已注册的异常处理程序(除非在gflags中设置“启用异常日志记录”,这启用了!exrlog扩展,但我现在不会这样做)。然而,我们知道注册可疑异常处理程序的DLL很可能是PayloadRestrictions.dll,所以我们将在IDA中打开它并查看。
当寻找对RtlAddVectoredExceptionHandler的调用(用于注册异常处理程序的函数)时,我们只看到两个结果:
两者都注册了相同的异常处理程序——MitLibExceptionHandler:
(顺便说一句——我不经常选择使用IDA反汇编器而不是Hex Rays反编译器,但PayloadRestrictions.dll使用了一些反编译器处理不好的东西,所以我将在本文中切换使用反汇编器和反编译器代码)
我们可以在这个异常处理程序上设置一个断点,并看到它从之前抛出页保护违规异常的相同地址(ntdll!LdrpSnapModule+0x23b)被调用:
查看异常处理程序本身,我们可以看到它相当简单:
它只处理两个异常代码:
STATUS_GUARD_PAGE_VIOLATION STATUS_SINGLE_STEP
当发生保护页违规时,我们可以看到MitLibValidateAccessToProtectedPage被调用。查看这个函数,我们可以看出其中很大一部分致力于与导入地址过滤相关的检查。我们可以根据与全局IatShadowPtr变量的地址比较以及对各种IAF函数的调用来猜测:
这里的一些代码与EAF相关,但为了简单起见,我们将跳过大部分(暂时)。通过快速扫描此函数及其调用的所有函数,看起来这里没有任何东西重置导出表页面上的PAGE_GUARD修饰符。
可能给我们提示的是回到WinDbg并继续程序执行:
我们立即在下一条指令处遇到另一个异常,这次是单步异常类型。单步异常通常是调试器在请求单步时触发的,例如在逐条指令遍历函数时。但在这种情况下,我要求调试器继续执行,而不是单步,所以不是WinDbg触发了这个异常。
触发单步指令的方法是在上下文记录中的EFLAGS寄存器中设置陷阱标志(第8位)。如果我们看向MitLibValidateAccessToProtectedPage的末尾,我们可以看到它正是这样做的:
到目前为止,我们已经看到PayloadRestrictions.dll执行了以下操作:
在导出表页面上设置PAGE_GUARD修饰符。 当导出表页面被访问时,用MitLibExceptionHandler捕获异常,如果这是保护页违规,则调用MitLibValidateAccessToProtectedPage。 在EFLAGS中设置陷阱标志,以便在执行恢复时在下一条指令上生成单步异常。
这与MitLibExceptionHandler恰好处理两个异常代码——保护页违规和单步——的事实相匹配。因此,在下一条指令上,我们收到现在预期的单步异常,并直接进入MitLibHandleSingleStepException:
这显然是原始输出的清理版本。我省略了检查全局变量是什么以及重命名它们的一些工作,因为这不是一个特别有趣的步骤——例如,要检查我命名为pNtProtectVirtualMemory的变量指向什么函数,我只需在WinDbg中转储指针,并看到它指向NtProtectVirtualMemory。
回到重点——这个函数中有一些东西我们现在会忽略,稍后再回来。我们可以关注的是对NtProtectVirtualMemory的调用,它(至少通过一个代码路径)将保护设置为PAGE_GUARD和PAGE_READONLY。即使没有完全理解一切,我们也可以做出有根据的猜测,说这很可能是KernelBase.dll导出表保护页标志被重置的地方。
既然我们知道了我们看到的两个异常背后的机制,我们可以回到MitLibValidateAccessToProtectedPage,回顾我们之前跳过的所有部分,看看当保护页违规发生时会发生什么。我们首先看到的是检查故障地址是否在IatShadow页面内。我们可以继续忽略这个,因为它与另一个我们尚未为此进程启用的功能(IAF)相关。我们继续到下一部分,我将其标题为FaultingAddressIsNotInShadowIat:
为了方便起见,我已经重命名了这里使用的一些变量,但我们将回顾我是如何得出这些名称和标题的,以及这整个部分的作用。首先,我们看到DLL使用了三个全局变量——g_MitLibState,一个包含PayloadRestrictions.dll使用的各种数据的大型全局结构,以及两个我选择称为NumberOfModules和NumberOfProtectedRegions的未命名变量——我们很快就会看到为什么我选择这些名称。
乍一看,我们可以看出这段代码正在循环中运行。在每次迭代中,它访问g_MitLibState+0x50+索引中的某个结构。这意味着在g_MitLibState+0x50处有某个数组,其中每个条目是某个未知结构。从这段代码中,我们可以看出数组中的每个结构的大小为0x28字节。现在,我们可以尝试静态搜索DLL中初始化此数组的函数,并尝试找出结构包含的内容,或者我们可以回到WinDbg并转储内存中已初始化的数组:
在转储未知内存时,使用dps命令检查数据中是否有任何已知符号是有用的。查看内存中的数组,我们可以看到有3个条目。使用我们看到每个结构中的第一个字段是一个模块的基地址:Ntdll、KernelBase和Kernel32。紧随其后的是一个ULONG。根据上下文和对齐方式,我们可以猜测这可能是DLL的大小。快速的WinDbg查询显示这是正确的:
0:007> dx @$curprocess.Modules.Where(m => m.Name.Contains(“ntdll.dll”)).Select(m => m.Size)
@$curprocess.Modules.Where(m => m.Name.Contains(“ntdll.dll”)).Select(m => m.Size)
[0x19] : 0x211000
0:007> dx @$curprocess.Modules.Where(m => m.Name.Contains(“kernelbase.dll”)).Select(m => m.Size)
@$curprocess.Modules.Where(m => m.Name.Contains(“kernelbase.dll”)).Select(m => m.Size)
[0x7] : 0x39a000
0:007> dx @$curprocess.Modules.Where(m => m.Name.Contains(“kernel32.dll”)).Select(m => m.Size)
@$curprocess.Modules.Where(m => m.Name.Contains(“kernel32.dll”)).Select(m => m.Size)
[0xc] : 0xc2000
接下来我们有一个指向模块基本名称的指针: 0:007> dx -r0 (wchar_t*)0x00007ffe1a4926b0 (wchar_t*)0x00007ffe1a4926b0 : 0x7ffe1a4926b0 : “ntdll.dll” [Type: wchar_t ] 0:007> dx -r0 (wchar_t)0x00000218f42a7d68 (wchar_t*)0x00000218f42a7d68 : 0x218f42a7d68 : “kernelbase.dll” [Type: wchar_t ] 0:007> dx -r0 (wchar_t)0x00000218f42a80c8 (wchar_t*)0x00000218f42a80c8 : 0x218f42a80c8 : “kernel32.dll” [Type: wchar_t *]
还有另一个指向模块完整路径的指针: 0:007> dx -r0 (wchar_t*)0x00000218f42a7970 (wchar_t*)0x00000218f42a7970 : 0x218f42a7970 : “C:\WINDOWS\SYSTEM32\ntdll.dll” [Type: wchar_t ] 0:007> dx -r0 (wchar_t)0x00000218f42a7d40 (wchar_t*)0x00000218f42a7d40 : 0x218f42a7d40 : “C:\WINDOWS\System32\kernelbase.dll” [Type: wchar_t ] 0:007> dx -r0 (wchar_t)0x00000218f42a80a0 (wchar_t*)0x00000218f42a80a0 : 0x218f42a80a0 : “C:\WINDOWS\System32\kernel32.dll” [Type: wchar_t *]
最后我们有一个ULONG,在此函数中用于指示是否检查此范围,因此我将其命名为CheckRipInModuleRange。当放在一起时,我们可以构建以下结构: typedef struct _MODULE_INFORMATION { PVOID ImageBase; ULONG ImageSize; PUCHAR ImageName; PUCHAR FulleImagePath; ULONG CheckRipInModuleRange; } MODULE_INFORMATION, *PMODULE_INFORMATION;
我们可以在IDA中定义这个结构,并获得更好的代码视图,但我试图让本文专注于分析此功能,所以我只是用字段名称注释了idb。
现在我们知道这个数组包含什么,我们可以更好地了解这段代码的作用——它遍历此数组中的结构,并检查访问受保护页面的指令指针是否在其中一个模块内。当循环完成——或者代码发现故障RIP在其中一个模块中时——它将r8设置为模块的索引(如果未找到模块,则保留为-1),并继续进行下一个检查:
这里我们有另一个循环,这次遍历g_MitLibState+0x5D0处的数组,其中每个结构的大小为0x18,并将其与触发异常的地址(在我们的例子中,是KernelBase导出表内的地址)进行比较。现在我们已经知道该做什么了,所以我们将去转储内存中的那个数组:
我们这里有三个条目,每个条目包含看起来像是起始地址、结束地址和一些标志的东西。让我们看看每个这些范围是什么:
第一个范围从NTDLL的基地址开始,跨越0x160字节,因此几乎覆盖了NTDLL头部。 第二个范围是我们从文章开始就一直在寻找的——这是KernelBase.dll导出表。 第三个范围是Kernel32.dll导出表(我不会展示我们如何找出这一点,因为我们在文章前面已经为KernelBase做过这个)。
可以安全地假设这些是PayloadRestrictions.dll保护的内存区域,并且此检查旨在确保此保护页违规是由其受保护范围之一触发的,而不是进程中的其他受保护页面。
我不会对此函数中的其他检查进行太多细节说明,因为那将主要涉及重复相同的步骤,而且这篇文章已经很长了。相反,我们将进一步查看函数的这一部分:
如果指令指针在注册的模块之一中找到,则调用此代码路径。即使不查看这里调用的任何函数内部,我们也可以猜测MitLibMemReaderGadgetCheck查看访问受保护页面的指令,并将它们与预期指令进行比较,如果指令被认为是“坏的”,则调用MitLibReportAddressFilterViolation来报告意外行为。
如果保存的RIP不在已知模块之一中,则采取不同的路径,这涉及两个最终检查。第一个检查保存的RSP是否在堆栈内,如果不是,则调用MitLibReportAddressFilterViolation来报告潜在的利用:
第二个调用RtlPcToFileHeader以获取保存的RIP所在模块的基地址,如果未找到则报告违规,因为这意味着受保护页面是从动态代码内部访问的,而不是映像:
所有调用MitLibReportAddressFilterViolation的情况最终都会导致调用MitLibTriggerFailFast:
这最终会终止进程,从而阻止潜在的利用。如果未发现违规,该函数会为下一条将运行的指令启用单步异常,整个循环再次开始。
当然,我们可以继续深入研究DLL以了解此功能的初始化、正在搜索的gadgets或报告违规时