如何控制.NET CLR使用日志实现EDR规避

 

0x00 前言

近几年来,研究人员公开了规避端点安全解决方案(如A/V、EDR以及日志记录工具)的各种技术,其中涉及到的方法通常在具体技巧和实现上有所不同,但都是以有效规避作为最终目标。防御方可以利用操作系统自带的原生功能以及各类支持的框架来构建检测方案,其中有一种方法可以用来检测潜在的、有趣的.NET行为:监控公共语言运行时(CLR)的使用日志(Usage Log)来检测.NET执行事件。

在本文中,我们将研究防御方如何利用.NET Usage Log进行恶意行为检测及安全取证响应,也会分析规避日志监控探测的方法,讨论捕捉Usage Log篡改行为的潜在方式。

 

0x01 使用.NET CLR Usage Log检测可疑行为

当.NET应用被执行、或者程序集(assembly)被(攻击者)注入到另一个进程的内存空间时,.NET运行时会被加载,以便执行程序集代码、处理各种各样的.NET管理任务。其中由CLR(clr.dll)启动的一个任务是:当程序集首次在(用户)会话上下文中执行完毕时,CLR会以执行进程命名创建一个Usage Log文件。这个日志文件中包含.NET程序集模块数据,目的是为.NET本地镜像自动生成(auto-NGEN)提供信息文件。

在进程退出前,CLR通常会将信息写入如下某个文件路径:

  • <SystemDrive>:\Users\<user>\AppData\Local\Microsoft\CLR_<version>_(arch)\UsageLogs
  • <SystemDrive>:\Windows\<System32|SysWOW6$=4>\config\systemprofile\AppData\Local\Microsoft\CLR_<version>_(arch)\UsageLogs

比如在下图中我们可知,在首次“优雅地”结束powershell.exe进程前,系统会创建powershell.exe.log使用日志:

从DFIR(数字取证及应急响应)和威胁捕捉角度来看,分析Usage Log事件调查有很大价值(大家可疑参考MENASEC Applied Research Team之前发表过的研究文章)。从端点监控角度来看,端点检测及响应方案(EDR)很可能会监控Usage Log文件创建事件来识别加载.NET CLR的可疑的、或者不太可能出现的进程。比如,Olaf Hartong(@olafhartong)正在维护一个Sysmon-Modular项目,其中配置了一个规则,用来监控.NET 2.0活动以及存在风险性的LOLBIN(Living off the land Binaries)。红队及攻击者肯定也能推测出,许多商用产品也会通过类似的方式来监控Usage Log(比如用来捕捉Cobalt Strike的execute-assembly)。

在深入分析规避技术前,我们先来简单讨论下.NET中的Configuration Knobs(配置选项)。

 

0x02 .NET CLR配置选项

微软在为.NET Framework保留了大量有价值的文档并随后发布了开源的.NET Core的同时,也为我们提供了.NET生态系统功能组件内部工作机制的有价值的信息。通常情况下,.NET是一个非常强大的开发平台以及运行时框架,可以用来构建和运行.NET托管的各种应用。.NET有一个强大功能(特别是在Windows上):能够调整.NET公共语言运行时(CLR)的配置和行为,以方便开发及调试场景。我们可以通过.NET CLR Configuration Knobs完成该任务,而这些配置选项由环境变量、注册表设置、配置文件/属性所控制,可以由CLRConfig获取。

滥用配置选项并不是一个新颖的概念。其他研究者已经探索过利用配置选项来执行任意命令以及/或者规避防御控制机制的各种技术。近期的案例包括:Adam Chester(@xpn)使用ETWEnabled CLR配置选项来禁用Windows事件跟踪(ETW),Paul Laîné(@am0nsec)使用GCName CLR配置选项来指定自定义的垃圾回收器DLL,以便加载任意代码、绕过应用控制方案。Casey Smith(@subTee)也探索了各种.NET技术,包括利用COR_PROFILER加载非托管代码,以规避防御机制、绕过UAC;此外,Ghost Loader也采用过AppDomainManager注入技术(@netbiosX对此有更详细的介绍)。

 

0x03 调整.NET配置选项注册表设置

有趣的是,我们可以通过在注册表中设置NGenAssemblyUsageLog CLR配置选项、或者配置环境变量来控制.NET Usage Log的输入位置。我们可以设置一个任意值(比如伪造的输出位置或者垃圾数据),这样CLR将不会为.NET执行过程创建使用日志文件。我们可以通过如下注册表项来设置NGenAssemblyUsageLog CLR配置选项字符串值:

HKCU\SOFTWARE\Microsoft\.NETFramework
HKLM\SOFTWARE\Microsoft\.NETFramework

HKCU中设置的值适用于活动用户上下文,会影响原先使用的<SystemDrive>:\Users\<user>\AppData\Local\Microsoft\CLR_<version>_(arch)\UsageLogs以及/或者Microsoft Office Hub路径。在HKLM中设置的值适用于系统上下文,会影响原先使用的<SystemDrive>:\Windows\<System32|SysWOW64>\config\systemprofile\AppData\Local\Microsoft\CLR_<version>_(arch)\UsageLogs路径。下面我们举个例子,演示预期行为及被篡改后的行为。

比如我们可以将如下源码编译成64位NET应用test.exe

在执行应用前,测试主机上的UsageLogs目录为空。

执行程序时,会弹出一个简单的消息框:

观察UsageLogs目录,可以看到系统创建了名为test.exe.log的一个文件,其中包含程序集模块信息:

下面,我们先从UsageLogs目录中删除test.exe.log文件:

在重新执行.NET应用前,我们先确认HKCU中存在.NETFramework注册表项,使用如下命令:

reg query "HKCU\SOFTWARE\Microsoft\.NETFramework"

此时该注册表项存在,并且不包含其他值或者子项(注意:如果.NETFramework不存在,则会被创建)。接下来,我们将NGenAssemblyUsageLog配置选项字符串值添加到.NETFramework中:

reg.exe add "HKCU\SOFTWARE\Microsoft\.NETFramework" /f /t REG_SZ /v "NGenAssemblyUsageLog" /d "NothingToSeeHere"

再次执行程序:

与预期相符,test.exe.log文件并没有在UsageLogs目录中出现:

大家可能会问:当我们将NGenAssemblyUsageLog设置为任意值时,CLR会如何处理?其实CLR只是将任意字符串“适当”插入构造的路径上。比如,如果我们将路径数据设置为eeeee,执行.NET应用,那么CLR会将该字符串值插入构造出的路径中:

由于这个路径不存在,因此Usage Log并不会被写入磁盘上。如下图所示,CLR会在clr.dll中硬编码并提取部分UsageLogs路径:

 

0x04 滥用.NET配置选项环境变量

我们也可以通过COMPlus_开头的环境变量来设置CLR配置选项。如下例所示,我们可以在命令提示符中将COMPlus_NGenAssemblyUsageLog设置为任意值(如zzzz)。当PowerShell(.NET应用)被调用时,就会从cmd.exe父进程继承COMPlus_NGenAssemblyUsageLog环境变量:

退出PowerShell后,可以发现Usage Log文件(powershell.exe.log)从未在UsageLogs目录中创建:

当Adam Chester(@xpn)发表关于禁用ETW机制的.NET CLR配置选项时,他也发布了在子进程启动时注入COMPlus_ETWEnabled环境变量的PoC。稍微修改程序中的部分变量后,我们可以使用这种技术来禁用Usage Log输出,如下代码片段所示:

编译并执行程序后,在启动PowerShell.exe时,COMPlus_NGenAssemblyUsageLog环境变量会被设置为zz

与预期相符,在退出PowerShell会话后,Usage Log也不会被创建:

注意:大家可以访问此处下载修改版的PoC。

 

0x05 强制终止进程

.NET配置选项提供了一种影响日志流的优雅方式,然而我们还是有一些方法,能够在不更改配置的情况下终端Usage Log创建过程。这些方法存在更大的风险,可能会破坏进程及程序执行流。

当程序“优雅地”退出时,Usage Log会被生成。比如,当使用隐式或者显式return语句,或者在(C#)托管代码中使用Environment.Exit()方法时,就可以优雅地退出:

然而,当进程被强制终止时,Usage Log过程会被中断,永远不会落盘。比如,我们可以使用Process.Kill()方法实现该效果(可能会丢失数据):

 

0x06 模块卸载

在另一个有趣的风险测试场景中,我们可以破坏已加载的模块(DLL)来破坏进程,使其过早退出,从而破坏CLR Usage Log创建过程。为了完成该任务,我们可以利用.NET委托函数指针以及由The Wover(@TheRealWover)和b33f(@FuzzySec)开发的功能强大的DInvoke库。在测试场景中,我们为FreeLibrary() Win32 API函数声明了一个委托函数,调用该函数从正在运行的.NET托管进程中卸载模块。移除单个模块或者较少的模块组合可能也会达到同样的效果,然而我们将卸载多个.NET模块,以增加进程不稳定的机率,强制终止并破坏Usage Log创建过程(备注:这里我们选择以.NET模块为目标,但也可以卸载其他DLL)。

为了成功卸载模块,我们首先必须使用DInvoke的GetLibraryAddress()函数来获取FreeLibrary()函数的库地址指针。然后,我们可以通过.NET的“Interop”服务中的GetDelegateForFunctionPointer() 方法,将函数指针转化为FreeLibrary() API方法的可调用的委托。接下来我们使用DInvoke的GetPebLdrModuleEntry()方法,在.NET进程的进程执行块(PEB)中搜索每个模块的基地址引用,从而获取已加载的每个模块(DLL)的句柄。最后,我们调用FreeLibrary委托函数,配合每个模块的句柄,将其中内存中卸载。我们用来测试的PoC代码如下所示:

编译并执行代码后,Usage Log文件创建过程被成功中断:

如果大家想了解关于DInvoke的更多信息,可以阅读The Wover(@TheRealWover)和b33f(@FuzzySec)发表的这篇文章,卸载DLL模块的PoC代码可以访问此处下载。

 

0x07 防御方案

继续监控Usage Log文件及目录

为Usage Log创建和修改操作实现分析、签名、检测机制。尽管有各种针对性的攻击技术,这种监控方案仍然非常有价值,攻击者在执行.NET工具时,并没有始终考虑到采用Usage Log篡改技术。

查找(不常规的)非托管程序及脚本的日志,通常这些程序不会加载CLR。我们可以利用Olaf Hartong(@olafhartong)的Sysmon-Modular配置规则,或者以这个Elastic Security查询规则作为基准。此外,Samir(@SBousseaden)也提供了非常棒的检测技巧,可以监控使用.NET工具的WinRM横向移动行为。

此外,也可以审计并监控删除Usage Log的行为,攻击者可能会删除这些文件以掩盖其踪迹(参考MENASEC的这篇文章)。

监控可疑的.NET运行时加载行为

如果攻击者采用了Usage Log规避技术,那么识别可疑的.NET CLR运行时加载行为可能是一种有趣的检测机制。非托管进程如果加载了CLR(比如MS Office),那么可能是一种攻击特征。

监控对CLR配置选项的添加或修改行为

Roberto Rodriguez(@Cyb3rWard0g)发表过一篇文章,讨论了检测对[COMPLUS_]ETWEnabled配置选项的修改行为,其中包括SACL审核建议、Sysmon配置设置、Sigma规则以及Yara规则。这些方法同样可疑用来检测针对[COMPLUS_]NGenAssemblyUsageLog配置选项的修改。简单总结以下包括如下2种方式:

1、在HKCU\Software\Microsoft\.NETFramework以及HKLM\Software\Microsoft\.NETFramework注册表项中寻找NGenAssemblyUsageLog字符串。Roberto表示,当启用了审核对象访问策略,并且目标注册表键的写入、设置事件被审计时,会生成Event ID 4657:

2、在用户/系统环境变量以及临时环境变量中寻找以COMPlus_开头的环境变量,比如进程命令行、转换日志等。

监控进程模块篡改行为

在大多数环境中,监控“可疑的”进程终止事件可能不切实际,然而从正在运行的进程中卸载DLL可能是一个有趣的检测点。spotheplanet(@spotheplanet)在一篇文章中提到,我们可以使用ETW的Microsoft-Windows-Kernel-Process来监控模块卸载行为。

(完)