01
介绍
每当新的Windows版本发布时,我都会比较Windows Defender应用程序控制(WDAC,以前叫Device Guard)代码的完整性策略架构(位于中%windir%\schemas\CodeIntegrity\cipolicy.xsd),看下有没有任何有趣的新功能。Windows 10 1803发布时,我注意到一个叫“启用:动态代码安全性”(Enabled: Dynamic Code Security)的新策略规则选项。搜索这个功能名称时,我什么也没找到。这个功能的名称让我很感兴趣,因为它和我写的一篇文章有关,该文章讲述了动态.NET代码编译方法的竞争条件漏洞。这个漏洞导致了通用WDAC绕过,而Microsoft当时没有修复这个漏洞。
本文的目的不仅是描述这个新功能的机制,更重要的是,我想借此机会来讲一下如何绕过这个功能。尽管安全圈有大量不错的安全性研究,但很少有研究人员提供得出结论的思路。而我个人更关心的是“思路”而不是“结论”,这是撰写本文的主要动机。
02
设置环境:启用“启用:动态代码安全性”,进行观察
为了测试“启用:动态代码安全性”选项,我将新的配置选项应用于%windir%\schemas\CodeIntegrity\ExamplePolicies的AllowAll.xml策略,产生了以下简单的策略:
<?xml version="1.0" encoding="utf-8"?>
该策略允许执行所有用户模式和内核模式代码。在这一点上,我不确定这个宽松的策略是否会产生任何明显的执行差异,但是值得一试。我用以下PowerShell命令启用了该策略,然后重新启动:
ConvertFrom-CIPolicy -XmlFilePath。\ AllowAll_Modified.xml -BinaryFilePath C:\ Windows \ System
我的第一个强制测试:调用Add-Type(用来触发原始竞争条件绕过)。如果启用了WDAC功能后,C#编译和受信任代码(即每个策略批准)的加载就可以正常工作。在我的博客文章中,我导入了“ PSDiagnostics”模块,触发竞争条件,因为它是调用Add-Type的已签名模块。但是,在启用“动态代码安全性”的情况下,尝试导入模块失败。
(“动态代码安全性”功能导致PowerShell触发异常)
为了取证该错误和“动态代码安全性”的启用有关,我在没有执行Device Guard、以及使用未经修改的AllowAll.xml策略执行Device Guard这两种情况下,测试PSDiagnostics模块是否加载成功。安全研究的一项重要技能是应用程序根本原因分析,其中包括切分问题。所以,我可以确认的内容如下:
在代码完整性策略中启用“动态代码安全性”会中断受信任代码对Add-Type的调用,原因不明。这和我的假设相反,因为调用Add-Type的受信任代码应该是可以执行的。但从研究的角度来看,这个异常可以让我识别在代码“动态代码安全性”中的执行位置。
03
切分问题
找出在上述截图中引发异常的代码,第一件事是获取异常的堆栈跟踪。这里有两个例外,所以我会转储这两个的堆栈跟踪:
(在触发两个异常后,没有堆栈跟踪)
打开.NET反编译器和调试器,查找异常的根源之前,应该先验证C#编译。为此,用procmon来验证是否删除了编译文件(即临时.cs和.dll文件)。编译的发生与否有助于缩小调查范围。
运行procmon、为powershell.exe进程及其任何子进程过滤“进程创建”和“ WriteFile”操作后,可以确认没有创建编译工件:
(在procmon.exe中查看C#编译文件对应的进程)
为了验证提供的csc.exe的命令行参数是否正确,需要检查procmon跟踪中的临时.cmdline文件(该文件为csc.exe提供大量参数)的内容。因为这些文件很快就会被删除,所以我运行下面这行代码来获取文件:
while ($true) { ls $Env:TEMP\*.cmdline | cp -Destination C:\Test\ }
这是.cmdline文件的内容:
/t:library /utf8output /EnforceCodeIntegrity /R:"System.dll" /R:"C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll" /R:"System.Core.dll" /out:"C:\Users\UnprivilegedUser\AppData\Local\Temp\jz24g5tn.dll" /debug- /optimize+ /warnaserror /optimize+ "C:\Users\UnprivilegedUser\AppData\Local\Temp\jz24g5tn.0.cs"
/EnforceCodeIntegrity选择与代码完整性的执行有关。这是一个很好的线索。
现在有几个调查途径:逆向csc.exe,确定/EnforceCodeIntegrity执行方式,或者识别提供/EnforceCodeIntegrity选择给csc.exe的.NET代码…或者两者都调查一遍。最简单的方法是识别提供选择的.NET代码。但在执行这个操作前,需要检查一下是否有记录命令行选择。csc.exe的内置提供了一些上下文:
配置操作系统,检查编译输入的代码完整性,使用执行代码完整性的程序启用加载编译集。
要记住,在做安全性研究/逆向工程时,找到正确答案的方法不止一个。您只需要跟踪面包屑(向多个方向转向),直到您到达森林中的一片空地(是的,就跟着隐喻一起走)即可为您提供到达最终目的地所需的清晰度。逆向工程相当于收集拼图碎片,但你对拼图的完成状态没有清晰的概念。
用.NET反编译器dnSpy来查找/EnforceCodeIntegrity字符串。成功。它找到并反编译了Microsoft.CSharp.CSharpCodeGenerator.CmdArgsFromParametersSystem.dll里的以下代码片段:
if (FileIntegrity.IsEnabled)
现在,识别让FileIntegrity.IsEnabled正确返回的条件,因为.cmdline文件里有/EnforceCodeIntegrity,所以可以推断出这是正确的。
单击“ IsEnabled”,观察其引用(右键单击,选择“ 分析”),你会看到它已设置为以下代码:
private static readonly Lazy<bool> s_lazyIsEnabled = new Lazy<bool>(delegate()
这是基于对wldp.dll函数的所有引用进行研究的线索/路径的重大发现。因为我之前已经逆逆向了wldp.dll(Windows锁定策略),因为用户模式代码用DLL 来获取WDAC执行状态/策略。上面代码段的wldp.dll函数是1803的新功能,所以我要进行逆向。
有很多代码都是逆向的。那么,应该从哪里开始?如果想找到问题答案,你要时常提醒自己,你做逆向的目标是什么。自己操作越多,目标就会有所转换,或者更加广泛。我的第一个目标是确认启用“动态代码安全性”会缓解我报告和写过的Device Guard绕过。当缓解措施没有效果后,下一个目标便是找出失效的根本原因。目前的重点仍然是找出受信任代码无法调用Add-Type的原因。
所以,找到Add-Type问题的根本原因后,我会做个记录,以便到时返回wldp.dll函数,了解它们的执行方式。
04
根本原因分析
现在,我仍不清楚为什么无法从合法代码中调用Add-Type。原始异常提供的上下文:
‘c:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll’ could not
我觉得这和把System.Mangement.Automation.dll作为.cmdline文件的引用有关:
/R:"C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll"
错误代码0xd0000428没有什么有价值的信息。不过,它后面可能会有参考价值。逆向工程的一些线索多多少少都会有些价值。可以把0xd0000428当作NTSTATUS值基于0xd前缀转换为HRESULT。
查看针对Add-Type命令的反编译代码,发现要给csc.exe提供System.Management.Automation.dll参考。如果编译PowerShell的相关代码(例如,cmdlet),我更倾向于用-ReferencedAssemblies参数添加参考到Add-Type。
由于之前的堆栈跟踪没有提示异常的起源,打开WinDbg,跟踪引用System.Management.Automation.dll的csc.exe的kernel32!CreateFileW(最常用于文件操作的函数)调用。在clr.dll里,用CreateFileW返回句柄,很快就能调用wldp!WldpQueryDynamicCodeTrust。前面讲到要关注wldp.dll函数,所以我记下了WldpQueryDynamicCodeTrust的返回值。果然是0xd0000428,它转换为以下错误:
Windows无法验证此文件的数字签名。最近的硬件或软件更改可能安装了错误签名或损坏的文件,或者可能是来历不明的恶意软件。
我在WinDbg用“!error ”命令执行了错误代码转换。习惯于识别错误代码类型会很有帮助。我一下就能看出0xc0000428是一个HRESULT值,从NTSTATUS值转换过来的。知道这一点后,就可以在SDK或WDK中搜索该值。这个值在ntstatus.h中定义,并命名为STATUS_INVALID_IMAGE_HASH。
0xd0000428正是异常消息中报告的错误代码。此时,在不了解WldpQueryDynamicCodeTrust如何执行的情况下,我的直觉告诉我,也许有些代码忘记了建立信任或验证System.Management.Automation.dll的图像完整性。现在,我已经看到了对wldp.dll函数的引用。试下能否通过删除命令行引用来规避这个错误。
用和之前相同的内容覆盖该文件,减去System.Management.Automation.dll引用,删除System.Management.Automation.dll集引用的捆绑.cmdline文件。执行此操作时,.cmdline文件中还有其他可以劫持的内容吗?如何删除/EnforceCodeIntegrity?这些后面会讲到。
执行.cmdline劫持,排除System.Management.Automation.dll的程序集引用之后,PSDiagnostics模块中的Add-Type调用运行得很好!bug就留给.NET团队来修复。但是,如果没有提供其他程序集引用,用C#编译方法的其他应用程序(例如msbuild.exe)不会受到此错误的影响。PowerShell是代码执行的常见媒介,现在可以规避该错误了,继续“动态代码安全性”功能的研究。
我们的下一个目标是了解wldp.dll里和动态代码相关的导出函数:
WldpIsDynamicCodePolicyEnabled
WldpQueryDynamicCodeTrust
WldpSetDynamicCodeTrust
05
逆向新的WLDP函数
从WldpIsDynamicCodePolicyEnabled开始,用加载符合把它加载到IDA,生成一个相对简单的函数(目前暂时不加注释):
(IDA中未注释的WldpIsDynamicCodePolicyEnabled函数)
这个函数仅包含NtQuerySystemInformation的简单调用以及某种比较。目前尚不清楚从NtQuerySystemInformation检索什么类型的信息。确定检索的信息,可以通过第一个参数(RCX- x64 ABI中函数的第一个参数)-0xA4(十进制164)查看传递的枚举值。虽然该枚举值没有被记录,但可以转储SYSTEM_INFORMATION_CLASS枚举,提供上下文。以下是WinDbg执行的命令:
dt ole32!SYSTEM_INFORMATION_CLASS
把枚举转储到WinDbg后,0xA4解析为“ SystemCodeIntegrityPolicyInformation”。这个枚举值指定NtQuerySystemInformation返回结构的类型。但其返回的结构也没有记载。为了确定可能的返回结构类型,我认为应该在加载的符号中搜索包含字符串“ CODEINTEGRITY”和“ INFORMATION”的结构。
dt ole32!*CODEINTEGRITY*INFORMATION*
它返回了一个不错的候选结构定义——ole32!SYSTEM_CODEINTEGRITYPOLICY_INFORMATION。那么,要如何得知它是不是正确的结构?这需要把WinDbg报告的结构大小和传递给NtQuerySystemInformation —0x20的大小进行比较:
dt -v ole32!SYSTEM_CODEINTEGRITYPOLICY_INFORMATION
大小匹配,可以确信这是正确的结构,并且结构名称和枚举值匹配。现在有足够的信息把结构应用到IDA的函数,接下来就可以把重点放在执行从NtQuerySystemInformation返回的数据比较的函数:
(WldpIsDynamicCodePolicyEnabled带注释的基本块,用于验证已配置的代码完整性选项)
那么,“选项”字段指的是什么?为什么将其与0x110进行比较?这个字段也没有记录下来,但有时可以在.NET代码中查找枚举和结构定义,从而进行规避。System.Management.Automation.dll代码有一些值:
internal enum CodeIntegrityPolicyOptions : uint
0x110引用的0x10(16)转换为“ CODEINTEGRITYPOLICY_OPTION_UMCI_ENABLED”,但该枚举中没有0x100。我只能假定添加了0x100(256),.NET枚举没有更新,没有需要该值。如果同时启用UMCI和“动态代码安全性”选项,“ DynamicCodePolicy”也要跟着启用,即对二进制0x10和0x100进行运算,结果为0x110。这似乎很直观,因为缓解和我们在本文中谈论的功能有关,动态代码执行仅与用户模式代码强制(UMCI)场景相关。
06
逆向WldpSetDynamicCodeTrust
WldpSetDynamicCodeTrust也是一个非常简单的函数。它不是调用NtQuerySystemInformation,而是调用NtSetSystemInformation,如果想了解该函数,那就要确定给NtSetSystemInformation的枚举值和结构类型。
(执行IDA中未注释的WldpSetDynamicCodeTrust)
因此,第一个需要解析的参数(通过RCX传递)枚举值为0xC7。使用上面讨论过的发现进程,0xC7解析为“ SystemCodeIntegrityVerificationInformation”,它对应另一个未记录的结构:SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION。
dt -v ole32!SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION
所以,我们只能看到WldpSetDynamicCodeTrust接收作为参数的文件句柄,并将其传递给NtSetSystemInformation。如果要查看NtSetSystemInformation的执行,你会发现它只是一个syscall。所以,想要知道SystemCodeIntegrityVerificationInformation过渡到内核后会执行什么操作,我们要逆内核代码。一个办法是用内核调试器跟踪内核的syscall,另一个是找到可能和“动态代码信任”功能相关的代码,在该功能上设置断点,看结果是不是这个函数。尝试第二个方法应该能很快就得到结果。
寻找相关功能可以从ntoskrnl.exe开始,因为它是在内核执行NtSetSystemInformation的模块。根据我的经验,代码完整性/图像验证功能是在ci.dll(ci-代码完整性)执行。那是我第一个想看的地方,所以我把它加载到IDA,应用符号搜索名称中带有“ DynamicCode”的函数。
搜索后显示以下函数:
SIPolicyDynamicCodeSecurityEnabled
CiValidateDynamicCodePages
CipQueryDynamicCodeTrustClaim
CiSetDynamicCodeTrustClaim
CiHvciValidateDynamicCodePages
CiSetDynamicCodeTrustClaim函数并不复杂,它只执行一个操作 —— 在接收到的文件句柄(FILE_OBJECT)上设置NTFS扩展属性。
(CiSetDynamicCodeTrustClaim设置NTFS扩展属性)
“ $ Kernel.Purge.TrustClaim”扩展属性名称是新的,我很好奇和该扩展属性关联的数据。
FsRtlSetKernelEaFile的第二个参数采用FILE_FULL_EA_INFORMATION结构。在IDA中可以看到它的填充方式,但把它转储到WinDbg也很有用:
kd> dt OLE32!FILE_FULL_EA_INFORMATION @rdx
示例中的“ dd”(转储dword)命令转储扩展属性的值。目前尚不清楚0x80001值是指什么。“ L3”会命令WinDbg转储3个DWORD值,这些值等于0xC字节 —— EaValueLength字段报告的值。
在用户模式下,调用WldpSetDynamicCodeTrust,内核把扩展属性“ $ Kernel.Purge.TrustClaim”应用于有某种标记的文件,以供后面引用。
最好确认下我们在调试器中命中了CiSetDynamicCodeTrustClaim,查看堆栈框架。如下所示,我们确实从“ MarkAsTrusted” .NET方法(上面有讲过)获得了这个函数:
kd> k
07
逆向WldpQueryDynamicCodeTrust
查看IDA,可以看到WldpQueryDynamicCodeTrust执行的操作是逆向:
(WldpQueryDynamicCodeTrust反汇编的带注释的部分)
这个截图显示了WldpQueryDynamicCodeTrust函数的主要部分,它用相同的枚举值调用NtSetSystemInformation,该枚举值在WldpSetDynamicCodeTrust中传递NtQuerySystemInformation。内核设置的扩展属性“ $ Kernel.Purge.TrustClaim”不执行任何验证。相反,它相信内核已经对它进行验证,它只会看NtQuerySystemInformation是否返回错误/警告 —— 错误/警告设置了高位(即大于或等于0x80000000)的返回值。它用的是“ jns ”指令。
为什么用户模式会信任验证扩展属性的内核。首先看一下CipQueryDynamicCodeTrustClaim的执行。我会跳过一些函数执行,仅显示执行扩展属性数据验证的相关部分:
(CipQueryDynamicCodeTrustClaim扩展属性数据验证)
CipQueryDynamicCodeTrustClaim检索“ $ Kernel.Purge.TrustClaim”扩展属性的数据部分。然后,它将前两个字节与1(上面截图的倒数第二个指令)进行比较。如果将其设置为1,那么CipQueryDynamicCodeTrustClaim认为文件是受信任的。所以,这给为什么尽早设置至少一部分扩展属性数据提供了一些上下文:
kd> dd @rdx+21 L3
静态0x0008的用途还尚不明确,但我并不太担心,因为已验证的只是0x0001值。
WldpIsDynamicCodePolicyEnabled,WldpQueryDynamicCodeTrust和WldpSetDynamicCodeTrust在用户和内核模式下执行以下操作:
WldpIsDynamicCodePolicyEnabled —— 验证是否同时执行了用户模式代码完整性(UMCI)和动态代码安全性(即“ Enabled:Dynamic Code Security”选项)。
WldpSetDynamicCodeTrust —— 在文件上设置NTFS扩展属性“ $ Kernel.Purge.TrustClaim”。
WldpQueryDynamicCodeTrust —— 验证是否在文件上设置了“ $ Kernel.Purge.TrustClaim” NTFS扩展属性。
设置和读取文件上的扩展属性是为了缓解.cs竞争条件劫持攻击?其实它是用来让受信任的用户模式代码可以将已删除的.cs文件标记为受信任的,然后可以在之后验证该文件是否来源于受信任的进程。扩展属性用“ $ Kernel.Purge”前缀的好处是,如果文件被覆盖,内核会自动删除扩展属性。也就是说,劫持.cs文件的行为会强制删除扩展属性,从而使文件“不受信任”。从表面上看,这似乎是个不错的缓解措施...假设缓解措施的应用方式正确,即确保将正确的文件标记为受信任的文件,把不应被标记为受信任的文件标为不受信任的文件。
08
攻击面分析
绕过 “动态代码安全性”缓解措施,需要解决以下问题:
是否会影响调用任意文件的WldpSetDynamicCodeTrust?例如,如果我执行了.cs文件劫持,是否可以以某种方式影响代码(例如MarkAsTrusted方法),使得攻击者提供的文件被标记为受信任?
有没有受信任文件没有得到验证,或者没有得到正确验证?如果是这样,我是否可以影响代码流,使获得的路径不会验证攻击者提供的文件。
既不在当前进程加载wldp.dll,也不提供导出动态代码信任函数的wldp.dll版本,我能否获得主机进程?
能否让WldpIsDynamicCodePolicyEnabled报告未强制执行动态代码安全性?报告未强制执行该路径可能是阻力最小的路径,因为如果未启用此功能,则不会执行文件验证。这个问题是我的攻击研究的首要重点。
09
尝试规避WldpIsDynamicCodePolicyEnabled
有很多动态的部分与动态编译C#代码有关,这加强了对编译文件的受信任程度。尝试规避WldpIsDynamicCodePolicyEnabled,需要强制C#编译进程加载我编写的不受信任的DLL,主要着眼于编译过程的最后阶段-在已编译的DLL上调用System.Reflection.Assembly.Load(byte [ ])。这让我想到了System.CodeDom.Compiler.FromFileBatch方法:
if (!FileIntegrity.IsEnabled)
如果未启用“ FileIntegrity”, DLL会通过常规Assembly.Load方法加载。以下是填充“IsEnabled”属性的代码:
此代码段的执行流程如下:
调用LoadLibraryEx,确保把wldp.dll加载到当前进程。需要明确的是,必须把wldp.dll加载到进程中,因为它是执行的“ DynamicCode”函数逆向的DLL。如果没有把wldp.dll加载到进程中,那就无法进行动态代码验证。攻击者能否以某种方式让wldp.dll无法加载到当前进程中?在正常情况下,把wldp.dll复制到与执行程序相同的目录,执行Windows Defender应用程序控制,逆向PE文件中无关紧要的位(例如,Rich头中的位),然后把wldp.dll签名渲染称无效,让它无法加载。为了缓解这种攻击情况,用2048 flag调用LoadLibraryEx,2048 flag 引用LOAD_LIBRARY_SEARCH_SYSTEM32选项。覆盖默认的DLL加载顺序,首先从%windir%\ System32加载wldp.dll,从而缓解我刚刚描述的攻击。具体点来说,是以非管理员身份缓解了攻击。管理员可以通过修改System32目录中的wldp.dll来执行此攻击。
确保wldp.dll导出执行动态代码验证所需的函数:WldpIsDynamicCodePolicyEnabled,WldpSetDynamicCodeTrust和WldpQueryDynamicCodeTrust。这些是wldp.dll的新函数,并非所有Windows版本都有这些函数。顺便说一下,还有另一种攻击情形。攻击者可能在当前目录中提供未执行过这些函数的就版本wldp.dll。但这不能绕过检查!LoadLibraryEx再次派上用场。
调用WldpIsDynamicCodePolicyEnabled,如果它指示启用了动态代码策略,则返回True。现在,我没有逆向内核代码完整性策略中是否启用动态代码安全性的方式。可能存在值得探索的攻击面。好奇心强的人可以操作一下。
我不确定是否可以绕过“ FileIntegrity.IsEnabled”检查。在下一节继续进行探索。
10
尝试规避WldpQueryDynamicCodeTrust
假设我无法绕过“ FileIntegrity.IsEnabled”检查,继续执行FromFileBatch的下一行:
if (!FileIntegrity.IsTrusted(fileStream2.SafeFileHandle))
如果未被标记为受信任,则此代码段会引发异常,且不会加载已编译的DLL。第一步是识别是否把已编译的DLL标记为受信任的代码,即在文件上调用WldpSetDynamicCodeTrust。.NET中没有该代码,csc.exe也没有,所以我假设该代码可能在csc.exe加载的DLL中。为了确认下,我在Powershell.exe启动时把WinDbg附加到csc.exe,设置了加载wldp.dll的断点(sxe ld wldp),然后在WldpSetDynamicCodeTrust上设置断点。我只有一次到达断点。我登陆了PEWriter::writemscorpehost.dll中的函数。
(PEWriter:读取函数调用 WldpSetDynamicCodeTrust)
这里的攻击情形要在调用WldpSetDynamicCodeTrust前覆盖已编译的DLL。这是不可能的,但是,因为PEWriter::write持有DLL的句柄,并且在保持该句柄的同时拒绝覆盖它,其他任何进程都会被拒绝访问。从获得句柄开始,把DLL写入磁盘,再到调用WldpSetDynamicCodeTrust的那一刻起,就不会释放该句柄。另外,mscorpehost.dll用与System.dll相同的LoadLibraryEx缓解措施来调用wldp.dll函数,防止了上一部分所述的攻击。
之前我提到过,还可以劫持已删除的.cmdline文件,删除/EnforceCodeIntegrity选择。但删除这个选择的副作用是,无法把已编译的DLL标记为受信任,但FromFileBatch有望能让该文件被标记为受信任。所以,当FromFileBatch验证DLL的信任时,它不会被标记为受信任,然后引发异常。
11
尾声
在评估“动态代码安全性”缓解措施时,仍然有可能存在未经探索的攻击面。我特别关注的是用不安全的Assembly.Load(byte [ ])方法加载未签名DLL。一个绕过矢量可能会出现再某处,但从表面看,绕过并不明显。除了前面提到的程序集参考错误外,我还要表扬下Microsoft在缓解竞争条件绕过方面的投入。
12
结论
经过所有这些努力,我没有发现任何绕过。在整个过程中,我了解了如何实施我认为是有效的缓解措施(基于当前的知识/创造力)。在此过程中,我也可能提高了我的进攻研究方法。另外,如果不动手操作下,怎么会发现错误?对我来说,寻找bug就是提高自己的能力。
我花了很多时间来记录这个过程和我的方法,希望本篇文章能对你有所帮助。
谢谢阅读!
木星安全实验室(MxLab),由中国网安·广州三零卫士成立,汇聚国内多名安全专家和反间谍专家组建而成,深耕工控安全、IoT安全、红队评估、反间谍、数据保护、APT分析等高级安全领域,木星安全实验室坚持在反间谍和业务安全的领域进行探索和研究。