Skip to content

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

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 be given in an NVISO webcast in 2021, 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 will not re-iterate what I’m going to talk about in this talk. 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 to this, 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 DLL’s 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 has:

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 actually well documented by our dear friends over at Microsoft themselves, but from a high-level perspective it comes down to this:

Generally speaking, 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 the following steps:

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

An example of how this works is given in my presentation, I’ll update the blogpost once the talk is online with a link.

Thanks to this method, it’s also possible to call c# assemblies in powershell. Which could be very useful in environments that have been applocked, but still allows you to run powershell.

Below is a very quick example on how to invoke C# in powershell:

C# code:

PowerShell code:

Executing this powershell script will result in the following output:

You could also get the bytearray over the internet instead of trying to load it from localdisk by using the webclasses in powershell.

There are some drawbacks to this method..

The major drawback of using this method is that you’ll have to invoke every method using the MethodBase.Invoke call, which means you are not able to leverage the DLL directly in your code as you would normally use in your development. Which would mean adapting your code to support this way of loading, which could cause you to refactor basically every single function call you make to your dependency.

Another drawback of this method 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 of .NET assemblies search for dependencies is by using the AppDomain.AssemblyResolve method. Application domains provide an isolation boundary for security, reliability, and versioning, and for unloading assemblies. Application domains are typically created by runtime hosts, which are responsible for bootstrapping the common language runtime before an application is run.

The AssemblyResolve method is a callback function that fires when the app domain is unable to locate a referenced assembly. I didn’t know about this method until RastaMouse, the hero that he is, mentioned it to me. He also wrote an accompanying blog post to this one, which can be found here: https://offensivedefence.co.uk/posts/assembly-resolve/
Using this method allows you to create your assembly just like you normally would, invoking all calls to dependencies as normal.

There is a small gotcha however, that I encountered during testing.
If you put the assemblyresolve method in your main method, and you invoke method calls from dependencies in your main method as well, the assemblyresolve function might not work. This is because its working as a subscription service, and because your dependencies are also located in the main method, the resolve method will not have any subscribers yet. The workaround is to either:

  • create a static class and creating a constructor that sets this assembly resolves method, thus creating it before the Main method is being called.
  • put your dependency logic in a separate method and call that method from Main.

Below is a small PoC to illustrate the working of what I’ve described above:

NvisoLib is a small test dll I created containing the following code:

using System;

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

    }
}

AppDomainResolveTest is a test executable, leveraging NvisoLib

As seen, NvisoLib is being leveraged like you normally would leverage any other dependency, I’ve put the logic in a sepperate function and call it from Main.

I’ve implemented the RevolveAssembly function with a pretty trivial implementation of just fetching it from another location on disk. You could of course alter the implementation as you see fit, for example fetching it over the internet.

Executing this program without having nvisolib.dll in its directory or in the GAC results in the following:

Conclusion

In this blogpost, we’ve explored how we can leverage reflection to load dependencies at runtime, effectively elliminating the need to weave dependencies together in 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.

Published inTips & Tutorials

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *