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:
- 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 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:
extern "C" __declspec(dllexport) bool __stdcall InstallDirect
(LPSTR lpstrKey, LONG lID1, LONG lID2)
{
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:
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.
#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)
{
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);
if (MsiGetProperty(hInstall, MSIK_PIDKEY, tchKey, &dwBuffer) == ERROR_SUCCESS)
{
CW2A convKey(tchKey);
strcpy_s(achKey, sizeof(achKey), convKey);
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--;
}
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)
{
CW2A convManuID(tchManuID);
CW2A convProdID(tchProdID);
lManuID = strtol(convManuID, &lpstrEnd, BASE_HEX);
lProdID = strtol(convProdID, &lpstrEnd, BASE_HEX);
if (InstallDirect(achKey, lManuID, lProdID))
pstrResult = MSIV_TRUE;
}
}
}
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:
bool _stdcall CheckKeyValues(LPSTR pchKey, LONG lManuID, LONG lProdID, LONG lCaps)
{
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.
#define REGK_SOFTWARE_NBBF "SOFTWARE\\Nbbf"
#define FMT_REGKEY "%08lX%08lX"
bool _stdcall InstallKey(LPSTR lpstrKey, LONG lManuID, LONG lProdID)
{
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));
sprintf_s(achRegKey, sizeof(achRegKey), FMT_REGKEY, lProdID, lManuID);
OverlayFromIDs(lManuID, lProdID, achOverlay);
SetKeyDateTime(achRegValue);
blnReturn = false;
hkRoot = NULL;
try
{
if (!In(achRegKey, achOverlay))
throw 1;
if (!In(achRegValue, achOverlay))
throw 2;
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)
{
}
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
- Using Orca, open the MSI file and find the
Binary
table. - 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. - In the
Value
field, click on the browse button and find the Nbbf
DLL.
Step 2: Define the Custom Action
- Select the
CustomAction
table. - 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. - In the
Source
field, specify the logical name of the DLL. - 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. - 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.
- Select the
Property
table. - Add a new row and specify the value
PIDCHECK
for the Property
field. - In the
Value
field, specify the value FALSE
. - Add a new row and specify the value
MANUFACTURERID
for the Property
field. - In the
Value
field, specify an appropriate value. - Add a new row and specify the value
PRODUCTID
for the Property
field. - In the
Value
field, specify an appropriate value. - Select the row for the
PIDTemplate
property. - 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.
- Select the
ControlEvent
table. - Sort by the
Dialog
field and find the block of rows corresponding to the CustomerInfoForm
dialog. - Select the row containing
NextButton
in the Control
field and ValidateProductID
in the Event
field. - Modify the
Event
field so that it contains DoAction
. - Modify the
Argument
field so that it contains the logical name of the action that you specified in step 2, item 2. - Select the row containing
NextButton
in the Control
field and NewDialog
in the Event field. - 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