如何检测.NET恶意使用行为(Part 1)

一、前言

PowerShell在功能性、普及性以及易用性方面非常强大,多年以来一直都是攻击工具中不可或缺的一部分。正因为如此,微软开始为PowerShell引入更加好用的日志记录选项,并将其接入AMSI(Anti-Malware Scan Interface,反恶意软件扫描接口),这意味着人们在识别恶意PowerShell方面会更加得心应手。

然而道高一尺魔高一丈,攻击者也在改进技术,避免直接使用powershell.exe,而是使用类似PowerPick使用的方法,避免攻击行为被检测到。规避检测技术的另一种方法就是直接使用.NET,因为PowerShell本质上是基于.NET的具体实现。

事实上,现在的攻击框架(如Cobalt Strike)基本都支持在其他进程中执行非管控型PowerShell以及动态加载执行自定义的.NET代码。根据这些现象我们可以总结出一句话:防御方需要更好地理解.NET的工作机制。

 

二、加载.NET ASSEMBLY

对于本地代码(native code),如果我们想跟踪高级代码的行为,可以观察进程的启动/停止事件以及模块的加载/卸载事件。或者我们也可以枚举当前的进程,然后通过PEB(process environment block)中链表来枚举这些进程已加载的模块。以PowerShell为例,我们可以使用WinDBG分析PEB,看到如下信息:

以上只截取了部分内容,这里需要注意的是我们可以看到其中包含某些.NET模块。然而,如果我们想列出PowerShell中已加载的assembly(程序集),我们会发现有些程序集并没有显示在PEB列表中。

比如Microsoft.Powershell.PSReadline就符合这种现象。我们没法通过WinDBG在PEB中找到相应信息,具体是什么原因呢?

微软给了一些说明,表示从CLR 4开始已经不使用LoadLibrary来加载程序集。然而文章并没有解释为什么有些程序集位于PEB中而有些没有。

原因可能在于Native Image Generator(NGEN,本地映像生成器)。NGEN用来将许多托管应用编译成本地映像,以优化执行性能,避免在运行时使用JIT机制。本地映像常见于许多.NET实用程序中,但并非适用于所有情况。当使用常见的原生方法来加载本地映像时,我们可以像处理标准本地库一样观察这些对象。

PSReadline并没有这种本地映像,因此无法通过WinDBG找到相关信息。有趣的是,我们发现Process Hacker依然可以在模块输出信息中找到PSReadline的身影。

然而,从上图中我们可以看到NGEN .NET库被高亮标记出来,显示为.NET模块,DLL类型,而PSReadline为“Mapped image”(映射映像)。这是因为Process Hacker使用了多种方法来识别已加载的模块,大家可以访问此处阅读详细代码。

一般说来,NGEN映像会以“.ni.dll”结尾,表明它们为本地映像,我们也可以从这些映像的路径来确认这一点,这些路径专为本地镜像而准备。

如果我们观察.NET assemblies的输出信息,可以更加清晰地确认这一点。我们可以发现输出信息中基本每个模块都会带上“Native”标志,同时会关联一个本地映像路径,而PSReadline不具备这两个特点。

 

三、从字节数组中加载.NET Assemblies

目前为止我们分析的都是存在于磁盘上的.NET Assemblies,就像标准的原生DLL一样。然而.NET还支持直接从字节数组中加载assemblies.aspx),这一功能也经常被类似Cobalt Strike之类的工具滥用。

这种情况下,我们直接从byte[]数组中加载DLL,加载到内存中,然后反射式识别出要调用的方法,最后打印出“Hello World!”。从加载器角度来看,被加载的甚至不是文件形式,因此我们无法从Process Hacker的模块视图中找到映射的映像。

然而我们可以在.NET assemblies视图中找到相关信息:

因此在这种情况下,我们无法看到标准的DLL映像加载事件,无法在PEB中找到相关信息,也无法通过查找映射映像或者可能被执行的通用内存映射文件来找到这些目标。

然而,Process Hacker却可以在另一个进程的.NET assemblies视图中找到这个目标,这非常有趣,问题是如何做到这一点?

如果我们查看该功能的实现代码,我们发现这与几个.NET相关的ETW Provider有关,具体如下:

Microsoft-Windows-DotNETRuntime          {E13C0D23-CCBC-4E12-931B-D9CC2EEE27E4}
Microsoft-Windows-DotNETRuntimeRundown   {A669021C-C450-4609-A035-5AF59AF4DF18}

Rundown provider能够给出关于预先存在的assemblies及其他信息的上下文,而另一个provider能够给出未来事件发生时的详细信息,assembly的加载事件只是这些信息中的一部分。

有多种方法可以记录和探索ETW信息,比如使用计算机管理器、LogMan命令行工具、Windows性能分析器、Tracerpt、微软消息分析器(Microsoft Message Analyzer)等来配置provider。在本例中,我们使用一个简单的python脚本,利用pywintrace库来搜索并展示相关信息。

 

四、探索.NET ETW Provider

大家可以访问此处链接了解上述ETW Provider的相关文档。我们先关注这两个provider提供的assembly及模块加载事件。如果我们运行python脚本,只关注相关事件,grep查找我们的PowerShell进程,就可以看到如下输出信息:

使用verbose模式我们能看到更多的信息,但上述信息足以覆盖我们所需的大多数有用事件及相关字段。

这只是一个进程的部分截取信息,我们可以看到使用byte[]数组加载的DemoAssembly的确很突出,因为该事件中并不包含DLL或者EXE的完整磁盘路径。然而上图中还存在一个Anoymously Hosted DynamicMethods Assembly的实例看上去与之类似,只是在标志上有些细微的差别。

如果我们整体观察所有输出结果,会发现有多个对象满足这个条件。然而,如果我们更加精确地限定查询目标,就可以让byte[]数组加载的DemoAssembly脱颖而出。比如,如果我们启用--high-risk-only标志,我们可以得到如下输出结果:

如上图所示,rundown provider通过DomainModuleDCStart_V1以及ModuleDCStart_V2事件告诉我们DemoAssembly已被加载,另外PDB信息也能告诉我们一些信息。在第二个例子中,我们重新加载了同一个DemoAssembly字节数组,展示runtime provider对加载动作的实时观测结果。

 

五、总结

根据前面分析,我们知道使用标准的跟踪及枚举系统中已加载的模块方法并不总是适用于.NET Assemblies的加载及使用操作。然而,使用特定的ETW provider还是能够帮我们获取关于当前系统状态及未来事件的有价值信息。

这种机制可以为我们所用,发现系统上可疑的assembly加载操作,不论是合法assemblies的恶意使用还是自定义的assemblies的纯内存加载都难逃法眼。

这种技术的有点在于.NET是PowerShell以及其他技术(DotNetToJS)的底层基础,在其他恶意操作场景中,只要最后涉及到.NET的执行或者.NET的恶意使用都可以被该技术覆盖。

目前我们只考虑了assemblies的加载动作。在本文第二部分中,我们将了解如何使用JIT以及interop相关ETW事件来深入分析某个进程的具体行为,这样就能更针对性地跟踪与.NET有关的潜在恶意行为。

(完)