一、前言
在Part 1中(译文),我们研究了.NET是如何成为攻击工具中不可或缺的一部分的,越来越多攻击者直接使用它,并且多年来通过Powershell间接使用了它。然后,我们讨论了.NET程序集加载与传统DLL加载之间的一些差异,以及观察.NET程序集的方法,包括检测以byte[]数组加载的纯内存驻留程序集。
我们使用的机制之一是利用与 .NET CLR(Common Language Runtime/公共语言运行时)相关的两个不同的ETW provider。今天我们将深入研究这些provider提供的更多信息。这篇文章假设读者了解Part 1的内容,因此建议先阅读。
二、跟踪JIT编译
当加载的程序集中的给定.NET方法尚未执行时,内存中存在CIL(Common Intermediate Language/通用中间语言)代码,但实际执行该CIL代码的本地代码还不存在。C#编译将生成CIL代码,但需要由JIT引擎根据需要在运行时将其转换为本地代码。这正是JIT编译的本质,只有在实际调用该方法时才会生成本地代码。
你可以在另一文章中看到相关的一些背景知识,文章介绍了在第一次执行时,本地代码是如何由JIT编译器生成的,在随后的调用中直接使用缓存的本地代码,不再需要解释CIL。
这对于.NET执行的自省(introspection)有一个非常有趣的副作用,因为我们之前介绍的CLR ETW provider有与JIT编译相关的事件,这意味着我们应该能够在.NET方法第一次使用时得到一个事件。这为我们提供了更多关于调用CLR的进程中实际发生了什么的信息。如果我们启用正确的关键字来跟踪MethodJittingStarted事件,然后如Part 1中介绍的那样动态调用DemoAssembly,我们的脚本将有如下输出:
现在,我们不仅可以通过byte[]数组看到程序集的动态加载,还可以看到构造函数(.ctor)和noNames()方法都是由JIT引擎编译的,这说明它们都至少执行了一次。在本例中,我们看到一个非常简单的示例,但对我们了解程序集的实际操作很有帮助。
三、一个恶意例子 -使用SharpPick的Meterpreter PowerShell Stager
现在让我们考虑一个实际的恶意例子,它包含比单个方法调用更多的功能。对于这个例子,我们将使用msfvenom中的psh-net输出类型,并通过SharpPick动态执行它,而不需要实际使用powershell.exe本身。
在这里我们可以看到,对于合法的.NET应用程序来说,有几个方法调用可能被认为是有较高的风险以及较不常见的:
- CompileAssemblyFromSource() -在运行时动态编译C#代码,在内存中生成程序集
- FromBase64String() -有大量合法用例,但也经常被攻击框架和恶意软件使用,特别是用于编码shellcode。
- 本地API调用 -特别是Virtualalloc()和CreateThread()经常被恶意使用,但.NET程序通常不会将这些函数用于显式本地内存或线程管理。
但是,如果我们使用与以前相同的技术查找MethodJittingStarted消息,我们不会看到任何这些方法的JIT编译事件。不过,我们将看到一些与SharpPick本身的使用有关的事件:
因此,在这个例子中,我们看到SharpPick工具本身中Main()和RunPS()方法的编译仍然很有用,但是在提供的脚本中没有一个更有趣的方法调用。要理解为什么会这样,我们需要回想一下Part 1中关于标准.NET程序集和使用本地映像生成器(NGEN)预编译的本地程序集之间的区别的内容。
Microsoft提供的大多数常见.NET程序集都有NGEN生成的本地程序集,因此这些程序集已经存在本地代码,不需要进行JIT编译。这就解释了在Meterpreter stager中缺少JIT编译方法的原因。但是还有其他的事件类型可以通过其他方式进行自省。
四、JIT方法内联(Inlining)与互操作(Interop)
与本地编译非常相似,JIT编译有许多提高代码效率的优化。其中之一是方法内联(Inlining),是指将频繁调用的方法的代码直接复制到调用方法的代码中,避免连续的方法调用开销的过程。许多度量标准决定了什么时候这样做是有利的,但是一般来说,那些大小非常小且频繁调用的方法是最适合方法内联的。
这为自省引入了一种有趣的可能性,因为CLR ETW provider在成功和失败的情况下都可以观察方法内联。这样做的结果是,即使对于NGEN编译的程序集,如果标准.NET程序集调用这些程序集中的方法,那么我们可以通过MethodJitInliningSucceded和MethodJitInliningFailed事件类型生成方法内联过程的成功或失败情况来观察这些方法:
在这里,我们可以看到调用FromBase64String()的方法内联尝试失败,这个调用是由SharpPick动态加载提供的Powershell脚本生成的动态类调用的,这使我们进一步了解了CLR在这种情况下的行为。
我们可以跟踪的另一组有趣的事件是Interop事件。这些都是在调用本地Win32 API时生成的。这是非常有用的,因为恶意使用Powershell或.NET来利用P/Invoke来执行本地代码是很常见的,而且通常有一组与此相关的高风险API调用,例如:用于本地内存分配的调用、用于代码注入的跨进程访问、或用于执行本地代码的线程管理。如果我们将所有这些事件类型放在一起,并将重点放在高风险事件上,那么通过SharpPick执行的meterpreter stager的输出如下:
在这种情况下,我们讨论了一些更令人关切的安全问题:
- RunPS()方法指示SharpPick的使用(非常详细,但很容易失败)
- 在非Powershell进程中加载System.Management.Automation assembly程序集
- 通过byte[]数组加载动态程序集(在本例中为随机名称)
- FromBase64String()方法的使用
- 对Virtualalloc()和CreateThread()的Interop调用
五、另一个恶意的例子 -GhostPack的SafetyKatz
Metasploit的meterpreter是一个很好的例子,但也是一个较老的例子。作为恶意使用.NET的一个更现代的例子,我们将分析SafetyKatz,这是优秀的GhostPack工具套件(https://www.harmj0y.net/blog/redteaming/ghostpack/)的一部分
GhostPack目前是C#工具的集合,它取代了许多以前只使用Powershell的攻击性工具,因此它们避免了对更新版本的Powershell所做的检测和日志记录改进。一个例子是SafetyKatz,它使用C#获取lsass进程的minidump,然后使用C#中的动态PE加载机制动态加载MimiKatz并从minidump提取凭据,这避免了Powershell日志记录,也避免了MimiKatz。如果我们正在查看其中的一些输出,可以看到一系列潜在危险的Interop调用。
如果我们查看关联的命名空间,这些名称空间来自Microsoft程序集。这与SafetyKatz使用的一些.NET方法的内部实现有关,例如查询流程是否具有很高的完整性等。但是,如果我们继续关注特定危险功能的列表,同时注意名称空间,我们可以看到一些非常有用的信息:
现在我们可以看到一些令人关切的行为:
- 来自应用程序主空间的Interop调用
- 用于获取实际进程转储的MiniDumpWriteDump()
- 用于MimiKatz的PE加载的Virtualalloc()、LoadLibrary()、CreateThread()等
- MemoryStream()和FromBase64String()与MimiKatz二进制内存中的解压缩相关的JIT事件
六、关于性能
为了跟踪某些方法级别的事件类型,我们需要在ETW provider中启用详细日志记录。有些JIT事件可能非常嘈杂,特别是如果系统上加载了大量非本地.NET程序集。PyWinTrace库仅用作ETW的研究和探索工具,与使用C#或C/C+实现相比,处理ETW事件的效率极低。但是,对于实验室环境中的恶意.NET执行,它仍然是有用的。
尽管在生产环境中执行CLR事件跟踪的任何尝试都将利用高效的ETW进程管道,但JIT级别的跟踪可能仍然会产生很高的负载。但是,加载程序集和互操作跟踪组合仍然非常强大,并且产生的数据速率比完全JIT跟踪要低得多。
七、总结
在本文中,我们在前面工作的基础上,研究了如何使用JIT和Interop跟踪,通过观察事件,而不是简单的加载程序集信息,对调用.NET CLR的进程的行为有更深入的了解。从检测和动态恶意软件分析的角度来看,这些数据都非常有用。
通过几个例子,我们研究了当使用SharpPick通过.NET动态执行Meterpreter Powershell stager以及使用最近发布的SafetyKatz工具通过C#执行MimiKatz时,如何方便地显示高度可疑的指示符,这是GhostPack框架的一部分。