65.9K
CodeProject 正在变化。 阅读更多。
Home

一个简单的软件密钥,用于保护软件组件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (54投票s)

2004年11月29日

18分钟阅读

viewsIcon

370564

downloadIcon

9276

本文介绍了一种实现软件密钥的方法,该方法可能有助于保护软件组件。

引言

在本文中,我们将介绍一种实现简单软件密钥的方法,该方法可能有助于保护软件组件(例如,EXE、DLL、COM 等)免遭滥用并跟踪安装。

我们想强调的是,这只是一个示例。我们知道,绕过本文软件密钥提供的保护相对容易。

然而,我们认为本文有助于我们更好地理解软件密钥背后的机制。

顺便说一句,在接下来的几节中,我们将介绍如何绕过此保护以及一些可以帮助攻击者更困难工作的想法。

背景

为了解释背景思想,我们需要描述我们的软件密钥可能适用的上下文。

假设我们有一个软件产品,它封装在一个可执行文件(EXE)中,并且有一个基于安装的费用策略。请注意,以下说明也适用于其他类型的组件(例如,DLL、COM 等)。假设我们想控制客户的安装次数。本文提出的软件密钥(及其限制)允许我们实现上述意图。具体来说,它允许我们授予客户在我们组件(EXE)上使用的权利,但只能在特定计算机上使用。如果他尝试在另一台计算机上安装我们的可执行文件,该组件将无法运行。

工作原理

假设我们是某个组件“protected_comp.exe”的供应商。为了使用我们的组件,客户需要使用我们提供给他的工具“softwarekey_customertool.exe”生成一个“机器码”。然后,他需要将此“机器码”发送给我们。“机器码”可以看作是在其中运行工具“softwarekey_customertool.exe”并且将执行“protected_comp.exe”的计算机的签名。

实质上,此签名是通过使用 MAC(介质访问控制)地址(即,通常是 Ethernet 地址)作为种子获得的。我们可以将机器码生成视为将函数 f 应用于 MAC 地址。

MachineCode = f (MACaddress)

请注意,此过程可以很容易地封装在网页中,例如,将控制台应用程序“softwarekey_customertool.exe”变成一个 ActiveX 控件。

使用此“机器码”,我们可以使用工具“softwarekey_vendortool.exe”创建一个“软件密钥”(也称为“许可证号”)。该工具以“机器码”为输入,将生成一个“软件密钥”(即“许可证号”),该密钥仅对生成“机器码”的计算机有效。我们可以将软件密钥生成视为将函数 g 应用于机器码。

SoftwareKey = g (MachineCode)

注意:在本文提供的代码中,函数 fg 只是 MAC 地址的简单排列。为了提高保护,应使用更复杂的算法。此外,可以使用 MAC 地址以外的数据作为种子(供参考,CPU 标识符、Windows 序列号等),但我们的选择似乎更容易且相当通用。我们将在后面的部分介绍一些可能的技巧。

生成“软件密钥”后,我们需要将其发送给客户。收到“软件密钥”后,客户必须将其安装在文件“protected_comp.ini”中。此时,客户可以使用我们的组件“protected_comp.exe”。为简单起见,我们将软件密钥保存在“ini”文件中。也可以将其保存在注册表中。此外,还可以创建一个工具来自动化软件密钥的安装。

下图试图说明生成和安装一个对特定计算机 A 生效的软件密钥所需的步骤。

Figure 1 - Flow diagram for our software key

代码内部

核心类

在本节中,我们将简要介绍 CSoftwareKey 类,其方法将在“protected_comp.exe”、“softwarekey_vendortool.exe”和“softwarekey_customertool.exe”中使用。

class CSoftwareKey
{
public:

    static RETVALUE RetrieveMACAddress(
            BYTE            pMACaddress[MAC_DIM]);

    static RETVALUE ComputeMachineCode(
            const BYTE      pMACaddress[MAC_DIM], 
            BYTE            pMachineCode[MACHINE_CODE_DIM]); 

    static RETVALUE ComputeSoftwareKey(
            const BYTE      pMachineCode[MACHINE_CODE_DIM], 
            BYTE            pSoftwareKey[SOFTWAREKEY_DIM]); 

    static RETVALUE VerifySoftwareKey(
            const char*      pSoftwareKeyString, 
            bool*            pIsValid);

    static RETVALUE GetSoftwareKeyStringFromIniFile(
            const char*       pFilePath, 
            char**            pSoftwareKeyString);

    static RETVALUE Buffer2String(
            const BYTE*        pBuffer, 
            const unsigned int pBufferSize, 
            char**             pString);

    static RETVALUE String2Buffer(
            const char*        pString,
            BYTE**             pBuffer, 
            unsigned int*      pBufferSize);
};

方法 RetrieveMACAddress 检索 MAC 地址。为此,如果操作系统是 Windows 2000、ME 或 XP,我们使用了 API UuidCreateSequential;否则,如果操作系统是 NT,我们使用了 CoCreateGuid。这是因为,在 Windows XP/2000 中,CoCreateGuid 内部调用的 UuidCreate 函数出于安全原因生成一个 UUID,该 UUID 无法追溯到生成它的计算机的 Ethernet/令牌环地址。然而,正如 MSDN 库所说,在 Windows XP/2000 中,API UuidCreateSequential 返回一个 UUID,该 UUID 是 MAC 地址的函数。我们可以为所有操作系统使用 UuidCreateSequential,但不幸的是,它并不总是在 Windows NT 中可用,这取决于已安装的服务包。

注意:在与 NT 相关的代码中,我们没有检查已安装的服务包版本。这意味着,如果安装了某个特定的服务包,此函数在 Windows NT 中将无法正常工作。

没有网卡的计算机也会出现另一个问题。API UuidCreateUuidCreateSequential 将返回一个常量 UUID。这意味着两台没有网卡的计算机将生成相同的机器码。解决此问题的一种可能性,以及一般性地提高软件密钥提供的保护,是生成一个机器码,该机器码从多个值的组合开始,而不是仅从 MAC 地址开始。此方法稍后将详细介绍。

方法 ComputeMachineCode 从 MAC 地址开始生成机器码。它是上面介绍的函数 f 的实现。创建的机器码只是 MAC 地址的一个简单排列。

方法 ComputeSoftwareKey 使用机器码作为种子生成软件密钥。它是上面介绍的函数 g 的实现。创建的软件密钥只是机器码的一个简单排列。

方法 VerifySoftwareKey 检查“.ini”文件中安装的软件密钥是否有效。它从 MAC 地址开始计算一个软件密钥,并将其与保存在“protected_comp.ini”文件中的软件密钥进行比较。

方法 GetSoftwareKeyStringFromIniFile 检索“.ini”文件中安装的软件密钥。

方法 Buffer2String 将缓冲区(即字节序列)转换为字符串。这对于生成机器码和软件密钥的可打印版本很有用。这样,它们就可以轻松传递,例如,在电子邮件的文本中,而无需使用 MIME。在我们的示例中,我们将每个字节转换为三个十进制数字的三个字符。例如,字节 0xCD 将转换为数字十进制三个字符(即字符串)“205”。

方法 String2Buffer 将字符串转换为字节序列。请注意,该函数假定每个字节在字符串中表示为三个数字十进制字符。例如,数字十进制三个字符“056”将转换为字节“0x38”。

注意:在上述方法 Buffer2StringString2Buffer 中,我们可以使用“base64”算法,但为了简单起见,我们选择了上述方法。总而言之,“base64”算法将输入缓冲区的每三个字节表示为给定字母表中四个可打印字符的输出字符串。这样,生成的字符串的长度就小于我们算法生成的字符串。事实上,我们的算法为每三个字节生成九个字符(数字字符)。一个可能的改进是允许我们生成更小的输出字符串,即将每个字节转换为一对十六进制字符。在这种情况下,字节 0xCD 将转换为“CD”。因此,对于输入流中的每三个字节,我们可以得到六个十六进制字符。但是,由于我们的机器码和软件密钥非常短,而且这只是一个示例,所以我们不会使用这些更强大的算法(尽管将它们封装到上述两个方法中应该相当简单)。

在后面的几节中,我们将简要介绍上述类在本文提出的可执行文件中使用的方式。

受保护的组件 - “protected_comp.exe”

这是要保护的软件组件的模拟。它假定供应商提供的软件密钥已安装在“protected_comp.ini”文件中。这个简单的控制台应用程序将显示一个消息框,指示是否安装了有效的软件密钥。

此组件使用 CSoftwareKey 的以下方法:

  • GetSoftwareKeyStringFromIniFile;
  • VerifySoftwareKey.

方法 VerifySoftwareKey 内部使用以下方法:

  • RetrieveMACAddress;
  • ComputeMachineCode;
  • ComputeSoftwareKey;
  • Buffer2String.

客户工具 - “softwarekey_customertool.exe”

客户使用此工具来生成机器码。

此工具使用 CSoftwareKey 的以下方法:

  • RetrieveMACAddress;
  • ComputeMachineCode;
  • Buffer2String.

用法是:

softwarekey_customertool    -g

供应商工具 - “softwarekey_vendortool.exe”

供应商使用此工具来生成与给定机器码匹配的软件密钥。此工具使用 CSoftwareKey 的以下方法:

  • String2Buffer;
  • ComputeSoftwareKey;
  • Buffer2String.

用法是:

softwarekey_vendortool     -v machinecode

绕过软件密钥的提示

在本节中,我们只提供一些绕过软件密钥保护的提示。我们可以使用反汇编器或调试器打开可执行文件“protected_comp.exe”,以查找调用显示消息框的 API 的位置。特别是,我们寻找显示消息“wrong software key”的消息框,希望在其周围找到验证已安装软件密钥的代码。

由于我们的示例非常简单,最重要的是,我们是:) 这样写的,所以我们很容易找到验证已安装软件密钥的代码。在下面的代码片段中,我们显示了这段代码的转储,并添加了一些注释,这些代码是从汇编器级别的调试器中提取的。

; Call of method CSoftwareKey::GetSoftwareKeyStringFromIniFile(...)
004012E0  SUB ESP,8
004012E3  LEA EAX,DWORD PTR SS:[ESP+4]
004012E7  MOV DWORD PTR SS:[ESP+4],0
004012EF  PUSH EAX
004012F0  PUSH 004070B4                    
004012F5  CALL 004011A0

; Call of method CSoftwareKey::VerifySoftwareKey(...)
004012FA  MOV EDX,DWORD PTR SS:[ESP+C]
004012FE  LEA ECX,DWORD PTR SS:[ESP+B]
00401302  PUSH ECX
00401303  PUSH EDX
00401304  MOV BYTE PTR SS:[ESP+13],0
00401309  CALL 00401030

0040130E  MOV AL,BYTE PTR SS:[ESP+13]
00401312  ADD ESP,10
00401315  TEST AL,AL
00401317  PUSH 0              ; Style = MB_OK|MB_APPLMODAL


; Hex pattern of the following instruction and some context is 
; (....0x6A 0x00 0x75 0x1B 0x68 0xAC....).
; This information should be made accessible by the debugger.
00401319  JNZ SHORT 00401336                                        


; Call of function MessageBoxA contained in user32.dll
0040131B  PUSH 004070AC                               ; Title = "error"
00401320  PUSH 00407098                               ; Text = "wrong software key"
00401325  PUSH 0                                      ; hOwner = NULL
00401327  CALL DWORD PTR DS:[<;&USER32.MessageBoxA>]   ; MessageBoxA
0040132D  MOV EAX,1
00401332  ADD ESP,8
00401335  RETN

; Call of function MessageBoxA contained in user32.dll
00401336  PUSH 00407094                    ; Title = "ok"
0040133B  PUSH 00407074                    ; Text = "correct software key installed"
00401340  PUSH 0                           ; hOwner = NULL
00401342  CALL DWORD PTR DS:[<;&USER32.MessageBoxA>] ; MessageBoxA
00401348  XOR EAX,EAX
0040134A  ADD ESP,8
0040134D  RETN

拥有一点汇编知识,我们可以很容易地理解地址“00401319”中的指令正在验证软件密钥。事实上,在地址“00401336”处,我们可以看到显示消息框“correct software key installed”的代码。现在,我们可以用一个无条件跳转“JMP SHORT 00401336”来替换这条指令(条件跳转指令“JNZ SHORT 00401336”)。这样做,我们就可以轻松绕过保护。

更详细地说,要移除软件密钥保护,一种可能的方法是:

  • 使用汇编器级别的调试器,我们确定代表我们要修改的指令的十六进制模式(即字节序列)。在我们的例子中,“JNZ SHORT”的十六进制模式是“0x75”。考虑其周围的字节也很有用。所以,我们考虑模式“0x6A 0x00 0x75 0x1B 0x68 0xAC”。

    正如我们稍后将看到的,这将有助于我们在可执行文件中搜索要更改的“正确”字节。重要的是我们使用的工具能够显示与每个汇编指令对应的可执行文件的十六进制转储;

  • 现在,我们可以使用二进制编辑器(供参考,MS Visual Studio)以二进制文件模式打开文件“protected_comp.exe”;
  • 然后,我们搜索二进制路径“0x6A 0x00 0x75 0x1B 0x68 0xAC”(请注意,字节“0x75”对应于 JNZ SHORT 指令,而其余的字节是为了简化搜索而围绕该指令的字节……);
  • 然后,我们可以将字节“0x75”替换为“0xEB”(请注意,“0xEB”是 JMP SHORT 的数值),并将此篡改的文件另存为“protected_comp_patched.exe”;
  • 现在,使用在文件“protected_comp.ini”中安装了错误软件密钥的可执行文件运行此程序,我们将看到一个显示“correct software key installed”的消息框,而不是预期的显示“wrong software key”的消息框。

请注意,在这个简单的例子中,我们只需更改可执行文件中的一个字节就足以绕过软件密钥保护。在下一节中,我们将展示一些可能的增强此工作难度的方法。可以直接使用的“protected_comp_patched.exe”副本可在“softwarekey_demo.zip”中下载。

增强软件密钥的提示

在本节中,我们将提出一些可能有助于增强本文软件密钥提供的保护的思路。目标只是提供一些有用的注意事项。这里提出的所有方法都可以被绕过。它们只能增加攻击者所需的努力。

这些方法可以分为以下几段中详细介绍的两组:

  • 代码混淆和篡改检测:这些技术试图使确定要修改代码的“正确位置”的工作变得复杂,或者检测代码篡改;
  • 增强函数 fg:这些技术试图使函数 fg 复杂化。这里的目标是使从已知的机器码确定软件密钥的工作更加困难。

代码混淆和篡改检测

为了绕过我们的软件密钥提供的保护,我们在上一段中修改了代码。我们在这一段中提供的建议试图使确定代码中“正确位置”的工作更加复杂,或者检测代码篡改。

  • 有一些编码技术可以使汇编(数据和代码)尽可能难以阅读和操作。忘记所有良好的编程范例,如面向对象编程、数据封装等,可能会很有用。粗略地说,我们可以说我们的代码越像“意大利面条代码”,效果就越好。这样做的目的是迫使我们的编译器也产生一个棘手的机器码。这个问题已经被详细研究过,并被正式命名为“代码混淆”。
  • 将验证软件密钥的代码与向用户显示错误密钥的代码分开存放是一个好主意。例如,在前一段中,我们很容易识别出要篡改的字节,因为条件指令紧邻显示错误密钥已安装的消息框的指令。此外,我们可以添加多个检查,而不仅仅是一个;
  • 在这种情况下,我们不使用消息框来指示安装了错误的软件密钥,而是强行在代码中引入一些错误。这样,程序将无法正常运行,用户将更难理解要篡改的代码部分;
  • 一种可用于检测代码或数据篡改的方法是计算和验证可能被篡改的数据和代码的校验和;
  • 为了让攻击者感到困惑,我们可以动态修改代码的某些部分(供参考,插入软件密钥验证)。

这些技术(以及其他技术)的组合可以使识别和修改管理软件密钥的汇编代码的工作更加困难。

增强函数 fg

如前所述,在本文提供的代码中,函数 fg 只是简单的排列。为了提高保护,应使用更复杂的算法。我们在此段中提供的建议试图使这些函数复杂化。

第一个简单的解决方案,适用于 fg,是使用更大的尺寸(即字节数)来表示机器码和软件密钥。

为了增强函数 f,我们可以从以下后续值的组合开始生成机器码,而不是仅使用 MAC 地址:

  • 处理器 ID(通常可以使用特定的机器码指令访问);
  • Windows 序列号(可在注册表中访问,通常保存在路径为“\\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ProductID”的密钥中);
  • 硬盘卷 ID(在某些情况下,可以使用 API GetVolumeInformation 或特定的中断地址访问);
  • 其他“相当”稳定的系统信息。

这意味着将机器码计算作为上述参数的函数。

MachineCode = f (MACaddress, ProcessorID, WindowsSerialNumber, …)

使用这些信息可能是一项繁重的任务,因为它们的检索可能因不同的硬件设备或操作系统而异。

为了增强函数 g,我们可以使用公钥密码学。以下是一种可能的解决方案。在这种情况下,对于每个客户的计算机,供应商都会生成一对密钥<"PrivateKey", "PublicKey">。然后,供应商将包含“softwarekey_customertool.exe”、“protected_comp.exe”、“protected_comp.ini”和“PublicKey”的软件包发送给客户。像往常一样,客户使用“softwarekey_customertool.exe”生成其机器码,并将其发送给供应商。

MachineCode = f (MACaddress,...)

收到机器码后,供应商通过使用“PrivateKey”加密机器码来生成软件密钥。现在我们有

SoftwareKey = g (MachineCode) = Encrypt(PrivateKey, MachineCode)

其中 Encrypt 是一个函数,它使用密钥 PrivateKey 来加密 MachineCode。此时,供应商将软件密钥发送给客户,客户将其安装在“protected_comp.ini”中。为了检查安装的软件密钥,“protected_comp.exe”将执行一些由以下伪代码描述的代码。

sk = CSoftwareKey::GetSoftwareKeyFromIniFile(...);

mc = Decrypt(PublikKey, sk);

mac = CSoftwareKey::RetrieveMACAddress(...);

mc1 = CSoftwareKey::ComputeMachineCode(mac); // this is the function "f"

if (mc != mc1)
{
    "wrong software key"
}

这样,我们就使发现函数 g 的任务变得像公钥密码学一样复杂。请注意,加密的强度与发现密钥的难度有关,而这又取决于所使用的密码套件和密钥的长度。

另请注意,使用建议的方法,客户方不需要“PrivateKey”。

注意:让我们对客户为绕过我们软件密钥提供的保护可以采取的方法发表一些看法。这些想法应该有助于我们理解使用复杂的函数 fg 的重要性。假设客户已经获得了一台计算机 A 的有效软件密钥,并希望在一台他没有有效软件密钥的计算机 B 上使用“protected_comp.exe”。在我们的场景中,客户拥有生成机器码的工具(实质上,他拥有函数 f 的实现)。

一方面,如果他知道函数 g,他就可以(显然)自己生成软件密钥,用于任何他想要的机器。因此,隐藏 g 的需求是显而易见的。

另一方面,如果他知道函数 f 及其参数 p1,...,pn ,他就可以在计算机 B 上(如果可能)篡改它们,使得使用 f 计算的 B 的机器码与为计算机 A(他拥有有效软件密钥)生成的机器码相同。例如,如果我们仅使用 MAC 地址作为 f 的参数,那么将网卡移动到计算机 B 上应该足以使为 A 获得的软件密钥在计算机 B 上也有效。因此,选择 p1,...,pn 使我们能够获得“尽可能可靠”的机器签名非常重要。考虑到上述情况,使用复杂的函数 f 至少在隐藏 p1,...,pn 参数集用于机器码计算方面似乎很有用。

软件密钥的替代方案

在下一节中,我们将提出一些使用软件密钥的众所周知的替代方法。

远程代码执行

想法是在供应商端尽可能多地执行应用程序的代码。主要缺点是:

  • 它需要主动连接。请注意,在某些情况下这可能是一个问题;
  • 它需要确保客户和供应商站点之间传输的数据的完整性和机密性;
  • 它需要在供应商站点拥有一个强大的服务器来执行应用程序的一部分。

硬件密钥

想法是将执行组件所需的一部分代码保存在硬件密钥中(在最坏的情况下,只有软件密钥)。主要缺点是,由于它们不是免费的,所以它们不适合简单或“个人”组件。

限制

  • 为简化起见,我们为函数 fg 使用了简单的排列。这些算法应该做得更复杂。出于同样的原因,我们为机器码和软件密钥使用了较短的尺寸(以字节为单位)。在实际场景中,应该增加它们。如前所述,使用密码学可以获得更好的解决方案。此外,函数 f 仅取决于 MAC 地址。将其依赖于其他值,如处理器 ID、Windows 序列号等,可能是一个好主意;
  • 为了清晰起见,我们没有使用上述任何技术来提高软件密钥的保护性;
  • 我们没有在具有多个网卡(通常是 Ethernet 网卡)的计算机或具有特定操作系统配置(例如集群)的计算机上测试“CSoftwareKey::RetrieveMACAddress”方法。在这种情况下,如果出现问题,“IPHLPAPI.DLL”中包含的隐藏 API “GetAdaptersInfo”可能很有用;
  • 如前所述,在 CSoftwareKey::RetrieveMACAddress 方法的与 NT 相关的代码中,我们没有检查已安装的服务包版本。这意味着,如果安装了某个特定的服务包,此函数在 Windows NT 中将无法正常工作;
  • 在附加代码中,存在许多错误情况;即使是设想到的地方,也没有得到管理。

下载

下载“softwarekey.zip”包含源代码和可执行文件。解压此文件,您会找到以下目录:

  • protected_comp”包含组件“protected_comp.exe”的源代码;
  • softwarekey_customertool”包含工具“softwarekey_customertool.exe”的源代码;
  • softwarekey_vendortool”包含工具“softwarekey_vendortool.exe”的源代码;
  • softwarekey_demo”包含可直接使用的可执行文件。

历史

  • 2004 年 11 月 - 第一个版本。
© . All rights reserved.