Intro

Continuing with my dnlib kick, this post will demonstrate how to “convert” a .NET DLL to an EXE. We’ll use the same DLL code as in the previous post.

using System.Windows.Forms;

namespace DemoDLL
{
    public class Demo
    {
        public static void Execute()
        {
            MessageBox.Show("Hello from DemoDLL");
        }
    }
}

If you’ve dabbled in C# enough, you’ll likely already know that within IDEs (such as Visual Studio), you can easily toggle the output type of a C# app between Console Application, Windows Application and Class Library. DLLs don’t require anything special and the only real “requirement” of a console app is for it to have an Entry Point, e.g.

static void Main(string[] args)
{
    // do stuff
}

To convert a DLL to an EXE, all we really have to do is provide an entry point and change the “module type” of the assembly. We’ll make our entry point run the existing Execute method.

Creating Main

To create the new Main method, we use the MethodDefUser method in dnlib, which allows us to craft arbitrary methods. This method takes a name, a signature and a set of optional flags. The signature defines both the return and input types for the method. Main will return void and take in a string[] called args.

var main = new MethodDefUser("Main", MethodSig.CreateStatic(module.CorLibTypes.Void, new SZArraySig(module.CorLibTypes.String)))
{
    Attributes = MethodAttributes.Static,
    ImplAttributes = MethodImplAttributes.IL | MethodImplAttributes.Managed
};

Add the parameter(s) with ParamDefUser which can take a name and position. We use position 1 because 0 is reserved for the method’s return type.

main.ParamDefs.Add(new ParamDefUser("args", 1));

Those steps have only defined the signature - now we need to give it a body (the “stuff” to execute). Create a new (empty) CilBody and assign it to main’s Body.

var mainBody = new CilBody();
main.Body = mainBody;

Now we can add instructions to the body - since we want to execute an existing method, we need a reference to it. As stated in the previous post, I just use Linq.

var exec = type.Methods.FirstOrDefault(m => m.Name == "Execute");

Then we can add a call opcode to the body, passing in the reference to the target method (followed by a ret).

mainBody.Instructions.Add(OpCodes.Call.ToInstruction(exec));
mainBody.Instructions.Add(OpCodes.Ret.ToInstruction());

The last two steps are to add this new Main method to the Demo type (class) and then specify it as the module’s EntryPoint.

type.Methods.Add(main);
module.EntryPoint = main;

ModuleKind

Now we’re ready to write the assembly to a new file. First define a new ModuleWriterOptions instance.

var opts = new ModuleWriterOptions(module);

opts.ModuleKind is currently set to Dll and opts.PEHeadersOptions.Characteristics has the flags ExecutableImage | LargeAddressAware | Dll.

Change ModuleKind to Console and strip the Dll flag from the PEHeaderOptions.

opts.ModuleKind = ModuleKind.Console;
opts.PEHeadersOptions.Characteristics = dnlib.PE.Characteristics.ExecutableImage | dnlib.PE.Characteristics.LargeAddressAware;

Finally, write the assembly to a new EXE.

module.Write(@"C:\Temp\DemoEXE.exe", opts);

dnSpy

This is how the assembly looks in dnSpy. We didn’t mess with changing the namespace or anything, so there are still references to the original DemoDLL name.

And executing the assembly works as expected.

Final Code

using dnlib.DotNet;
using dnlib.DotNet.Emit;
using dnlib.DotNet.Writer;

using System.Linq;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var module = ModuleDefMD.Load(@"C:\Temp\DemoDLL.dll");
            var type = module.GetTypes().FirstOrDefault(t => t.Name == "Demo");

            // Create the Main signature
            var main = new MethodDefUser("Main", MethodSig.CreateStatic(module.CorLibTypes.Void, new SZArraySig(module.CorLibTypes.String)))
            {
                Attributes = MethodAttributes.Static,
                ImplAttributes = MethodImplAttributes.IL | MethodImplAttributes.Managed
            };

            // Add Main param
            main.ParamDefs.Add(new ParamDefUser("args", 1));

            // Create Main body
            var mainBody = new CilBody();
            main.Body = mainBody;

            // Instance of Execute method
            var exec = type.Methods.FirstOrDefault(m => m.Name == "Execute");

            // Add Call and Ret instructions to Main body
            mainBody.Instructions.Add(OpCodes.Call.ToInstruction(exec));
            mainBody.Instructions.Add(OpCodes.Ret.ToInstruction());

            // Add Main method and set EntryPoint
            type.Methods.Add(main);
            module.EntryPoint = main;

            var opts = new ModuleWriterOptions(module);
            opts.ModuleKind = ModuleKind.Console;
            opts.PEHeadersOptions.Characteristics = dnlib.PE.Characteristics.ExecutableImage | dnlib.PE.Characteristics.LargeAddressAware;

            module.Write(@"C:\Temp\DemoEXE.exe", opts);
        }
    }
}