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:
STDMETHODIMP CValidate::Validate(LONG lID1, LONG lID2, LONG lID3, VARIANT_BOOL* pbValid)
{
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.
bool _stdcall RetrieveKey(LPSTR lpstrKey, LONG szKey, LONG lManuID, LONG lProdID)
{
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;
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
{
if (!In(achRegKey, achOverlay))
throw 1;
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)
{
}
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:
STDMETHODIMP CValidate::Elapsed(LONG lID1, LONG lID2, LONG* plElapsed)
{
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;
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);
time(&tNow);
gmtime_s(&tGmNow, &tNow);
if (lYear == tGmNow.tm_year)
lDayOfYear = tGmNow.tm_yday - lDayOfYear;
else
{
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