December 21, 2020

A tale of .NET assemblies, cobalt strike size constraints, and reflection

Exploring how reflection and AppDomain.AssemblyResolve can bypass Cobalt Strike's 1MB execute-assembly limit by loading .NET dependencies at runtime.

Originally published on redteamer.tips


Fairly recently, I’ve been drawn into the wondrous world of reflection, so much so that I decided to create a talk about it. The talk will go into topics as what reflection actually is and how it can be used to create assemblies that load their dependencies reflectively, to make the loader as innocent as possible.

This blogpost has been brought to life due to a recent engagement I’ve had, which sparked me into diving a bit deeper into the more “advanced” methods of reflection.

The Cobalt Strike problem

Cobalt Strike is a popular command and control software used by a ton of consultancy firms around the globe (and unfortunately, some threat groups as well). Cobalt Strike has the capability of executing .NET assemblies in memory by spawning a new process and bootstrapping the CLR (interpreter for .NET) onto the process. This behavior is also often called a CLR harness.

This functionality is awesome and works very well, but there is a drawback: Cobalt Strike can only execute the assembly if the assembly is self-contained. This means if your assembly uses any dependencies (other assemblies, such as DLLs for example) this presents a problem. You can bypass this problem using two options:

  • Use ILMerge or Fody (or some other linker) to “weave” the dependencies into each other, making the assembly carry all its dependencies on its own. This has been my go-to approach since forever basically.
  • Use reflection to load in your dependencies at execution time. This is what this blog post is about.

The problem with weaving dependencies together is that you are effectively “bloating” your assembly, making it larger and larger the more dependencies it has. This brings us to the second issue:

Cobalt Strike cannot execute assemblies in memory if they are larger than 1MB.

Usually assemblies are not that big, so this is not often presenting a big problem. However in some edge cases, your assembly can become quite a bit bigger than 1MB — which forces us into loading our dependencies at execution time.

How does .NET load dependencies anyway?

In order for us to understand how to create an assembly that will load its dependencies at run time, it’s important to understand how .NET loads its references in the first place. The full technical explanation is well documented by Microsoft, but from a high-level perspective it comes down to this:

You add references to your project and use the using keyword in the header of the program. A classic example would be using System.Web; — this will make your assembly call System.Web.dll, which lives in the GAC (Global Assembly Cache). The GAC is located in %WinDir% which makes it not that interesting for an attacker since WinDir can’t be written to without elevated privileges.

If an assembly does not find the reference in the GAC, it will look for the reference in the directory where the assembly lives. If it’s not found there either, it will error out (provided no configuration files are present, but config files are out of scope for this blog post).

Enter the world of reflection

One way to get around the behaviour of searching for dependencies is by using reflection. Classically, reflection follows these steps:

  1. Load the assembly you want to use with Assembly.Load
  2. Get the correct Type (= class) using Assembly.GetType
  3. Get the actual method you want to invoke using Type.GetMethod
  4. “Activate” the Type using Activator.CreateInstance
  5. Invoke the method using MethodBase.Invoke

Thanks to this method, it’s also possible to call C# assemblies in PowerShell — which could be very useful in environments that have been AppLockered, but still allow you to run PowerShell.

Example: invoking C# from PowerShell

A simple C# assembly:

using System;
using System.Reflection;

namespace HelloFromDotNetFramework
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hi from {0}", Assembly.GetExecutingAssembly());
        }
    }
}

Loading and invoking it from PowerShell:

clear
$bytes = [System.IO.File]::ReadAllBytes("C:\Users\jeanm\source\repos\HelloFromDotNetFramework\bin\Release\HelloFromDotNetFramework.exe")
$asm = [System.Reflection.Assembly]::Load($bytes)
$params = {null}
[HelloFromDotNetFramework.Program]::Main($params)

You could also get the byte array over the internet instead of loading from local disk by using the web classes in PowerShell.

Drawbacks

The major drawback of using this method is that you’ll have to invoke every method using the MethodBase.Invoke call, which means you can’t leverage the DLL directly in your code as you normally would. This could force you to refactor basically every single function call you make to your dependency.

Another drawback is that all your methods need to be public, and some “complex” operations can break. For example, in my testing, I could not get a Timer.Elapsed event working correctly.

Enter the world of app domains

Another method of getting around the “normal” behavior is by using AppDomain.AssemblyResolve. Application domains provide an isolation boundary for security, reliability, and versioning, and for unloading assemblies.

The AssemblyResolve method is a callback that fires when the app domain is unable to locate a referenced assembly. I didn’t know about this method until RastaMouse mentioned it to me. He also wrote an accompanying blog post.

Using this method allows you to create your assembly just like you normally would, invoking all calls to dependencies as normal.

The gotcha

If you put the AssemblyResolve handler in your Main method and also invoke dependency calls from Main, the resolve handler might not work. This is because it works as a subscription service, and your dependencies are resolved before the handler has any subscribers yet. The workaround:

  • Create a static class with a constructor that sets the AssemblyResolve handler, so it runs before Main.
  • Put your dependency logic in a separate method and call that method from Main.

Proof of concept

NvisoLib — a simple test DLL:

using System;

namespace NvisoLib
{
    public class Test
    {
        public void Say(String message)
        {
            Console.WriteLine(message);
        }
    }
}

AppDomainResolveTest — the executable that loads NvisoLib at runtime via AssemblyResolve:

using System;
using NvisoLib;
using System.Reflection;

namespace AppDomainResolveTest
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
            SomeDLLFunction();
        }

        static void SomeDLLFunction()
        {
            NvisoLib.Test test = new NvisoLib.Test();
            test.Say("hi from nvisolib");
        }

        static Assembly ResolveAssembly(object sender, ResolveEventArgs args)
        {
            Assembly asm = null;
            String name = new AssemblyName(args.Name).Name;
            Console.WriteLine("attempting to load {0} from an external location.", name);
            asm = Assembly.LoadFrom(@"C:\Users\jeanm\source\repos\NvisoLib\bin\Release\" + name + ".dll");
            return asm;
        }
    }
}

Notice how NvisoLib is leveraged like you normally would leverage any other dependency — the logic is in a separate function called from Main. The ResolveAssembly implementation here is trivial (fetching from disk), but you could alter it to fetch over the internet.

Conclusion

We’ve explored how to leverage reflection to load dependencies at runtime, effectively eliminating the need to weave dependencies together into one big assembly. This allows us to remain within the 1MB range of Cobalt Strike’s execute-assembly functionality.

Additionally, we briefly looked into how we can execute C# assemblies in PowerShell through the power of reflection as well.