Windows 应用程序的复制保护(第 4 部分)
描述了Windows应用程序密钥注册、安装和验证方法的实现

引言
这是关于一个允许您保护软件免受未授权使用的系统的四部分系列文章的第四部分。本系列将遵循以下提纲
- 描述系统架构
- 描述许可证密钥的创建
- 描述许可证密钥的安装过程
- 描述安装的验证过程
在此过程中,我们将了解**n位分组反馈加密**(也称为**密码分组链接**;参见**维基百科文章**),以及一种混淆方法,该方法应该能够有效地防止任何试图理解系统工作原理的尝试。为了增加逆向工程系统的难度,结果将是一组用非托管C++编写的COM对象。可以使用互操作从.NET Framework调用适当的方法。
背景
在第三部分中,我们研究了许可证密钥的安装方法,包括直接安装和通过Microsoft Installer安装。此外,我们还研究了如何使用Orca实用程序编辑MSI文件以实现实际集成。
免责声明
在我们开始之前,需要正确设定您的期望。具体来说,阅读本系列文章的任何人都不要自欺欺人地认为这个系统,或者任何复制保护机制,都是牢不可破的。你应该问自己的问题不是“是否有人能破解这个”,而是“这个人是否有能力去做,他们是否认为弄清楚它的工作原理是合理的投资?”记住:有志者事竟成。
验证
从业务角度来看(相对于本主题),验证有一个强制性组件,可能还有一个可选组件
- 确定已安装的密钥有效
- 如果适用,确定评估期尚未过期
正如你可能想象的那样,第一项相当简单。记住这是一个COM对象,以下是代码
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;
}
如你所见,我们所做的只是检索密钥(如果存在),使用制造商和产品ID。然后我们检查密钥值是否匹配。
需要注意的是,除非制造商和产品ID匹配,否则我们无法检索密钥。因此,最后这个操作基本上简化为确认已正确许可请求的功能。我们可以用一些代码替换对CheckKeyValues
的调用,但这需要先解码密钥并推断信息,所以我选择重用现有的函数。
RetrieveKey
函数是我们之前没有见过的。让我们来看看它。
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;
}
如果这段代码让您想起了我们上次看到的InstallKey
代码,这并非巧合,因为唯一的区别实质上是使用了RegQueryValueExA
而不是RegSetValueExA
。
评估期
作为第三部分的提醒,我们在许可证密钥中存储密钥安装的年份和一年中的第几天(而不是月份和日期)。这使我们能够轻松计算经过的天数。以下是代码,它稍微复杂一些
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;
}
纯粹主义者会注意到我的“闰年确定”测试只有99%的准确率,因为我没有考虑可以被100整除的年份不是闰年这一事实。我的回应是,如果我的代码在2100年仍在使用,我会很乐意修复它,但我怀疑不会发生这种情况。这种编码技术在计算机科学界正式被称为“hack”。
摘要
在这篇文章中,我们研究了如何验证已安装的许可证密钥的正确性。在了解了许可证密钥的安装方式后,我们意识到验证例程本质上相当简单。
最后,我更改了之前各部分已可下载的代码。本部分系列的下载包含完整的解决方案和测试应用程序;请删除您以前的所有代码,然后再次下载以确保您拥有正确的代码版本。
就是这样!本系列到此结束;我希望这对您有所帮助,并在您的应用程序开发过程中具有价值。
历史
- 2009年9月20日 - 初始版本