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.
bool _stdcall In(LPSTR pchKey, LPSTR pchOverlay)
{
int iLenKey;
int iLenOver;
LPSTR pbBuf;
int iIndex;
LPSTR pchVal;
char bVal1;
char bVal2;
bool blnReturn;
iLenKey = strlen(pchKey);
iLenOver = strlen(pchOverlay);
pbBuf = (LPSTR)calloc(iLenKey, sizeof(char));
for (iIndex = 0; iIndex < iLenKey; iIndex++)
{
pchVal = strchr(BASE32_CHARSET, pchKey[iIndex]);
if (!pchVal)
break;
bVal1 = (char)(pchVal - BASE32_CHARSET);
pchVal = strchr(BASE32_CHARSET, pchOverlay[iIndex % iLenOver]);
if (!pchVal)
break;
bVal2 = (char)(pchVal - BASE32_CHARSET);
pbBuf[iIndex] = bVal1 ^ bVal2;
if (iIndex > 0)
pbBuf[iIndex] ^= pbBuf[iIndex - 1];
pchKey[iIndex] = (char)BASE32_CHARSET[pbBuf[iIndex]];
}
free(pbBuf);
blnReturn = (iIndex == iLenKey);
if (!blnReturn)
memset(pchKey, 0, iLenKey);
return blnReturn;
}
bool _stdcall In(LPSTR pchKey, LONG lManuID, LONG lProdID)
{
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.
bool _stdcall Out(LPSTR pchKey, LPSTR pchOverlay)
{
int iLenKey;
int iLenOver;
LPSTR pbBuf;
int iIndex;
LPSTR pchVal;
char bVal1;
char bVal2;
char bVal3;
bool blnReturn;
iLenKey = strlen(pchKey);
iLenOver = strlen(pchOverlay);
pbBuf = (LPSTR)calloc(iLenKey, sizeof(char));
for (iIndex = iLenKey - 1; iIndex >= 0; iIndex--)
{
pchVal = strchr(BASE32_CHARSET, pchKey[iIndex]);
if (!pchVal)
break;
bVal1 = (char)(pchVal - BASE32_CHARSET);
pchVal = strchr(BASE32_CHARSET, pchOverlay[iIndex % iLenOver]);
if (!pchVal)
break;
bVal2 = (char)(pchVal - BASE32_CHARSET);
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;
}
pchKey[iIndex] = (char)BASE32_CHARSET[pbBuf[iIndex]];
}
free(pbBuf);
blnReturn = (iIndex < 0);
if (!blnReturn)
memset(pchKey, 0, iLenKey);
return blnReturn;
}
bool _stdcall Out(LPSTR pchKey, LONG lManuID, LONG lProdID)
{
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.
STDMETHODIMP CCreate::Create(LONG lID1, LONG lID2, LONG lID3, BSTR * pbstrKey)
{
CHAR achKey[MAX_KEYLEN + 1];
HRESULT hrReturn;
memset(achKey, PAD_KEY, MAX_KEYLEN);
achKey[MAX_KEYLEN] = 0;
SetKeyDateTime(achKey);
LongToKey(lID1, achKey, OFF_MANUFACTURER);
LongToKey(lID2, achKey, OFF_PRODUCT);
LongToKey(lID3, achKey, OFF_CAPS);
_strupr_s(achKey, sizeof(achKey));
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.
#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)
{
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.
void _stdcall LongToKey(LONG lNumber, LPSTR lpstrKey, INT iOffset)
{
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.
#define MAX_PARTLEN 4
#define MAX_KEYLEN 25
#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.