Click here to Skip to main content
15,887,376 members
Articles / Programming Languages / C#

Copy Protection for Windows Applications (Part 4)

Rate me:
Please Sign up or sign in to vote.
4.97/5 (21 votes)
21 Sep 2009LGPL34 min read 110.8K   2.3K   127   63
Describes the implementation of a key registration, installation, and validation methodology for Windows applications
Image 1

Introduction

This is the fourth 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 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 3, we looked at the means by which license keys are installed, both directly and via the Microsoft Installer. Furthermore, we looked at how MSI files are edited using the Orca utility to accomplish the actual integration.

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.

Validation

Validation, from a business perspective (relative to this topic), has one mandatory component and possibly a second:

  • Determine that the installed key is valid
  • Determine that the evaluation period hasn't expired, if applicable

The first item is rather simple as you may imagine. Bearing in mind that this is a COM object, here is the code:

C++
STDMETHODIMP CValidate::Validate(LONG lID1, LONG lID2, LONG lID3, VARIANT_BOOL* pbValid)
//----------------------------------------------------------------------------------------
// This method validates that there is an installed license key 
// matching the manufacturer and product IDs. 
// It also checks that the requested capabilities were licensed.
//----------------------------------------------------------------------------------------
{
   CHAR achKey[MAX_KEYLEN + 1];
   HRESULT hrReturn;

   *pbValid = VARIANT_FALSE;
   hrReturn = NBBF_CANT_RETRIEVE;

   if (RetrieveKey(achKey, sizeof(achKey), lID1, lID2))
   {
      hrReturn = S_OK;

      if (CheckKeyValues(achKey, lID1, lID2, lID3))
         *pbValid = VARIANT_TRUE;
   }

   return hrReturn;
}

As you can see, all we are doing is retrieving the key, if it exists, using the manufacturer and product IDs. Then we are checking the key values to see if they match.

It should be noted that we couldn't retrieve the key unless the manufacturer and product IDs matched. Therefore, this last action is essentially boiled down to confirming that the requested capabilities were properly licensed. We could replace the call to CheckKeyValues with some code, but it would require decoding the key and extrapolating the information first, so I opted to reuse an existing function instead.

The RetrieveKey function is one we haven't seen before. Let's take a look at it.

C++
bool _stdcall RetrieveKey(LPSTR lpstrKey, LONG szKey, LONG lManuID, LONG lProdID)
//----------------------------------------------------------------------------------------
// This function retrieves the license key from the registry. 
// Unlike InstallKey where we could have extracted the manufacturer and product IDs 
// from the license key, we do not (yet) know what the
// license key is so this is impossible to do.
//----------------------------------------------------------------------------------------
{
   CHAR achRegKey[MAX_KEYLEN+1];
   CHAR achRegValue[MAX_KEYLEN+1];
   CHAR achOverlay[MAX_KEYLEN+1];
   bool blnReturn;
   DWORD dwSzValue;
   HKEY hkRoot;
   DWORD dwDisp;
   DWORD dwType;

   //------------------------------------------------------------------------------------
   // See comments in InstallKey about the need to obfuscate 
   // the registry key name and the product key value.
   //-------------------------------------------------------------------------------------
   sprintf_s(achRegKey, sizeof(achRegKey), FMT_REGKEY, lProdID, lManuID);

   OverlayFromIDs(lManuID, lProdID, achOverlay);

   blnReturn = false;
   lpstrKey[0] = 0;
   dwType = REG_SZ;
   dwSzValue = sizeof(achRegValue);
   hkRoot = NULL;

   try
   {
      //---------------------------------------------------------------------------------
      // Generate the registry key name.
      //
      // 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;

      //----------------------------------------------------------------------------------
      // Query the registry.
      //----------------------------------------------------------------------------------
      if (RegCreateKeyExA(HKEY_LOCAL_MACHINE,
                     REGK_SOFTWARE_NBBF, 
                     0,
                     NULL,
                     REG_OPTION_NON_VOLATILE,
                     KEY_READ,
                     NULL,
                     &hkRoot,
                     &dwDisp) != ERROR_SUCCESS)
         throw 2;

      if (RegQueryValueExA(hkRoot,
                      achRegKey,
                      0,
                      &dwType,
                      (LPBYTE)achRegValue,
                      &dwSzValue) != ERROR_SUCCESS)
         throw 3;

      if (!Out(achRegValue, achOverlay))
         throw 4;

      strncat_s(lpstrKey, szKey, achRegValue, sizeof(achRegValue));
      blnReturn = true;
   }
   catch (int iCode)
   {
   }

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

   return blnReturn;
}

If this code reminds you of the InstallKey code that we saw last time, it is not a coincidence since the only difference essentially is the use of RegQueryValueExA instead of RegSetValueExA.

Evaluation Periods

As a reminder from part 3, we store in the license key the year and the day of the year (rather than the month and day of the month) that the key was installed. This allows us to easily calculate the number of days elapsed. Here is the code, which is a tad more complex:

C++
STDMETHODIMP CValidate::Elapsed(LONG lID1, LONG lID2, LONG* plElapsed)
//----------------------------------------------------------------------------------------
// This method returns the number of days that have elapsed 
// since the license for the specified manufacturer and product IDs was installed.
//----------------------------------------------------------------------------------------
{
   CHAR achKey[MAX_KEYLEN + 1];
   HRESULT hrReturn;
   CHAR achPart[MAX_PARTLEN + 1];
   LONG lDate;
   LPSTR lpstrEnd;
   LONG lYear;
   LONG lDayOfYear;
   time_t tNow;
   struct tm tGmNow;

   if (!RetrieveKey(achKey, sizeof(achKey), lID1, lID2))
      hrReturn = NBBF_CANT_RETRIEVE;

   else
   {
      hrReturn = S_OK;

      //----------------------------------------------------------------------------------
      // Get the date from the installation data
      //----------------------------------------------------------------------------------
      memset(achPart, 0, sizeof(achPart));
      strncpy_s(achPart, sizeof(achPart), &achKey[OFF_DATE], MAX_PARTLEN);
      lDate = strtol(achPart, &lpstrEnd, BASE_HEX);
      lYear = PARTTOYEAR(lDate);
      lDayOfYear = PARTTOYEARDAY(lDate);

      //----------------------------------------------------------------------------------
      // Get the current UTC date
      //----------------------------------------------------------------------------------
      time(&tNow);
      gmtime_s(&tGmNow, &tNow);

      //----------------------------------------------------------------------------------
      // Calculate the number of days elapsed from the installation date until now
      //----------------------------------------------------------------------------------
      if (lYear == tGmNow.tm_year)
         lDayOfYear = tGmNow.tm_yday - lDayOfYear;

      else
      {
         //-------------------------------------------------------------------------------
         // If we're here, it's because the years are different.  
         // Therefore, calculate the number of days from the installation date 
         // until the end of the year, and then add the full
         // year's worth of days until we've accounted for all of the years.  
         // Finally, add the partial year's worth of days.
         //-------------------------------------------------------------------------------
         if ((lYear % CONV_YEARSPERLEAP) == 0)
            lDayOfYear = CONV_DAYSPERLEAPYEAR - lDayOfYear + 1;

         else
            lDayOfYear = CONV_DAYSPERYEAR - lDayOfYear + 1;

         while (lYear < tGmNow.tm_year)
         {
            lDayOfYear += ((lYear % CONV_YEARSPERLEAP) == 0) ? 
				CONV_DAYSPERLEAPYEAR : CONV_DAYSPERYEAR;
            lYear++;
         }

         lDayOfYear += tGmNow.tm_yday;
      }

      *plElapsed = lDayOfYear;
   }

   return hrReturn;
}

Purists will note that my "leap year determination" test is only 99% accurate since I don't take into consideration the fact that years divisible by 100 aren't leap years. My response is that if my code is still in use in the year 2100 I will happily fix it, but I doubt that'll happen. This coding technique is officially known in Computer Science circles as "a hack."

Summary

In this article, we looked at how installed license keys are validated for correctness. Having seen how license keys are installed, we realized that the validation routines are rather trivial in nature.

As a final note, I have made changes to code along the way that was already available for download in previous parts. The download for this part of the series contains the entire set of solutions and the test application; please delete any previous code you have and download it again to ensure that you have the correct version of the code.

That's it! This concludes the series; I hope this has been helpful and has been valuable in your application development travails.

History

  • September 20, 2009 - Initial version

License

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


Written By
Sales
United States United States
Larry Salomon has been writing code since he started in Basic on a TRS-80 Model I computer (4K of RAM!) in 1980. Professionally, he started in the OS/2 arena in the late 80's until he switched to Windows development in 1996.

During his multi-decade career, he has coauthored two programming books and published an electronic magazine for just over 3 years. He has written applications in a variety of languages - C# is currently his favorite - and has a few applications available for sale on the Android Application Store.

Currently, Larry works in corporate software sales in the NYC area. You may follow him via his blog at http://larrysalomon.blogspot.com

Comments and Discussions

 
QuestionUpgrades and patches Pin
Microdev19-Apr-14 1:11
Microdev19-Apr-14 1:11 
AnswerRe: Upgrades and patches Pin
rajas26-Sep-15 18:17
rajas26-Sep-15 18:17 
I know that it has been more than a year, and you have probably found answers and moved on - but I saw this article just now as I was looking for doing something like this, and thought I would put in my two cents for someone like me who comes along later. The answers below address the questions in reverse order

There are enough answers about using equivalent of HKLM keys after the security changes made in Vista / Win 7 etc., that I will not answer here. It is easy enough to change the code asked for here. In fact if you use Wix to generate the install file, (more on that below ), then you will be able to address the location to store the key.

Wix ( and I suppose other programs that generate install files do this as well ) does help you with the second question. The tutorial shows you how to set/regenerate product GUID to affect the change you want - how to leave multiple versions, or how to make sure previous versions are uninstalled. This can get slightly more involved if your software has a number of dlls some of which have not changed with new releases.

The install process using the windows installer does check for previous installed versions and give you that information. You can direct the install process based on this information - including for example if the installed version had certain capabilities (say 'basic' version), and your upgrade file is for 'premium' version, and you do not want to approve that - (or approve that because they paid for the difference). In any case, you do not need to use the code in these articles any differently, all your requirements can be met from the installation process.
AnswerRe: Upgrades and patches Pin
Foolomon4-May-17 6:46
Foolomon4-May-17 6:46 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA22-Jun-12 4:29
professionalȘtefan-Mihai MOGA22-Jun-12 4:29 
BugValidation correctly also when i change some characters... Pin
Last_Winter12-Sep-11 22:55
Last_Winter12-Sep-11 22:55 
GeneralRe: Validation correctly also when i change some characters... Pin
Foolomon18-Sep-11 11:45
Foolomon18-Sep-11 11:45 
GeneralNice article BUT... Pin
John Browne9-May-11 13:41
John Browne9-May-11 13:41 
GeneralRe: Nice article BUT... Pin
Foolomon9-May-11 15:40
Foolomon9-May-11 15:40 
GeneralRe: Nice article BUT... Pin
John Browne10-May-11 5:24
John Browne10-May-11 5:24 
GeneralRe: Nice article BUT... Pin
Foolomon23-May-11 2:55
Foolomon23-May-11 2:55 
GeneralWon't Build Pin
gmhanna26-Apr-11 12:30
gmhanna26-Apr-11 12:30 
GeneralRe: Won't Build Pin
Foolomon26-Apr-11 13:22
Foolomon26-Apr-11 13:22 
GeneralRe: Won't Build Pin
elias haddad3-Dec-12 22:46
elias haddad3-Dec-12 22:46 
QuestionHow to register NbbfC and NbbfV on deloy machine Pin
hungho31-Mar-11 0:00
hungho31-Mar-11 0:00 
AnswerRe: How to register NbbfC and NbbfV on deloy machine Pin
Foolomon31-Mar-11 3:05
Foolomon31-Mar-11 3:05 
Generalfor VB.net Pin
phowarso4-Jan-10 22:28
phowarso4-Jan-10 22:28 
GeneralRe: for VB.net Pin
Foolomon5-Jan-10 1:51
Foolomon5-Jan-10 1:51 
GeneralMy vote of 1 Pin
Bartosz Wójcik13-Oct-09 9:14
Bartosz Wójcik13-Oct-09 9:14 
Generaldisappointing article... Pin
Member 56662912-Oct-09 17:19
Member 56662912-Oct-09 17:19 
GeneralRe: disappointing article... Pin
Foolomon12-Oct-09 17:24
Foolomon12-Oct-09 17:24 
GeneralRe: disappointing article... Pin
Member 56662914-Oct-09 6:57
Member 56662914-Oct-09 6:57 
GeneralRe: disappointing article... Pin
Foolomon14-Oct-09 7:11
Foolomon14-Oct-09 7:11 
GeneralRe: disappointing article... Pin
Dustin Townsend15-Nov-09 7:05
Dustin Townsend15-Nov-09 7:05 
GeneralRe: disappointing article... Pin
Jim Fotiadis4-Mar-11 4:47
Jim Fotiadis4-Mar-11 4:47 
GeneralRe: disappointing article... Pin
Foolomon4-Mar-11 5:10
Foolomon4-Mar-11 5:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.