Windows 应用程序的复制保护(第 2 部分)
描述了一种用于 Windows 应用程序的关键注册、安装和验证方法的实现。
引言
这是关于一个允许您保护软件免受未经授权使用的系统的四部分系列文章的第二部分。该系列将遵循以下大纲:
- 描述系统架构
- 描述许可证密钥的创建
- 描述许可证密钥的安装过程
- 描述安装的验证过程
在此过程中,我们将探讨 **n 位分组反馈加密**(也称为 **密码分组链接**;请参阅 **维基百科文章**),以及一种混淆方法,该方法应足以防止任何试图理解系统如何工作的尝试。为了增加系统逆向工程的难度,最终结果将是一组用非托管 C++ 编写的 COM 对象。互操作性可用于从 .NET 框架调用适当的方法。
背景
在第一部分中,我们研究了系统的总体目标以及实现这些目标的方式。最后,我们以高层次的术语讨论了加密/解密是如何工作的。
免责声明
在开始之前,需要正确设定您的期望。特别是,阅读本系列文章的任何人都不应自欺欺人地认为该系统或任何复制保护机制是万无一失的。您应该问自己的问题不是“有人能破解它吗?”而是“那个人是否有能力做到这一点,他们是否认为花费时间来弄清楚它是合理的?”请记住:有志者,事竟成。
加密和解密
在第一部分结束时,我们以高层次的术语讨论了编解码器。现在,让我们来检查代码本身。
bool _stdcall In(LPSTR pchKey, LPSTR pchOverlay)
//---------------------------------------------------------
// This function encrypts the specified key using
// the specified overlay. It uses a cipher block
// chaining algorithm (sometimes known as n-bit block
// feedback encryption) which essentially uses
// the previous encryption result as input to the current
// encryption iteration. The upshot of this
// is that data at the beginning of the string affects
// the encrypted results after it.
//---------------------------------------------------------
{
int iLenKey;
int iLenOver;
LPSTR pbBuf;
int iIndex;
LPSTR pchVal;
char bVal1;
char bVal2;
bool blnReturn;
iLenKey = strlen(pchKey);
iLenOver = strlen(pchOverlay);
//-------------------------------------------------------------
// The resulting data is the same size as the input,
// so build the results in a separate buffer.
//-------------------------------------------------------------
pbBuf = (LPSTR)calloc(iLenKey, sizeof(char));
for (iIndex = 0; iIndex < iLenKey; iIndex++)
{
//----------------------------------------------------------
// Find the position of the current input byte
// in the digit set. If not found then exit.
//----------------------------------------------------------
pchVal = strchr(BASE32_CHARSET, pchKey[iIndex]);
if (!pchVal)
break;
//----------------------------------------------------------
// Convert to a numerical value.
//----------------------------------------------------------
bVal1 = (char)(pchVal - BASE32_CHARSET);
//----------------------------------------------------------
// Repeat the above conversion for the current overlay byte.
//----------------------------------------------------------
pchVal = strchr(BASE32_CHARSET, pchOverlay[iIndex % iLenOver]);
if (!pchVal)
break;
bVal2 = (char)(pchVal - BASE32_CHARSET);
//---------------------------------------------------
// XOR the two values plus the n-1'th encrypted
// byte if this is not the first byte of data.
//---------------------------------------------------
pbBuf[iIndex] = bVal1 ^ bVal2;
if (iIndex > 0)
pbBuf[iIndex] ^= pbBuf[iIndex - 1];
//----------------------------------------------------
// Convert back from a numerical
// value to the appropriate digit.
//----------------------------------------------------
pchKey[iIndex] = (char)BASE32_CHARSET[pbBuf[iIndex]];
}
//-------------------------------------------------------
// Cleanup.
//-------------------------------------------------------
free(pbBuf);
blnReturn = (iIndex == iLenKey);
if (!blnReturn)
memset(pchKey, 0, iLenKey);
return blnReturn;
}
bool _stdcall In(LPSTR pchKey, LONG lManuID, LONG lProdID)
//-----------------------------------------------------
// This overloaded function converts the specified
// manufacturer and product IDs into the encryption
// overlay then calls the other version
// of the In function to encrypt the data.
//-----------------------------------------------------
{
CHAR achOverlay[MAX_KEYLEN + 1];
OverlayFromIDs(lManuID, lProdID, achOverlay);
return In(pchKey, achOverlay);
}
尽管结果数据的长度与输入数据的长度相同,但由于我们正在转换为 32 位字符集,因此我们无法就地加密。换句话说,**XOR** 操作发生在数据转换为字符集之前,因此我们需要将这些数值存储在一个单独的缓冲区中。
请注意使用了重载函数。这是由于代码重构,发现大多数调用都将制造商和产品 ID 显式转换为加密叠加层。第二个函数只是将该操作很好地封装起来,以帮助保持代码的整洁。
您可能会问我为什么为加密例程(以及解密例程,我们稍后会看到)使用了如此含糊的函数名称。前英特尔董事长安迪·格罗夫曾说过(意译以突出我的观点):“只有当你错了的时候,你才是偏执狂。”我宁愿使用听起来含糊的名称,也不愿有人翻遍对象代码寻找代码如何工作的线索。
如第一部分所述,解密本质上是加密例程的相反操作。
bool _stdcall Out(LPSTR pchKey, LPSTR pchOverlay)
//------------------------------------------------------
// This function decrypts the specified key using
// the specified overlay. It uses a cipher block
// chaining algorithm (sometimes known as n-bit
// block feedback encryption) which essentially uses
// the previous decryption result as input
// to the current decryption iteration.
//------------------------------------------------------
{
int iLenKey;
int iLenOver;
LPSTR pbBuf;
int iIndex;
LPSTR pchVal;
char bVal1;
char bVal2;
char bVal3;
bool blnReturn;
iLenKey = strlen(pchKey);
iLenOver = strlen(pchOverlay);
//------------------------------------------------------------------
// The resulting data is the same size as the input,
// so build the results in a separate buffer.
//------------------------------------------------------------------
pbBuf = (LPSTR)calloc(iLenKey, sizeof(char));
for (iIndex = iLenKey - 1; iIndex >= 0; iIndex--)
{
//---------------------------------------------------------------
// Find the position of the current input byte
// in the digit set. If not found then exit.
//---------------------------------------------------------------
pchVal = strchr(BASE32_CHARSET, pchKey[iIndex]);
if (!pchVal)
break;
//---------------------------------------------------------------
// Convert to a numerical value.
//---------------------------------------------------------------
bVal1 = (char)(pchVal - BASE32_CHARSET);
//---------------------------------------------------------------
// Repeat the above conversion for the current overlay byte.
//---------------------------------------------------------------
pchVal = strchr(BASE32_CHARSET, pchOverlay[iIndex % iLenOver]);
if (!pchVal)
break;
bVal2 = (char)(pchVal - BASE32_CHARSET);
//-------------------------------------------------------
// XOR the two values plus the n-1'th encrypted
// byte if this is not the first byte of
// data. Unlike the encryption routine
// where this value was already calculated, we have
// to manually calculate it here before XOR'ing it.
//-------------------------------------------------------
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;
}
//--------------------------------------------------------------
// Convert back from a numerical value to the appropriate digit.
//--------------------------------------------------------------
pchKey[iIndex] = (char)BASE32_CHARSET[pbBuf[iIndex]];
}
//-------------------------------------------------------------
// Cleanup.
//-------------------------------------------------------------
free(pbBuf);
blnReturn = (iIndex < 0);
if (!blnReturn)
memset(pchKey, 0, iLenKey);
return blnReturn;
}
bool _stdcall Out(LPSTR pchKey, LONG lManuID, LONG lProdID)
//-----------------------------------------------------------
// This overloaded function converts the specified
// manufacturer and product IDs into the decryption
// overlay then calls the other version
// of the Out function to decrypt the data.
//-----------------------------------------------------------
{
CHAR achOverlay[MAX_KEYLEN + 1];
OverlayFromIDs(lManuID, lProdID, achOverlay);
return Out(pchKey, achOverlay);
}
创建许可证密钥
许可证密钥的创建相对简单,因为繁重的工作是由加密(和解密)例程执行的。
STDMETHODIMP CCreate::Create(LONG lID1, LONG lID2, LONG lID3, BSTR * pbstrKey)
//-------------------------------------------------------
// This COM method generates a license key given
// the manufacturer and product IDs and a bit-flag
// set of licensed capabilities. These are specified
// in lID1, lID2, and lID3 respectively. A BSTR
// is returned as the result.
//-------------------------------------------------------
{
CHAR achKey[MAX_KEYLEN + 1];
HRESULT hrReturn;
//----------------------------------------------------
// Initialize the key.
//----------------------------------------------------
memset(achKey, PAD_KEY, MAX_KEYLEN);
achKey[MAX_KEYLEN] = 0;
//----------------------------------------------------
// Put the current date / time components in the
// key so that the result has some degree of
// randomness to it. Remember: the contents
// at the beginning of the string to be encrypted
// affect the encrypted result from that point on.
//----------------------------------------------------
SetKeyDateTime(achKey);
//----------------------------------------------------
// Add the manufacturer and product IDs
// and the licensed capabilities.
//----------------------------------------------------
LongToKey(lID1, achKey, OFF_MANUFACTURER);
LongToKey(lID2, achKey, OFF_PRODUCT);
LongToKey(lID3, achKey, OFF_CAPS);
//----------------------------------------------------
// Convert the key to uppercase before processing
// just in case there are some lowercase
// hexadecimal digits. The base 32 digit
// set uses uppercase characters only.
//----------------------------------------------------
_strupr_s(achKey, sizeof(achKey));
//----------------------------------------------------
// Encrypt, copy the result into a BSTR, then return.
//----------------------------------------------------
if (!In(achKey, lID1, lID2))
hrReturn = OLE_E_CANTCONVERT;
else
{
CComBSTR objReturn(achKey);
*pbstrKey = objReturn.Copy();
hrReturn = S_OK;
}
return hrReturn;
}
我们已经几次看到 `SetKeyDateTime` 函数被调用。它的操作很重要,所以让我们看看它是如何工作的。
//--------------------------------------------------------------------
// TIMETOPART converts an hour / minute / second tuplet
// into a single number that may be converted
// to hexadecimal for addition into the unencrypted string.
// Note that, due to the number of bits
// available in a MAX_PARTLEN component,
// the hour is ignored. This is strictly used for adding
// randomization to the encrypted string
// so the loss of precision is acceptable.
//
// Seconds are stored in 7 bits (0-63). Minutes are stored
// in 7 bits (0-63). This yields a number
// with 14 bits of precision.
//
// DATETOPART converts a month / day / year (1900 offset)
// into a single number that may be converted
// to hexadecimal for addition into the unencrypted string.
//
// Year is stored as an offset from the year 2000
// and uses 7 bits (0-63). Day of the year is stored
// in 9 bits (0-512). This yields a number with 16 bits of precision.
//--------------------------------------------------------------------
#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)
//----------------------------------------------------------
// This function adds some components of the current time
// to the front of the key so that the
// rest of the key is randomized to some degree.
// The date is also added as the year - 2000 plus
// the day of the year. This will easily allow us
// to calculate the number of days elapsed since
// the product was installed.
//----------------------------------------------------------
{
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);
}
这个函数的“奥秘”在于 `TIMETOPART` 和 `DATETOPART` 宏。这两个宏创建了一个 16 位整数,该整数由 `tm` 结构中存储的时间和日期组件组成。虽然时间值除了为加密的许可证密钥带来随机性之外没有其他用途,但日期值很重要,因为它们允许我们计算自许可证密钥中存储的日期以来经过的天数。我们将在第 4 部分讨论验证时看到这一点。
您可能想知道 `LongToKey` 是否有什么神秘之处。正如您在下面的代码中将看到的,它只是一个长整型到 ASCII 的转换加上复制到许可证密钥结构中的相应位置。
void _stdcall LongToKey(LONG lNumber, LPSTR lpstrKey, INT iOffset)
//------------------------------------------------------------
// This function converts the specified number to text
// and stores it in the specified part location
// within the key buffer.
//------------------------------------------------------------
{
CHAR achPart[MAX_KEYLEN + 1];
sprintf_s(achPart, sizeof(achPart), FMT_LONGTOHEX, lNumber);
memcpy_s(&lpstrKey[iOffset], MAX_PARTLEN, achPart, MAX_PARTLEN);
}
许可证密钥结构
读完最后一句话后,您应该问自己:“各个组件是如何存储在许可证密钥中的?”因此,让我们讨论一下许可证数据是如何存储的。
#define MAX_PARTLEN 4
#define MAX_KEYLEN 25
//------------------------------------------------------------------------
// These are the actual components of the non-encrypted string.
// These are concatenated together to
// form the entire string, which is then passed
// to the encryption routine to actually generate the license key.
//
// PARTOFFSET is a macro that allows us to easily determine
// where each part goes or may be found for decryption.
//
// OFF_TIME is the offset of the time component.
// Note that this needs to be first so that the
// seconds component adds a fair amount of randomness
// to the encrypted string due to the nature
// of the algorithm used (n-bit block feedback).
//
// OFF_DATE is the offset of the date component.
// This allows us to determine when the key was
// generated, but could be used to substitute the date
// the key was used to install the software.
// This lends itself to a "30 day evaluation" type of license.
//
// OFF_MANUFACTURER and OFF_PRODUCT are the manufacturer and product IDs
//
// OFF_CAPS is a 16-bit (based on MAX_PARTLEN specifying
// the number of hexadecimal digits in the
// number) set of flags that the caller can use
// to specify components that are accessible using
// the license key.
//------------------------------------------------------------------------
#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)
查看上面的定义,您可以看到数据只是存储在许可证密钥字符串中的特定位置。根据上面的定义,格式是:
1 2
0123456789012345678901234
-------------------------
ttttuddddummmmuppppuccccu
`tttt` 是时间值。`dddd` 是日期值。`mmmm` 是制造商 ID 值。`pppp` 是产品 ID 值。而 `cccc` 是功能值。所有 `u` 实例都是未使用的数字。可以想象,您可以根据需要在 `OFF_*` 宏的每个定义中更改“数组”的“索引”,只要它们在 0-4 的范围内并且是唯一的。
摘要
在本系列文章的这一部分中,我们研究了至关重要的加密和解密函数。我们研究了许可证密钥结构,如果您注意的话,就会发现许可证密钥数据开头的时间存储如何为加密后的许可证密钥增加了(大量的)随机性。最后,我们介绍了一些在所有三个组件中使用的实用例程。
下次,我们将研究密钥的安装和检索机制。
历史
- 2009 年 9 月 10 日 - 初始版本。