破坏Windows Defender应用程序的控制功能——安全研究案例
2021-01-25 17:49:45 Author: sec.thief.one(查看原文) 阅读量:174 收藏

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"?><SiPolicy xmlns="urn:schemas-microsoft-com:sipolicy">  <VersionEx>1.0.0.0</VersionEx>  <PolicyTypeID>{A244370E-44C9-4C06-B551-F6016E563076}</PolicyTypeID>  <PlatformID>{2E07F7E4-194C-4D20-B7C9-6F44A6C5A234}</PlatformID>  <Rules>    <Rule>      <Option>Enabled:Unsigned System Integrity Policy</Option>    </Rule>    <Rule>      <Option>Enabled:Advanced Boot Options Menu</Option>    </Rule>    <Rule>      <Option>Enabled:UMCI</Option>    </Rule>    <Rule>      <Option>Enabled:Update Policy No Reboot</Option>    </Rule>    <Rule>      <Option>Enabled:Dynamic Code Security</Option>    </Rule>  </Rules>  <!--EKUS-->  <EKUs />  <!--File Rules-->  <FileRules>    <Allow ID="ID_ALLOW_A_1" FileName="*" />    <Allow ID="ID_ALLOW_A_2" FileName="*" />  </FileRules>  <!--Signers-->  <Signers />  <!--Driver Signing Scenarios-->  <SigningScenarios>    <SigningScenario Value="131" ID="ID_SIGNINGSCENARIO_DRIVERS_1" FriendlyName="Auto generated policy on 08-17-2015">      <ProductSigners>        <FileRulesRef>          <FileRuleRef RuleID="ID_ALLOW_A_1" />        </FileRulesRef>      </ProductSigners>    </SigningScenario>    <SigningScenario Value="12" ID="ID_SIGNINGSCENARIO_WINDOWS" FriendlyName="Auto generated policy on 08-17-2015">      <ProductSigners>        <FileRulesRef>          <FileRuleRef RuleID="ID_ALLOW_A_2" />        </FileRulesRef>      </ProductSigners>    </SigningScenario>  </SigningScenarios>  <UpdatePolicySigners />

该策略允许执行所有用户模式和内核模式代码。在这一点上,我不确定这个宽松的策略是否会产生任何明显的执行差异,但是值得一试。我用以下PowerShell命令启用了该策略,然后重新启动:

ConvertFrom-CIPolicy -XmlFilePath。\ AllowAll_Modified.xml -BinaryFilePath C:\ Windows \ System 32 \ CodeIntegrity \ SIPolicy.p7b

我的第一个强制测试:调用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){    stringBuilder.Append("/EnforceCodeIntegrity ");}

现在,识别让FileIntegrity.IsEnabled正确返回的条件,因为.cmdline文件里有/EnforceCodeIntegrity,所以可以推断出这是正确的。

单击“ IsEnabled”,观察其引用(右键单击,选择“ 分析”),你会看到它已设置为以下代码:

private static readonly Lazy<bool> s_lazyIsEnabled = new Lazy<bool>(delegate(){    Version version = Environment.OSVersion.Version;    if (version.Major < 6 || (version.Major == 6 && version.Minor < 2))    {        return false;    }    bool result;    using (SafeLibraryHandle safeLibraryHandle = SafeLibraryHandle.LoadLibraryEx("wldp.dll", IntPtr.Zero, 2048))    {        if (safeLibraryHandle.IsInvalid)        {            result = false;        }        else        {            IntPtr moduleHandle = UnsafeNativeMethods.GetModuleHandle("wldp.dll");            if (!(moduleHandle != IntPtr.Zero) || !(IntPtr.Zero != UnsafeNativeMethods.GetProcAddress(moduleHandle, "WldpIsDynamicCodePolicyEnabled")) || !(IntPtr.Zero != UnsafeNativeMethods.GetProcAddress(moduleHandle, "WldpSetDynamicCodeTrust")) || !(IntPtr.Zero != UnsafeNativeMethods.GetProcAddress(moduleHandle, "WldpQueryDynamicCodeTrust")))            {                result = false;            }            else            {                int num = 0;                int errorCode = UnsafeNativeMethods.WldpIsDynamicCodePolicyEnabled(out num);                Marshal.ThrowExceptionForHR(errorCode, new IntPtr(-1));                result = (num != 0);            }        }    }    return result;});

这是基于对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.dllcould notbe opened — ‘Common Language Runtime Internal error: 0xd0000428

我觉得这和把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_INFORMATIONstruct _SYSTEM_CODEINTEGRITYPOLICY_INFORMATION, 4 elements, 0x20 bytes   +0x000 Options          : Uint4B   +0x004 HVCIOptions      : Uint4B   +0x008 Version          : Uint8B   +0x010 PolicyGuid       : struct _GUID, 4 elements, 0x10 bytes

大小匹配,可以确信这是正确的结构,并且结构名称和枚举值匹配。现在有足够的信息把结构应用到IDA的函数,接下来就可以把重点放在执行从NtQuerySystemInformation返回的数据比较的函数:

(WldpIsDynamicCodePolicyEnabled带注释的基本块,用于验证已配置的代码完整性选项)

那么,“选项”字段指的是什么?为什么将其与0x110进行比较?这个字段也没有记录下来,但有时可以在.NET代码中查找枚举和结构定义,从而进行规避。System.Management.Automation.dll代码有一些值:

internal enum CodeIntegrityPolicyOptions : uint{  CODEINTEGRITYPOLICY_OPTION_ENABLED = 1u,  CODEINTEGRITYPOLICY_OPTION_AUDIT_ENABLED,  CODEINTEGRITYPOLICY_OPTION_WHQL_SIGNED_ENABLED = 4u,  CODEINTEGRITYPOLICY_OPTION_EV_WHQL_SIGNED_ENABLED = 8u,  CODEINTEGRITYPOLICY_OPTION_UMCI_ENABLED = 16u,  CODEINTEGRITYPOLICY_OPTION_SCRIPT_ENFORCEMENT_DISABLED = 32u,  CODEINTEGRITYPOLICY_OPTION_HOST_POLICY_ENFORCEMENT_ENABLED = 64u,  CODEINTEGRITYPOLICY_OPTION_POLICY_ALLOW_UNSIGNED = 128u}

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_INFORMATIONstruct _SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION, 3 elements, 0x18 bytes   +0x000 FileHandle       : Ptr64 to Void   +0x008 ImageSize        : Uint4B   +0x010 Image            : Ptr64 to Void

所以,我们只能看到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   +0x000 NextEntryOffset  : 0   +0x004 Flags            : 0 ''   +0x005 EaNameLength     : 0x18 ''   +0x006 EaValueLength    : 0xc   +0x008 EaName           : [1]  "$"kd> dd @rdx+21 L3ffffb48f`f390bed1  00080001 00000000 00000000

示例中的“ dd”(转储dword)命令转储扩展属性的值。目前尚不清楚0x80001值是指什么。“ L3”会命令WinDbg转储3个DWORD值,这些值等于0xC字节 —— EaValueLength字段报告的值。

在用户模式下,调用WldpSetDynamicCodeTrust,内核把扩展属性“ $ Kernel.Purge.TrustClaim”应用于有某种标记的文件,以供后面引用。

最好确认下我们在调试器中命中了CiSetDynamicCodeTrustClaim,查看堆栈框架。如下所示,我们确实从“ MarkAsTrusted” .NET方法(上面有讲过)获得了这个函数:

kd> k # Child-SP          RetAddr           Call Site00 ffff818a`78d97578 fffff806`2349a087 CI!CiSetDynamicCodeTrustClaim01 ffff818a`78d97580 fffff800`e5cb1e06 CI!CiSetInformation+0x2702 ffff818a`78d975b0 fffff800`e57c0223 nt!NtSetSystemInformation+0x17c2fe03 ffff818a`78d97a80 00007ffe`ef53d3c4 nt!KiSystemServiceCopyEnd+0x1304 000000ec`ec88e6d8 00007ffe`eaec4259 ntdll!NtSetSystemInformation+0x1405 000000ec`ec88e6e0 00007ffe`c33d39fa wldp!WldpSetDynamicCodeTrust+0x2906 000000ec`ec88e730 000002a6`acbf0bf8 System_ni!DomainBoundILStubClass.IL_STUB_PInvoke(Int32 ByRef)$##6000000+0x14a07 000000ec`ec88e738 000002a6`acc03880 0x000002a6`acbf0bf808 000000ec`ec88e740 000002a6`acbf0bf8 0x000002a6`acc0388009 000000ec`ec88e748 00007ffe`c4de46d5 0x000002a6`acbf0bf80a 000000ec`ec88e750 00007ffe`c33c96bf clr!ThePreStub+0x550b 000000ec`ec88e800 000002a6`acb4bf50 System_ni!System.CodeDom.Compiler.FileIntegrity.MarkAsTrusted(Microsoft.Win32.SafeHandles.SafeFileHandle)$##6003CEB+0xf

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 L3ffffb48f`f390bed1  00080001 00000000 00000000

静态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

攻击面分析

绕过 “动态代码安全性”缓解措施,需要解决以下问题:

  1. 是否会影响调用任意文件的WldpSetDynamicCodeTrust?例如,如果我执行了.cs文件劫持,是否可以以某种方式影响代码(例如MarkAsTrusted方法),使得攻击者提供的文件被标记为受信任?

  2. 有没有受信任文件没有得到验证,或者没有得到正确验证?如果是这样,我是否可以影响代码流,使获得的路径不会验证攻击者提供的文件。

  3. 既不在当前进程加载wldp.dll,也不提供导出动态代码信任函数的wldp.dll版本,我能否获得主机进程?

  4. 能否让WldpIsDynamicCodePolicyEnabled报告未强制执行动态代码安全性?报告未强制执行该路径可能是阻力最小的路径,因为如果未启用此功能,则不会执行文件验证。这个问题是我的攻击研究的首要重点。

09

尝试规避WldpIsDynamicCodePolicyEnabled

有很多动态的部分与动态编译C#代码有关,这加强了对编译文件的受信任程度。尝试规避WldpIsDynamicCodePolicyEnabled,需要强制C#编译进程加载我编写的不受信任的DLL,主要着眼于编译过程的最后阶段-在已编译的DLL上调用System.Reflection.Assembly.Load(byte [ ])。这让我想到了System.CodeDom.Compiler.FromFileBatch方法:

if (!FileIntegrity.IsEnabled){    compilerResults.CompiledAssembly = Assembly.Load(array, null, options.Evidence);    return compilerResults;}if (!FileIntegrity.IsTrusted(fileStream2.SafeFileHandle)){    throw new IOException(SR.GetString("FileIntegrityCheckFailed", new object[]    {        options.OutputAssembly    }));}compilerResults.CompiledAssembly = CodeCompiler.LoadImageSkipIntegrityCheck(array, null, options.Evidence);return compilerResults;

如果未启用“ FileIntegrity”, DLL会通过常规Assembly.Load方法加载。以下是填充“IsEnabled”属性的代码:

此代码段的执行流程如下:

  1. 调用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来执行此攻击。

  2. 确保wldp.dll导出执行动态代码验证所需的函数:WldpIsDynamicCodePolicyEnabled,WldpSetDynamicCodeTrust和WldpQueryDynamicCodeTrust。这些是wldp.dll的新函数,并非所有Windows版本都有这些函数。顺便说一下,还有另一种攻击情形。攻击者可能在当前目录中提供未执行过这些函数的就版本wldp.dll。但这不能绕过检查!LoadLibraryEx再次派上用场。

  3. 调用WldpIsDynamicCodePolicyEnabled,如果它指示启用了动态代码策略,则返回True。现在,我没有逆向内核代码完整性策略中是否启用动态代码安全性的方式。可能存在值得探索的攻击面。好奇心强的人可以操作一下。

我不确定是否可以绕过“ FileIntegrity.IsEnabled”检查。在下一节继续进行探索。


10

尝试规避WldpQueryDynamicCodeTrust

假设我无法绕过“ FileIntegrity.IsEnabled”检查,继续执行FromFileBatch的下一行:

if (!FileIntegrity.IsTrusted(fileStream2.SafeFileHandle)){    throw new IOException(SR.GetString("FileIntegrityCheckFailed", new object[]    {        options.OutputAssembly    }));}

如果未被标记为受信任,则此代码段会引发异常,且不会加载已编译的DLL。第一步是识别是否把已编译的DLL标记为受信任的代码,即在文件上调用WldpSetDynamicCodeTrust。.NET中没有该代码,csc.exe也没有,所以我假设该代码可能在csc.exe加载的DLL中。为了确认下,我在Powershell.exe启动时把WinDbg附加到csc.exe,设置了加载wldp.dll的断点( sxe ld wldp ),然后在WldpSetDynamicCodeTrust上设置断点。我只有一次到达断点。我登陆了PEWriter::writemscorpehost.dll中的函数。

PEWriter::write 函数(https://github.com/dotnet/coreclr/blob/20275aa647c5733bc5b1929cba3fd1094c67fb1d/src/dlls/mscorpe/pewriter.cpp#L2179-L2276)是开源的。但调用WldpSetDynamicCodeTrust的最新版本不在GitHub上。没关系。过时的源代码能让IDA反汇编更加容易。这是调用WldpSetDynamicCodeTrust的代码部分:

(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分析等高级安全领域,木星安全实验室坚持在反间谍和业务安全的领域进行探索和研究。

文章来源: https://sec.thief.one/article_content?a_id=af9c9f2b1ef5201dc5d134854c674218
如有侵权请联系:admin#unsafe.sh