PowerShell has been a staple of offensive tooling for many years now due to its power, prevalence and simplicity. Consequently, Microsoft began introducing better logging options for PowerShell, as well as plugging it in to the Anti-Malware Scan Interface (AMSI) – meaning the industry became much better at spotting malicious PowerShell.
However, techniques then evolved to help attackers stay under the radar by avoiding the use of powershell.exe itself, such as using the approach used by PowerPick. Another technique to evade detection was to simply use .NET directly, since PowerShell is essentially all based on .NET.
In fact, modern attack frameworks (such as Cobalt Strike) support executing both unmanaged PowerShell within other processes as well as dynamically loading custom .NET code for execution. All this points to one conclusion – defenders need better visibility of .NET.
.NET Assembly Loading
When it comes to native code, we can track high-level code behaviour by observing process start/stop events and module load/unload events. Alternatively, we can enumerate current processes and then enumerate their loaded modules from the linked lists stored in the process environment block (PEB). If we use PowerShell as an example itself, we can analyze the PEB using WinDBG and we might see something like the following:
This is an incomplete snippet but notice that in this instance we can actually see some .NET modules listed there. However, if we were to list the loaded assemblies from within powershell, we would find some examples that are not present in the PEB list.
An example in this case would be Microsoft.Powershell.PSReadline. This is not listed in the PEB within WinDBG… and the question is, why not?
There are some details (here) that show how as of CLR 4, LoadLibrary is not used to load assemblies. However, that doesn’t explain why some are listed in the PEB and why some aren’t.
The answer is likely down to the use of the Native Image Generator (NGEN). This is used to compile many managed applications down to native images for performance reasons, so as to avoid JIT at runtime. Native images may often be both present and used for common .NET utilities, but not for everything. As native images load using normal native methods, we gain visibility of them in the same way we do with standard native libraries.
PSReadline does not have a native image in this case, and so this wasn’t present in WinDBG. Interestingly, we can see Process Hacker has still been able to show PSReadline in its module output.
However, you can see how the NGEN .NET libraries are highlighted as .NET modules and given a type of DLL whereas PSReadline is given as a mapped image. This is just due to the fact that Process Hacker uses multiple methods to try to identify loaded modules. The code for this can be found here.
Generally speaking, NGEN images will end in a “.ni.dll” to indicate they are a native image and it will be clear from their path that they are contained in a location for native images.
This is even clearer when we look at the output for .NET assemblies specifically. You can see that almost every module has a “Native” flag and an associated native image path, whereas PSReadline has neither.
Loading .NET Assemblies from a Byte Array
Everything we have looked at up until now has been related to .NET assemblies that exist on disk, much like standard native DLLs. However, .NET gives us the option to load assemblies directly from a byte array and this is something that tools like Cobalt Strike have provided the ability to abuse.
In this case, we load a DLL purely from a byte array and then load it in-memory and reflectively identify a method to call that prints “Hello World!”. This is not file backed from the loader’s perspective and so we cannot even see it in process hacker’s module view for mapped images.
However, we can see it within the .NET assemblies view:
So in this instance, we wouldn’t see a standard DLL image load event, we wouldn’t find it in the PEB and we can’t find it by looking for mapped images or general memory mapped files that might be executable.
However, process hacker can find it retrospectively in another process in the .NET assemblies view, which is really interesting. The question is, how is this achieved?
If we look at the code implementing it we find it is due to the use of a couple of ETW providers related to .NET. These are:
The rundown provider gives context on pre-existing assemblies and other information, whereas the non-rundown provider can give more detailed information on future events as they happen. Assembly loading is just one facet of this information.
There are many ways to record and explore ETW information, such as configuring providers in computer management, using the LogMan command line utility, Windows Performance Recorder, Windows Performance Analyzer, Tracerpt, Microsoft Message Analyzer etc. In this case, we will use a simple python script that makes use of the pywintrace library for research and demonstration purposes.
Exploring .NET ETW Providers
The ETW providers given above are documented here. To start, we will focus on assembly and module load events from both the rundown provider and the runtime provider. If we run the python script and focus just on the relevant events from these and grep for our PowerShell process specifically, we will see output much like the following:
There is much more information available in verbose mode but this covers some of the most useful events and related fields.
This is just a snippet of one process, but you can see that the byte array loaded DemoAssembly does stand out somewhat due to the lack of a full path for a DLL or EXE on disk for some of its events. However, there is an instance of “Anonymously Hosted DynamicMethods Assembly” that appears similar, minus slight differences in the flags.
If we look across the results for the system as a whole, we will find that there are various common examples of this with a range of names. However, if we target our queries more specifically, we can still make the byte array loaded DemoAssembly standout. For example, if we enable the “—high-risk-only” flag we get the following output:
In this instance, the rundown provider has given us information on an already loaded version of DemoAssembly via the DomainModuleDCStart_V1 and the ModuleDCStart_V2 events. The presence of PDB information is also of value. In the second instance, we reloaded the same byte array DemoAssembly again to demonstrate the runtime provider giving visibility of it loading real-time.
We’ve seen how the loading and use of .NET assemblies is not always visible through standard mechanisms for tracing or enumerating loaded modules on a system. Yet, we’ve seen how specific ETW providers can be used to get some really valuable context about both current system state and future events.
This could be used as a mechanism to discover suspicious assembly loading on a system, either in the form of legitimate assemblies being used maliciously, or potentially custom assemblies being loaded purely in memory.
The great thing about this is that due to .NET being the underlying basis for PowerShell and other techniques such as DotNetToJS, this same technique can be used to cover a range of malicious scenarios when the end result is .NET execution, as well as covering direct malicious use of .NET.
So far, we’ve only considered the loading of assemblies. In the second part of this blog post, we will look at how JIT and interop related ETW events can be used to gain deep insight into what type of behaviour a process is engaging in. This will provide a far more targeted tracking of potentially malicious behaviour via .NET.