« Back to home

RunDLL32 your .NET (AKA DLL exports from .NET)

If you follow redteaming trends, you will have seen a focus shift from Powershell post-exploitation tools to the .NET framework. With Powershell environments becoming more restrictive thanks to AMSI, CLM and ScriptBlock logging, tool developers have now started to switch to C# as their goto language for malware and post-exploitation tooling.

In this post I wanted to look at a technique which is by no means new to .NET developers, but may prove useful to redteamers crafting their tools... exporting .NET static methods within a DLL... AKA using RunDLL32 to launch your .NET assembly.

Managed DLL Export - The Long Version

Many of the tools and techniques employed during an engagement rely on DLL's crafted by the redteam. Check out Oddvar's LOLBAS project and you will see numerous ways to execute arbitrary DLL's, and with AppLocker warning of performance issues when enabling DLL rules, we often  see these fly under the radar of software restriction policies.

In the .NET world, we normally deal with class assemblies rather than the DLL's that many tools have come to expect. Being a .NET class assembly, we are not provided with export address table entries required by tools such as RunDLL32 to reference internal methods. But it turns out that there is a way for us to get the best of both worlds... enter .export.

To begin, let's take a simple C# snippet which will display a message box:

namespace Test
{
   public class TestClass
   {
       public static void TestMethod()
       {
           MessageBox.Show(".NET Assembly Running");
       }
   }
}

Compiled as a .NET assembly, which can view the intermediate language which makes up the executable using Microsoft's ildasm tool:

This tool also provided us with a few command line switches to disassemble our code to a flat ".il" file:

ildasm.exe /out:TestUnmanaged.il TestUnmanaged.dll

Now that we have the intermediate language corresponding to our C#, we can mark our static method with a .export descriptor, which tells the assembler to create an export entry within the created DLL, for example:

.class public auto ansi beforefieldinit Test.TestClass
       extends [mscorlib]System.Object
{
  
  .method public hidebysig static void  TestMethod() cil managed
  {
    .export [1]   // <--- Magic export descriptor

    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      ".NET Assembly Running"
    IL_0006:  call       valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
    IL_000b:  pop
    IL_000c:  ret
  } // end of method TestClass::TestMethod

Here we are exposing the TestMethod method with an ordinal of 1. With this done, we need to recompile the ilasm into a .NET class assembly using:

ilasm.exe TestUnmanaged.il /DLL /output=TestUnamanged.dll

Now if we take our generated DLL and view its export table, we see:

And if we try to invoke using RunDLL32.exe:

We also see that upon invoking our static method, the CLR is loaded into the process, giving us the full power of the .NET framework within our previously unmanaged process:

By allowing the Windows loader to do the heavy lifting for us, we have a nice way to inject the CLR into an unmanaged process, similar to the traditional COM method that the likes of Cobalt Strike's execute-assembly use (see my post on AppLocker bypass here for just how this COM method works).

But does this technique extend beyond RunDLL32? Let's create a very simple DLL loader written in C which will load our .NET DLL:

#include <Windows.h>

typedef void(*TestMethod)();

int main()
{
	HMODULE managedDLL = LoadLibraryA("TestUnmanaged.dll");
	TestMethod managedMethod = (TestMethod)GetProcAddress(managedDLL, "TestMethod");
	managedMethod();
}

When executed, we see that clr.dll is again loaded within our unmanaged process:

And our .NET method executes just fine:

Obviously this also means that we can remotely inject our .NET assembly into a process, for example, using the Win32 CreateRemoteThread call. Let's choose something which doesn't have the .NET CLR loaded by default, iexplore.exe:

And again if we view the loaded DLL's after we have loaded our DLL:

Managed DLL Export - The Quick Version

So, with the low-level bits shown and with an understanding of how tihs work, what can we do to make this a bit more developer friendly? What if you just want to deal with C# and really don't want to be messing around with ilasm... enter "DllExport".

DllExport is a project provided by Denis Kuzmin, aka 3F, and can be found on his Github page here. The developer experience can be a bit messy, mostly due to the manager provided which modifies a project to support the functionality, but once you know how the tool works, it's simple to replicate the above method within Visual Studio.

To begin, we need to install DllExport via a NuGet package using the normal process, in which you receive the following dialog:

At this point, you are prompted to remove the NuGET package before continuing. Once removed, you will see a .bat file called DllExport_Configure.bat within the project which opens the ".NET DLLExport Manager", providing a number of options to configure our project with unmanaged DLL exports:

Completing this process, you now have access to the [DllExport] attribute. Taking the the above code sample, we can now export a static method using:

namespace Test
{
   public class TestClass
   {
       [DllExport]
       public static void TestMethod()
       {
           MessageBox.Show(".NET Assembly Running");
       }
   }
}

And once we have compiled our project:

Now we have a nice way to add exports to our class assembly, let's see if this works with something more established, like harmj0y's SafetyCatz tool. After adding the DllExport NuGet, we'll need to tweak the source slightly to get this to work.

First we will modify the static void Main(string[] args) entrypoint to be our a DLL export entry RunSafetyCatz, for example:

    [DllExport]
    static void RunSafetyCatz()
    {
        string[] args;

        if (!IsHighIntegrity())
        {
        Console.WriteLine("\n[X] Not in high integrity, unable to grab a handle to lsass!\n");
        }
        ...

Once this is done, we need to update the output type to be that of a Class Library:

And now we can test invoking SafetyCatz as a DLL using RunDLL32.exe:

If you want the console output to appear as above, you will need to use the Win32 call AttachConsole(-1), but I will leave this to the reader.

And there we have it, a nice way to load .NET assemblies within an unmanaged process. When I have some time I would like to look at the Windows loader a bit closer to understand just how this magic happens under the hood. If you have already completed this exercise and would like to share this, please give me a shout.