Securing .Net Managed Assemblies with Native Exe Interoperability

Introduction:

While it is easy and fun to use the disassembly features of .net to disclose the managed assembly contents, it also poses the problem of piracy of otherwise copyrighted content. If we forget for a moment the issues related to copyright and piracy etc., most of us at some point or the other might have shown concern about securing our own assemblies from such easy exposure. Towards this extent the following article describes a simple yet elegant way of securing our managed assemblies from easy disassembly by using the powerful interoperability features of .net.

The Problem:

Before we actually take up our solution, let us first clearly specify what our problem is. In a nutshell we could specify it something like, "No one should be able to disassemble our managed assemblies". Now, the first question is, how does one disassemble any assembly? Reflection is one of the most powerful features of .net that makes this kind of facility possible. Theoretically, one could load any managed assembly to see what all types are available in it and create instances of desired types, invoke methods, query for referenced assemblies and what not...? The metadata produced by the compiler as part of assembly generation makes all these operations trivial for the Reflection. Without this metadata, Reflection can not do its work. And the point to be noted is - native executables do not have any such metadata.

Thus we can safe-guard our managed assembly if we can hide its metadata from the dissembler. But, hiding the meta data is a very hazardous option given the fact that CLR needs access to it to execute the assembly. However, we could overcome this if we allow ourselves to hide the entire assembly instead. This is a simpler option and further we could use the very Reflection (that caused all this trouble) as our tool to achieve it.

Reflection:

The classes required for Reflection are available under the namespace System.Reflection, and among them, Assembly is one very important class. This class allows us to load and execute managed assemblies at runtime. It supports LoadFrom() and Load() methods to load an assembly from an external file or raw bytes. Once properly loaded, the property EntryPoint allows one to get details about the entry point of the loaded assembly. The EntryPoint is a MethodInfo type property that gives all information, such as parameter types, return type etc., for the entry point method. One important method that is supported by MethodInfo is Invoke(). This allows one to invoke the method for execution. For example, the following code loads a managed assembly from the supplied file path and invokes the entry point method.

public void LoadAndExecute(string strAssemblyPath)
{
    try
    {
        Assembly asm = Assembly.LoadFrom(strAssemblyPath);
        asm.EntryPoint.Invoke(null,null);
    }
    catch(Exception ex)
    { 
        MessageBox.Show(ex.Message);
    } 
}

The typical entry point for any C# assembly would be main(). Now, once we are equipped with the above code, let us see how we could use it to solve our problem.

.Net and Interoperability:

Anyone who is familiar with .net could easily recollect the interoperability as a feature of .net allowing code-reusability between managed assemblies and native executables. In other words, we could write a component in .net and use it in native code and vice-versa. That means, we could use Interoperability to bring all the features of .net to native code. And that includes Reflection also.

Note that earlier we discussed how Reflection allows one to load an assembly and execute it. Now, what would be more easier than hiding the managed assembly in a native exe and using Reflection at run-time to load and execute it?

That is, we could store our managed assembly as a resource in native exe and whenever we wish to execute the assembly, we could dump it onto hard disk in some unknown location (with share-exclusive permissions so that user cannot open it) and invoke it using the Interoperated Reflection mechanism. Once we are done with the execution, we could delete the assemblies from hard disk and leave no traces.

Even better, we could, instead of dumping the assemblies onto disk and invoking them from there, send them directly to the Interoperated Reflection as an in-memory byte array and execute them then and there. As we noted earlier, it is possible to load and execute an assembly from a byte array using the Assembly class's Load() method. The following managed code presents the same technique.

public interface IInvokeAssembly
{
    void LoadAndExecute(byte[] pBuf);
};

public class CInvokeAssembly : IInvokeAssembly
{
    public CInvokeAssembly()
    {
    }
    public void LoadAndExecute(byte[] pBuf)
    {
        try
        {
            Assembly asm = Assembly.Load(pBuf);
            asm.EntryPoint.Invoke(null,null);
        }
        catch(Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }
}

The interface IInvokeAssembly is required to interoperate with the native exe. Note that we are no longer using a string path to load the assembly from, but we are using a byte[] buffer. In the native exe we could create a COM pointer for this interface and invoke its LoadAndExecute() method supplying the assembly contents in a buffer as the argument. We need to use a SafeArray for this purpose as shown below.

void InvokeAssemblyResource()
{ 
    IInvokeAssemblyPtr pInvoker;  //COM Pointer to the .Net Interface

    if(FAILED(pInvoker.CreateInstance(CLSID_CInvokeAssembly)))
    {
        MessageBox(NULL,_T("Unable to Create Invoke Assembly Object !!"),_T("Error"),MB_OK|MB_ICONERROR);
        return;
    }

    HRSRC hRC = FindResource(NULL,MAKEINTRESOURCE(IDR_EMBEDDED_ASSEMBLY),"RT_EMBEDDED_ASSEMBLY");
    HGLOBAL hRes = LoadResource(NULL,hRC);
    DWORD dwSize = SizeofResource(NULL,hRC);

    SAFEARRAY* pSA = NULL;

    if(NULL !=(pSA = SafeArrayCreateVector(VT_UI1, 0, dwSize)))
    {
        LPVOID pBuf = NULL;

        if(FAILED(SafeArrayAccessData(pSA,&pBuf)))
            MessageBox(NULL,_T("Unable to Access SafeArray Data"), _T("Error"),MB_OK|MB_ICONERROR);
        else
        {
            LPVOID hAsm = LockResource(hRes);

            memcpy(pBuf, hAsm, dwSize);
            
            UnlockResource(hRes);
            
            SafeArrayUnaccessData(pSA);
        }

        pInvoker->LoadAndExecute(pSA);	//Invoke the Reflection to load and Execute our Byte[]
    }
    else
        MessageBox(NULL,_T("Unable to Allocate Memory"),_T("Memory Allocate Error"),MB_OK|MB_ICONERROR);

    if(pSA) SafeArrayDestroy(pSA);
}

The above native code loads the assembly from a resource and creates a SafeArray from its contents. When supplied to the IInvokeAssemblyPtr, the SafeArray would be automatically converted into a byte[] by the .net Interoperability marshalling mechanism. (Note: Do not forget to call the CoInitialize() and CoUnintialize() methods in the native code before and after respectively. Without them we can not create the COM objects for the .Net interfaces)

Now with this generate-on-demand mechanism, we have completely eliminated the chance of user accessing the assemblies directly from the disk. Whenever the user wishes to execute the managed assembly, we load it from our native exe resource and execute it directly in memory. However, there is still a small loophole here. It is possible that the user can extract the resources of the native exe by using any Resource Editor programs available and do whatever he/she wishes to do. We want to address that possibility also.

The Solution:

Till now, we traced the following steps:

  1. We wanted to hide the metadata part of the managed assembly from the user. Towards this extent, we decided instead to hide the entire assembly itself.
  2. To hide the assembly from the user we wanted to store it as resource data in native exe and let the native exe generate the assembly on-demand and execute it using the Interoperated-Reflection methods. However, we realized that resource data in native exes is open to every one.

Now we all know that we could not hide the resources of native exe. But we could make them meaningless. That is, we could, instead of placing the managed assembly directly, place it encrypted. That way anyone who extracts it from resource would not be able to use it directly. Towards further security we could add custom headers to the managed assembly and encrypt the whole as a unit. Anyone who want to extract the managed assembly now not only need to decrypt it but also need to know about our custom header information to use it properly. Simple yet elegant solution. Of course, you could apply your own safety mechanisms to build on this further.

Proof of Concept Demo:

You can download a proof of concept demo that generates a native exe wrapper for any given managed assembly here: Download Demo. The operation of the demo is as follows:

  1. When you run the SecureAssembly.exe you would be presented with a dialog box that accepts a managed assembly file path, a native exe destination path and optional password.
  2. Once you select the appropriate managed assembly and native executable destination, you could create the secure native exe for the managed assembly by clicking the "Go!" button.
  3. When you click the "Go!" button, a native exe would be created at the selected destination path with the managed assembly embedded in its resources (after appropriately encrypting with the supplied password, of course).
  4. Now, Your managed assembly is safe. You could access it any time by executing the generated native exe.
  5. When you execute the native exe, you would be prompted for the password first. You should supply the same password that you have used for its creation (You can leave it blank if you did not specify any password).
  6. The supplied password would be used for decrypting the embedded managed assembly. Supplying a wrong password would result in a malformed assembly that can not be executed. It should be noted that, through out this process we do not store the password anywhere. We just use the password as and when supplied by the user. In this sense, a forgotten password is as good as a lost assembly.

The code required to load and execute the managed assembly is placed in a dll named InvokeAssembly.dll for Interoperability with the native code. You should first copy it to the directory of the native exe and register it by using the RegAsm command at command prompt before starting the native exe. Usage of RegAsm is as shown below.
<Dir>:\> RegAsm InvokeAssembly.dll

If you try to run the native exe without registering the dll, you would encounter a "Unable to Create Invoke Assembly Object" error.

Another issue worth noting is the prototype of the managed assembly entry point method. For simplicity the demo assumes the entry point method to be of type: static void Main(void). If your managed assembly entry point is defined differently, you might encounter a "Parameter Mismatch" error.

Conclusions

Reflection in .net allows any assembly to be loaded and executed at run time. It also allows the managed assemblies to be disassembled easily. However, by embedding the assemblies in native executables as encrypted resources, we could hide them from being disassembled. Once safely hidden in native exes, they could be accessed any time by using Reflection through native-managed application Interoperability.

Complete details on Interoperability can be found in the documentation: Interoperating with Unmanaged Code and details on Reflection could be gathered from: Loading and Executing User Code. Further depth in Reflection could be gained from: Emitting Dynamic Assemblies.

Appendix: An Extension with Common Language Runtime Hosting (CLR-Hosting)

While the concept of creating a custom COM interface object and interoperating with it to load and execute the managed assemblies gives satisfactory results, it might sometimes be required that the presence of external dll should be avoided and the interoperation should be carried out within a single process.

In such cases one can resort to the technique of CLR-Hosting to invoke the managed assembly execution from native code.

This technique is essentially same as that of COM Interoperability in that one has to create COM objects for the managed classes and has to invoke their methods. However, the difference lies in the fact that we just use the interfaces of existing managed classes exposed by "MSCorLib" engine, and do not create any custom interfaces of our own. By "Hosting" the CLR engine in our native application, we eliminate the need to create custom COM interfaces for the managed classes, there by removing the need for any unnecessary external dlls.

Hosting the CLR-Engine in a native application can be achieved by using the CorBindToRuntimeEx() native function from "MSCoreE.h", which returns a ICorRuntimeHost interface upon success.

The ICorRuntimeHost interface can then be used to query the default application domain, represented by the _AppDomain interface, to load the assembly in that domain. The assembly loading can be done by using the _AppDomain::Load(Byte[]) method, which returns an Assembly interface that can be used to invoke the entrypoint method thus triggering the execution of the assembly. The native code that loads the CLR and invokes the assembly is as shown below.

//
// InvokeAssemblyResource: Initializes the CLR and Invokes the Decrypted Assembly; ShutsDown the CLR at the end;
// 
bool InvokeAssemblyResource()
{		
    CComPtr<ICorRuntimeHost> spRuntimeHost;
    CComPtr<_AppDomain> spAppDomain;
    CComPtr<IUnknown> spUnk;

    bool bSuccess = false;

    if(FAILED(CorBindToRuntimeEx( NULL,	// Latest Version by Default
                L"wks",      // Workstation build
                STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN,
                CLSID_CorRuntimeHost ,
                IID_ICorRuntimeHost ,
                (void**)&spRuntimeHost)))
    {
        gErrMsg = _T("Unable to Bind CLR");
        return false;
    }
    if(FAILED(spRuntimeHost->Start()))
    {
        gErrMsg = _T("Unable to Start CLR");
        return false;
    }
    do
    {
        if(FAILED(spRuntimeHost->GetDefaultDomain(&spUnk)))
        {
            gErrMsg = _T("Unable to GetDefaultDomain");
            break;
        }
        if(FAILED(spUnk->QueryInterface(&spAppDomain.p)))
        {
            gErrMsg = _T("Unable to Query AppDomain Interface");
            break;
        }

        SAFEARRAY* pSA = GetDecryptedResource();
        if(pSA)
        {
            try
            {    // Invoke the Entry Point with No Arguments
                spAppDomain->Load_3(pSA)->EntryPoint->Invoke_3(_variant_t(), NULL);	
                bSuccess = true;  // Everything Went Fine !!
            }
            catch(_com_error ex)
            {
                gErrMsg = ex.ErrorMessage();
            }

            SafeArrayDestroy(pSA);
            pSA = NULL;	
        }
    }while(false);

    if(FAILED(spRuntimeHost->Stop()))
    {
        gErrMsg = _T("Unable to Stop CLR");
        return false;
    }

    return bSuccess;
}
Complete details of CLR-Hosting can be obtained from: Hosting the Common Language Runtime. Further, one might find the following articles also useful: Creating a host application for the .NET Common Language Runtime, Microsoft .NET: Implement a Custom Common Language Runtime Host for Your Managed App (Thanks to Mr. Guillermo Zambrino for pointing me this concept of CLR-Hosting and these reference links).

The proof of concept demo has now been extended to provide an option to generate the secured assembly based on external dll technique or CLR-Hosting technique. Based upon the choice, appropriate native executable would be dumped onto the harddisk embedding the assembly in its resources. (Note: In case of CLR-Hosting technique, there would be no need of InvokeAssembly.Dll or the use of RegAsm. But, the dll and its registration are required when opting the external dll technique.)

By   

P.GopalaKrishna

[email protected]

Index    Other Articles