-
-
[翻译]安全研究方法论案例研究——攻击Windows Defender Application Control
-
发表于: 2018-7-28 21:46 6296
-
我经常这样,每当发布新的Windows版本时,我都会比较下Windows Defender Application Control(WDAC,以前称为Device Guard)代码完整性策略架构(位于%windir%\schemas\CodeIntegrity\cipolicy.xsd ),看看是否有新的和有趣的功能。 我这样做是因为新的WDAC功能很少被记录下来,我总是对我感兴趣的新的安全功能感到好奇。 Windows 10 1803版本发布时,我注意到一个名为“Enabled:Dynamic Code Security(启用:动态代码安全)”的新策略规则选项。正如预期的那样,Google搜索此功能名称,我什么都没找到。 该功能的名字引起了我的兴趣,因为感觉它可能与我之前的一篇博客文章 (推荐阅读的背景知识)有关,那篇文章中我描述了动态.NET代码编译方法中的竞争条件(race condition)漏洞。 该漏洞可引起WDAC绕过,并且微软当时选择不将其修复。
我写这博客文章的目的不仅是描述这个新功能的机制,更重要的是,我想利用这个机会阐述我用于理解并且绕过该功能的方法论。 所以,如果你本来就对WDAC的功能感兴趣,那就太好了。 如果不是,那也没关系,但我希望你会遵循我用来理解没有相关记录的Windows功能的具体策略。 我写这篇文章是想让你知道,安全研究的内容很丰富,但研究人员很少会提供他们关于如何得出结论的独特见解。 我个人总是更关心“how”而不是“ what”。
为了测试“Enabled:Dynamic Code Security”选项,我将新的配置应用于%windir%\schemas\CodeIntegrity\ExamplePolicies中的AllowAll.xml策略,产生以下简单策略:
此策略允许执行所有用户模式和内核模式代码。 在这一点上,我不确定这个宽松的政策是否会产生任何明显的差异,但值得一试。 我使用以下PowerShell命令启用了策略,然后重新启动:
我的第一次测试是尝试调用Add-Type(我最初用于绕过竞争条件的触发点)。 我的假设是,启用此WDAC功能后,可信(即符合每个策略)的C#代码编译和后续加载应该可以正常工作。 在我的博客文章中,我导入了“PSDiagnostics”模块来触发竞争条件,因为它是一个调用Add-Type的签名模块(否则不允许执行不受信任代码的cmdlet)。 但是,在启用了“Dynamic Code Security”情况下,导入模块失败了。
“Dynamic Code Security”功能导致PowerShell抛出异常
这当然不是我所期望的。 为了确认该错误与“Dynamic Code Security”相关,我验证出在未执行Device Guard时,以及使用vanilla(未修改的AllowAll.xml策略)执行Device Guard时,加载PSDiagnostics模块没有问题。 安全研究的一项重要技能是应用程序根本原因分析,其中包括隔离问题。 所以,此时,我能够确认以下内容:
要找出在上面的截图中抛出异常的代码,我做的第一件事是获取异常的堆栈轨迹。 这里有两个异常,所以我将转储两者的堆栈轨迹:
抛出两个异常后没有堆栈轨迹
通过截图可以看到,没有任何帮助。 现在,在我打开我的.NET反编译器和调试器以找到异常的根本原因之前,我认为验证C#编译实际上是有用的。 为此,我将使用procmon来验证是否有编译文件(即临时的.cs和.dll文件)生成。 通过确认编译是否发生,可以帮助缩小我的调查范围。
在运行procmon并为powershell.exe进程及其子进程过滤“Process Create”和“WriteFile”操作之后,从我之前观察生成的DLL文件和cvtres.exe作为csc.exe的子进程启动的经验来看,我确认没有创建编译文件,:
在procmon.exe中查看C#编译文件
为了验证给csc.exe的命令行参数是否正确传递,我想检查procmon记录中存在的临时.cmdline文件(记录了大多数传递给csc.exe的参数)的内容。 因为这些文件很快就会被删除,所以我使用了一种愚蠢的暴力方法,使用下面的单行代码获取之,我在另一个PowerShell进程中运行代码,并且试图在另一个进程中导入PSDiagnostics:
这个小技巧很成功。 以下是.cmdline文件的内容:
紧接着, /EnforceCodeIntegrity选项吸引了我,因为我从未见过它,正如名称清楚描述的那样,它与代码完整性有关。 这是我现在能够在代码中搜索的一个很有价值的线索。
我现在面前有几条调查路径:我可以开始逆向csc.exe以确定/EnforceCodeIntegrity是如何工作的,或者我可以找出提供/EnforceCodeIntegrity选项给csc.exe 的.NET代码,或者两者都做。 我确定最简单的路径是找出提供开关的.NET代码。 但是,在执行此操作之前,我们先看下是否对此命令行开关有任何文档。 csc.exe的内置帮助为我提供了一些信息:
对编译器的所有输入代码做完整性检查,并允许其他执行了代码完整性检查的程序加载编译的程序集(如果操作系统是如此配置的话)。
不,那不是我的拼写错误。 在这我提醒一下,重要的是要记住,在进行安全研究/逆向时,没有一种所谓正确的方法一定可以帮助你得到所寻求的答案。 你只需按照面包屑(在多个方向上探索)直至你到达森林中的净土(是的,只需按照隐喻来做),最后你会得到通向最终目的地所需的那种一目了然【译者注:此句是比喻,源自《格林童话》的《糖果屋》,文中有一句“没关系,面包屑会告诉我们回家的路。”】。 逆向工程相当于是收集拼图,但你并不清楚最终这个大图片的样子。
无论如何,回到我们当前的难题......我使用我最喜欢的.NET反编译器, dnSpy来搜索/EnforceCodeIntegrity字符串,成功了, 它找到并反编译System.dll中的Microsoft.CSharp.CSharpCodeGenerator.CmdArgsFromParameters方法中的以下代码段:
棒棒。 现在要确定导致FileIntegrity.IsEnabled返回true的条件,这一点很明显,可以推断出这是因为.cmdline文件中有/EnforceCodeIntegrity 。
单击“IsEnabled”并观察其引用(右键单击,选择“Analyze”),你会看到它是使用以下代码设置的:
这绝对是基于对wldp.dll函数的所有引用进行调查的第一次爆炸性的线索发现。 此刻,我很高兴,因为我之前已经逆向了wldp.dll(Windows锁定策略),因为用户模式代码使用该DLL获取WDAC执行状态/策略。 上面的代码片段中的wldp.dll函数都是1803版本的新功能所以我肯定要做一些逆向工作。
这是一个适当的暂停点。 可能需要逆向很多代码。 从哪儿开始? 要回答这个问题,你要经常提醒自己逆向的目标是什么。 到目前为止,我对整个研究过程了解越多,我的目标就会发生变化和扩展。 我的第一个目标是确认启用“Dynamic Code Security”将阻止我提过的和博客中说过的Device Guard绕过。 当我发现该方法似乎无效时,我的目标随之就变成找出导致其无效的根本原因。 一个新的、间接的目标,引导我转换为攻击者,目的是找出可能欺骗System.dll(或其他可执行类似验证的程序)认为“FileIntegrity”未启用的条件。 我们稍后会回来。 我的优先事项仍然是确定为什么可信代码调用Add-Type不成功。 毕竟,绕过一个无效的缓解方法又有什么好处呢?
因此,我记着提醒自己在定位Add-Type问题的根本原因之后,自己回过头来再看看wldp.dll函数,并理解其工作原理。
现在,我还不清楚为什么Add-Type在被合法代码调用时会失败。 原始异常提供了一些信息:
我感觉这可能与.cmdline文件中包含System.Mangement.Automation.dll引用有关:
0xd0000428错误代码并没有告诉我们多少信息,谷歌搜索也没有发现任何显而易见的东西。 它可能在之后是一个引用框架,所以要重视它。 在逆向中,你可能拥有的一些线索会在以后的某个时刻突显价值。顺便说一下加分项, 你试试将0xd0000428识别为基于0xd前缀转换为HRESULT的NTSTATUS值。
查看Add-Type命令的反编译代码,我看到无法避免不使用包含System.Management.Automation.dll引用的csc.exe。 就个人而言,如果我编译与PowerShell相关的代码(例如cmdlet),我宁愿使用-ReferencedAssemblies参数将引用添加到Add-Type,但这只是我自己的想法。
由于早期的堆栈轨迹没有给我任何关于异常源自何处的提示,我决定用WinDbg,并且跟踪下引用了System.Management.Automation.dll的csc.exe中的kernel32!CreateFileW(最常用于文件操作的函数)的调用情况。 没过多久就看到wldp!WldpQueryDynamicCodeTrust通过clr.dll中的CreateFileW返回的句柄被调用。 由于我们之前已经确定wldp.dll函数很有用,我注意到了WldpQueryDynamicCodeTrust的返回值。 果然,它是0xd0000428,转换为以下人类可读错误信息:
Windows无法验证此文件的数字签名。 最近的硬件或软件更改可能安装了签名错误或损坏的文件,或者可能是来自未知来源的恶意软件。
顺便说一下,我使用WinDbg中的“ !error ”命令执行了错误代码转换,这是处理未知错误代码时非常有用的命令。 习惯于识别错误代码类型是有帮助的。 在这种情况下,我认识到0xd0000428是从NTSTATUS值0xc0000428转换而来的HRESULT值。 如果早知道这个,我原本可以在SDK或WDK中搜索该值。 该值在ntstatus.h中定义,名为STATUS_INVALID_IMAGE_HASH。
因此,0xd0000428的错误代码正是异常中报告的内容。 此时,在不知道WldpQueryDynamicCodeTrust如何工作的情况下,我的直觉告诉我,可能有代码忘记建立信任或验证System.Management.Automation.dll的完整性。 现在我已经看到了几个对wldp.dll函数的引用,我有理由认为我现在需要花时间理解它们。 我也不想确定为什么在验证System.Management.Automation.dll时返回了特定的错误代码。 我想看看我是否可以通过删除命令行引用来消除该错误。
我从生成的的.cmdline文件中移除System.Management.Automation.dll程序集引用行的策略是覆盖与之前相同的内容并去掉System.Management.Automation.dll引用的文件。 这基本上是我之前在博文中所做的,我将劫持.cmdline文件,而不是劫持.cs文件。 既然已经到此地步,还有什么我可以考虑在.cmdline文件中劫持的呢? 移除/EnforceCodeIntegrity咋样 ? 呵呵。 我们稍后再回过头来看看。
劫持.cmdline并移除System.Management.Automation.dll的程序集引用后,PSDiagnostics模块中的Add-Type调用运行良好! 我将把这个bug留给.NET团队去修复。 现在值得注意的是,如果没有提供额外的程序集引用,那么使用C#编译方法的其他应用程序(例如msbuild.exe)也不会遇到此错误。 考虑到PowerShell是代码执行的常用工具,很高兴知道我现在可以避开这个错误(当我对自己这么说时听起来真的很有趣 - “避开错误”)并继续研究“Dynamic Code Security“功能。 此外,我想利用这个机会详细说明根本原因分析方法。 我希望这部分对你有用。
我们的下一个目标是了解wldp.dll中以下与动态代码相关的导出函数的实现:
让我们从WldpIsDynamicCodePolicyEnabled开始,因为它听起来像是在设置或检索动态代码策略信任之前首先调用的函数。 把它放入IDA并加载符号,我们可以看到相对简单的功能(暂未做注释):
IDA中未做注释的WldpIsDynamicCodePolicyEnabled函数
所以,这个函数只包含对NtQuerySystemInformation的简单调用和某种比较。 目前还不清楚从NtQuerySystemInformation中检索到哪种信息。要确定检索的信息,你应该看下通过第一个参数传递的enum值(RCX - x64 ABI函数的第一个参数) - 0xA4(十进制为164)。 但是,没有看到关于该enum值的记录。 幸运的是, 调试符号可以通过转储SYSTEM_INFORMATION_CLASS enum来为我们提供一些信息系。 这可通过在WinDbg中使用以下命令实现:
在WinDbg中转储enum后,0xA4解析为“SystemCodeIntegrityPolicyInformation”。 此enum值指定NtQuerySystemInformation返回的结构类型。 返回的是什么结构? 可惜,这也没有相关记录。 为了确定返回的是什么类型的结构,我的想法是在加载的符号中搜索包含字符串“CODEINTEGRITY”和“INFORMATION”的结构。
我很幸运,它返回了一个很好的候选结构定义 - ole32!SYSTEM_CODEINTEGRITYPOLICY_INFORMATION。 现在,我如何确定这是正确的结构? 我将WinDbg中输出的结构大小与传递给NtQuerySystemInformation的结构大小(0x20)进行了比较:
此时,我确信这是正确的结构,因为大小匹配,并且结构的名称与enum值的名称相匹配。 现在我有足够的信息将结构应用到IDA中的函数,这使我可以专注于实现NtQuerySystemInformation返回数据比较的函数部分:
验证已配置的代码完整性选项的WldpIsDynamicCodePolicyEnabled中带注释的基本块
那么,“选项”字段是指什么,为什么它要与0x110相对比? 很遗憾,这个字段又没有相关记录,但有时你可以试试在.NET代码中找找enum和结构的定义。 我很幸运,System.Management.Automation.dll代码中存在一些值:
我看到0x110引用中的0x10(16)转换为“CODEINTEGRITYPOLICY_OPTION_UMCI_ENABLED”,但0x100不存在。 我只能假设0x100(256)是最近添加的并且.NET enum没有做相应更新,因为并不急需。 因此,我目前的假设是,如果同时启用了UMCI和“Dynamic Code Security”选项,则会启用“DynamicCodePolicy” - 即二进制OR运算0x10和0x100,结果是0x110。 这应该看起来很直观,因为缓解措施与我们在本文中讨论的功能有关,而动态代码执行仅与用户模式代码执行(即UMCI)情况相关。
接下来,我可以研究一下内核如何向WldpIsDynamicCodePolicyEnabled提供SYSTEM_CODEINTEGRITYPOLICY_INFORMATION结构以查看是否存在任何攻击面(即确定操作系统返回“DynamicCodePolicy” 未执行的方法),但我宁愿花时间先了解WldpSetDynamicCodeTrust的实现原理。 这绝对是我作为一个攻击者,想要多说两句的函数,因为顾名思义,它可以从用户模式对一个文件“设置信任”。 对于该功能而言,什么是“信任”? 不知道, 我们继续研究。
与WldpIsDynamicCodePolicyEnabled一样,WldpSetDynamicCodeTrust也是一个非常直接明了的函数。 它调用了NtSetSystemInformation ,而不是NtQuerySystemInformation,与之前一样,如果我们想要了解该函数,我们需要确定提供给NtSetSystemInformation的enum值和结构类型。
IDA中未做注释的WldpSetDynamicCodeTrust实现
因此,需要给第一个参数(再次通过RCX传递)解析的enum值是0xC7。 使用与上面讨论相同步骤,0xC7解析为“SystemCodeIntegrityVerificationInformation”,其对应于另一个没有相关记录的结构:SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION。
所以我们只能看到WldpSetDynamicCodeTrust接收文件句柄作为参数并将其传递给NtSetSystemInformation。 如果你看了NtSetSystemInformation的实现,你会发现它只是一个系统调用 。 因此,为了确定SystemCodeIntegrityVerificationInformation在转移到内核后的行为,我们需要逆向一些内核代码。 一种策略是使用内核调试器将踪内核中的系统调用,或尝试找到可能与“动态代码信任”功能相关的代码,在该函数上设置断点,并查看是否到达该函数。 如果幸运的话,我们会尝试后者,因为后者会更快产生结果。
第一个寻找相关功能的明显位置是ntoskrnl.exe,因为那是在内核端实现NtSetSystemInformation的模块。 根据经验,我也知道代码完整性/镜像验证功能是在ci.dll中实现的(ci – 表示代码完整性)。 这是我想要看的第一个地方,所以我将它加载到IDA,并应用符号,然后搜索其名称中包含“DynamicCode”的函数。
我的搜索结果有以下函数:
看到与WldpSetDynamicCodeTrust函数相关的不错的内核候选者了吗?CiSetDynamicCodeTrustClaim对我来说看起来不错。 为什么? 好吧,它与WldpSetDynamicCodeTrust的命名方式非常相似。
CiSetDynamicCodeTrustClaim不是一个非常复杂的函数,它本质上做了一件事 - 它在它接收的文件句柄(技术上说是, FILE_OBJECT )上设置了一个NTFS扩展属性 。
CiSetDynamicCodeTrustClaim设置NTFS扩展属性
“$ Kernel.Purge”字符串非常吸引我,因为我之前看到James Forshaw在一个Device Guard bug描述中描滥用过它。 “$ Kernel.Purge.TrustClaim”扩展属性名是新的,我很想知道哪些数据与该扩展属性是相关联的。
FsRtlSetKernelEaFile的第二个参数采用FILE_FULL_EA_INFORMATION结构。 可以在IDA中看到如何填充它,将其转储到WinDbg中也很有用:
示例中的“dd”(dump dword)命令转储扩展属性的值。 目前还不清楚0x80001值是指什么。 为了清楚起见,“L3”告诉WinDbg转储3个等于0xC字节的DWORD值 - EaValueLength字段的值。
因此,到目前为止,我的理解是,当在用户模式下调用WldpSetDynamicCodeTrust时,内核将“$ Kernel.Purge.TrustClaim”扩展属性应用于稍后要引用的某种标记文件。 逆向WldpQueryDynamicCodeTrust也应该证实这个理论。
此外,我们实际在调试器中点击CiSetDynamicCodeTrustClaim并查看堆栈帧是有帮助的的。 如下所示,我们确实从“MarkAsTrusted”.NET方法(如前所述)中找到了此函数:
我期望从WldpQueryDynamicCodeTrust的实现中看到的实际上是WldpSetDynamicCodeTrust的反向功能。 IDA中如下的展示证实了这个理论:
WldpQueryDynamicCodeTrust反汇编的注释部分
此截图显示了WldpQueryDynamicCodeTrust函数的主要部分,它使用在WldpSetDynamicCodeTrust中传递给NtSetSystemInformation的相同enum值调用NtQuerySystemInformation。 没有对内核设置的“$ Kernel.Purge.TrustClaim”扩展属性执行实际验证。 相反,它信任内核正确验证了它并且它只考虑NtQuerySystemInformation是否返回错误/警告 - 错误/警告是具有高位设置(即大于或等于0x80000000)的返回值。 多说一句,我之所以知道这些是基于对“ jns ”指令的使用认知。
因此,你可能很想知道为什么用户模式应该信任内核验证扩展属性。 我也好奇,所以我决定看看CipQueryDynamicCodeTrustClaim的实现。 我将跳过一些函数实现,只显示执行扩展属性数据验证的相关部分:
CipQueryDynamicCodeTrustClaim扩展属性数据验证
CipQueryDynamicCodeTrustClaim检索“$ Kernel.Purge.TrustClaim”扩展属性的数据部分。 然后,它将前两个字节与1比较(上面屏幕截图中的倒数第二个指令)。 如果设置为1,则CipQueryDynamicCodeTrustClaim认为该文件是可信的。 因此,这提供了一些关于为什么至少部分扩展属性数据之前已被设置的内容:
目前还不清楚静态0x0008值用于什么,但是我并不太关心它,因为我所看到的所有验证值都是0x0001。
所以,此刻,我相信我对如何从用户和内核模式实现WldpIsDynamicCodePolicyEnabled,WldpQueryDynamicCodeTrust和WldpSetDynamicCodeTrust有了相当清楚的理解。 总而言之,它们的行为如下:
那么为了避免.cs 竞争条件劫持攻击 ,最终设置和读取文件扩展属性的目的是什么呢? 好吧,它的用处是以便可信用户模式代码可以将生成的.cs文件标记为可信,然后之后可以将其验证为来自可信进程。 为扩展属性使用“$ Kernel.Purge”前缀的好处是,如果文件被覆盖,内核将自动去除扩展属性。 这意味着博文中描述的劫持.cs文件的行为将强制删除扩展属性,使文件“不可信”。从表面上看,这似乎应该是一个很好的缓解措施...前提是缓解应用正确操作 - 即确保将正确的文件标记为受信任,并且不该被标记为可信的文件不应当受信任。
既然我已经很好地理解了事情的原委,现在我已经可以研究潜在的攻击面。
想要绕过我现在非常熟悉的“Dynamic Code Security”缓解措施,我问自己以下问题:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [翻译]老木马新玩法:Qbot最新攻击手法探究 19006
- [翻译]Crimson远控木马分析 24652
- [翻译]DLL劫持自动化检测 24387
- [翻译]使用Frida框架hook安卓native方法(适合新手) 26742
- [翻译]蓝军测试中PROXY protocol的应用 8319