Windows 应用程序的复制保护
描述了 Windows 应用程序的关键注册、安装和验证方法的实现。
引言
这是关于一个允许您保护软件免遭未经授权使用的系统的四部分系列文章的第一篇。本系列将遵循以下大纲:
- 描述系统架构
- 描述许可证密钥的创建
- 描述许可证密钥的安装过程
- 描述安装的验证过程
在此过程中,我们将探讨**n 位反馈加密**(也称为**分组密码链接**;请参阅 **维基百科文章**),以及一种混淆方法,该方法应该能够有效地阻止任何试图理解系统工作原理的尝试。为了增加逆向工程系统的难度,最终将是一组用非托管 C++ 编写的 COM 对象。可以使用互操作性从 .NET 框架调用适当的方法。
背景
保护开发下一个伟大软件应用程序所投入的时间和精力的投资是许多开发人员担心的事情。如果您是软件巨头之一,那么黑客/破解者就会盯上您。但对于其他人来说,拥有一个可以防止随意复制的体面系统应该可以阻止除那些有技能和决心的人之外的所有人,这些人会利用您的软件而不为您提供的服务付费。
这在我脑海里一直是一个有趣的问题,但直到我写了一个相当大的应用程序(基于 Leslie Sanford 出色的 **MIDI C# Toolkit**)来管理我在乐队演出现场可 MIDI 控制的音乐设备时,我才开始认真对待这个想法。毕竟,我花了两年时间才使系统达到真正可用的程度,而且我认为该系统肯定具有市场潜力,因此我希望保护我的投资。
免责声明
在我们开始之前,需要正确设定您的期望。具体来说,本系列文章的读者不应该自欺欺人地认为该系统或任何复制保护机制是万无一失的。您应该问自己的问题不是“是否有人能破解它”,而是“这个人是否有能力做到这一点,他们是否认为花费时间和精力来弄清楚它是值得的?”记住:有志者事竟成。
目标
该系统的总体目标显然是防止未经授权分发您的应用程序。促成这一主要目标的更具体目标列于下文:
- 确保尝试逆向工程提供这些服务的代码并非易事
- 确保任何许可证密钥在存储时都“隐藏”得足够好,即不易找到,也不易更改
- 将创建许可证密钥的代码与验证许可证密钥的代码分开,以便您的应用程序只能分发验证例程
- 允许与 Microsoft 的安装实用程序或自制安装程序轻松集成
- 允许您的应用程序确定安装是否有效
- 允许您的应用程序指定已授权的功能,例如,允许使用运行时但不能使用设计器
- 允许您的应用程序确定安装日期,以便支持试用期
由于分发此类框架的普遍方式是 COM,因此我们将使用 COM 来实现**创建密钥**和**验证密钥**的功能。出于必要,**安装密钥**功能将仅仅是一个具有适当导出入口点的 DLL。这是因为 Microsoft 的安装实用程序(讽刺的是)不允许与外部 COM 对象集成。
需求映射
让我们将目标与实现这些目标的具体方法进行匹配。
- 逆向工程应非易事。代码将使用非托管语言编写。虽然仍然有可能使用反汇编器查看编译后的输出,但理解代码的难度要高得多,尤其是考虑到 IDL 编译器为支持与 COM 子系统所需的接口而创建的骨架结构。
- 隐藏许可证密钥。我们将使用 n 位反馈加密来加密许可证密钥提供的数据,还将使用它来加密密钥本身,然后再存储。
- 代码分离。我们将为每个主要功能组提供单独的可执行映像。
- 易于与 Microsoft 的安装实用程序集成。我们将在第 3 部分详细讨论这一点。
- 确定安装是否有效、指定已授权的功能和试用期。所有这些都与我们存储在密钥中的数据相关,将在第 2 部分和第 4 部分进行讨论。
除此之外,我还增加了指定制造商标识符和产品标识符的功能。这是为了允许我(最初的意图)编写一个 Web 服务,该服务接受打印在装箱单上的许可证密钥,并生成安装密钥作为响应。拥有这些标识符将允许我向希望使用此系统的软件供应商宣传此 Web 服务。
逻辑架构
以下是该系统的逻辑架构
上图说明了该系统的最终逻辑设计。随着系统功能的成熟,它演变成了这种结构。共享功能驻留在静态链接库中,并包含密码函数以及存储/检索系统。其他组件本质上相当简单;它们通常会将参数转换为内部结构,并调用共享库中的一个或多个函数。
接口
COM 接口如下所示。由于 COM 接口可以使用任何数量的公开可用工具轻松检查,因此参数名称故意含糊不清。诚然,这不如代码混淆那么强大,但由于总共有三个接口,我觉得混淆不值得费事。
BSTR NbbfCLib.Create(LONG lID1, LONG lID2, LONG lID3);
bool NbbfVLib.Validate(LONG lID1, LONG lID2, LONG lID3);
LONG NbbfVLIb.Elapsed(LONG lID1, LONG lID2);
如上所述,安装密钥功能——出于必要——是一个带有导出函数的标准 DLL。这些函数如下所示:
void InstallMSI(MSIHANDLE hInstall);
bool InstallDirect(LPSTR lpstrKey, LONG lID1, LONG lID2);
我们将结合讨论每个组件的特定功能来理解每个参数的含义。
N 位反馈加密
顺便说一句,我记得一天早上在淋浴时,“发现”了这个算法,当时我正准备去上班。当看到我不是第一个想到它的人时,我感到失望,但考虑到通常与密码学相关的难度,我暗自庆幸自己也没有显得太愚蠢。根据维基百科的条目,这种密码(在某些地方也称为**分组密码链接**)是由 IBM 于 1976 年开发的,并以“通过分组链接进行消息验证和传输错误检测”(美国专利 4074066)的名义获得了专利。
该密码基本上以以下方式工作。我们将使用 schar
来表示源(未加密)中的字符,pchar
来表示密码状覆盖中的字符,dchar
来表示完成密码操作后的字符。
XOR schar[0] and pchar[0] to get dchar[0]
For each i > 0, XOR schar[i] and pchar[i] and dchar[i-1]
解密过程完全相反。
顺便说一句:密码名称中的**n**是 8,因为 8 位是一个字符,这就是块大小。
由于我一次处理一个字节——并且因为我希望看起来像一个专业的注册密钥——我还借此机会将数据(它是数字的)从十六进制转换为 32 进制,使用修改后的字符集来表示大于 9 的值。在十六进制数中,从 **0-9A-F** ;32 进制将是 **0-9A-V**。但为了混淆,我将其一直扩展到字母 Z,并从中删除了四个字母。然后,我将数字 0-9 从开头移到了字母集内的不同位置。
转换工作方式如下:代码查看正在处理的十六进制数字,然后在伪 32 进制“数字”数组中查找其位置。该数组中的索引被视为原始数字的数值。对数字进行 XOR 运算以确保产生一个完整的数值范围。
重要的是要注意我选择的数字集的大小:它是 2 的幂。这意味着任何未使用的位(在这种情况下是最高 3 位)永远不会被设置,这意味着您永远不会遇到无法使用数字集表示的数值。同样重要的是要注意,由于十六进制数始终用作转换的输入,因此每个十六进制数字都必须包含在 32 进制转换中使用的字符集中。具体来说,该集合如下所示:
#define BASE32_CHARSET (LPSTR)"AB0CD1EF2HI3KL4MN5PQ6RS7TV8WX9YZ"
如果您想更改内容以防止您的使用能够被本文的读者“破解”,只需重新排列预处理器宏定义中的字母即可。
摘要
下一期将介绍用于创建密钥本身的机制。
历史
- 2009 年 9 月 7 日 - 初始版本。