Monday, June 13, 2022

C# pack reference dlls using dnlib

So yesterday I was playing around with the trial copy of the DevExpress winforms controls, and built my first winform off of the library. It looks really good when using the dark skin:










Looks awesome, what doesn't look awesome is this:





















There is a bunch of reference dlls that DevExpress needs to run. A lot of .NET applications suffer from this issue, having a bunch of external dlls that need to be placed alongside the exe, that really sucks for portability; which is something I value highly when I'm building a simple app to do something. 

So I started to think, is there a way to get rid of these dlls so that I can just have the exe here by itself instead?

The answer is yes, you can. There is 2 ways of doing this, take each .NET dll and merge all its code into your exe. The second way of doing this is to take each dll, and embed it into our project exe. This is what this article will be covering, since I built a tool to do this to a .NET exe automatically. 

In this article, i will be using a sample application to test the reference packer with, it is a simple winforms app that references a dll that when called, displays a mesagebox stating its the referenced dll.








Premise:

This process will consist of two steps.

First, we have to find which references we want to embed into the target assembly. Once we have a list of paths to the reference assemblies, we then embed them as embedded resources into the exe.

Once the references are embedded, we can then perform the final step, injecting the code to resolve the packed references. Lets walk though these steps now. 

Finding the referenced assemblies:


getReferencedAndOtherDlls() is a method that will use dnlib to get a list of all the referenced assemblies from our target exe. It then will filter out mscorlib, and any other System.* references that arent present alongside the target. It will also return any other dlls that aren't present in the reference list pulled with dnlib. This allows us to pack dlls that aren't referenced directly by the target, such as unmanaged dlls or dlls that are referenced by one of the direct references from the target exe. 

getReferencedAndOtherDlls will recursively load each reference it finds and check to see if any other dlls or references that dll has are present alongside the target exe. 

Once the list of reference paths is returned, we create a new EmbeddedResource for each path in the list and read the files binary content into the embedded reference, then thats added to the dnlib ModuleDef instance that is our target exe. 



























Looking pretty good so far! We now have the MessageLib.dll reference packed as an embedded resource into the ExampleApp.exe. Now for the not so simple step: injecting the assembly resolver code.

The assembly resolver:
According to the docs for the AppDomain.CurrentDomain.AssemblyResolve event, its fired when "The resolution of an assembly fails." This is how we will load our packed assemblies into the local AppDomain. We simply subscribe to this event, and here is where the magic happens.





































This is the code that will load our embedded references into the app domain. 
First, we read the args.Name prop, and split it with a comma delimiter, and store the first part in a string var. This is because args.Name returns a full assembly name string, i.e "MessageLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
So we have to get the first part, MessageLib and add a ".dll" to the end of the string. 
Next, we look that name up in our embedded resources, get the byte array of the dll, load that with Assembly.Load and then return the loaded Assembly instance for the resolver to use in our app domain. 

Seems simple enough right? Now we should be able to put a AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; into the first line of the Program.Main and all is good right? Wrong.
We actually have to create a static module initializer, and then inject the subscription to  AssemblyResolve, and the CurrentDomain_AssemblyResolve method into the module initializer for this to work.
I'm not sure if its called a module initializer, or an assembly initializer, but what I'm referring to is the static <Module>() method in the <Module> class that's emitted by the compiler. This is referred to by dnlib as the "GlobalType"











This way, we ensure that we are subscribed to the AssemblyResolve event as soon as the CLR starts executing our program. 
How in the world are we going to inject this into the module initializer, when you cant access that from C#? Time to call up our dear old friend, dnlib again.

Injecting the assembly resolver:
After doing some research, I determined that the best course of action for this would be to put the event subscription, and the CurrentDomain_AssemblyResolve  method into its own class in the packer exe.
I came across this stackoverflow post, which led me to this class in a fork of ConfuserEx.
It allows one to clone an entire class, or what dnlib calls a TypeDef, from one Module to another. Methods, fields, everything is cloned. 
This makes life VERY easy for me, because I already have a class called ModuleInit that I put the assembly resolver in. So now I can use this class to inject, or copy over the compiled ModuleInit class into our target exe. 





















First, we find or create the static constructor, the .cctor or static <Module>()
This is the method we will be putting our event subscription to AppDomain.CurrentDomain.AssemblyResolve in.
Next, we find the "ModuleInit" class that is compiled inside the reference packer exe. Once we have the TypeDef instancer for that class, we can copy it over to the target exe by calling InjectHelper.Inject with our ModuleInit typedef instance, and the module we want to inject it into. 
































From here, the code is pretty straight forward. Move all the methods inside the injected ModuleInit typedef into the <Module> typedef. Then remove the now empty ModuleInit type.
Next we find the "Run" method that we just copied to <Module> and close its instructions into the <Module>.cctor ( static <Module>() )

After that, delete the "Run" method out of <Module> and we are all set to write all the changes back to the target exe.






































Voila! We now have an executable that depends on MessageLib.dll, but does not have to have that dll sitting next to the exe to run. It will load the dll from memory when its needed.

This technique can make it harder for a reverse engineer (such as myself) to debug an application that depends on some referenced dll. Dnspy cant load the dll as a reference because it dosent exist on disk. This will break the debugging process if you tried to see where the call to the MessageLib.dll goes in the main executables code. This technique can also be the basis of an actual packer, because you can inject any prebuilt code you want; such as an entire assembly loader/decrypter. 
This also makes your exe much more portable as well, not having to have a bunch of other dlls sitting next to it to run. This kind of thing would be great for installers and the like. 

Thanks for following along with this! You can find the complete source to this project here:

Stay tune for more projects like this coming here soon!

No comments:

Post a Comment

C# Packing more reference dlls, but this time with compression!

Last week, I talked about packing a dotnet program's reference dlls into the exe using dnlib (post linked here ). It works great, so i w...