Windows 应用程序的复制保护(第 3 部分)
描述了 Windows 应用程序的关键注册、安装和验证方法的实现
引言
这是关于一个允许您保护软件免遭未经授权使用的系统的四部分系列文章的第三部分。该系列将遵循以下大纲:
- 描述系统架构
- 描述许可证密钥的创建
- 描述许可证密钥的安装过程
- 描述安装的验证过程
在此过程中,我们将研究**n 位块反馈加密**(也称为**密码块链接**;请参阅**维基百科文章**),以及一种混淆方法,该方法应该可以有效地阻止任何试图理解系统工作原理的尝试。为了增加系统逆向工程的难度,结果将是一组用非托管 C++ 编写的 COM 对象。可以使用互操作性从 .NET Framework 调用适当的方法。
背景
在第 2 部分中,我们研究了加密/解密例程的源代码以及前者如何用于生成许可证密钥。最后,我们检查了许可证密钥的“数据结构”,以便我们开始了解系统的运作方式。在本文中,我们将探讨许可证密钥的安装过程,包括直接安装和通过 Microsoft Installer 进行安装。
免责声明
在我们开始之前,您需要正确设定您的期望。特别是,阅读本系列文章的任何人都不应自欺欺人地认为该系统或任何复制保护机制都是天衣无缝的。您应该问自己的问题不是“任何人都能破解它吗?”,而是“破解者是否具备相应的技能,并且他们是否会认为破解它是一项合理的时间投资?”请记住:有志者事竟成。
直接安装
在我们看代码之前,我们需要做以下几件事:
- 理解设计决策背后的原因
- 理解安装的用例
Microsoft Installation SDK 仅允许通过具有导出函数的“标准”DLL 进行集成。因此,我们决定也将直接安装例程作为导出函数来编写。
现在考虑用例。在这种情况下,安装程序知道它正在安装什么。具体来说,这些信息包含在制造商和产品 ID 中,这些 ID 在安装程序中被硬编码。另一方面,用户拥有一个许可证密钥,该密钥要么是合法的(即,它印在包装上;您通过电子邮件发送;等等),要么不是。我们的安装例程应首先将硬编码的值与用户提供的许可证密钥中存储的值进行比较。如果匹配,则安装继续;如果不匹配,则停止。
现在让我们看看直接安装例程的代码。
extern "C" __declspec(dllexport) bool __stdcall InstallDirect
(LPSTR lpstrKey, LONG lID1, LONG lID2)
//---------------------------------------------------------------------------------------
// This function installs the key in the system.
// Essentially, it decodes the specifies key;
// compares the stored values for the manufacturer and
// product IDs against those that were passed;
// and if they match then the key is installed.
//
// The usage is intended to be this way: the installer
// specifies values for the manufacturer and
// product IDs. The user specifies the installation key.
// When the installation key is valid then
// all proceeds as expected.
//---------------------------------------------------------------------------------------
{
bool blnReturn;
blnReturn = false;
if (Out(lpstrKey, lID1, lID2))
if (CheckKeyValues(lpstrKey, lID1, lID2, 0))
if (InstallKey(lpstrKey, lID1, lID2))
blnReturn = true;
return blnReturn;
}
正如您所见,这里并没有什么复杂的事情发生。我们只是想将许可证密钥中的制造商和产品 ID 与安装程序提供的值进行比较。如果匹配,我们假定安装密钥有效并继续。(我们将在本文后面介绍 CheckKeyValues
和 InstallKey
函数。)
Microsoft 的安装 SDK
正如我们之前所述,SDK 允许通过具有导出函数的标准 DLL 进行集成。这一点很重要,因为我们不仅需要检索用户输入的许可证密钥、制造商和产品 ID,还需要将验证结果传回 SDK。实现此目的的通信机制是通过以下两个 API:
UINT MsiGetProperty(MSIHANDLE hInstall, LPSTR pszName,
LPSTR pszValueBuf, LPDWORD pdwValueBuf);
UINT MsiSetProperty(MSIHANDLE hInstall, LPSTR pszName, LPSTR pszValue);
hInstall
是安装程序的“句柄”,它被提供给我们的 DLL 函数。pszName
指向我们要获取或设置的属性的名称。pszValueBuf
指向用于存储结果的缓冲区,而pdwValueBuf
指向一个双字,在入口时包含缓冲区的大小,在出口时包含值的字节大小。pszValue
指向要与属性关联的值。
使用这些 API 很简单,您可以想象。现在,让我们看看我们的 DLL 函数的代码。
#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)
//---------------------------------------------------------------------------------------
// This function installs the key via integration with the Microsoft Installer.
//---------------------------------------------------------------------------------------
{
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);
//---------------------------------------------------------------------------------
// Microsoft's installer stores the license key that
// the user entered in a key/value pair
// named PIDKEY.
//---------------------------------------------------------------------------------
if (MsiGetProperty(hInstall, MSIK_PIDKEY, tchKey, &dwBuffer) == ERROR_SUCCESS)
{
//------------------------------------------------------------------------------
// Convert the key from wide-character to ASCII (8-bit).
//------------------------------------------------------------------------------
CW2A convKey(tchKey);
strcpy_s(achKey, sizeof(achKey), convKey);
//-----------------------------------------------------------------------------
// The Windows Installer includes the literals in the key mask,
// so we need to remove the '-'
// characters.
//------------------------------------------------------------------------------
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--;
}
//-----------------------------------------------------------------------------
// Get the manufacturer and product IDs from the .msi file.
//------------------------------------------------------------------------------
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)
{
//-----------------------------------------------------------------------
// the .msi file contains wide-character versions of the values,
// so convert them to ASCII so that we can then convert them to numbers.
//-----------------------------------------------------------------------
CW2A convManuID(tchManuID);
CW2A convProdID(tchProdID);
lManuID = strtol(convManuID, &lpstrEnd, BASE_HEX);
lProdID = strtol(convProdID, &lpstrEnd, BASE_HEX);
//------------------------------------------------------------------------
// Code reuse for the win!
//------------------------------------------------------------------------
if (InstallDirect(achKey, lManuID, lProdID))
pstrResult = MSIV_TRUE;
}
}
}
//-----------------------------------------------------------------------------------
// Unfortunately, if we return MSIV_FALSE the installer
// provides no real feedback to the user
// so we have to display - ick! - a message box on our own.
// It is, in my opinion, rather
// shortsighted of Microsoft to provide no way of displaying
// a message with the same look
// and feel of the installer.
//-----------------------------------------------------------------------------------
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;
}
在 ATL(ActiveX Template Library)头文件中,有一个非常有用的宏(CW2A
,代表“将宽字符转换为 ASCII”),它允许我们将 Unicode 转换为 ASCII 字符集。通常,您不想这样做,但您应该回想起我们的加密例程将结果字符串中的字符限制在我们“base 32”集合中,它是 ASCII 字符集合的子集。因此,这样做没有任何危险。由于我们的 DLL 被编译为 Unicode DLL,因此我们需要在多个位置使用此宏,因为 MSI SDK 以宽字符字符串的形式返回其值。
其他需要注意的事项:用户输入的许可证密钥存储在名为 PIDKEY
的键中,验证结果存储在名为 PIDCHECK
的键中。我们还可以使用 Microsoft 的 **Orca** 实用程序添加我们自己的键/值对到 MSI,该实用程序(顺便说一句)也用于将我们的 DLL 集成到 MSI 中。毫不奇怪,我们正在寻找两个名为 MANUFACTURERID
和 PRODUCTID
的键/值对,它们包含我们将在用户提供的许可证密钥中查找的值。
其他函数
让我们看看 CheckKeyValues
和 InstallKey
函数。
bool _stdcall CheckKeyValues(LPSTR pchKey, LONG lManuID, LONG lProdID, LONG lCaps)
//---------------------------------------------------------------------------------------
// This function extracts the manufacturer and product IDs
// and the licensed capabilities from the
// specified key and compares them to the specified values.
// Note that licensed capabilities can
// be a superset of the lCaps argument, so we need to perform
// a binary AND operation to ensure that
// all of the bits are set rather than a logical comparison for equality.
//---------------------------------------------------------------------------------------
{
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));
}
此函数的操作相对简单:它从未加密的许可证密钥中提取制造商和产品 ID 以及已许可功能的值,并执行相应的比较。
请注意,功能表示为一组位标志。这使您可以提供一个包含所有已许可功能的许可证密钥,但只需检查构成您应用程序的每个可执行文件中的适当位。
InstallKey
函数更复杂一些。它遵循以下逻辑:
- 根据制造商和产品 ID 创建注册表项名。我们不使用已许可功能,因为调用验证例程的组件可能不知道完整的已许可功能集,但我们仍然需要查找注册表中的完整值。
- 在安装之前更新许可证密钥中的日期,以便我们能够正确计算自安装以来的天数(例如,对于 30 天产品评估)。
- 基于可计算的叠加层加密注册表项名和密钥值。
- 将值写入注册表。
#define REGK_SOFTWARE_NBBF "SOFTWARE\\Nbbf"
#define FMT_REGKEY "%08lX%08lX"
bool _stdcall InstallKey(LPSTR lpstrKey, LONG lManuID, LONG lProdID)
//---------------------------------------------------------------------------------------
// This function installs the specified key.
// While we could extract the manufacturer and product
// IDs from the key itself, this method is only called
// from a routine that already has them
// separated. Therefore, we'll require the caller to
// specify them as arguments to save some time.
//---------------------------------------------------------------------------------------
{
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));
//------------------------------------------------------------------------------------
// We need to store the product key as a value other
// than the product key to at least make it
// more difficult to deduce, i.e. a simple registry search
// for the product key won't yield the
// answer. Therefore, we will produce a string of the
// hexadecimal digits of the product and
// manufacturer IDs and encrypt the key yielding the
// subkey in the registry under which we will
// store the encrypted product key.
//-------------------------------------------------------------------------------------
sprintf_s(achRegKey, sizeof(achRegKey), FMT_REGKEY, lProdID, lManuID);
//-------------------------------------------------------------------------------------
// Create the encryption overlay and reset the date / time
// in the key to the current date / time.
// This latter operation is required since the generation
// of the key has no idea when the product
// will actually be installed. Therefore, the Elapsed method
// of the validation object will not
// be able to return an accurate number unless we do this.
//-------------------------------------------------------------------------------------
OverlayFromIDs(lManuID, lProdID, achOverlay);
SetKeyDateTime(achRegValue);
blnReturn = false;
hkRoot = NULL;
try
{
//---------------------------------------------------------------------------------
// Generate the registry key name and value to be stored therein.
//
// 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;
if (!In(achRegValue, achOverlay))
throw 2;
//--------------------------------------------------------------------------------
// Update the registry.
//--------------------------------------------------------------------------------
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)
{
}
//-----------------------------------------------------------------------------------
// Cleanup.
//-----------------------------------------------------------------------------------
if (hkRoot != NULL)
RegCloseKey(hkRoot);
return blnReturn;
}
与 Microsoft Installer 集成
这个主题真正应该放在另一篇文章中,但我们将简要介绍一下您如何与使用 Visual Studio 中的**安装和部署向导**创建的 MSI 文件集成。有关更详细的讨论,请参阅以下 Web 位置:
在开始之前,您需要 MSI SDK 附带的 Orca 实用程序。请注意,每次在解决方案中构建 **Setup** 项目时,您的 MSI 都会重新创建,并且需要重新执行这些步骤。因此,您可能不希望在每次构建解决方案时自动构建项目。
另请注意,上述链接存在一些问题,特别是 Microsoft 网站上下载 SDK 的链接目前无法正常工作,而 Robert Graham 的文章写于一段时间以前。如果您使用 Visual Studio 2008,他的一些步骤是不必要的,因为 Visual Studio 在构建 **Setup** 项目时会为您完成这些步骤。
步骤 1:将 Nbbf DLL 添加到 MSI 文件
- 使用 Orca 打开 MSI 文件并找到
Binary
表。 - 添加一个新行,并在
Name
字段中为 DLL 指定一个逻辑名称。我们需要使用此值引用 DLL,因此请记住您在此处输入的内容。 - 在
Value
字段中,单击浏览按钮并找到Nbbf
DLL。
步骤 2:定义自定义操作
- 选择
CustomAction
表。 - 添加一个新行,并在
Action
字段中为操作指定一个逻辑名称。我们需要使用此值引用操作,因此请记住您在此处输入的内容。 - 在
Source
字段中,指定 DLL 的逻辑名称。 - 在
Target
字段中,指定以下值:_InstallMSI@4
。@ 符号左边的文本是实际导出函数的名称。值 4 表示传递给函数的参数在堆栈上所需的字节数。总而言之,整个字符串将被写入导出函数表。 - 在
Type
字段中,指定值 1。这向 Microsoft Installer 表明 Binary 表中相应行中的内容代表一个 DLL。
步骤 3:配置现有属性值并添加新属性值
所有这些都通过 MsiGetProperty
和 MsiSetProperty
API 由 InstallMSI
函数引用。
- 选择
Property
表。 - 添加一个新行,并在
Property
字段中指定值PIDCHECK
。 - 在
Value
字段中,指定值FALSE
。 - 添加一个新行,并在
Property
字段中指定值MANUFACTURERID
。 - 在
Value
字段中,指定适当的值。 - 添加一个新行,并在
Property
字段中指定值PRODUCTID
。 - 在
Value
字段中,指定适当的值。 - 选择
PIDTemplate
属性的行。 - 修改
Value
字段,使其包含<#####-#####-#####-#####-#####>
。这指定了客户信息对话框中许可证密钥字段的格式。
步骤 4:配置对话框操作
如果您不完全遵循这些步骤,MSI 将被损坏并且无法按预期工作。在此步骤中,我们将修改安装程序中步骤之间的导航以及控制导航的逻辑。
- 选择
ControlEvent
表。 - 按
Dialog
字段排序,找到对应于CustomerInfoForm
对话框的行块。 - 选择
Control
字段中包含NextButton
,Event
字段中包含ValidateProductID
的行。 - 将
Event
字段修改为DoAction
。 - 将
Argument
字段修改为第 2 步第 2 项中指定的动作的逻辑名称。 - 选择
Control
字段中包含NextButton
,Event
字段中包含NewDialog
的行。 - 将
Condition
字段修改为以下值:(PIDCHECK="TRUE") AND CustomerInfoForm_NextArgs<>"" AND CustomerInfoForm_ShowSerial<>""
关闭 Orca,因为您已完成!您现在应该能够使用您通过本系列文章和前几部分文章中包含的示例应用程序生成的许可证密钥来安装您的应用程序。
摘要
在本文中,我们研究了将许可证密钥存储在注册表中的所有重要机制。我们不仅从“直接”代码角度检查了系统的这一方面,还使用了 MSI SDK 与 Microsoft Installer 进行通信。最后,我们研究了如何使用 Microsoft 的安装数据库工具 Orca 将我们的 DLL 与 MSI 文件集成。
下次,我们将研究密钥验证。
历史
- 2009 年 9 月 16 日 - 初始版本