Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Copy Protection for Windows Applications (Part 3)

4.87/5 (10 votes)
16 Sep 2009LGPL39 min read 52.5K   2.3K  
Describes the implementation of a key registration, installation, and validation methodology for Windows applications

Image 1

Introduction

This is the third of a four part series of articles on a system that allows you to protect your software against unlicensed use. This series will follow the outline below:

  1. Describe the architecture of the system
  2. Describe the creation of the license key
  3. Describe the process of installing a license key
  4. Describe the process of validating an installation

Along the way, we will look at n-bit block feedback encryption (also known as cipher-block chaining; see the Wikipedia article), and an obfustication method that should suitably prevent any attempts to understand how the system works. To add to the difficulty of reverse engineering the system, the result will be a set of COM objects written in unmanaged C++.  Interop can be used to invoke the appropriate methods from the .NET Framework.

Background

In part 2, we looked at the source code to the encryption / decryption routines and how the former is used to generate a license key. Finally, we examined the "data structure" that is the license key so that we can begin to gain understanding into how the system works. In this article, we will look at the process of installing the license key both in a direct manner and via the Microsoft Installer.

Disclaimer

Before we begin, your expectations need to be properly set. Specifically, no one reading this series should delude themselves into thinking this system, or any copy-protection mechanism for that matter, is ironclad. The question you should be asking yourself is not "can anyone crack this" but instead "does the person have the skills to do it and will they think it is a reasonable investment of their time to figure out how it works?" Remember: where there's a will, there's a way.

Direct Installation

Before we look at the code, we need to do the following:

  • Understand the reason behind a design decision
  • Understand the use case for installation

The Microsoft Installation SDK only allows integration via a "standard" DLL with exported functions. It was decided, then, to author the direct installation routine as an exported function as well.

Now consider the use case. In this instance, the installer knows what it is installing.  Specifically, this knowledge is embodied in the manufacturer and product IDs, which are hard coded into the installer. The user, on the other hand, has a license key that is either legitimate (i.e. it was printed on the packaging; delivered by you via email; etc.) or isn't. Our installation routine should first compare the hard coded values with those stored in the license key that was provided by the user. If there is a match, then the installation continues; if not, it stops.

Now let's look at the code for the direct installation routine:

C++
extern "C" __declspec(dllexport) bool __stdcall InstallDirect
	(LPSTR lpstrKey, LONG lID1, LONG lID2)
//---------------------------------------------------------------------------------------
// This function installs the key in the system.  
// Essentially, it decodes the specifies key;
// compares the stored values for the manufacturer and 
// product IDs against those that were passed;
// and if they match then the key is installed.
//
// The usage is intended to be this way:  the installer 
// specifies values for the manufacturer and
// product IDs.  The user specifies the installation key.  
// When the installation key is valid then
// all proceeds as expected.
//---------------------------------------------------------------------------------------
{
   bool blnReturn;

   blnReturn = false;

   if (Out(lpstrKey, lID1, lID2))
      if (CheckKeyValues(lpstrKey, lID1, lID2, 0))
         if (InstallKey(lpstrKey, lID1, lID2))
            blnReturn = true;

   return blnReturn;
}

As you can see, there really isn't anything complicated happening here. We simply want to compare the manufacturer and product IDs in the license key with the values supplied by the installer. If there is a match, we assume that the installation key is valid and continue. (We will look at the CheckKeyValues and InstallKey functions later in this article.)

Microsoft's Installation SDK

As we stated before, the SDK allows for integration via a standard DLL with exported functions. This is important because we need to not only retrieve the license key entered by the user, the manufacturer and product IDs, and communicate the validation result back to the SDK. The communication mechanism to accomplish this is via the following two APIs:

C++
UINT MsiGetProperty(MSIHANDLE hInstall, LPSTR pszName, 
	LPSTR pszValueBuf, LPDWORD pdwValueBuf);
UINT MsiSetProperty(MSIHANDLE hInstall, LPSTR pszName, LPSTR pszValue);
  • hInstall is the Installer "handle" and is provided to our DLL function.
  • pszName points to the name of the property we wish to get or set.
  • pszValueBuf points to the buffer to hold the results and pdwValueBuf points to a double word that, on entry, contains the size of the buffer but on exit contains the size of the value in bytes.
  • pszValue points to the value to associate with the property.

Using the APIs is simple, as you can imagine. Now, let's look at the code for our DLL function.

C++
#define MSIK_PIDKEY TEXT("PIDKEY")
#define MSIK_PIDCHECK TEXT("PIDCHECK")
#define MSIK_MANUFACTURERID TEXT("MANUFACTURERID")
#define MSIK_PRODUCTID TEXT("PRODUCTID")
#define MSIV_TRUE TEXT("TRUE")
#define MSIV_FALSE TEXT("FALSE")

extern "C" __declspec(dllexport) UINT __stdcall InstallMSI(MSIHANDLE hInstall)
//---------------------------------------------------------------------------------------
// This function installs the key via integration with the Microsoft Installer.
//---------------------------------------------------------------------------------------
{
   PTCHAR pstrResult;
   DWORD dwBuffer;
   TCHAR tchKey[MAX_PATH];
   CHAR achKey[MAX_PATH];
   TCHAR tchManuID[MAX_PATH];
   TCHAR tchProdID[MAX_PATH];
   LONG lManuID;
   LONG lProdID;
   LPSTR lpstrEnd;

   pstrResult = MSIV_FALSE;

   dwBuffer = sizeof(tchKey) / sizeof(TCHAR);

   //---------------------------------------------------------------------------------
   // Microsoft's installer stores the license key that 
   // the user entered in a key/value pair
   // named PIDKEY.
   //---------------------------------------------------------------------------------
   if (MsiGetProperty(hInstall, MSIK_PIDKEY, tchKey, &dwBuffer) == ERROR_SUCCESS)
   {
      //------------------------------------------------------------------------------
      // Convert the key from wide-character to ASCII (8-bit).
      //------------------------------------------------------------------------------
      CW2A convKey(tchKey);

      strcpy_s(achKey, sizeof(achKey), convKey);

      //-----------------------------------------------------------------------------
      // The Windows Installer includes the literals in the key mask, 
      // so we need to remove the '-'
      // characters.
      //------------------------------------------------------------------------------
      dwBuffer = strlen(achKey);

      for (int iIndex = 0; iIndex < (int)dwBuffer; iIndex++)
         if (strchr(BASE32_CHARSET, achKey[iIndex]) == 0)
         {
            strcpy_s(&achKey[iIndex], sizeof(achKey) - iIndex, &achKey[iIndex + 1]);
            iIndex--;
            dwBuffer--;
         }

      //-----------------------------------------------------------------------------
      // Get the manufacturer and product IDs from the .msi file.
      //------------------------------------------------------------------------------
      dwBuffer = sizeof(tchManuID) / sizeof(TCHAR);

      if (MsiGetProperty(hInstall, MSIK_MANUFACTURERID, 
			tchManuID, &dwBuffer) == ERROR_SUCCESS)
      {
         dwBuffer = sizeof(tchProdID) / sizeof(TCHAR);

         if (MsiGetProperty(hInstall, MSIK_PRODUCTID, 
			tchProdID, &dwBuffer) == ERROR_SUCCESS)
         {
            //-----------------------------------------------------------------------
            // the .msi file contains wide-character versions of the values, 
            // so convert them to ASCII so that we can then convert them to numbers.
            //-----------------------------------------------------------------------
            CW2A convManuID(tchManuID);
            CW2A convProdID(tchProdID);

            lManuID = strtol(convManuID, &lpstrEnd, BASE_HEX);
            lProdID = strtol(convProdID, &lpstrEnd, BASE_HEX);

            //------------------------------------------------------------------------
            // Code reuse for the win!
            //------------------------------------------------------------------------
            if (InstallDirect(achKey, lManuID, lProdID))
               pstrResult = MSIV_TRUE;
         }
      }
   }

   //-----------------------------------------------------------------------------------
   // Unfortunately, if we return MSIV_FALSE the installer 
   // provides no real feedback to the user
   // so we have to display - ick! - a message box on our own.  
   // It is, in my opinion, rather
   // shortsighted of Microsoft to provide no way of displaying 
   // a message with the same look
   // and feel of the installer.
   //-----------------------------------------------------------------------------------
   if (_tcscmp(pstrResult, MSIV_FALSE) == 0)
      MessageBox(GetForegroundWindow(), 
               TEXT("Either an invalid license key was specified or an error occurred."), 
               TEXT("Error"), 
               MB_OK | MB_ICONERROR | MB_APPLMODAL);

   MsiSetProperty(hInstall, MSIK_PIDCHECK, pstrResult);
   return 0;
}

In the ATL (ActiveX Template Library) header files, there is a very useful macro (CW2A, which stands for "covert wide-character to ASCII") that allows us to convert from Unicode to the ASCII character set. Normally, you wouldn't want to do this but you'll recall that our encryption routine limits the characters in the resulting string to our "base 32" set, which is a subset of the set of ASCII characters. Therefore, there is no danger in doing this. Since our DLL is compiled as a Unicode DLL, we need to use this macro in several locations since the MSI SDK returns its values as wide character strings.

Other things to note: the license key entered by the user is stored in a key named PIDKEY and the validation result is stored in a key named PIDCHECK. We can also add our own key / value pairs to the MSI using Microsoft's Orca utility, which (incidentally) is also used to integrate our DLL into the MSI. Not surprisingly, we are looking for two key / value pairs named MANUFACTURERID and PRODUCTID, which contain the values that we will look for in the user supplied license key.

Other Functions

Let's look at the CheckKeyValues and InstallKey functions:

C++
bool _stdcall CheckKeyValues(LPSTR pchKey, LONG lManuID, LONG lProdID, LONG lCaps)
//---------------------------------------------------------------------------------------
// This function extracts the manufacturer and product IDs 
// and the licensed capabilities from the
// specified key and compares them to the specified values.  
// Note that licensed capabilities can
// be a superset of the lCaps argument, so we need to perform 
// a binary AND operation to ensure that
// all of the bits are set rather than a logical comparison for equality.
//---------------------------------------------------------------------------------------
{
   CHAR achPart[MAX_PARTLEN + 1];
   LONG lCompManuID;
   LONG lCompProdID;
   LONG lCompCaps;
   LPSTR lpstrEnd;

   memset(achPart, 0, sizeof(achPart));
   strncpy_s(achPart, sizeof(achPart), &pchKey[OFF_MANUFACTURER], MAX_PARTLEN);
   lCompManuID = strtol(achPart, &lpstrEnd, BASE_HEX);

   memset(achPart, 0, sizeof(achPart));
   strncpy_s(achPart, sizeof(achPart), &pchKey[OFF_PRODUCT], MAX_PARTLEN);
   lCompProdID = strtol(achPart, &lpstrEnd, BASE_HEX);

   memset(achPart, 0, sizeof(achPart));
   strncpy_s(achPart, sizeof(achPart), &pchKey[OFF_CAPS], MAX_PARTLEN);
   lCompCaps = strtol(achPart, &lpstrEnd, BASE_HEX);

   return ((lManuID == lCompManuID) && (lProdID == lCompProdID) 
		&& ((lCaps & lCompCaps) == lCaps));
}

This function is relatively simple in its operation: it extracts the values of the manufacturer and product IDs as well as the licensed capabilities from the unencrypted license key and performs the appropriate comparison.

Note that the capabilities are expressed as a set of bit flags. This allows you to provide a license key that contains all licensed capabilities but only checks for the appropriate bit(s) in each executable that comprises your application.

The InstallKey function is a bit more complicated.  It follows this logic:

  • Create a registry key name based on the manufacturer and product IDs. We do not use the licensed capabilities because the component calling the validation routine will probably not know the full set of licensed capabilities, yet we still need to look up the full value in the registry.
  • Update the date in the license key before installing so that we may properly calculate the number of days elapsed from installation (for 30-day product evaluations, for example).
  • Encrypt both the registry key name and the key value based on a calculatable overlay.
  • Write the values to the registry.
C++
#define REGK_SOFTWARE_NBBF      "SOFTWARE\\Nbbf"

#define FMT_REGKEY            "%08lX%08lX"

bool _stdcall InstallKey(LPSTR lpstrKey, LONG lManuID, LONG lProdID)
//---------------------------------------------------------------------------------------
// This function installs the specified key.  
// While we could extract the manufacturer and product
// IDs from the key itself, this method is only called 
// from a routine that already has them
// separated.  Therefore, we'll require the caller to 
// specify them as arguments to save some time.
//---------------------------------------------------------------------------------------
{
   CHAR achRegKey[MAX_KEYLEN+1];
   CHAR achRegValue[MAX_KEYLEN+1];
   CHAR achOverlay[MAX_KEYLEN+1];
   bool blnReturn;
   HKEY hkRoot;
   DWORD dwDisp;

   achRegValue[0] = 0;
   strncat_s(achRegValue, sizeof(achRegValue), lpstrKey, sizeof(achRegValue));

   //------------------------------------------------------------------------------------
   // We need to store the product key as a value other 
   // than the product key to at least make it 
   // more difficult to deduce, i.e. a simple registry search 
   // for the product key won't yield the 
   // answer.  Therefore, we will produce a string of the 
   // hexadecimal digits of the product and 
   // manufacturer IDs and encrypt the key yielding the 
   // subkey in the registry under which we will 
   // store the encrypted product key.
   //-------------------------------------------------------------------------------------
   sprintf_s(achRegKey, sizeof(achRegKey), FMT_REGKEY, lProdID, lManuID);

   //-------------------------------------------------------------------------------------
   // Create the encryption overlay and reset the date / time 
   // in the key to the current date / time.
   // This latter operation is required since the generation 
   // of the key has no idea when the product
   // will actually be installed.  Therefore, the Elapsed method 
   // of the validation object will not
   // be able to return an accurate number unless we do this.
   //-------------------------------------------------------------------------------------
   OverlayFromIDs(lManuID, lProdID, achOverlay);
   SetKeyDateTime(achRegValue);

   blnReturn = false;
   hkRoot = NULL;

   try
   {
      //---------------------------------------------------------------------------------
      // Generate the registry key name and value to be stored therein.
      //
      // Using exceptions for error handling is a bad practice 
      // generally but since this will
      // never be used in a high-performance application 
      // we shouldn't be terribly concerned.
      //---------------------------------------------------------------------------------
      if (!In(achRegKey, achOverlay))
         throw 1;

      if (!In(achRegValue, achOverlay))
         throw 2;

      //--------------------------------------------------------------------------------
      // Update the registry.
      //--------------------------------------------------------------------------------
      if (RegCreateKeyExA(HKEY_LOCAL_MACHINE,
                     REGK_SOFTWARE_NBBF, 
                     0,
                     NULL,
                     REG_OPTION_NON_VOLATILE,
                     KEY_WRITE,
                     NULL,
                     &hkRoot,
                     &dwDisp) != ERROR_SUCCESS)
         throw 3;

      if (RegSetValueExA(hkRoot, 
                     achRegKey,
                     0,
                     REG_SZ, 
                     (LPBYTE)achRegValue, 
                     strlen(achRegValue) + 1) != ERROR_SUCCESS)
         throw 4;

      blnReturn = true;
   }
   catch (int iCode)
   {
   }

   //-----------------------------------------------------------------------------------
   // Cleanup.
   //-----------------------------------------------------------------------------------
   if (hkRoot != NULL)
      RegCloseKey(hkRoot);

   return blnReturn;
}

Integration with Microsoft's Installer

This topic really belongs in a separate article, but we will briefly look at the means by which you integrate with an MSI file that you create using the Setup and Deployment Wizard in Visual Studio. A more thorough treatment of the topic may be found in the following web locations:

Before you can begin, you need the Orca utility that comes with the MSI SDK. Understand that every time you build the Setup project in your solution, your MSI will be recreated and these steps will need to be re-executed. Therefore, you may not want the project to be built automatically whenever the solution is built.

Also note that there are some problems with the links above, notably that the link to download the SDK on Microsoft's site does not work currently and Robert Graham's article was written some time ago. If you use Visual Studio 2008, some of his steps are unnecessary because Visual Studio does them for you when your Setup project is built.

Step 1:  Add the Nbbf DLL to the MSI File

  1. Using Orca, open the MSI file and find the Binary table. 
  2. Add a new row and specify a logical name for the DLL in the Name field.  We will need to reference the DLL using this value, so remember what you enter here.
  3. In the Value field, click on the browse button and find the Nbbf DLL.

Step 2:  Define the Custom Action

  1. Select the CustomAction table.
  2. Add a new row and specify a logical name for the action in the Action field.  We will need to reference the action using this value, so remember what you enter here.
  3. In the Source field, specify the logical name of the DLL.
  4. In the Target field, specify the following value:  _InstallMSI@4. The text left of the @ sign is the name of the actual function that is exported. The value 4 represents the number of bytes on the stack that is required for arguments that are passed to the function. Together, this entire string is written to the exported functions table.
  5. In the Type field, specify the value 1. This indicates to the Microsoft Installer that the contents of the corresponding row in the Binary table represents a DLL.

Step 3:  Configure Existing and Add New Property Values

These are all referenced by the InstallMSI function via the MsiGetProperty and MsiSetProperty APIs.

  1. Select the Property table.
  2. Add a new row and specify the value PIDCHECK for the Property field.
  3. In the Value field, specify the value FALSE.
  4. Add a new row and specify the value MANUFACTURERID for the Property field.
  5. In the Value field, specify an appropriate value.
  6. Add a new row and specify the value PRODUCTID for the Property field.
  7. In the Value field, specify an appropriate value.
  8. Select the row for the PIDTemplate property.
  9. Modify the Value field so that it contains <#####-#####-#####-#####-#####> .  This specifies the format of the license key field in the Customer Information dialog.

Step 4:  Configure Dialog Actions

If you do not follow these steps exactly, the MSI will be corrupted and will not work as expected.  In this step, we are modifying the navigation between the steps in the installer and the logic that controls the navigation.

  1. Select the ControlEvent table.
  2. Sort by the Dialog field and find the block of rows corresponding to the CustomerInfoForm dialog.
  3. Select the row containing NextButton in the Control field and ValidateProductID in the Event field.
  4. Modify the Event field so that it contains DoAction.
  5. Modify the Argument field so that it contains the logical name of the action that you specified in step 2, item 2.
  6. Select the row containing NextButton in the Control field and NewDialog in the Event field.
  7. Modify the Condition field so that in contains the following value:  (PIDCHECK="TRUE") AND CustomerInfoForm_NextArgs<>"" AND CustomerInfoForm_ShowSerial<>""

Close Orca because you're done! You should now be able to install your application using a license key that you generate using the sample application included in this and the prior parts of this series of articles.

Summary

In this article, we looked at the all important mechanisms by which license keys are stored in the registry. We not only examined this aspect of the system from a "straight up" code perspective but also using the MSI SDK to communicate with the Microsoft Installer. Finally, we looked at how to integrate our DLL with an MSI file using Microsoft's Installation Database tool, Orca.

Next time, we will look at the key validation.

History

  • September 16, 2009 - Initial version

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)