Sec Hotspot 首页  排行榜  收藏本站  技术博客  RSS
统计信息
已收录文章数量:18974 篇
已收录公众号数量:91 个
本站文章为爬虫采集,如有侵权请告知
已收录微信公众号
阿里云先知 网安寻路人 网信中国 区块链大本营 白说区块链 区块链投资家 区块链官微 区块链铅笔Blockchain HACK学习呀 二道情报贩子 合天智汇 小白帽学习之路 小米安全中心 弥天安全实验室 SAINTSEC SecPulse安全脉搏 TideSec安全团队 360安全卫士 游侠安全网 计算机与网络安全 安全祖师爷 安全学习那些事 腾讯安全联合实验室 黑客技术与网络安全 安全圈 腾讯御见威胁情报中心 Python开发者 Python之禅 编程派 Python那些事 Python程序员 安全威胁情报 吾爱破解论坛 行长叠报 安在 i春秋 嘶吼专业版 E安全 MottoIN 网信防务 网安杂谈 数说安全 互联网安全内参 漏洞战争 安全分析与研究 邑安全 ChaMd5安全团队 天融信阿尔法实验室 安全牛 SecWiki 安全学术圈 信安之路 漏洞感知 浅黑科技 Secquan圈子社区 奇安信集团 奇安信 CERT 国舜股份 雷神众测 盘古实验室 美团安全应急响应中心 瓜子安全应急响应中心 顺丰安全应急响应中心 蚂蚁金服安全响应中心 携程安全应急响应中心 滴滴安全应急响应中心 字节跳动安全中心 百度安全应急响应中心 腾讯安全应急响应中心 网易安全应急响应中心 OPPO安全应急响应中心 京东安全应急响应中心 Bypass CNNVD安全动态 安恒应急响应中心 天融信每日安全简报 奇安信威胁情报中心 看雪学院 黑白之道 水滴安全实验室 安全客 木星安全实验室 云鼎实验室 绿盟科技安全预警 白帽汇 深信服千里目安全实验室 腾讯玄武实验室 长亭安全课堂 FreeBuf 绿盟科技 nmask
破坏Windows Defender应用程序的控制功能——安全研究案例
本文来自公众号:木星安全实验室   2021.01.25 16:49:45


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