如何利用COM绕过AppLocker CLM

一、前言

约束语言模式(Constrained Language Mode,CLM)是限制PowerShell的一种方法,可以限制PowerShell访问类似Add-Type之类的功能或者许多反射(reflective)方法(攻击者可以通过这些方法,在后续渗透工具中利用PowerShell运行时作为攻击媒介)。

结合微软的描述,这个功能通常可以作为一种安全控制方案,防御方能够利用该功能阻止运行类似Invoke-Mimikatz之类的工具,因为这些工具非常依赖于反射技术。

当我准备渗透部署了CLM的目标环境时,我需要快速了解有哪些潜在方法能够绕过这种保护措施。我搭建了一个Windows 10测试环境,通过默认设置的规则来部署CLM。在本文中,我会与大家分享我的研究结果,介绍如何以非管理员用户身份绕过这种保护机制。

 

二、进入正题

在测试环境中,我们要做的第一件事就是启用AppLocker。在本文中我们将使用Windows默认部署的规则来限制脚本执行。运行Application Identity服务后,我们可以使用如下命令确保本机已启用CLM:

$ExecutionContext.SessionState.LanguageMode

这里我们可以看到该命令的返回结果为ConstrainedLanguage,表明现在我们处于受限环境中。我们可以尝试使用PowerShell中的受限命令来验证这一点:

Add-Type "namespace test { }"

系统的确启用了CLM,那么我们如何才能绕过这个障碍?

 

三、AppLocker CLM中的New-Object

令人惊讶的是,当我开始搜寻CLM存在的攻击面时,我发现当通过AppLocker启用CLM时,New-Object依然可用(尽管仍受到一些限制)。这似乎与预期的场景不一致,但我们的确可以使用如下命令来确认这一点:

New-Object -ComObject WScript.Shell

这给我留下了一种完美方式,可以从PowerShell内部来操控PowerShell进程,这是因为COM对象由DLL托管,而DLL会被加载到调用进程中。那么我们如何才能创建可以被加载的COM对象?如果我们使用ProcMon工具来观察New-Object -ComObject xpntest的调用过程时,我们可以看到该过程会多次请求HKEY_CURRENT_USER注册表项:

仔细观察后,我们可以使用如下脚本来创建HKCU中所需的注册表键值:

现在如果尝试加载我们的COM对象,可以看到我们的DLL已被加载到PowerShell进程空间中:

非常好,现在我们已经可以在受限的上下文环境中,将任意DLL载入PowerShell进程中,无需调用动作太大的CreateRemoteThread或者WriteProcessMemory。但我们的目标是绕过CLM,如何利用我们的非托管(unmanaged)DLL加载方式来完成这个任务?我们可以利用.NET CLR,或者更确切一点,我们可以通过非托管DLL加载.NET CLR来调用.NET assembly(程序集)。

 

四、从非托管DLL到托管DLL到反射

现在我们可以使用类似Cobalt Strike之类的工具,该工具提供了Execute-Assembly功能,可以将CLR加载到非托管进程中,操作起来非常方便。之前我在GIST上公开了一份代码,可以不依赖Cobalt Strike来完成这个任务:

这里我不会讨论代码的详细内容(如果大家感兴趣可以去阅读微软的官方示例),该代码可以让DLL加载.NET CLR,然后加载.NET assembly,然后将执行权传递给特定的方法。

该过程完成后,我们就可以访问.NET,更重要的一点是,我们可以访问.NET的反射功能。接下来我们需要找到启用/关闭CLM的具体位置。

反汇编组成PowerShell的.NET assembly(即System.Management.Automation.dll)后,我们可以看到程序集的System.Management.Automation.Runspaces.RunspaceBase.LanguageMode属性中有一个地方可以标识当前的语言模式。由于我们要使用反射技术,因此需要找到引用Runspace的变量,在运行时修改该变量。我发现要完成该任务,最好的办法就是通过Runspaces.Runspace.DefaultRunspace.SessionStateProxy.LanguageMode,如下所示:

将代码编译成.NET assembly,现在我们就可以通过反射方式禁用CLM,然后只需要创建一个PowerShell脚本加以运行即可:

这样就能大功告成,详细过程可参考演示视频

 

五、背后原理

那么为什么COM可以绕过这种保护机制,PowerShell如何处理COM加载过程?

我们可以在SystemPolicy.IsClassInApprovedList方法中找到答案,该方法用来检查是否允许我们向New-Object提供的CLSID。深入分析该方法,我们可以看到主要工作由如下关键代码负责:

if (SystemPolicy.WldpNativeMethods.WldpIsClassInApprovedList(ref clsid, ref wldp_HOST_INFORMATION, ref num, 0u) >= 0 && num == 1) { ... }

该调用只是WldpIsClassInApprovedList函数(位于wldp.dll中)的一个封装函数,而后者用来检查CLSID是否匹配DeviceGuard(现在为Windows Defender Application Control)策略。由于该方法没有考虑到AppLocker,这意味着通过检查的任何CLSID都可以畅行无阻。

 

六、奇怪的场景

结合前面分析,当我测试这种技术时,我陷入了一个奇怪的场景,当我们使用如下方法设置CLM时,这种技术将无法正常工作:

$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"

这让我有点困惑,因为之前我经常使用如上命令来测试载荷,此时我们面临的环境又有什么不同呢?回到我们的反汇编代码上,我们可以在Microsoft.Powershell.Commands.Utility.dll程序集中找到问题的答案,具体路径位于NewObjectCommand类的BeginProcessing方法中:

这里我们可以看到代码中存在2条路径,具体取决于CLM的启用方式。如果SystemPolicy.GetSystemLockdownPolicy返回Enfore,那么就会执行第1条路径,此时AppLocker或者DeviceGuard处于启用状态(并非我们使用ExecutionContext.SessionState.LanguageMode的场景)。如果直接设置这个属性,我们会直接进入if (!flag)…代码段,此时就会抛出异常。从这里我们可以看到,CLM的行为实际上会根据具体的启用方法(是通过AppLocker、DeviceGuard启用还是通过LanguageMode属性来启用)而有所不同。

本文介绍的方法并不是绕过CLM的唯一方法,即使粗略分析PowerShell,我们也能找到实现类似效果的其他潜在方法。如果大家想了解其他技巧,可以参考Oddvar Moe在Debycon上的精彩演讲

(完)