红队烹饪指南: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技术的主要概念:我们可以使用第三方.NET脚本语言,就像我们这些年来一直使用PowerShell一样。我们所要做的就是在默认存在于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引擎中调用它,它会中断。
这导致我进行了非常长的Google搜索会话,试图找到一个支持原生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版本,并启用了Defender with AMSI和脚本块日志记录。您设置了一个具有相同防御和控制措施的实验室环境,以便在实际在目标端点上执行之前测试您的负载。
我们将使用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传递给构造函数,这消除了Boolang嵌入PowerShell时的一堆错误。(有关详细信息,请参见脚本中的注释。)
第69-72行接下来是魔法酱,我们将之前定义的ScriptInput对象添加到CompilerParameters中,然后指定我们想要CompileToMemory管道。正如您可以想象的,这告诉Boolang完全在内存中编译代码而不是将其假脱机到磁盘,这是BYOI的主要技术好处之一。我们还告诉编译器允许Duck Typing,这使得编写Boolang代码更类似于Python。☺
在第75-81行,我们手动告诉编译器将mscorlib、System和System.Core作为引用添加到我们的Boolang代码中,因为我们需要它们来访问基本的.NET API。您显然可以在这里添加更多引用,如果您想从Boo代码中访问更多.NET命名空间。
我们还添加了4个Boolang程序集作为引用。从技术上讲,您不必添加Boolang DLL作为引用以使编译器成功编译Boolang代码。但是,如果您想做一些“编译器盗梦空间”或从Boo代码中访问任何Boo工具链,您将需要这些程序集。此外,Boo.Lang.Extension.dll提供了一些“语法糖”,例如上下文管理器,使事情看起来更美观。
在第85行,我们将CompilerParameters对象传递给BooCompiler。在第88行最终通过调用$compiler.Run()编译我们的代码后,我们检查是否有生成的程序集,这表明编译成功。然后我们调用GetType()来检索我们的入口类。我们的入口类将被称为“MyScriptModule”,因为我们没有在Boo源代码中定义类,所以它会自动为我们创建一个,使用我们之前在第64行传递给StringInput对象的“假文件名”。它将获取“