Introduction
Applications built for .NET tend to be composed of a large number of assemblies and it's not a secret that .NET code can be easily decompiled using dissasemblers. It might be useful to pack all application assemblies into a single native executable, thus reducing application size and providing some degree of code protection. In this article, we'll look into a simple way of creating a .NET application packer tool. The complete source code and documentation of the tool is free to download at the Cellbi Web site.
Native Executable Template
First of all, we need to create a native executable, which we will use as a container for a target .NET application. We'll use C++ language for this purpose.
Since our template is a native application, we'll need to start .NET runtime manually from our code, then create a default AppDomain
and then load our assemblies there. Only after that, it's OK to transfer execution to the managed code. Let's define the NativeLauncher
class to handle this.
Here is the NativeLauncher
definition:
class NativeLauncher
{
public:
NativeLauncher();
int Launch(LPWSTR runtime, LPTSTR cmdLine);
};
NativeLauncher
class contains only one method: Launch
, it has two parameters runtime
- to specify target .NET runtime version, and cmdLine
- command line string
passed.
Below is the code for the main
method of our executable template:
#define NET11 L"v1.1.4322";
HINSTANCE hInst;
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
hInst = hInstance;
LPWSTR runtime = NET11;
NativeLauncher* launcher = new NativeLauncher();
int result = launcher->Launch(runtime, lpCmdLine);
delete launcher;
launcher = NULL;
return result;
}
We just create an NativeLauncher
instance and call its Launch
method, passing .NET runtime version and command line string
.
Starting .NET Runtime
To start .NET runtime from NativeLauncher
, we will use the CorBindToRuntimeEx
function available from the mscorlib.tlb library. This function will bind to the specified .NET runtime and return a pointer to the ICorRuntimeHost
interface. ICorRuntimeHost
interface exposes the Start
method, which we will use to start the .NET runtime.
Below is the sample code:
CComPtr<ICorRuntimeHost> pHost;
HRESULT hr = CorBindToRuntimeEx(runtime,
NULL,
NULL,
CLSID_CorRuntimeHost,
IID_ICorRuntimeHost,
(void **)&pHost);
if (FAILED(hr))
return hr;
Note that CLSID_CorRuntimeHost
, IID_ICorRuntimeHost
are defined in the mscorlib.tlb
library.
In order to use the CorBindToRuntimeEx
function, we need to import the mscorlib.tlb
library. Here is how to do this:
#import <mscorlib.tlb> raw_interfaces_only
using namespace mscorlib;
Loading .NET Assemblies
Once we started the .NET runtime host, it is fine to load any .NET assembly into the default AddDomain
and then execute our managed code. ICorRuntimeHost
interface has the GetDefaultDomain
method which can be used to obtain a pointer to the default AppDomain
.
Below is the code illustrating this:
CComPtr<IUnknown> pUnk;
hr = pHost->GetDefaultDomain(&pUnk);
if (FAILED(hr))
return hr;
CComPtr<_AppDomain> appDomain;
hr = pUnk->QueryInterface(&appDomain.p);
if (FAILED(hr))
return hr;
Now we can load the .NET assembly into the default AppDomain
using any of the Load
methods exposed by _AppDomain
.
Here is the code:
CComSafeArray<unsigned char> *rawAssembly = new CComSafeArray<unsigned char>();
rawAssembly->Add(packerApiLibLength, packerApiLib);
CComPtr<_Assembly> assembly;
hr = appDomain->Load_3(*rawAssembly, &assembly);
if (FAILED(hr))
return hr;
At the code above, we load our assembly from an array or bytes. The array is stored in the packerApiLib
variable.
Executing Managed Code
The next step is to create a .NET class and obtain its pointer. This is easy to do via the CreateInstance
method exposed by the _Assembly
interface. We just pass the full name of the class to create a pointer to result value:
CComVariant launcher;
hr = assembly->CreateInstance(_bstr_t("Cellbi.AppPacker.Api.NetLauncher"), &launcher);
if (FAILED(hr))
return hr;
if (launcher.vt != VT_DISPATCH)
return E_FAIL;
We use CComVariant
to store the pointer to newly created managed instance.
Ok, it's now time to call methods defined on the created instance, but we need to find the right method first:
CComPtr<IDispatch> disp = launcher.pdispVal;
DISPID dispid;
OLECHAR FAR* methodName = L"Launch";
hr = disp->GetIDsOfNames(IID_NULL, &methodName, 1, LOCALE_SYSTEM_DEFAULT, &dispid);
if (FAILED(hr))
return hr;
And now we call the method found. The Invoke
method defined on the IDispatch
interface does this:
TCHAR szPath[MAX_PATH];
if (!GetModuleFileName(NULL, szPath, MAX_PATH))
return E_FAIL;
CComVariant *path = new CComVariant(szPath);
CComVariant *cmdLineArg = new CComVariant(cmdLine);
CComVariant FAR args[] = {*cmdLineArg, *path};
DISPPARAMS noArgs = {args, NULL, 2, 0};
hr = disp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD,
&noArgs, NULL, NULL, NULL);
if (FAILED(hr))
return hr;
After the method is executed, we need to stop the .NET runtime:
hr = pHost->Stop();
if (FAILED(hr))
return hr;
That's all, I hope this article will be useful. Please let me know if there are any problems.
In this article, we didn't cover managed code used to pack .NET assemblies into the native executable template. The complete source code can be obtained at Cellbi.AppPacker.
History
- December 5th, 2007 : Initial release
- February 19th, 2008 : 1.0.0.20 build - Runtime version support added