首页
社区
课程
招聘
[翻译]安全研究方法论案例研究——攻击Windows Defender Application Control
发表于: 2018-7-28 21:46 6141

[翻译]安全研究方法论案例研究——攻击Windows Defender Application Control

2018-7-28 21:46
6141

安全研究方法论案例研究——攻击Windows Defender Application Control


研究简介和研究动机

我经常这样,每当发布新的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”并且观察

为了测试“Enabled:Dynamic Code Security”选项,我将新的配置应用于%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\System32\CodeIntegrity\SIPolicy.p7b

我的第一次测试是尝试调用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模块没有问题。 安全研究的一项重要技能是应用程序根本原因分析,其中包括隔离问题。 所以,此时,我能够确认以下内容:

  • 出于当前未知原因,在代码完整性策略中启用“Dynamic Code Security”会中断对可信代码Add-Type的调用,这并不是我所预期的。 这对我来说是违反直觉的,因为调用Add-Type的可信代码应该不会有问题。 然而,从研究的角度来看,抛出异常也有好处,因为它为我提供了一个具体的位置来识别“Dynamic Code Security”的执行位置。


隔离问题

要找出在上面的截图中抛出异常的代码,我做的第一件事是获取异常的堆栈轨迹。 这里有两个异常,所以我将转储两者的堆栈轨迹:



抛出两个异常后没有堆栈轨迹


通过截图可以看到,没有任何帮助。 现在,在我打开我的.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:

  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字符串,成功了, 它找到并反编译System.dll中的Microsoft.CSharp.CSharpCodeGenerator.CmdArgsFromParameters方法中的以下代码段:

  if(FileIntegrity.IsEnabled) 
  { 
  stringBuilder.Append(“/ EnforceCodeIntegrity”); 
  }

棒棒。 现在要确定导致FileIntegrity.IsEnabled返回true的条件,这一点很明显,可以推断出这是因为.cmdline文件中有/EnforceCodeIntegrity 。


单击“IsEnabled”并观察其引用(右键单击,选择“Analyze”),你会看到它是使用以下代码设置的:

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版本的新功能所以我肯定要做一些逆向工作。


这是一个适当的暂停点。 可能需要逆向很多代码。 从哪儿开始? 要回答这个问题,你要经常提醒自己逆向的目标是什么。 到目前为止,我对整个研究过程了解越多,我的目标就会发生变化和扩展。 我的第一个目标是确认启用“Dynamic Code Security”将阻止我提过的和博客中说过的Device Guard绕过。 当我发现该方法似乎无效时,我的目标随之就变成找出导致其无效的根本原因。 一个新的、间接的目标,引导我转换为攻击者,目的是找出可能欺骗System.dll(或其他可执行类似验证的程序)认为“FileIntegrity”未启用的条件。 我们稍后会回来。 我的优先事项仍然是确定为什么可信代码调用Add-Type不成功。 毕竟,绕过一个无效的缓解方法又有什么好处呢?


因此,我记着提醒自己在定位Add-Type问题的根本原因之后,自己回过头来再看看wldp.dll函数,并理解其工作原理。


根本原因分析

现在,我还不清楚为什么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 be opened — ‘Common Language Runtime Internal error: 0xd0000428’

我感觉这可能与.cmdline文件中包含System.Mangement.Automation.dll引用有关:

/R:"C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll”

0xd0000428错误代码并没有告诉我们多少信息,谷歌搜索也没有发现任何显而易见的东西。 它可能在之后是一个引用框架,所以要重视它。 在逆向中,你可能拥有的一些线索会在以后的某个时刻突显价值。顺便说一下加分项, 你试试将0xd0000428识别为基于0xd前缀转换为HRESULTNTSTATUS值。


查看Add-Type命令的反编译代码,我看到无法避免不使用包含System.Management.Automation.dll引用的csc.exe。 就个人而言,如果我编译与PowerShell相关的代码(例如cmdlet),我宁愿使用-ReferencedAssemblies参数将引用添加到Add-Type,但这只是我自己的想法。


由于早期的堆栈轨迹没有给我任何关于异常源自何处的提示,我决定用WinDbg,并且跟踪下引用了System.Management.Automation.dll的csc.exe中的kernel32!Cre​​ateFileW(最常用于文件操作的函数)的调用情况。 没过多久就看到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
  • WldpQueryDynamicCodeTrust
  • WldpSetDynamicCodeTrust


逆向新的WLDP功能

让我们从WldpIsDynamicCodePolicyEnabled开始,因为它听起来像是在设置或检索动态代码策略信任之前首先调用的函数。 把它放入IDA并加载符号,我们可以看到相对简单的功能(暂未做注释):



IDA中未做注释的WldpIsDynamicCodePolicyEnabled函数


所以,这个函数只包含对NtQuerySystemInformation的简单调用和某种比较。 目前还不清楚从NtQuerySystemInformation中检索到哪种信息。要确定检索的信息,你应该看下通过第一个参数传递的enum值(RCX - x64 ABI函数的第一个参数) - 0xA4(十进制为164)。 但是,没有看到关于该enum值的记录。 幸运的是, 调试符号可以通过转储SYSTEM_INFORMATION_CLASS enum来为我们提供一些信息系。 这可通过在WinDbg中使用以下命令实现:

  dt ole32!SYSTEM_INFORMATION_CLASS

在WinDbg中转储enum后,0xA4解析为“SystemCodeIntegrityPolicyInformation”。 此enum值指定NtQuerySystemInformation返回的结构类型。 返回的是什么结构? 可惜,这也没有相关记录。 为了确定返回的是什么类型的结构,我的想法是在加载的符号中搜索包含字符串“CODEINTEGRITY”和“INFORMATION”的结构。

  dt ole32!* CODEINTEGRITY * INFORMATION *

我很幸运,它返回了一个很好的候选结构定义 - ole32!SYSTEM_CODEINTEGRITYPOLICY_INFORMATION。 现在,我如何确定这是正确的结构? 我将WinDbg中输出的结构大小与传递给NtQuerySystemInformation的结构大小(0x20)进行了比较:

dt -v ole32!SYSTEM_CODEINTEGRITYPOLICY_INFORMATION

struct _SYSTEM_CODEINTEGRITYPOLICY_INFORMATION, 4 elements, 0x20 bytes

   +0x000 Options          : Uint4B

   +0x004 HVCIOptions      : Uint4B

   +0x008 Version          : Uint8B

   +0x010 PolicyGuid       : struct _GUID, 4 elements, 0x10 bytes

此时,我确信这是正确的结构,因为大小匹配,并且结构的名称与enum值的名称相匹配。 现在我有足够的信息将结构应用到IDA中的函数,这使我可以专注于实现NtQuerySystemInformation返回数据比较的函数部分:



验证已配置的代码完整性选项的WldpIsDynamicCodePolicyEnabled中带注释的基本块


那么,“选项”字段是指什么,为什么它要与0x110相对比? 很遗憾,这个字段又没有相关记录,但有时你可以试试在.NET代码中找找enum和结构的定义。 我很幸运,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 enum没有做相应更新,因为并不急需。 因此,我目前的假设是,如果同时启用了UMCI和“Dynamic Code Security”选项,则会启用“DynamicCodePolicy” - 即二进制OR运算0x10和0x100,结果是0x110。 这应该看起来很直观,因为缓解措施与我们在本文中讨论的功能有关,而动态代码执行仅与用户模式代码执行(即UMCI)情况相关。


接下来,我可以研究一下内核如何向WldpIsDynamicCodePolicyEnabled提供SYSTEM_CODEINTEGRITYPOLICY_INFORMATION结构以查看是否存在任何攻击面(即确定操作系统返回“DynamicCodePolicy” 未执行的方法),但我宁愿花时间先了解WldpSetDynamicCodeTrust的实现原理。 这绝对是我作为一个攻击者,想要多说两句的函数,因为顾名思义,它可以从用户模式对一个文件“设置信任”。 对于该功能而言,什么是“信任”? 不知道, 我们继续研究。


逆向WldpSetDynamicCodeTrust

与WldpIsDynamicCodePolicyEnabled一样,WldpSetDynamicCodeTrust也是一个非常直接明了的函数。 它调用了NtSetSystemInformation ,而不是NtQuerySystemInformation,与之前一样,如果我们想要了解该函数,我们需要确定提供给NtSetSystemInformation的enum值和结构类型。



IDA中未做注释的WldpSetDynamicCodeTrust实现


因此,需要给第一个参数(再次通过RCX传递)解析的enum值是0xC7。 使用与上面讨论相同步骤,0xC7解析为“SystemCodeIntegrityVerificationInformation”,其对应于另一个没有相关记录的结构:SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION。

dt -v ole32!SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION

struct _SYSTEM_CODEINTEGRITYVERIFICATION_INFORMATION, 3 elements, 0x18 bytes

   +0x000 FileHandle       : Ptr64 to Void

   +0x008 ImageSize        : Uint4B

   +0x010 Image            : Ptr64 to Void

所以我们只能看到WldpSetDynamicCodeTrust接收文件句柄作为参数并将其传递给NtSetSystemInformation。 如果你看了NtSetSystemInformation的实现,你会发现它只是一个系统调用 。 因此,为了确定SystemCodeIntegrityVerificationInformation在转移到内核后的行为,我们需要逆向一些内核代码。 一种策略是使用内核调试器将踪内核中的系统调用,或尝试找到可能与“动态代码信任”功能相关的代码,在该函数上设置断点,并查看是否到达该函数。 如果幸运的话,我们会尝试后者,因为后者会更快产生结果。


第一个寻找相关功能的明显位置是ntoskrnl.exe,因为那是在内核端实现NtSetSystemInformation的模块。 根据经验,我也知道代码完整性/镜像验证功能是在ci.dll中实现的(ci – 表示代码完整性)。 这是我想要看的第一个地方,所以我将它加载到IDA,并应用符号,然后搜索其名称中包含“DynamicCode”的函数。

我的搜索结果有以下函数:

  • SIPolicyDynamicCodeSecurityEnabled
  • CiValidateDynamicCodePages
  • CipQueryDynamicCodeTrustClaim
  • CiSetDynamicCodeTrustClaim
  • CiHvciValidateDynamicCodePages

看到与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中也很有用:

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 L3

ffffb48f`f390bed1  00080001 00000000 00000000

示例中的“dd”(dump dword)命令转储扩展属性的值。 目前还不清楚0x80001值是指什么。 为了清楚起见,“L3”告诉WinDbg转储3个等于0xC字节的DWORD值 - EaValueLength字段的值。


因此,到目前为止,我的理解是,当在用户模式下调用WldpSetDynamicCodeTrust时,内核将“$ Kernel.Purge.TrustClaim”扩展属性应用于稍后要引用的某种标记文件。 逆向WldpQueryDynamicCodeTrust也应该证实这个理论。


此外,我们实际在调试器中点击CiSetDynamicCodeTrustClaim并查看堆栈帧是有帮助的的。 如下所示,我们确实从“MarkAsTrusted”.NET方法(如前所述)中找到了此函数:

kd> k

 # Child-SP          RetAddr           Call Site

00 ffff818a`78d97578 fffff806`2349a087 CI!CiSetDynamicCodeTrustClaim

01 ffff818a`78d97580 fffff800`e5cb1e06 CI!CiSetInformation+0x27

02 ffff818a`78d975b0 fffff800`e57c0223 nt!NtSetSystemInformation+0x17c2fe

03 ffff818a`78d97a80 00007ffe`ef53d3c4 nt!KiSystemServiceCopyEnd+0x13

04 000000ec`ec88e6d8 00007ffe`eaec4259 ntdll!NtSetSystemInformation+0x14

05 000000ec`ec88e6e0 00007ffe`c33d39fa wldp!WldpSetDynamicCodeTrust+0x29

06 000000ec`ec88e730 000002a6`acbf0bf8 System_ni!DomainBoundILStubClass.IL_STUB_PInvoke(Int32 ByRef)$##6000000+0x14a

07 000000ec`ec88e738 000002a6`acc03880 0x000002a6`acbf0bf8

08 000000ec`ec88e740 000002a6`acbf0bf8 0x000002a6`acc03880

09 000000ec`ec88e748 00007ffe`c4de46d5 0x000002a6`acbf0bf8

0a 000000ec`ec88e750 00007ffe`c33c96bf clr!ThePreStub+0x55

0b 000000ec`ec88e800 000002a6`acb4bf50 System_ni!System.CodeDom.Compiler.FileIntegrity.MarkAsTrusted(Microsoft.Win32.SafeHandles.SafeFileHandle)$##6003CEB+0xf

逆向WldpQueryDynamicCodeTrust

我期望从WldpQueryDynamicCodeTrust的实现中看到的实际上是WldpSetDynamicCodeTrust的反向功能。 IDA中如下的展示证实了这个理论:



WldpQueryDynamicCodeTrust反汇编的注释部分


此截图显示了WldpQueryDynamicCodeTrust函数的主要部分,它使用在WldpSetDynamicCodeTrust中传递给NtSetSystemInformation的相同enum值调用NtQuerySystemInformation。 没有对内核设置的“$ Kernel.Purge.TrustClaim”扩展属性执行实际验证。 相反,它信任内核正确验证了它并且它只考虑NtQuerySystemInformation是否返回错误/警告 - 错误/警告是具有高位设置(即大于或等于0x80000000)的返回值。 多说一句,我之所以知道这些是基于对“ jns ”指令的使用认知。


因此,你可能很想知道为什么用户模式应该信任内核验证扩展属性。 我也好奇,所以我决定看看CipQueryDynamicCodeTrustClaim的实现。 我将跳过一些函数实现,只显示执行扩展属性数据验证的相关部分:



CipQueryDynamicCodeTrustClaim扩展属性数据验证


CipQueryDynamicCodeTrustClaim检索“$ Kernel.Purge.TrustClaim”扩展属性的数据部分。 然后,它将前两个字节与1比较(上面屏幕截图中的倒数第二个指令)。 如果设置为1,则CipQueryDynamicCodeTrustClaim认为该文件是可信的。 因此,这提供了一些关于为什么至少部分扩展属性数据之前已被设置的内容:

kd> dd @rdx+21 L3

ffffb48f`f390bed1  00080001 00000000 00000000
注:rdx的Twitter

目前还不清楚静态0x0008值用于什么,但是我并不太关心它,因为我所看到的所有验证值都是0x0001。


所以,此刻,我相信我对如何从用户和内核模式实现WldpIsDynamicCodePolicyEnabled,WldpQueryDynamicCodeTrust和WldpSetDynamicCodeTrust有了相当清楚的理解。 总而言之,它们的行为如下:

  • WldpIsDynamicCodePolicyEnabled - 验证是否执行了用户模式代码完整性(UMCI)和动态代码安全(即“Enabled:Dynamic Code Security”选项)。
  • WldpSetDynamicCodeTrust - 在文件上设置“$ Kernel.Purge.TrustClaim”NTFS扩展属性。
  • WldpQueryDynamicCodeTrust - 验证是否在文件上设置了“$ Kernel.Purge.TrustClaim”NTFS扩展属性。

那么为了避免.cs 竞争条件劫持攻击 ,最终设置和读取文件扩展属性的目的是什么呢? 好吧,它的用处是以便可信用户模式代码可以将生成的.cs文件标记为可信,然后之后可以将其验证为来自可信进程。 为扩展属性使用“$ Kernel.Purge”前缀的好处是,如果文件被覆盖,内核将自动去除扩展属性。 这意味着博文中描述的劫持.cs文件的行为将强制删除扩展属性,使文件“不可信”。从表面上看,这似乎应该是一个很好的缓解措施...前提是缓解应用正确操作 - 即确保将正确的文件标记为受信任,并且不该被标记为可信的文件不应当受信任。


既然我已经很好地理解了事情的原委,现在我已经可以研究潜在的攻击面。


攻击面分析

想要绕过我现在非常熟悉的“Dynamic Code Security”缓解措施,我问自己以下问题:

  1. 是否可以对任意文件调用WldpSetDynamicCodeTrust?例如,如果我劫持.cs文件,我是否可能以某种方式修改代码(如MarkAsTrusted方法)将攻击者的文件标记为受信任?
  2. 是否有文件信任未经过验证或未经过正确地验证?如果是这样,我是否可以更改代码流使得攻击者的文件不会发生被验证。
  3. 我是否可以以某种方式让宿主进程要么不将wldp.dll加载到当前进程中,要么提供不导出动态代码信任函数的wldp.dll版本?
  4. 我是否可以更改WldpIsDynamicCodePolicyEnabled返回动态代码安全未执行的状态?这可能是阻力最小的路径,因为如果未启用该功能,则不会执行文件验证。这个问题将是我攻击研究的第一个焦点。


鉴于我目前对缓解实施的理解以及我对扩展属性实现的理解,我认为这些问题已经很全面了。但作为研究人员,不应该声称已经考虑过所有可能的攻击面。你作为一名攻击性研究者,本身就受到了知识和创造力的限制,这两者以后都有可能扩展并使你认识到以前从未了解到的攻击面。


试图绕过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”属性是如何填充的,代码如下(之前也展示过):

 

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;

});

此代码段的工作流程如下:

  1. 通过调用LoadLibraryEx确保将wldp.dll加载到当前进程中。为了清楚起见,必须将wldp.dll加载到进程中,因为它是实现我们之前逆向的“DynamicCode”函数的DLL。如果未将wldp.dll加载到进程中,则无法进行动态代码验证。攻击者能够以某种方式迫使wldp.dll无法加载到当前进程中吗?对!在正常情况下很容易,只需通过将wldp.dll复制到与执行程序相同的目录来强制执行Windows Defender Application Control,在PE文件中翻转一个无关紧要的位(例如,Rich头中的位),修改wldp.dll的签名为无效,就能导致无法加载。为了缓解这种攻击情形,这就是使用2048 flag调用LoadLibraryEx的原因,该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”验证。接下来是什么?请看下一节。


试图绕过WldpQueryDynamicCodeTrust

所以,假设我无法绕过“FileIntegrity.IsEnabled”检查,我只需进入FromFileBatch方法的下一行:

if (!FileIntegrity.IsTrusted(fileStream2.SafeFileHandle))

{

    throw new IOException(SR.GetString("FileIntegrityCheckFailed", new object[]

    {

        options.OutputAssembly

    }));

}

这段代码会抛出异常,如果未标记为受信任,则不加载已编译的DLL。我意识到这可能听起来多余,但我更喜欢做出以下逆逻辑陈述:如果文件被标记为受信任,则不会抛出异常。用这种方式陈述的逻辑使得作为攻击者的我更明了了,我应该尝试找到一种方法来更改代码以将编译的DLL标记为可信。验证可行性的第一步是找到标记编译的DLL为可信的代码 - 即在文件上调用WldpSetDynamicCodeTrust。 .NET中没有该代码,因此我查看了csc.exe。它也没有,所以我认为该代码可能存在于由csc.exe加载的DLL中。为了验证,我在powerhell.exe启动时将WinDbg附加到csc.exe,为wldp.dll加载时设置断点(sxe ld wldp),然后在WldpSetDynamicCodeTrust上设置断点。我只找到了一次断点。我找到了PEWriter::writemscorpehost.dll中的函数。


幸运的是,该PEWriter::write函数是开源的。不幸的是,调用WldpSetDynamicCodeTrust的最新版本还没有发布在GitHub上。没关系。旧版本源代码使IDA反汇编更加容易。这是调用WldpSetDynamicCodeTrust的代码部分:



PEWriter::write函数调用WldpSetDynamicCodeTrust


所以这里的攻击场景需要在调用WldpSetDynamicCodeTrust之前覆盖已编译的DLL。这是不可能的,但是因为PEWriter::write包含DLL的句柄,并且在句柄处于hold状态时,任何其他进程将无法将之覆盖。从获取句柄开始将DLL写入磁盘到调用WldpSetDynamicCodeTrust的过程中,不会释放句柄。此外,mscorpehost.dll使用与System.dll相同的LoadLibraryEx缓解措施来调用wldp.dll函数,从而防止上一节中描述的攻击。


我之前提到过,也可以劫持生成的cmdline文件并删除/EnforceCodeIntegrity开关。我能够成功劫持文件并删除开关,但最终,这并没有输出任何有价值的东西。删除开关的副作用是编译的DLL不会被标记为受信任,但FromFileBatch方法需要文件被标记为受信任。因此,当FromFileBatch验证DLL的可信度时,它将不会被标记为受信任,并且将抛出异常。


尾声

在评估“动态代码安全”缓解措施上,仍有可能存在尚未探索出来的攻击面。我特别关注的是使用不安全的Assembly.Load(byte [])方法可以加载未签名的DLL。在某处可能存在攻击性的绕过方法,目前并不明了。除了前面提到的程序集引用bug之外,我还要赞扬微软在缓解竞争条件绕过上投入了大量精力。


结论

所以,经过所有这些努力,我找到绕过方法了吗? 并没有。我作为逆向人员和研究员失败了吗?也没有失败一说!在整个过程中,我学会了如何实现我所认为有效的缓解措施(基于我目前的知识/创造力)。我也在这个过程中使我的攻击性研究方法变得更成熟。另外,如果你不先看bug,怎么会发现bug呢?对我来说,寻找bug不仅仅是寻找bug,也满足了我的好奇心。


我花了很多时间记录整个过程和我的方法,所以我希望你觉得它有趣,有教育意义,并能有所激励。我也希望这篇文章能激励你讨论并记录你针对特定问题的研究方法。有什么更好的方法能够帮助那些会被过程吓倒的人呢?!


此外,希望微软修复这个愚蠢的程序集引用bug。如果不修复,那么那些看起来需要耗费很大精力才能实现的缓解措施将没有任何意义。


谢谢阅读!


原文链接:https://posts.specterops.io/documenting-and-attacking-a-windows-defender-application-control-feature-the-hard-way-a-case-73dd1e11be3a

编译:看雪翻译小组 SpearMint

校对:看雪翻译小组 skeep



[课程]Android-CTF解题方法汇总!

最后于 2019-2-1 09:47 被kanxue编辑 ,原因:
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//