Home About

Common Language Runtime: Who? why? how?

Introduction

In this post I would like to go into detail on what exactly the Common Language Runtime is, how it works, and how Cobalt Strike uses execute-assembly to run .NET Assemblies through its beacons. The primary goal, for me, is to write my own code for executing .NET Assemblies. Before jumping into Cobalt Strike, I want to explore the theory of CLR and how it operates. Its also worth noting that each subheading in this post can all be their own blog post, I'm just scratching the surface.

What is CLR?

The Common Language Runtime (CLR), can be thought of as a Virtual Machine which operates as a component of the .NET Framework. Its job is to interpret the .NET Code at Runtime and act as a Execution Engine[1] by compiling from an "Intermediate Language" with a "Just-In-Time" Compiler. The flow would look something like this:

https://medium.com/@mirzafarrukh13/common-language-runtime-dotnet-83e0218edcae

A few things need to be explained here, and they will be explained shortly. However, all of this lends itself to .NET Code being considered "Managed Code". Microsoft explain it well[2]:

To put it very simply, managed code is just that: code whose execution is managed by a runtime. In this case, the runtime in question is called the Common Language Runtime or CLR, regardless of the implementation (Mono or .NET Framework or .NET Core). CLR is in charge of taking the managed code, compiling it into machine code and then executing it. On top of that, runtime provides several important services such as automatic memory management, security boundaries, type safety etc.

Intermediate Languages

The Intermediate Language (IL) is the result of compiling .NET Code. IL are binary instructions that are defined within the Common Language Infrastructure (CLI) Specification[3]. Once the code is compiled into IL, it targets the CIL rather than platform/processor specific object code. The pro here is that the CIL is CPU independant and can be executed in any environment supporting the underlying CLR, hence why .NET now runs on Linux with Mono.

The execution process looks like this[3] (courtesy of Wikipedia):

  1. Source code is converted to CIL bytecode and a CLI assembly is created.
  2. Upon execution of a CIL assemblies, its code is passed through the runtime's JIT compiler to generate native code. Ahead-of-time compilation may also be used, which eliminates this step, but at the cost of executable-file portability.
  3. The computer's processor executes the native code.

With that, the final step is to look at the actual CLR.

The Runtime

The runtime will automatically handle any objects and their references. When they are no longer needed, they will be released. This is where Garbage Collection comes in. The Garbage Collector (GC) is used as an automatic memory manager. It manages to allocation and release of memory within the application. If you compare this to memory management in C, this has its immediate benefits:

  1. Frees developers from having to manually release memory.
  2. Allocates objects on the managed heap efficiently.
  3. Reclaims objects that are no longer being used, clears their memory, and keeps the memory available for future allocations. Managed objects automatically get clean content to start with, so
  4. Provides memory safety by making sure that an object cannot use the content of another object.

This document goes into depth on how the GC handles memory, I won't be going into that here.

Next, the CLR specifically.

The CLR itself

I would say this is where the magic happens, but this whole process is pretty cool.

So the compilation, for C#, is typically done with CSC.EXE. Once compilation is done, the aforementioned Intermediate Language is produced, alongside metadata. The metadata contains types, members and references used by the application[4]. The metadata and IL are then passed into a CLR on execution which uses the Just-in-time compilation.

To look at the IL of a file, ILDASM.EXE can be used from the Windows SDK. Opening up Seatbelt:

ILDASM.EXE disassembling SEATBELT.EXE

As expected, its fairly easy to disassemble IL and reveal the functionality. If we create a dummy .NET application and open it up with Process Hacker, and then check out the .NET Assemblies tab:

MSCORLIB.DLL Loaded

Opening the DLL in DotPeek shows it contains the following Namespaces:

System
System.Collections
System.Configuration.Assemblies
System.Diagnostics
System.Diagnostics.SymbolStore
System.Globalization
System.IO
System.IO.IsolatedStorage
System.Reflection
System.Reflection.Emit
System.Resources
System.Runtime.CompilerServices
System.Runtime.InteropServices
System.Runtime.InteropServices.Expando
System.Runtime.Remoting
System.Runtime.Remoting.Activation
System.Runtime.Remoting.Channels
System.Runtime.Remoting.Contexts
System.Runtime.Remoting.Lifetime
System.Runtime.Remoting.Messaging
System.Runtime.Remoting.Metadata
System.Runtime.Remoting.Metadata.W3cXsd2001
System.Runtime.Remoting.Proxies
System.Runtime.Remoting.Services
System.Runtime.Serialization
System.Runtime.Serialization.Formatters
System.Runtime.Serialization.Formatters.Binary
System.Security
System.Security.Cryptography
System.Security.Cryptography.X509Certificates
System.Security.Permissions
System.Security.Policy
System.Security.Principal
System.Text
System.Threading
Microsoft.Win32

Additionally, the full name of the file is revealed: Microsoft Common Language Runtime Class Library. This appears to be the DLL responsible for all the aforementioned namespaces.

The other thing of interest within the Process Hacker output is the AppDomain data. During my googling on this entire process for this blog, it hadn't came up until now.

An Application Domains provide isolation to the process. Its intended to provide a boundary so that the code running cannot affect any unrelated processes. Typically, these application domains are created by the runtime host and are responsible for bootstrapping the CLR[5]. CSharp-Corner does a good job of detailing the AppDomain.

.NET and Red Teaming

Now that the fundamentals of CLR have been explored (briefly), its now a good time to get into the main purpose of this blog and discuss the usage of .NET during Red Team Engagements.

So, when discussing .NET and post-exploitation, execute-assembly will inevitably come up as its the go to method for executing .NET in memory. Considering other C2s and Frameworks have been implementing this functionality, it makes sense to use Cobalt Strike's execute-assembly as the case study.

Before discussing how it works, I want to take a look at how it looks during execution. For this, I will use the following C# to keep a process open long enough for me to poke around in Process Hacker:

using System;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Read();
            Thread.Sleep(900000);
        }
    }
}

For reference, this is the process that the beacon is currently under:

Cobalt Strike Beacon Process (PID 4900)

When this is executed, execute-assembly will pull the spawnto value, and inject the .NET into that process:

.NET injected into GPUPDATE.EXE

As seen above, ConsoleApp1 can be found in the new process (GPUPDATE.EXE) which is accompanied by the MSCORLIB.DLL that was discussed earlier on. From a Blue Team perspective, Dom Chell did some research into this and built Sniper which aided in finding some Indicators of Compromise for this behaviour. In this blog post from Raphael Mudge confirms that the spawnto value is used for this, and also confirms that a sacrifical process is used to execute the .NET Assembly. Again, this is also confirmed when the GPUPDATE.EXE process ends when the .NET Assembly finishes running.

Cool, so we can see how execute-assembly operates on a host, lets look into the code.

Adam Chester has already discussed how execute-assembly works at a code level in his MDSec blog: Hiding your .NET ETW.

Implementing a custom CLR

Now for the fun part. As I previously mentioned, Adam Chester discusses the implementation of building a CLR in C. Essentially, it uses three methods:

  1. ICLRMetaHost: Provides various methods that can be used to get version numbers, installed CLRs, runtimes loaded into a process and to discover the CLR version used to compile a given assembly.
  2. ICLRRuntimeInfo: Provides methods for returning information about a given CLR. Such as version, directory and load status.
  3. ICLRRuntimeHost: Provides methods for start/stopping the CLR, as well as configuring AppDomains, and various other powerful methods.

Using the above methods, executing .NET Assemblies is fairly straight forward:

#include <metahost.h>
#include <stdio.h>
#pragma comment(lib, "mscoree.lib")

int main()
{
    ICLRMetaHost* CLRMetaHost = NULL;
    ICLRRuntimeInfo* CLRRuntimeInfo = NULL;
    ICLRRuntimeHost* CLRRuntimeHost = NULL;

    // Create instance of CLR
    if (CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&CLRMetaHost) != S_OK) {
        printf("[!] CLRCreateInstance()\n");
        return 2;
    }

    // Get the v4.0.30319 Runtime
    if (CLRMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*)&CLRRuntimeInfo) != S_OK) {
        printf("[!] ICLRMetaHost.GetRuntime()\n");
        return 2;
    }

    // Load CLR into current process
    if (CLRRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&CLRRuntimeHost) != S_OK) {
        printf("[!] ICLRRuntimeInfo.GetInterface()\n");
        return 2;
    }

    // Initialise te CLR in current process
    if (CLRRuntimeHost->Start() != S_OK) {
        printf("[!] ICLRRuntimeHost.Start()\n");
        return 2;
    }

    // Store return
    DWORD pReturnValue;

    if (CLRRuntimeHost->ExecuteInDefaultAppDomain(
        L"C:\\Users\\mez0\\Desktop\\ConsoleApp1\\ConsoleApp1\\bin\\Debug\\ConsoleApp1.exe", // Path to .NET Assembly
        L"ConsoleApp1.Program", // Namespace.(public)Class
        L"EntryPoint", // (public)Method
        L"Mez0 is in .NET!", // Arguments
        &pReturnValue) != S_OK) {
        printf("[!] ICLRRuntimeHost.ExecuteInDefaultAppDomain()\n");
    }
    
    // Safely Release() everything
    CLRRuntimeInfo->Release();
    CLRMetaHost->Release();
    CLRRuntimeHost->Release();
    return 0;
}

The .NET being executed:

using System.Windows;

namespace ConsoleApp1
{
    public class Program
    {
        public static int EntryPoint(string arguments)
        {
            MessageBox.Show(arguments);
            return 0;
        }
        public static void Main()
        {

        }
    }
}

Running the C:

Executing .NET with the Unmanaged API

With that sorted, lets take a look at the process. Here is the UnmanagedExecuteAssembly.EXE process:

And looking in the .NET assemblies tab:

.NET Assemblies of the Unmanaged Execution

All is as expected. At this point, I have achieved what I set out to do.

Conclusion

My goal in this post was to explore how execute-assembly works on the machine, and at a code level. Code-wise, I was expecting there to be a lot more, but it seems the Windows API for doing this stuff just works(?). As it stands, I'm not a big fan of creating a "sacrificial process" to execute .NET. It would be interested to see if using BOF Files and the unmanaged code can get around that issue. I'm not 100% sure of how BOFs work at the moment, so I cant say.

I didn't spend anytime looking into the events generated by both the Cobalt Strike implementation and the custom because it has been looked at already, and I have referenced two of those blogs within this one. They are:

  1. Detecting and Advancing In-Memory .NET Tradecraft by Dom Chell.
  2. Hiding your .NET ETW by Adam Chester.

With that said, I will probably end up doing a comparison at some point, just for my own clarity, but not in this post. Additionally, I may end up expanding on this when I get around to looking into ETW.

References

  1. .NET Framework (CLR): Execution Engine
  2. What is "Managed Code"?
  3. Common Intermediate Language
  4. Common Language Runtime Hook for Persistence
  5. Application Domains