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

Copy Protection for Windows Applications (Part 2)

4.88/5 (13 votes)
10 Sep 2009LGPL34 min read 39.2K   2.1K  
Describes the implementation of a key registration, installation, and validation methodology for Windows applications.

Image 1

Introduction

This is the second 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:

  • Describe the architecture of the system
  • Describe the creation of the license key
  • Describe the process of installing a license key
  • 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 obfuscation 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 1, we looked at the overall objectives of the system and the means by which we could achieve those objectives. Finally, we discussed in high level terms how the encryption / decryption works.

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.

Encryption and Decryption

At the end of part 1, we talked about the codec in high-level terms. Now, let's examine the code itself.

C++
bool _stdcall In(LPSTR pchKey, LPSTR pchOverlay)
//---------------------------------------------------------
// This function encrypts the specified key using
// the specified overlay. It uses a cipher block
// chaining algorithm (sometimes known as n-bit block
// feedback encryption) which essentially uses
// the previous encryption result as input to the current
// encryption iteration. The upshot of this
// is that data at the beginning of the string affects
// the encrypted results after it.
//---------------------------------------------------------
{
   int iLenKey;
   int iLenOver;
   LPSTR pbBuf;
   int iIndex;
   LPSTR pchVal;
   char bVal1;
   char bVal2;
   bool blnReturn;

   iLenKey = strlen(pchKey);
   iLenOver = strlen(pchOverlay);

   //-------------------------------------------------------------
   // The resulting data is the same size as the input,
   // so build the results in a separate buffer.
   //-------------------------------------------------------------
   pbBuf = (LPSTR)calloc(iLenKey, sizeof(char));

   for (iIndex = 0; iIndex < iLenKey; iIndex++)
   {
      //----------------------------------------------------------
      // Find the position of the current input byte
      // in the digit set. If not found then exit.
      //----------------------------------------------------------
      pchVal = strchr(BASE32_CHARSET, pchKey[iIndex]);

      if (!pchVal)
         break;

      //----------------------------------------------------------
      // Convert to a numerical value.
      //----------------------------------------------------------
      bVal1 = (char)(pchVal - BASE32_CHARSET);

      //----------------------------------------------------------
      // Repeat the above conversion for the current overlay byte.
      //----------------------------------------------------------
      pchVal = strchr(BASE32_CHARSET, pchOverlay[iIndex % iLenOver]);

      if (!pchVal)
         break;

      bVal2 = (char)(pchVal - BASE32_CHARSET);

      //---------------------------------------------------
      // XOR the two values plus the n-1'th encrypted
      // byte if this is not the first byte of data.
      //---------------------------------------------------
      pbBuf[iIndex] = bVal1 ^ bVal2;

      if (iIndex > 0)
         pbBuf[iIndex] ^= pbBuf[iIndex - 1];

      //----------------------------------------------------
      // Convert back from a numerical
      // value to the appropriate digit.
      //----------------------------------------------------
      pchKey[iIndex] = (char)BASE32_CHARSET[pbBuf[iIndex]];
   }

   //-------------------------------------------------------
   // Cleanup.
   //-------------------------------------------------------
   free(pbBuf);

   blnReturn = (iIndex == iLenKey);

   if (!blnReturn)
      memset(pchKey, 0, iLenKey);

   return blnReturn;
}

bool _stdcall In(LPSTR pchKey, LONG lManuID, LONG lProdID)
//-----------------------------------------------------
// This overloaded function converts the specified
// manufacturer and product IDs into the encryption
// overlay then calls the other version
// of the In function to encrypt the data.
//-----------------------------------------------------
{
   CHAR achOverlay[MAX_KEYLEN + 1];

   OverlayFromIDs(lManuID, lProdID, achOverlay);

   return In(pchKey, achOverlay);
}

Although the resulting data is the same length as the input data, we cannot encrypt in place because we are doing the conversion to our base 32 character set. In other words, the XOR operations take place on the data prior to the conversion, so we need to store those numerical values in a separate buffer.

Note the use of the overloaded function. This is due to a code refactoring, where it was discovered that the majority of invocations did an explicit conversion of the manufacturer and product IDs into the encryption overlay. The second function simply packages that up nicely to help keep the code clean.

You may ask why I used such vague function names for the encryption routines (and decryption, as we'll see). Former Intel chairman Andy Grove once said (paraphrased to highlight my point), "you're only paranoid if you're wrong". I'd rather use vague sounding names than to have someone go rummaging through the object code looking for hints as to how the code works.

Decryption is, as previously noted in part 1, essentially the opposite of the encryption routine.

C++
bool _stdcall Out(LPSTR pchKey, LPSTR pchOverlay)
//------------------------------------------------------
// This function decrypts the specified key using
// the specified overlay. It uses a cipher block
// chaining algorithm (sometimes known as n-bit
// block feedback encryption) which essentially uses
// the previous decryption result as input
// to the current decryption iteration.
//------------------------------------------------------
{
   int iLenKey;
   int iLenOver;
   LPSTR pbBuf;
   int iIndex;
   LPSTR pchVal;
   char bVal1;
   char bVal2;
   char bVal3;
   bool blnReturn;

   iLenKey = strlen(pchKey);
   iLenOver = strlen(pchOverlay);

   //------------------------------------------------------------------
   // The resulting data is the same size as the input,
   // so build the results in a separate buffer.
   //------------------------------------------------------------------
   pbBuf = (LPSTR)calloc(iLenKey, sizeof(char));

   for (iIndex = iLenKey - 1; iIndex >= 0; iIndex--)
   {
      //---------------------------------------------------------------
      // Find the position of the current input byte
      // in the digit set. If not found then exit.
      //---------------------------------------------------------------
      pchVal = strchr(BASE32_CHARSET, pchKey[iIndex]);

      if (!pchVal)
         break;

      //---------------------------------------------------------------
      // Convert to a numerical value.
      //---------------------------------------------------------------
      bVal1 = (char)(pchVal - BASE32_CHARSET);

      //---------------------------------------------------------------
      // Repeat the above conversion for the current overlay byte.
      //---------------------------------------------------------------
      pchVal = strchr(BASE32_CHARSET, pchOverlay[iIndex % iLenOver]);

      if (!pchVal)
         break;

      bVal2 = (char)(pchVal - BASE32_CHARSET);

      //-------------------------------------------------------
      // XOR the two values plus the n-1'th encrypted
      // byte if this is not the first byte of
      // data. Unlike the encryption routine
      // where this value was already calculated, we have
      // to manually calculate it here before XOR'ing it.
      //-------------------------------------------------------
      pbBuf[iIndex] = bVal1 ^ bVal2;

      if (iIndex > 0)
      {
         pchVal = strchr(BASE32_CHARSET, pchKey[iIndex - 1]);

         if (!pchVal)
            break;

         bVal3 = (char)(pchVal - BASE32_CHARSET);

         pbBuf[iIndex] ^= bVal3;
      }

      //--------------------------------------------------------------
      // Convert back from a numerical value to the appropriate digit.
      //--------------------------------------------------------------
      pchKey[iIndex] = (char)BASE32_CHARSET[pbBuf[iIndex]];
   }

   //-------------------------------------------------------------
   // Cleanup.
   //-------------------------------------------------------------
   free(pbBuf);

   blnReturn = (iIndex < 0);

   if (!blnReturn)
      memset(pchKey, 0, iLenKey);

   return blnReturn;
}

bool _stdcall Out(LPSTR pchKey, LONG lManuID, LONG lProdID)
//-----------------------------------------------------------
// This overloaded function converts the specified
// manufacturer and product IDs into the decryption
// overlay then calls the other version
// of the Out function to decrypt the data.
//-----------------------------------------------------------
{
   CHAR achOverlay[MAX_KEYLEN + 1];

   OverlayFromIDs(lManuID, lProdID, achOverlay);

   return Out(pchKey, achOverlay);
}

Creating the License Key

The creation of the license key is relatively simple since the heavy-lifting is performed in the encryption (and decryption) routines.

C++
STDMETHODIMP CCreate::Create(LONG lID1, LONG lID2, LONG lID3, BSTR * pbstrKey)
//-------------------------------------------------------
// This COM method generates a license key given
// the manufacturer and product IDs and a bit-flag
// set of licensed capabilities. These are specified
// in lID1, lID2, and lID3 respectively. A BSTR
// is returned as the result.
//-------------------------------------------------------
{
   CHAR achKey[MAX_KEYLEN + 1];
   HRESULT hrReturn;

   //----------------------------------------------------
   // Initialize the key.
   //----------------------------------------------------
   memset(achKey, PAD_KEY, MAX_KEYLEN);
   achKey[MAX_KEYLEN] = 0;

   //----------------------------------------------------
   // Put the current date / time components in the
   // key so that the result has some degree of 
   // randomness to it. Remember:  the contents
   // at the beginning of the string to be encrypted
   // affect the encrypted result from that point on.
   //----------------------------------------------------
   SetKeyDateTime(achKey);

   //----------------------------------------------------
   // Add the manufacturer and product IDs
   // and the licensed capabilities.
   //----------------------------------------------------
   LongToKey(lID1, achKey, OFF_MANUFACTURER);
   LongToKey(lID2, achKey, OFF_PRODUCT);
   LongToKey(lID3, achKey, OFF_CAPS);

   //----------------------------------------------------
   // Convert the key to uppercase before processing
   // just in case there are some lowercase
   // hexadecimal digits. The base 32 digit
   // set uses uppercase characters only.
   //----------------------------------------------------
   _strupr_s(achKey, sizeof(achKey));

   //----------------------------------------------------
   // Encrypt, copy the result into a BSTR, then return.
   //----------------------------------------------------
   if (!In(achKey, lID1, lID2))
      hrReturn = OLE_E_CANTCONVERT;

   else
   {
      CComBSTR objReturn(achKey);

      *pbstrKey = objReturn.Copy();
      hrReturn = S_OK;
   }

   return hrReturn;
}

We've seen the SetKeyDateTime function invoked a few times. Its operation is important, so let's take a look at how it works.

C++
//--------------------------------------------------------------------
// TIMETOPART converts an hour / minute / second tuplet
// into a single number that may be converted
// to hexadecimal for addition into the unencrypted string.
// Note that, due to the number of bits
// available in a MAX_PARTLEN component,
// the hour is ignored. This is strictly used for adding
// randomization to the encrypted string
// so the loss of precision is acceptable.
//
// Seconds are stored in 7 bits (0-63). Minutes are stored
// in 7 bits (0-63). This yields a number
// with 14 bits of precision.
//
// DATETOPART converts a month / day / year (1900 offset)
// into a single number that may be converted
// to hexadecimal for addition into the unencrypted string.
//
// Year is stored as an offset from the year 2000
// and uses 7 bits (0-63). Day of the year is stored
// in 9 bits (0-512). This yields a number with 16 bits of precision.
//--------------------------------------------------------------------
#define TIMETOPART(h, m, s) (((s & 0x7F) << 7) | (m & 0x7F))
#define DATETOPART(yd, y) ((((y - 100) & 0x7F) << 9) | yd)

#define PARTTOYEAR(l) ((l >> 9) + 100)
#define PARTTOYEARDAY(l) (l & 0x1FF)

#define LWORD(lValue) (int)(lValue & 0xFFFF)
#define UWORD(lValue) (int)((lValue & 0xFFFF0000) >> 16)

void _stdcall SetKeyDateTime(LPSTR lpstrKey)
//----------------------------------------------------------
// This function adds some components of the current time
// to the front of the key so that the
// rest of the key is randomized to some degree.
// The date is also added as the year - 2000 plus
// the day of the year. This will easily allow us
// to calculate the number of days elapsed since
// the product was installed.
//----------------------------------------------------------
{
   time_t tNow;
   struct tm tGmt;

   time(&tNow);
   gmtime_s(&tGmt, &tNow);

   LongToKey(LWORD(TIMETOPART(tGmt.tm_hour, tGmt.tm_min, 
                              tGmt.tm_sec)), lpstrKey, OFF_TIME);
   LongToKey(LWORD(DATETOPART(tGmt.tm_yday, tGmt.tm_year)), lpstrKey, OFF_DATE);
}

The "mystery" of this function is in the macros TIMETOPART and DATETOPART. These two macros create a 16-bit integer comprised of components from the time and date as stored in the tm structure. While the time values are unused except for the randomness they bring to the encrypted license key, the date values are significant in that they allow us to calculate the number of days that have elapsed since the date stored in the license key. We'll see this used in part 4 when we discuss validation.

You're probably wondering if LongToKey has any mystery to it. As you'll see in the code below, it is simply a combination of a long to ASCII conversion plus a copy into the appropriate place in the license key structure.

C++
void _stdcall LongToKey(LONG lNumber, LPSTR lpstrKey, INT iOffset)
//------------------------------------------------------------
// This function converts the specified number to text
// and stores it in the specified part location
// within the key buffer.
//------------------------------------------------------------
{
   CHAR achPart[MAX_KEYLEN + 1];

   sprintf_s(achPart, sizeof(achPart), FMT_LONGTOHEX, lNumber);
   memcpy_s(&lpstrKey[iOffset], MAX_PARTLEN, achPart, MAX_PARTLEN);
}

License Key Structure

After reading that last sentence, you should be asking yourself, "how are the various components stored in the license key?" Therefore, let's discuss how the license data is stored.

C++
#define MAX_PARTLEN 4
#define MAX_KEYLEN 25

//------------------------------------------------------------------------
// These are the actual components of the non-encrypted string.
// These are concatenated together to
// form the entire string, which is then passed
// to the encryption routine to actually generate the license key.
// 
// PARTOFFSET is a macro that allows us to easily determine
// where each part goes or may be found for decryption.
//
// OFF_TIME is the offset of the time component.
// Note that this needs to be first so that the
// seconds component adds a fair amount of randomness
// to the encrypted string due to the nature
// of the algorithm used (n-bit block feedback).
//
// OFF_DATE is the offset of the date component.
// This allows us to determine when the key was
// generated, but could be used to substitute the date
// the key was used to install the software.
// This lends itself to a "30 day evaluation" type of license.
//
// OFF_MANUFACTURER and OFF_PRODUCT are the manufacturer and product IDs
//
// OFF_CAPS is a 16-bit (based on MAX_PARTLEN specifying
// the number of hexadecimal digits in the
// number) set of flags that the caller can use
// to specify components that are accessible using
// the license key.
//------------------------------------------------------------------------
#define PARTOFFSET(part) (MAX_PARTLEN * part + part)
#define OFF_TIME PARTOFFSET(0)
#define OFF_DATE PARTOFFSET(1)
#define OFF_MANUFACTURER PARTOFFSET(2)
#define OFF_PRODUCT PARTOFFSET(3)
#define OFF_CAPS PARTOFFSET(4)

Looking at the definitions above, you can see that the data is simply stored in specific locations within the license key string. Per the definitions above, the format is:

          1         2
0123456789012345678901234
-------------------------
ttttuddddummmmuppppuccccu

tttt is the time value. dddd is the date value. mmmm is the manufacturer ID value. pppp is the product ID value. And, cccc is the capabilities value. All instances of u are unused digits. As you can imagine, you may safely change the "indices" into the "array" for each of the parts in the definitions for each of the OFF_* macros as you see fit, as long as they are in the range 0-4 and are unique.

Summary

In this part of the series, we've looked at the all-important encryption and decryption functions. We've looked at the license key structure and, if you were paying attention, how the storage of the time at the very beginning of the license key data adds a (substantial) amount of randomness to the resulting license key after it is encrypted. Finally, we introduced some utility routines that are used throughout all three components.

Next time, we will look at the key installation and retrieval mechanism.

History

  • September 10, 2009 - Initial version.

License

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