.NET 程序集混淆技术用于逃避内存扫描

admin 2024年8月12日21:14:39评论14 views字数 6290阅读20分58秒阅读模式

.NET 程序集混淆技术用于逃避内存扫描

利用基于 .NET 的工具,通过反射方式将程序集加载到内存中,是一种 常见的后开发 TTP多年来,威胁行为者和红队一直在使用 .NET。使用 .NET 具有多种吸引力。首先,.NET 框架预装在 Windows 操作系统的所有最新版本中,具有很高的可移植性和兼容性。此外,.NET(尤其是 C#)提供了简单的开发体验,具有许多用于通用协议和软件的库,可以快速进行原型设计和 PoC。因此,许多最有价值的攻击性操作工具,例如SharpHound,Certify或者Rubeus,都是用 C# 编写的或者已经移植到 C#。

最迟随着2018 年的 Cobalt Strike 3.11,将execute-assembly 命令引入框架,基于 .NET 的间谍技术成为每个红队成员的稳定武器。然而,近年来,防御者赶上了这一趋势,采用了多种技术来检测内存中 .NET 程序集的执行。

这篇博文将简要概述内存中 .NET 程序集执行的常见工作原理以及存在的检测机制。我们在 r-tec 中采用的规避这些检测的技术之一是混淆。本文的最后一部分将展示我们如何通过内部混淆管道中的 CI/CD/DevOps 技术自动执行此方法。

1..NET 程序集的反射加载如何工作?

几乎所有现代 C2 框架都支持某种命令来在内存中执行 .NET 程序集,例如 cobalt strikes execute-assembly。当然,实现、行为和 IoC 因实现而异,但所有公共实现(至少我们知道的)都依赖于调用 .NET API 来通过公共语言运行时 (CLR) 进行代码反射。

此 API 使我们能够在运行时动态创建类型的实例、调用其方法并访问其成员。此外,通用语言运行时加载器还为我们管理应用程序域并确保正确加载依赖项。

在 C# 中,反射式加载程序集(到主机进程)只需 3 行代码,用于Assembly.Load从字节数组加载程序集:

Assembly assembly = Assembly.Load(assemblyBytes);MethodInfo entryPoint = assembly.EntryPoint;entryPoint.Invoke(null, new object[] { new string[] { "arg1", "arg2" } });

类似代码可以实现使用 C++,尽管稍微复杂一些,因为必须先加载 CLR。最后,所有这些技术最终都会 nLoadImage从调用本机System.Reflection.Assembly,这为我们带来了不同的检测机会。

2. 检测
就像在 PowerShell 技巧中一样,AMSI 也是使用内存中 .NET 执行时的障碍,因为自 .NET Framework 4.8 以来,从字节数组加载的所有程序集都会传递给Amsi扫描缓冲区一方面,这意味着有效载荷将被扫描恶意软件签名,另一方面,这也意味着这个障碍很容易克服——修补 AMSI,字节补丁或者无补丁例如通过硬件断点,很容易,而且已经被写过无数次了。
除了 AMSI 之外,第二个障碍是发送大量遥测数据,可以帮助防御者发现 .NET 运行时中的恶意活动,那就是 Windows 事件跟踪 (ETW)。本质上,ETW 是集成到 Windows 中的提供程序-订阅者技术,它允许应用程序记录事件并允许其他应用程序使用它们。
对于检测事件的用例Assembly.Load,以下内容两家供应商是最有趣的。这里DotNETRuntime提供了 .NET 运行时的实时事件,并且DotNETRuntimeRundown 提供程序列出了已加载到进程中的程序集的信息(当启用 ETW 跟踪时):
Microsoft-Windows-DotNETRuntime {E13C0D23-CCBC-4E12-931B-D9CC2EEE27E4} Microsoft-Windows-DotNETRuntimeRundown {A669021C-C450-4609-A035-5AF59AF4DF18}

我们可以使用以下方式调查这些提供商提供的信息进程黑客。如果我们使用反射方式将程序集加载到任何进程中Assembly.Load,例如通过 PowerShell 访问 .NET 框架,并且增强型SharpPack,我们看到程序集出现在默认的 AppDomain 中,并且我们的 Post-Exploitation 工具的名称以明文形式显示 - 这对于防御者来说是一个非常容易实现的目标。

.NET 程序集混淆技术用于逃避内存扫描

图 1:在 powershell.exe 中反射加载 Certify

.NET 程序集混淆技术用于逃避内存扫描

图2:Process Hacker 中的.NET 程序集视图
但是,由于这些 ETW 提供程序位于用户空间,我们可以简单地修补其中一个函数,例如from ,类似于我们修补 AMSI 的方式,使用众多函数之一NtTraceEvent ntdll.dll已知和公开的技术和工具。
这样,我们就阻止了进程进一步发送 ETW 遥测数据。虽然使消费者蒙蔽,从而逃避任何基于 ETW 的检测,但我们现在拥有了死 ETW 流的 IoC - 因此,修补最好以侵入性较小的方式进行,例如,仅过滤特定事件或提供虚假信息。仅在短时间内禁用 ETW 是不可行的,因为在恢复修补的字节后,.NET 程序集将再次出现。

.NET 程序集混淆技术用于逃避内存扫描

图 3:修补 ETW 后的 Process Hacker 视图

但是,在分析进程内存时,仍然可以找到我们的程序集的 MZ 和 PE 标头,以及整个程序集本身。因此,即使绕过 AMSI 和 ETW,如果行为可疑并检测到加载的程序集,EDR 仍然可以对我们的进程运行内存扫描,例如使用与已知后利用工具的签名匹配的 YARA 规则或使用TypeRef 哈希匹配。

.NET 程序集混淆技术用于逃避内存扫描

图4:内存中的PE映像

精明的读者在使用普通函数时可能会注意到,使用受保护的内存页来存储程序集。这是因为映像仅包含中间语言 (IL) 代码,该代码将由即时编译器 (JIT) 翻译并在需要时写入具有执行保护的内存页。防御者还可以利用此 JIT 编译通过 ETW 获得进一步的洞察,例如通过监视其编译来监视哪些函数被执行(请参阅 Assembly.Load RW https://blog.f-secure.com/detecting-malicious-use-of-net-part-2/以获得更详细的描述)。

虽然 PE 和 MZ 头可以被踩踏,但程序集本身却不能,至少在它运行时不能。

当然,还有其他机会可以检测恶意 .NET 程序集的执行,无论是通过挂钩还是其本机对应物,或者通过监视一般行为(在不常见进程中加载 CLR、监视 Windows API、监视网络流量等),其中后者是最难绕过的。Assembly.Load疼痛金字塔模型此处也适用:虽然某些检测很容易绕过,但这些检测越抽象,就越难绕过。

讨论所有这些检测机会和每种检测的绕过技术超出了本文的范围。但是,我们发现对程序集本身进行自动混淆是针对许多内存扫描和基于 AMSI 或 ETW 的检测的有效且高效的措施。因此(目前)仅靠适当的混淆 就足以逃避我们客户环境中的检测。

3. 混淆和.NET

虽然 .NET 具有介绍中提到的一些优点,例如易于开发和可移植性,但也存在一些源自这些功能的 OPSEC 缺点。要理解这一点,必须了解什么构成了 .NET 等托管框架以及这些功能来自何处。

.NET 在生成机器代码之前将源代码编译为中间语言 (IL)。此 IL 是程序的抽象表示,由 Common-Language-Runtime 即时编译为目标体系结构。这类似于 Java 和 Java 虚拟机 (JVM) 的关系。

一方面,这对我们操作员有好处,因为我们可以更轻松地转换这种抽象语言来混淆二进制文件,即使无法访问源代码。另一方面,这让逆向工程师的工作变得轻松很多,因为类名、方法名和其他元数据都嵌入到了程序集中。此外,反编译也非常简单,可以恢复源代码进行全面分析。相比之下,反汇编后的基于 C 的二进制文件就没那么容易分析了。

这可以通过打开 Rubeus 的编译版本来说明间谍软件 反编译器并将其与实际源代码进行比较:

.NET 程序集混淆技术用于逃避内存扫描

图5:反编译的Asreproast类

.NET 程序集混淆技术用于逃避内存扫描

图6:Asreproast类的实际源代码

可以看出,反编译后的源代码几乎与实际源代码匹配。使用明文形式的标识符名称,这也为防御者提供了一种更直接的方式来编写基于 .NET 的工具的检测规则。对特定方法或类名的简单搜索可以高度确定真阳性 - 除非您碰巧找到了具有类的合法工具Asreproast 。这意味着,对于我们的用例,混淆器至少应该使用随机或伪随机名称重命名所有标识符。

但除了标识符之外,还有更多可以且应该被混淆的东西。如果我们看一下元数据,就会发现一些明显的 IoC:

.NET 程序集混淆技术用于逃避内存扫描

图 7:Rubeus.exe 的元数据

这些程序集元数据条目来自与项目对应的文件。这里的一些条目非常明显,例如和,即使所有标识符都被混淆了,它们也只是说明并泄露了该程序包含的内容。另一个需要更改的重要条目是属性,如果项目暴露给 COM,则该属性是 COM GUID,作为唯一标识符,它非常适合防御者查找。AssemblyInfo.csAssemblyTitle AssemblyProductRubeus Guid

一个好的混淆器会负责重写所有这些属性,例如埃森哲的密码专家如果我们让 Codecepticon 混淆 Rubeus,所有属性都会被重写为随机的、空的或所谓值得信赖的属性:

.NET 程序集混淆技术用于逃避内存扫描

图 8:混淆的元数据

除了元数据、命名空间和标识符之外,字符串也是防御者编写检测规则的另一个绝佳机会 - 与大多数编程语言一样,字符串以明文形式存储在二进制文件中。在这里,我们建议您检查您选择的混淆器如何加密/混淆字符串。有些工具只是对字符串进行 base64 编码,我们认为这还不够,因为这些字符串可以轻松解码并且是可预测的(例如,UTF-8 总是编码为)。其他则采用更复杂的方法,例如实际加密。菊花链式混淆器可以在这里提供帮助,例如,使用一个混淆器进行命名空间和标识符重命名,另一个混淆器仅用于字符串加密,等等。Rubeus UnViZXVz

我们还没有发现任何混淆器,它们也会篡改类型引用哈希。TypeRef Hash 可以与 Imphash 进行比较,后者可用于通过散列其导入来识别类似的 PE。由于 .NET PE 通常只导入,因此常规 Imphash 没有用。TypeRef Hash 是基于导入的 .NET TypeNamespaces 和 TypeNames 生成的(例如与 TypeName 一起使用)。混淆器可以更改此哈希,例如通过任意添加导入,但我们似乎还没有遇到任何基于 TypeRef Hashes 的端点检测。mscoree.dllSystem.ReflectionAssemblyTitleAttribute

虽然出于显而易见的原因,我们不会泄露我们在流程中使用的混淆器的具体链,但有许多混淆器可供免费和付费版本使用,它们都具有不同的功能,例如:

  • https://github.com/yck1509/ConfuserEx

  • https://github.com/Accenture/Codecepticon

  • https://github.com/obfuscar/obfuscar

  • https://github.com/0xb11a1/yetAnotherObfuscator

  • https://github.com/BinaryScary/NET-Obfuscate

另一个非详尽的混淆器列表可以在以下存储库的 README 中找到:https://github.com/NotPrab/.NET-Obfuscator

在选择混淆器或混淆器链时,还有另一个需要考虑的因素。就像加壳器和解壳器一样,一些混淆器可以使用以下工具自动反混淆:4点- 这使得蓝队的工作更加轻松。应该避免使用这些混淆器。

现在我们已经知道了要混淆 .NET 程序集的内容和方法。但是,手动对每个我们想要在工作中执行的程序集执行此操作非常繁琐且容易出错 - 这就需要我们实现自动化,并让我们深入了解 DevOps。

4. 混淆管道

对于自动化,可以采用不同的方法。由于 .NET 可以交叉编译,我们的管道可以在所有基于 Linux Docker 容器的经典 CI/CD 管道下运行,例如 GitLab CI/CD、GitHub Actions 等。但是,根据我们的经验,在 .NET 的情况下,从 Windows “本地”工作要简单得多,而且许多混淆工具也是基于 Windows 的。

如果你曾经参加过 HackTheBox 等 CTF 比赛,或者在实验室里使用过 C2 框架和 .NET 攻击工具,那么你可能已经意识到@弗朗维克与夏普收藏SharpCollection 是一个包含常见 .NET 后漏洞利用工具最新版本的存储库,它由通过免费层运行的 CI/CD 管道自动更新微软 Azure DevOps。

幸运的是,Flangvik 也做了很棒的视频展示 SharpCollection 管道的工作原理以及如何实现它。因此,我们以这个想法为基础来实现我们自己的 C# 混淆管道。

虽然设置管道的过程超出了本文的范围,但该过程可以总结如下:

  • 设置 Azure DevOps 项目

  • 将虚拟机/主机设置为 Azure 代理(执行编译和混淆工作的机器)

  • 为每个 .NET 程序集的 GitHub 存储库创建一个管道

再次强调,有关实施细节,请参阅上述视频,或者如果你喜欢阅读文字,这篇博文经过@_xpn_:https://twitter.com/_xpn_

对于我们的管道,我们决定每天一大早,Azure 应该为列表中的每个存储库运行混淆管道。

.NET 程序集混淆技术用于逃避内存扫描

图 9:管道触发器

然后,我们的管道模板针对每个项目运行以下步骤:

  • 重命名项目(因为我们选择的混淆器不会执行此操作)

  • 使用 NuGet 安装依赖项

  • 运行我们的源代码混淆器

  • 生成项目Build the project

  • 运行另一个字符串混淆工具(在二进制文件上)

  • 将输出文件移动到我们的输出目录

  • 将新的二进制文件推送到我们的内部 git 存储库

.NET 程序集混淆技术用于逃避内存扫描

图 10:混淆流程步骤

这样,我们就可以始终拥有所有 .NET 工具的最新版本,这些工具在 GitLab 存储库中组织有序,经过全新混淆,可与我们的 C2 代理一起使用:

.NET 程序集混淆技术用于逃避内存扫描

图 11:包含混淆程序集的 GitLab 存储库
如果混淆正确完成,您甚至可能根本不需要绕过 AMSI 或 ETW 来进行 C2 内存执行,因为很可能所有基于签名的 IoC 都不再可见。
最后,需要解决一些操作安全问题:如果您在 Azure Pipeline Host 上编译来自公共 GitHub 存储库的代码,其中一个后门可能会导致 Host 受到攻击。为避免这种情况,建议使用存储库的本地副本,其中仅推送经过审核的代码以更新工具。如果您不使用本地副本,则必须根据组织的最佳实践对 Azure Pipeline Host 进行强化,并将其与公司网络隔离。安全公司和安全专业人员是威胁行为者的有利可图的目标,因为这些公司通常处理非常敏感的数据。r-tec 还观察到威胁行为者针对进攻性安全人员目的是窃取内部工具和其他知识产权。最后,与任何公共代码或工具一样,为了避免运行带有后门的工具,在客户环境中执行任何有效负载之前,必须进行彻底的源代码审查。
5. 总结
通过 .NET 反射 API 从内存运行 .NET 程序集是红队成员和威胁行为者都使用的最常见代码执行 TTP 之一。虽然 ETW 和 AMSI 都可以用于检测,但通过修补或硬件断点绕过这些措施并不费力 - 这在许多 C2 框架中已经实现自动化。但是,内存扫描是一种更难绕过或避免的检测。因此,我们还会混淆程序集、更改元数据、加密字符串和重命名标识符,以避免基于签名的检测。使用 DevOps / CI / CD,这种方法可以大规模自动化,用于整个后开发武器库,每天都会生成不同的程序集。虽然从我们的角度来看,这目前已经足够隐蔽,但随着其他检测机会(如本文所述),未来可能需要采取其他措施。

原文始发于微信公众号(Ots安全):.NET 程序集混淆技术用于逃避内存扫描

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年8月12日21:14:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   .NET 程序集混淆技术用于逃避内存扫描http://cn-sec.com/archives/3055445.html

发表评论

匿名网友 填写信息