Securing Managed Assemblies with Native EXE Interoperability

Thursday Oct 7th 2004 by Palem GopalaKrishna
Share:

Learn about a simple yet elegant way to secure managed assemblies from easy disassembly by using the powerful interoperability features of .NET.

Introduction

The Problem

Reflection

.NET and Interoperability

The Solution

Proof of Concept Demo

Conclusions

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 you forget for a moment the issues related to copyright and piracy and so forth, most of you at some point or another might have shown concern about securing your own assemblies from such easy exposure. Towards this extent, the following article describes a simple yet elegant way of securing your managed assemblies from easy disassembly by using the powerful interoperability features of .NET.

The Problem

Before you actually take up our final solution, let's first clearly specify what your problem is. In a nutshell, you could specify it something like, "No one should be able to disassemble my 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 whatnot. The metadata produced by the compiler as part of assembly generation makes all these operations trivial for Reflection. Without this metadata, Reflection could not do its work. And, the point to be noted is: Native executables do not have any such metadata.

Thus, you can safeguard your managed assembly if you can hide its metadata from the dissembler. However, hiding the metadata is a very hazardous option given the fact that CLR also needs access to it to execute the assembly. However, you could overcome this if you allow yourselves to hide the entire assembly instead. This is a simpler option; further, you could use the very Reflection (that caused all this trouble) as your tool to achieve it.

Reflection

The classes required for Reflection are available under the System.Reflection namespace and among them Assemblyis one very important class. This class allows you to load and execute assemblies at runtime. It supports overloaded Load() methods to load an assembly from an external file or raw bytes. Once properly loaded, the EntryPoint property allows one to get details about the entry point of the loaded assembly. EntryPoint is a MethodInfo-type property that gives all information, such as parameter types, return type, and so on, 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.Load(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 can 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 also includes Reflection.

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 a hard disk in some unknown location (with share-exclusive permissions so that the user cannot open it) and invoke it by using the Interoperated Reflection mechanism. Once we are done with the execution, we could delete the assemblies from the hard disk and leave no traces.

Even better, we could, instead of dumping the assemblies onto a 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 by using the Assembly class's Load() method. The following 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 IInvokeAssembly interface 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);
      }

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

   if(pSA) SafeArrayDestroy(pSA);
}

The preceding 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 cannot create the COM objects for the .NET interfaces.

Now, with this generate-on-demand mechanism, we have completely eliminated the chance of a user accessing the assemblies directly from the disk. Whenever the user wants 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

Until 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 everyone.

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 needs to decrypt it but also needs to know about our custom header information to use it properly. A simple yet elegant solution. Of course, you could apply your own safety mechanisms to build on this further.

Proof of Concept Demo

You could download a proof of concept demo that generates a native EXE wrapper for any given managed assembly from the SecureAssembly.zip link. 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 an 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 could not be executed.

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 the 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 will encounter a "Unable to Create Invoke Assembly Object" 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.

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved