利用基于 .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,这为我们带来了不同的检测机会。
Microsoft-Windows-DotNETRuntime {E13C0D23-CCBC-4E12-931B-D9CC2EEE27E4}
Microsoft-Windows-DotNETRuntimeRundown {A669021C-C450-4609-A035-5AF59AF4DF18}
我们可以使用以下方式调查这些提供商提供的信息进程黑客。如果我们使用反射方式将程序集加载到任何进程中Assembly.Load,例如通过 PowerShell 访问 .NET 框架,并且增强型SharpPack,我们看到程序集出现在默认的 AppDomain 中,并且我们的 Post-Exploitation 工具的名称以明文形式显示 - 这对于防御者来说是一个非常容易实现的目标。
图 1:在 powershell.exe 中反射加载 Certify
图 3:修补 ETW 后的 Process Hacker 视图
但是,在分析进程内存时,仍然可以找到我们的程序集的 MZ 和 PE 标头,以及整个程序集本身。因此,即使绕过 AMSI 和 ETW,如果行为可疑并检测到加载的程序集,EDR 仍然可以对我们的进程运行内存扫描,例如使用与已知后利用工具的签名匹配的 YARA 规则或使用TypeRef 哈希匹配。
图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 的编译版本来说明间谍软件 反编译器并将其与实际源代码进行比较:
图5:反编译的Asreproast类
图6:Asreproast类的实际源代码
可以看出,反编译后的源代码几乎与实际源代码匹配。使用明文形式的标识符名称,这也为防御者提供了一种更直接的方式来编写基于 .NET 的工具的检测规则。对特定方法或类名的简单搜索可以高度确定真阳性 - 除非您碰巧找到了具有类的合法工具Asreproast 。这意味着,对于我们的用例,混淆器至少应该使用随机或伪随机名称重命名所有标识符。
但除了标识符之外,还有更多可以且应该被混淆的东西。如果我们看一下元数据,就会发现一些明显的 IoC:
图 7:Rubeus.exe 的元数据
这些程序集元数据条目来自与项目对应的文件。这里的一些条目非常明显,例如和,即使所有标识符都被混淆了,它们也只是说明并泄露了该程序包含的内容。另一个需要更改的重要条目是属性,如果项目暴露给 COM,则该属性是 COM GUID,作为唯一标识符,它非常适合防御者查找。AssemblyInfo.csAssemblyTitle AssemblyProductRubeus Guid
一个好的混淆器会负责重写所有这些属性,例如埃森哲的密码专家如果我们让 Codecepticon 混淆 Rubeus,所有属性都会被重写为随机的、空的或所谓值得信赖的属性:
图 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 应该为列表中的每个存储库运行混淆管道。
图 9:管道触发器
然后,我们的管道模板针对每个项目运行以下步骤:
-
重命名项目(因为我们选择的混淆器不会执行此操作)
-
使用 NuGet 安装依赖项
-
运行我们的源代码混淆器
-
生成项目Build the project
-
运行另一个字符串混淆工具(在二进制文件上)
-
将输出文件移动到我们的输出目录
-
将新的二进制文件推送到我们的内部 git 存储库
图 10:混淆流程步骤
这样,我们就可以始终拥有所有 .NET 工具的最新版本,这些工具在 GitLab 存储库中组织有序,经过全新混淆,可与我们的 C2 代理一起使用:
原文始发于微信公众号(Ots安全):.NET 程序集混淆技术用于逃避内存扫描
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论