https://www.mdsec.co.uk/2020/03/hiding-your-net-etw/
After the introduction of PowerShell detection capabilities, attackers did what you expect and migrated over to less scrutinised technologies, such as .NET. Fast-forward a few years and many of us are now accustomed with the numerous .NET payloads available for post-exploitation. Suites of tools like those offered by GhostPack, as well as SharpHound are now part of our arsenals, and the engine responsible for powering their delivery is normally Cobalt Strike’s execute-assembly.
This one function changed how many RedTeam’s operate, and is in my mind one of the primary reasons for the continued popularity in .NET tooling, allowing operators to run Assemblies from unmanaged processes as they follow their post-exploitation playbook.
Now just as with PowerShell, over time defensive capabilities have been introduced by Microsoft and endpoint security vendors to help reduce the blind spots that .NET payload execution introduced (such as the now infamous AMSI which was introduced in .NET 4.8). One of the challenges as an attacker has been the continued use of this technology while trying to remain relatively silent. Now of course AMSI didn’t prove to be too much of a problem, but I fear that other techniques used by defenders haven’t received as much scrutiny.
So over a couple of posts I want to explore just how BlueTeam are going about detecting malicious execution of .NET, its use via methods such as execute-assembly, and how we as attackers can go about evading this, both by bypassing detection and limiting the impact should our toolkit be exposed.
This first post will focus on Event Threading for Windows (ETW) and how this is used to signal which .NET Assemblies are being executed from unmanaged processes.
To understand a defender’s detective capability, we first need to look at how techniques such as execute-assembly actually work.
The magic behind this method lies in 3 interfaces ICLRMetaHost, ICLRRuntimeInfo and ICLRRuntimeHost. To start the process of loading the CLR into our “unmanaged” process (otherwise known as a Windows process without the CLR started), we invoke the CLRCreateInstance method. Using this function will provide a ICLRMetaHost interface which exposes information on the list of .NET Frameworks available for us to work with:
ICLRMetaHost *metaHost = NULL;
IEnumUnknown *runtime = NULL;
if (CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&metaHost) != S_OK) {
printf("[x] Error: CLRCreateInstance(..)\\n");
return 2;
}
if (metaHost->EnumerateInstalledRuntimes(&runtime) != S_OK) {
printf("[x] Error: EnumerateInstalledRuntimes(..)\\n");
return 2;
}
Once a runtime is selected, we then instantiate our ICLRRuntimeInfo interface which in turn is used to create our ICLRRuntimeHost interface.
frameworkName = (LPWSTR)LocalAlloc(LPTR, 2048);
if (frameworkName == NULL) {
printf("[x] Error: malloc could not allocate\\n");
return 2;
}
// Enumerate through runtimes and show supported frameworks
while (runtime->Next(1, &enumRuntime, 0) == S_OK) {
if (enumRuntime->QueryInterface<ICLRRuntimeInfo>(&runtimeInfo) == S_OK) {
if (runtimeInfo != NULL) {
runtimeInfo->GetVersionString(frameworkName, &bytes);
wprintf(L"[*] Supported Framework: %s\\n", frameworkName);
}
}
}
// For demo, we just use the last supported runtime
if (runtimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&runtimeHost) != S_OK) {
printf("[x] ..GetInterface(CLSID_CLRRuntimeHost...) failed\\n");
return 2;
}
Once created, everything comes together via 2 method calls, ICLRRuntimeHost::Start which loads the CLR into our process, and ICLRRuntimeHost::ExecuteInDefaultAppDomain which allows us to provide our Assembly location along with a method name to execute:
// Start runtime, and load our assembly
runtimeHost->Start();
printf("[*] ======= Calling .NET Code =======\\n\\n");
if (runtimeHost->ExecuteInDefaultAppDomain(
L"myassembly.dll",
L"myassembly.Program",
L"test",
L"argtest",
&result
) != S_OK) {
printf("[x] Error: ExecuteInDefaultAppDomain(..) failed\\n");
return 2;
}
printf("[*] ======= Done =======\\n");
If you want to see this running end-to-end you can find the source to do this here.
Once compiled and executed, we can see just how easy it is to load a .NET Assembly in our unmanaged process:
Now that we know just how execute-assembly works, how do BlueTeam go about detecting its use? Well one common way is using Event Tracing for Windows (ETW), which was originally introduced for debugging and performance monitoring, but has evolved into a tool used by security products and threat hunters to expose potential indicators of compromise.