红队秘籍:BYOI(自带解释器)
Marcello Salvati
这篇篇幅较长的博文旨在为红队操作员提供思路,指导如何将BYOI(自带解释器)技术融入自定义工具中,激发恶意软件开发的创造力。本文也可作为.NET的“轻量”入门指南,介绍如何编写基础C2(命令与控制)代码,因此无论技能水平如何,读者都能有所收获。我们将从零开始,开发一个基础PowerShell植入程序,利用BYOI概念在最新版Windows 10上绕过Defender和AMSI。过程中,我还会解释支撑BYOI的一些核心概念。
我发布的演讲和工具/代码(如SILENTTRINITY和OffensiveDLR仓库中的概念验证示例)在引起社区对BYOI主要优势的关注方面取得了一定成功(我认为),但我肯定能更好地提供一些技术示例,供操作员在日常任务中使用并融入自有工具,而无需依赖我发布在Github上的内容。
过去一年左右,我还在多个会议上“厚颜无耻”地谈论BYOI载荷和技术。随着我对.NET开发和技术理解的不断深入和变化,我尝试不断更新演讲内容,反映新学到的知识并修正之前版本的错误。因此,如果您不熟悉BYOI背后的概念,我强烈建议观看我在BSides Puerto Rico 2019上的演讲,这是最新版本。但在撰写本文时,该会议的录像似乎尚未发布(演讲幻灯片在Github上),所以如果没有这个,我推荐观看我在Derbycon 2019上的演讲。尽管其中有些内容我现在知道是不正确的,当然也有些本可以解释得更好,但它足以让您了解核心概念(或者您也可以直接阅读本博文的后续部分)。
在深入代码之前,我们先花点时间回顾一些关键的.NET和BYOI要点,并讨论为什么我认为这种技术从攻击视角看既实用又优雅。
.NET关键概念
已经熟悉.NET的读者可以跳过本节,我添加此节是为了内容的完整性。
网上有大量文章(包括官方Microsoft文档)试图解释.NET是什么。就个人而言,我发现其中大多数要么不适合从未在.NET上开发过的人,要么过于模糊以至于解释变得毫无意义。这在我刚开始尝试编写C#时让事情变得不简单:我来自Python背景,没有正式的编程基础,这真是一个全新的世界。
以下是我对.NET的谦逊解释:
.NET是一个语言无关的开发平台,由一套工具、基础设施和库组成,使您能够创建跨平台应用程序。
麦克风掉落 让仇恨邮件开始吧。
我想指出那句话的一个关键点(尤其是在本博文的上下文中):没有提及平台实际使用什么编程语言。
很多人倾向于将特定的编程语言与.NET关联,最常见的是C#。然而,C#只是与平台交互的事实上的语言,而不是平台本身。.NET提供的基础设施和工具部分允许您编写自己的编程语言与之交互。这是一个极其重要的理解点:.NET是语言无关的,它不绑定特定的编程语言。
.NET还有各种实现(可以说是“风味”),这对许多.NET新手来说是个常见的痛点。最常见的“风味”是:
- .NET Framework
- .NET Core
“.NET Framework”是.NET的原始实现,已经存在很久了,这个特定的实现极度特定于Windows,并与操作系统本身紧密集成。需要提及的重要一点:在本博文后续部分,当我谈论.NET时,除非明确说明,否则我指的是这个特定实现。这是因为我们将专门为Windows构建植入程序,并使用“面向”.NET Framework的语言。
“.NET Core”是最新的实现,也是.NET平台本身的未来。.NET Framework将在不久的将来被.NET Core取代。与.NET Framework不同,.NET Core是真正的跨平台,如果您想构建跨平台应用程序,您会“面向”.NET Core。
我们还需要讨论.NET程序集,因为它们是开发平台的基本组成部分。.NET程序集是所有.NET语言可以解释和执行的一个单一执行单元。通过编译任何.NET语言,您会得到一个可执行文件或DLL形式的.NET程序集。
关于.NET程序集的几个关键点:
- .NET程序集格式与通过非托管语言(如C++、C)生成的.exe或.dll不同。
- 它们可以被任何.NET语言(包括第三方语言)执行。
- 它们可以通过调用
Assembly.Load()进行反射加载。
本质上,.NET凭借Assembly.Load()函数原生支持反射式PE/DLL注入。我们将在嵌入过程中大量使用此函数。
.NET Framework 4.8版本引入了对.NET程序集的AMSI扫描。所以现在每当您调用Assembly.Load()函数时,该程序集将被传递给AMSI/Defender进行检查。
BYOI关键概念
攻击工具从PowerShell迁移到C#带来了一些操作上的劣势。在我看来,最大的劣势是现在所有您的工具/载荷/植入程序都需要编译。这可能听起来没什么大不了的,但从长远来看,这在红队操作中可能是一个巨大的时间消耗,并且使事情变得非常繁琐。
别误会,C#编译可以使用CI/CD管道完全自动化(参见Dominic精彩的“攻击性开发:如何为您的红队实施DevOps”演讲)或使用Roslyn编译器(这是Covenant用来处理所有必要编译的方法),然而这两种方法在设置、样板代码和/或要求方面仍然需要相当重的开销。它们不如PowerShell技术过去那样灵活、简单和直接,或者任何使用脚本语言的技术。
我想要的是将范式转移回PowerShell风格的技术:只需在服务器端托管一些源代码,将其扔到受感染的端点,并动态评估/执行它,最好在内存中(这正是PowerShell Empire所做的),只是不使用实际的PowerShell(因为所有现有的保护措施),同时仍然能够访问那些强大的.NET Framework API。听起来要求很高?结果发现,并非如此!
之前我们谈到.NET平台是语言无关的,并提供基础设施来构建您自己的编程语言与之交互。因此,存在许多.NET语言,一些由Microsoft官方支持,其他由第三方构建。其中一部分是脚本语言,不需要编译(至少表面上是)。此外,所有这些语言都构建在相同底层平台上的一个后果是它们都可以相互操作,这也意味着它们暴露API/提供极其简单的方法来在另一种.NET语言中嵌入或“托管”一种语言。
这是支撑BYOI技术的主要概念:我们可以像这些年使用PowerShell一样使用第三方.NET脚本语言。我们所要做的就是在一个默认存在于Windows中的.NET语言(如C#甚至PowerShell)中“托管”(嵌入)一个,然后砰!我们又回到了PowerShell风格技术的美好旧时光。
我想强调一点:这绝不是新颖的概念,事实上,您可能在不自知的情况下使用过这样做的工具:p0wnedshell、PowerLine和NPS都“托管”(嵌入)PowerShell运行时到C#二进制文件中,以便不通过主PowerShell可执行文件执行PowerShell代码。也有工具反向操作,将C#嵌入PowerShell。
我们要做的是将第三方脚本语言(本例中是Boolang)嵌入PowerShell。这些第三方语言中的每一个都带有自己独特的技术和OPSEC优势。
.NET脚本语言
当我刚开始研究这个时,我尝试的第一个.NET脚本语言是IronPython。我很快意识到,从攻击角度看,它并不是最实用的,原因有很多,首要的是它不原生支持PInvoke(从.NET语言调用非托管Windows API的能力):您必须使用其标准库实现中的一个模块,但如果在内存中运行的IronPython引擎内调用它,该模块会崩溃。
这导致我进行了非常长时间的谷歌搜索,试图找到一个原生支持PInvoke并且能够在内存中引擎/运行时/编译器内使用它的.NET脚本语言。
一段时间后,我在Github上的Boo仓库中偶然发现了这个wiki页面。此时,我的第一个问题是:“Boo到底是什么?”正如其所述:
“Boo是一种面向对象的静态类型编程语言,用于.NET和Mono运行时,具有受Python启发的语法,并特别关注语言和编译器的可扩展性。” – (来源: https://github.com/boo-lang/boo/wiki)
我后来从参加我DefCon Demo Labs演讲的一位与会者那里得知,Boolang最初是为Unity游戏引擎创建的脚本语言(知道得越多…)
从武器化角度看,Boo简直完美,它原生支持PInvoke,暴露“托管”API以将其嵌入其他语言,并直接在内存中编译。
在找到Boo之前,我偶然发现了许多其他语言。以下是我认为从武器化角度看特别有趣的语言列表(还有很多其他语言):
- Boolang
- ClearScript
- SSharp
- Dotnet-webassembly
- Jint
在所有这些中,ClearScript也非常有趣且值得一提,因为它是一个官方的Microsoft项目。它暴露了一个可以嵌入的Jscript/VBscript实现。此外,它允许您将.NET CLR暴露给Jscript/VBscript。这意味着您可以从ClearScript的Jscript/VBscript引擎调用.NET API。
有关此示例,您可以查看OffensiveDLR仓库中的Invoke-ClearScript.ps1脚本:该脚本将ClearScript Jscript引擎嵌入Posh脚本中,然后执行一些也调用.NET API的Jscript代码。
构建PoC BYOI PowerShell植入程序
虽然传统的PowerShell技术由于AMSI和脚本记录而被认为基本已死,但我们可以使用BYOI为其注入新的活力。此外,使用PowerShell作为“宿主”.NET语言突出了BYOI技术在规避和OpSec方面的一些主要优势。
让我们设定场景:您有一些想在端点上执行的后期利用代码,而您只想使用PowerShell,因为您非常怀旧。端点运行最新的Windows 10版本,并启用了带有AMSI和脚本块记录的Defender。您建立一个具有相同防御和控制的实验室环境,以便在实际在目标端点上执行之前测试您的载荷。
我们将使用AMSI测试样本字符串来模拟我们想要执行的“恶意代码”。这个字符串保证会触发Defender/AMSI,这意味着,如果这没有弹出警报,我们就知道我们成功绕过了它们(感谢@rastamouse将其放在Github上)。
https://gist.github.com/rasta-mouse/5cdf25b7d3daca5536773fdf998f2f08
我们将通过嵌入PowerShell脚本中的Boolang编译器执行我们的恶意代码。首先是从OffensiveDLR仓库获取Invoke-Boolang.ps1脚本。我们将使用此代码作为构建模板。
https://gist.github.com/byt3bl33d3r/7a3068441dab6184b4d3df46d71998a2
让我们分解一下这里发生了什么:
- 第4、8、12和16行有4个变量,分别包含Boo.Lang.dll、Boo.Lang.Compiler.dll、Boo.Lang.Parser.dll和Boo.Lang.Extensions.dll的压缩和Base64编码字符串。这是Boolang编译器启动所需的四个主要.NET程序集。
- 第20-26行包含Load-Assembly函数,该函数解压缩并解码给定的Base64编码和压缩的程序集,并将其放入字节数组。然后我们对后者调用
Assembly.Load(),以反射方式将该程序集加载到内存中。 - 第28-31行实际调用Load-Assembly函数,并将加载的程序集分配给各自的变量。
- 第57-62行包含我们想要执行的实际Boolang源代码,并将其分配给一个变量。我们稍后将用恶意代码替换它。
- 在第64行,我们创建一个StringInput对象并赋予它我们的Boolang源代码。这只是告诉Boolang编译器我们想从字符串编译源代码,而不是从磁盘上的文件。我们还给源代码一个“假文件名”:这很重要,因为我们在此设置的名称将用于生成的程序集的类型名称,我们需要反射调用它来实际执行我们的载荷。
- 在第67行,我们创建一个CompilerParameters对象并将false传递给构造函数,这消除了Boo嵌入PowerShell时的一堆错误。(详见脚本中的注释。)
第69-72行是魔法酱,我们将之前定义的ScriptInput对象添加到CompilerParameters中,然后指定我们想要CompileToMemory管道。正如您可以想象的,这告诉Boo完全在内存中编译代码,而不是将其假脱机到磁盘,这是BYOI的主要技术优势之一。我们还告诉编译器允许Duck Typing,这使得编写Boo代码更像Python。☺
在第75-81行,我们手动告诉编译器将mscorlib、System和System.Core作为引用添加到我们的Boo代码中,因为我们需要它们来访问基本的.NET API。显然,如果您想从Boo代码中访问更多.NET命名空间,可以在此添加更多引用。
我们还添加了4个Boo程序集作为引用。从技术上讲,您不必添加Boo DLL作为引用,编译器也能成功编译Boo代码。但是,如果您想进行一些“编译器嵌套”或从Boo代码内部访问任何Boo工具链,您将需要这些程序集。此外,Boo.Lang.Extension.dll提供了一些“语法糖”,如上下文管理器,使代码看起来更美观。
在第85行,我们将CompilerParameters对象传递给BooCompiler。最后在第88行通过调用$compiler.Run()编译我们的代码后,我们检查是否存在生成的程序集,这表明编译成功。然后我们调用GetType()来检索我们的入口类。我们的入口类将被称为“MyScriptModule”,因为我们没有在Boo源代码中定义类,所以它会自动为我们创建一个,使用我们之前在第64行传递给StringInput对象的“假文件名”。它会取“ .boo”扩展名之前的所有内容并附加“Module”。因此我们的类名将称为MyScriptModule。
然后我们调用GetMethod()来检索我们的Main Boo函数,并使用Invoke()执行它。
我们现在可以从PowerShell动态编译和执行Boo代码了!
好的,那么我们如何执行我们的“恶意代码”而不触发Defender/AMSI呢?嗯,一个天真的方法是将我们的AMSI测试字符串直接放入我们的Boolang源代码中:
这行不通,并且会触发Defender,因为我们使用PowerShell作为“宿主”语言:每当PowerShell脚本被执行时,它将被传递给AMSI。AMSI将看到我们的Boo源代码,因为它就在PowerShell脚本中,并标记它。我们需要做的是将我们的载荷(Boo源代码)与初始执行向量(我们的PowerShell脚本)分开。如果我们使用另一种语言作为宿主(比如C#),这可能不是必需的。
那么我们如何分离它们呢?我们可以将Boo代码放在磁盘上的单独文件中,但那将是作弊。我们将不得不编写一些C2!为了简单起见,我们将使用HTTP作为通信通道。
https://gist.github.com/byt3bl33d3r/1da9d0725a5f1bb8502fce5eda8faa42
我们添加了一个带有内部错误处理的while循环,它将使用Invoke-WebRequest CmdLet每5秒从URL http://172.16.164.1/source.boo下载Boo源代码。
我将把如何使这个PowerShell v2兼容以及将Boo载荷的输出返回到C2服务器作为练习留给读者。☺
让我们使用Python的内置HTTP服务器在服务器端托管我们的source.boo文件,其中包含我们的载荷,然后再次运行它:
您可以看到这次我们的AMSI测试字符串成功绕过了Defender。我们现在又回到了扔源代码而不是编译二进制文件的美好旧时光。☺ 此外,PowerShell记录对我们执行的Boolang代码没有洞察力:一旦代码被编译,我们就“转换”到了另一种.NET语言,PowerShell记录对此没有洞察力。
最后,与传统的C#技术相比,所需的设置/开销要少得多(至少在我看来)。
检测
检测BYOI技术很困难,取决于许多因素,首要的是使用的“宿主”语言。当使用PowerShell作为宿主语言时,由于存在大量的防御技术和记录,检测机会要多得多。
当使用Windows上默认存在的任何其他可以与.NET交互的语言(例如C#、VBA等)时,检测机会显著减少。虽然AMSI已在.NET 4.8中引入(这是朝正确方向迈出的一步),但由于BYOI载荷的动态特性,它并没有真正构成太大障碍。
因此,截至本文撰写时,组织确实无法在端点/服务器上实施任何直接、稳健的检测机制。然而,我们可以通过结合几种脆弱的检测机制并采用深度防御方法,为使用此类技术的攻击者“提高门槛”(以减慢他们的速度):
- 对所有第三方.NET脚本语言(例如Boolang、IronPython、SSharp等)的默认编译程序集进行AMSI签名,这必须由Microsoft完成。
- 检测在托管进程的AppDomain内加载的.NET脚本语言程序集的使用:这可以通过像ETW这样的技术实现。
- 应用程序白名单,以禁用Windows上默认存在的.NET脚本语言及其允许用户与之交互的可执行文件的使用。
- 启用所有将阻止传统PowerShell技术的标准缓解措施:脚本块记录、约束语言模式等。
特别是第一个将开始解决问题的根源。对第三方脚本语言运行时的AMSI签名只是一个创可贴:攻击者可以混淆/重新编译程序集以逃避签名检测,甚至制作自己的自定义脚本语言(提示)。就个人而言,我认为如果Microsoft在.NET中添加某种安全控制来启用和禁用加载和嵌入脚本语言的能力会更好,因为这种能力破坏了他们近年来取得的许多进展(我不完全确定这是否可能,并且这种能力是.NET平台的一个关键特性)。
关键要点
与每种技术一样,都有一些优点和缺点。
到目前为止,这个概念最大的问题是您无法利用所有已发布的C#工具。您可以在嵌入语言内部调用Assembly.Load()来运行您想要执行的工具,但是,这可能会在.NET >= 4.8上触发AMSI(想想SharpSploit):您将不得不在嵌入的脚本语言中重新实现该工具以创建有效的绕过。当涉及到Boolang时,这一点在一定程度上得到了缓解,因为SharpDevelop 4.4具有C#到Boo的转换器。您可以直接粘贴C#代码,它将转换为Boolang。这是一个疯狂的时间节省器:使用这个,我设法将GhostPack的Seatbelt移植到Boo,并在大约20-30分钟内将其作为后期利用模块添加到SILENTTRINITY中,而不是几周(代码库大约有6926行代码)。如果能创建转换器的无头版本并改进它,将会非常有趣。☺
如上所述,BYOI提供了惊人的灵活性,使我们能够为红队社区认为不安全或过时的技术注入新的活力,并将范式转移回在后期利用载荷中使用动态脚本语言。
有关使用不同嵌入和宿主语言组合的BYOI载荷的更多示例,我强烈鼓励您查看Offensive DLR仓库。
此外,SILENTTRINITY是我编写的另一个工具,它试图武器化其中一些概念并将其包装在一个功能齐全的C2工具中。