用户手册

VMProtect 完整中文用户手册,涵盖软件保护的全部功能和 API 文档。

简介

不存在保护软件免受未经授权使用和分发的理想方法。现有的任何系统都无法提供绝对的安全性,也无法阻止潜在的黑客对其进行破解。然而,使用高质量和高效的保护可以使破解软件变得极其困难,使投入的时间和精力完全不值得。虽然软件保护可以追求不同的目标,但任何保护系统的基础都是保护应用程序免受分析,因为抵抗逆向工程的能力决定了保护系统的整体效率。

术语表

如果不了解相应领域的专业术语,就无法有效地使用工具。以下术语表解释了 VMProtect 中使用的术语。

软件分析、破解与保护

软件产品可以通过静态分析或动态分析来进行分析。静态分析意味着保护破解算法基于对受保护应用程序的反汇编结果分析或反编译。动态分析用于破解加密或动态变化的可执行文件,因为对这些程序进行静态分析已证明是困难的。

在动态分析中,被破解的程序在调试器框架中执行。这样,程序运行期间发生的一切都可以被调试器控制。在动态分析期间,破解者使用调试模式逐一绕过程序的所有保护算法,特别是注册密钥的生成和检查过程。动态分析经常使用的另一个工具是跟踪被破解程序查询的文件、系统服务、端口和外部设备。

保护应用程序免受破解尝试的主要工具是软件保护器。大多数保护器提供的保护基于对原始可执行文件的加壳和/或加密,并高度重视保护解壳/解密过程。

这种算法通常不足以提供可靠的保护。如果应用程序受加壳保护,黑客可以在解壳器完成工作后立即对内存进行转储,轻松获取原始的未加壳文件。此外,有多种自动化工具可以破解最流行的保护器。加密也是如此:在获得适当的许可证密钥后,破解者可以解密受保护的代码部分。

一些软件保护器使用多种反调试技术。然而,每种技术都会显著影响受保护程序的性能。此外,反调试方法仅对动态分析有效,对静态分析完全无效。更甚的是,现代保护器使用的所有反调试方法都是众所周知的,破解者已经编写了许多实用程序来避免或绕过它们。

更有效的保护应用程序方式是混淆和虚拟化,它们使受保护应用程序的代码分析变得复杂。通常,这些保护方法的高效性基于人为因素:代码越复杂、应用程序使用的资源越多,破解者就越难理解程序逻辑,从而越难破解保护。

混淆通过添加多余的指令来"混淆"应用程序的代码。虚拟化将源代码转换为由模拟具有特定指令集的虚拟机的特殊解释器执行的字节码。因此,虚拟化导致结果代码具有高且不可逆的复杂性,如果正确应用,用这种方法保护的代码不包含显式恢复原始代码的方法。虚拟化的主要优势在于虚拟化的代码片段在执行期间不会转换为机器语言指令,这防止了破解者获取应用程序的原始代码。

什么是 VMProtect?

VMProtect 是新一代的软件保护工具。VMProtect 支持使用 C/C++、C#/VB .NET、Rust、Golang 编译的 x86/x86_64/ARM64 二进制文件和 .NET 程序集,适用于所有最流行的操作系统:Windows、Linux、macOS 和 Android。VMProtect 支持全系列可执行文件,即 Windows 版本可以处理 Linux/macOS 的二进制文件,反之亦然。

VMProtect 的基石原则是通过使应用程序代码和逻辑对于进一步分析和破解变得非常复杂,从而提供对应用程序代码免受审查的高效保护。VMProtect 应用的主要软件代码保护机制包括:虚拟化、变异以及结合变异和后续虚拟化的组合保护。

VMProtect 中使用的虚拟化方法的关键优势在于,执行虚拟化代码片段的虚拟机被嵌入到受保护应用程序的结果代码中。因此,受 VMProtect 保护的应用程序无需第三方库或模块即可运行。VMProtect 允许使用多个不同的虚拟机来保护同一应用程序的不同代码片段,从而使破解过程更加复杂。

VMProtect 中应用的代码变异方法基于混淆——一个向应用程序代码中添加各种多余的"垃圾"指令、"死"代码部分、随机条件跳转的过程。它还变异原始指令并将某些操作的执行转移到堆栈。

VMProtect 与其他软件保护器的关键区别在于它能够使用不同的方法保护代码的不同部分:部分代码可以虚拟化,另一部分被混淆,关键片段使用组合方法保护。

VMProtect 的另一个独特功能是将水印嵌入到应用程序的代码中。水印可以明确识别被破解程序副本的官方所有者。

版本对比

VMProtect 提供 3 个版本:Lite、Professional、Ultimate。

功能LiteProfessionalUltimate
混淆方法
变异✓✓✓
虚拟化✓✓✓
Ultra(变异+虚拟化)✓✓✓
保护选项
内存保护✓✓✓
导入保护✓✓✓
资源保护✓✓✓
加壳✓✓✓
调试器检测✓✓✓
虚拟化工具检测✓✓✓
附加功能
控制台版本✗✓✓
水印✗✓✓
脚本语言✗✓✓
授权系统✗✗✓
激活系统✗✗✓
虚拟文件✗✗✓

保护应用程序的建议

VMProtect 是保护应用程序代码免受分析和破解的可靠工具,但只有在正确构建应用内保护机制、避免可能破坏整个保护的典型错误时,才能实现最高效的使用。让我们回顾开发良好程序保护的关键要素。

注册过程

许多开发者在设计注册过程时犯的一个典型错误是将整个注册密钥检查封装到一个单独的函数中,该函数返回一个易于理解的值:

function CheckRegistration(const RegNumber: String): Boolean;
begin
  if RegNumber='123' then
   Result:=True
  else
   Result:=False;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
  ...
  if not CheckRegistration(RegNumber) then
   exit;
  Application.CreateForm(TForm2, Form2);
  Form2.ShowModal;
  ...
end;

采用这种方法,入侵者甚至不需要理解密钥检查算法。他只需修改检查过程开头的代码,使其始终返回正确的值:

function CheckRegistration(const RegNumber: String): Boolean;
begin
  Result:=True;
  exit;
  ...
end;

一种更有效的方法是将正确性检查嵌入到程序的主要操作逻辑中,使注册密钥检查的算法无法与调用过程的算法分离。我们建议将操作逻辑与注册密钥检查过程"融合",使程序在检查被绕过时无法正常工作:

function CheckRegistration(const RegNumber: String): Boolean;
begin
  if RegNumber='123' then
   begin
    Application.CreateForm(TForm2, Form2);
    Result:=True
   end
  else
    Result:=False;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
  ...
  Form2:=nil;
  if not CheckRegistration(RegNumber) then
   exit;
  Form2.ShowModal;
  ...
end;

如果以这种方式实现 CheckRegistration 函数,入侵者必须详细分析注册密钥检查的代码才能绕过它。如果此应用程序受 VMProtect 保护,建议对 CheckRegistration 函数和 TForm1.Button1Click 过程都进行虚拟化。为了使破解更加复杂,您可以开启 "Ultra" 保护模式来结合代码变异和后续虚拟化。

检查注册密钥

开发者犯的另一个关键错误是注册密钥检查的错误实现。通常,输入的密钥只是与正确的值进行简单比较。破解者可以通过跟踪字符串比较函数的参数轻松匹配正确的密钥值:

var ValidRegNumber: String;
...
function CheckRegistration(const RegNumber: String): Boolean;
begin
  if RegNumber=ValidRegNumber then
   Result:=True
  else
   Result:=False;
end;

为了避免这种情况,我们建议比较密钥的哈希值,而不是实际值。哈希函数是不可逆的,因此破解者无法从哈希中检索真实的密钥值。

保存检查结果

通常,即使在注册过程上花了大量时间的开发者,也没有对保护注册过程结果给予应有的关注。下面的例子使用全局变量存储注册状态。对于入侵者来说,找到全局变量轻而易举——他只需比较注册前后的数据段即可。

var IsRegistered: Boolean;
...
procedure TForm1.Button1Click(Sender: TObject);
begin
  ...
  if not IsRegistered then
   IsRegistered:=CheckRegistration(RegNumber);
  if  not IsRegistered then
   exit;
  ...
end;

为了避免这种情况,建议将与程序注册相关的所有检查结果存储在动态内存中:

type PBoolean = ^Boolean;
var IsRegistered: PBoolean;
...
procedure TForm1.Button1Click(Sender: TObject);
begin
  ...
  if not IsRegistered^ then
   IsRegistered^:=CheckRegistration(RegNumber);
  if  not IsRegistered^ then
   exit;
  ...
end;
...
initialization
  New(IsRegistered);

使用 VMProtect

在开始使用 VMProtect 之前,请查看以下章节:

准备项目

让我们来看一个非常简单的应用程序,它仅由一个窗体(Form1)、一个文本元素(Edit1)和一个按钮(Button1)组成。当点击 Button1 时,应用检查输入的密码是否正确并显示相应的消息。

Delphi 项目示例

密码使用非常简单的算法进行检查:第一步将其转换为数字形式,然后计算除以 17 的余数。如果余数等于 13,则密码正确:

function TForm1.CheckPassword: Boolean;
begin
  Result:=(StrToIntDef(Edit1.Text, 0) mod 17=13);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
  if CheckPassword then
   MessageDlg('Correct password', mtInformation, [mbOK], 0)
  else
   begin
    MessageDlg('Incorrect password', mtError, [mbOK], 0);
    Edit1.SetFocus;
   end;
end;

可以通过三种方式选择要保护的过程和函数:

重要提示:如果使用 PDB/MAP 文件选择要虚拟化的代码片段,序言和尾声也将被虚拟化,从而显著增强保护能力。此外,如果一个虚拟化函数从另一个虚拟化函数中调用,控制权在它们之间转移时不会实际跳转到被调用函数的地址。这也加强了程序的保护。

使用 PDB/MAP 文件

要创建 MAP 文件,您需要在编译器设置中启用相应选项。

Visual Studio

在 IDE 主菜单中,打开项目属性(Project – Properties),在 "Linker – Debugging" 选项卡中将 "Generate MAP File" 设置为 "Yes (/MAP)":

Visual Studio MAP 文件设置

Borland Delphi

在 Delphi IDE 主菜单中打开项目选项(Project – Options),在 "Linker" 选项卡中将 "MAP file" 部分设置为 "Detailed":

Delphi MAP 文件设置

启用 MAP 文件生成后,必须重新构建项目。加载 MAP 文件时,VMProtect 会比较 MAP 文件和受保护文件的修改日期和时间。如果不同,则不会加载 MAP 文件。

使用标记

要保护代码的各个片段以及保护字符串常量,您可以在源代码中插入特殊标记。标记是从 SDK 库导入的函数调用:

.NET

Windows

Linux

macOS

SDK 中的过程和函数不执行任何操作,仅作为 VMProtect 用来确定受保护代码边界的标签。受保护块的开头和结尾标记如下:

C/C++

#include "VMProtectSDK.h"
VMProtectBegin(MARKER_TITLE);
...
VMProtectEnd();

C#

using System.Reflection;

class Foo
{
    [Obfuscation(Feature = "virtualization", Exclude = false)]
    public Foo()
    {
...

Pascal

uses VMProtectSDK;
VMProtectBegin(MARKER_TITLE);
...
VMProtectEnd;

此外可以使用具有预定义编译类型的标记代替 VMProtectBegin:

当 VMProtect 分析受保护应用程序的代码时,它会定位所有对 VMProtectSDK 过程和函数的调用。受保护块的边界由标记对 VMProtectBegin 和 VMProtectEnd 定义。然后,VMProtect 处理受保护应用程序代码时会删除标记和所有对 VMProtectSDK 的引用,因此无需将这些库包含在安装包中。

重要提示:使用标记时,不应允许从非保护区域跳转到标记内部。如果使用标记的应用程序在保护后变得无法正常工作,您可以通过启用"调试模式"选项来检测来自非保护区域的跳转和地址。

.NET ObfuscationAttribute

VMProtect 支持以下 ObfuscationAttribute.Feature 值:

SDK 函数

SDK 函数可以集成到受保护应用程序的源代码中,用于设置受保护区域的边界、检测调试器或虚拟化工具。

代码标记

服务函数

授权函数

激活函数

重要提示:保护后,应用程序将不再需要 SDK 库。

GUI 版本

主窗口由以下元素组成:

VMProtect 主窗口

文件菜单

项目菜单

工具菜单

帮助菜单

工具栏

工具栏由以下元素组成:打开项目或受保护文件、保存项目、受保护文件的名称、编译项目、执行原始/受保护文件、默认操作、快速搜索框。

项目区域

"项目" 区域包含以下子区域:

保护的函数

此区域用于选择必须保护的函数。

编译类型:为每个受保护对象设置编译方式,以实现性能和代码安全性之间的最佳平衡:

锁定到序列号 — 如果启用此选项,受保护的函数在未输入有效序列号的情况下将不可用,并将终止应用程序。这样您可以限制未注册版本中某些功能的访问。

管理许可证

默认情况下,授权功能处于关闭状态。要启用它们,您需要在 "项目" 区域的 "许可证" 子区域中创建一对密钥。初始化完成后,"锁定到序列号" 选项将可用,您将能够创建和处理序列号。

"许可证" 区域在左侧面板显示许可证的完整列表,在主面板显示所选元素的参数。右侧面板显示所选许可证的详细信息,还允许封锁序列号、将其复制到剪贴板或查看硬件 ID 信息。

文件区域

"文件" 区域允许开发者将受保护 EXE 文件运行所需的额外数据(如图像、数据文件、文本资源和动态链接库)包含到其中。在执行受保护的 EXE 文件期间,包括 DLL 在内的所有类型的数据都直接从进程内存中加载,而不会将这些数据写入磁盘。

脚本区域

"项目" 区域的 "脚本" 子区域用于使用内置脚本语言编写脚本。您可以在此区域的主面板上编辑脚本代码。

选项区域

"选项" 区域允许您配置各种保护参数:

虚拟机选项

文件选项

检测选项

函数区域

"函数" 区域列出所有可供保护的函数。选择函数后,您可以在主面板上查看其属性和保护选项。对于每个函数,您可以指定编译类型并启用锁定到序列号。

详细信息区域

"详细信息" 区域显示有关受保护应用程序的各种信息。它还允许您从打包中排除某些数据段或资源。包含以下子区域:

控制台版本

在 GUI 模式下创建项目后,您可以使用控制台版本(VMProtect_Con.exe)。执行方式如下:

VMProtect_Con File [Output File] [-pf Project File] [-sf Script File] [-lf Licensing Parameters File] [-bd Build Date (yyyy-mm-dd)] [-wm Watermark Name] [-we]

重要提示:控制台版本不适用于 Lite 版本。

授权系统

授权系统创建并验证序列号的有效性。它支持按日期和时间限制受保护软件、锁定到特定硬件、使用序列号加密代码、限制免费更新期限等功能。该系统基于非对称加密算法,以最大限度地减少构建未经授权的序列号生成器("注册机")的可能性。

功能特性

工作原理

保护和授权的流程如下:

  1. 开发者在 VMProtect 中创建密钥对(公钥和私钥)。公钥将嵌入到受保护的应用程序中,私钥用于生成序列号。
  2. 开发者设置保护选项,选择要保护/虚拟化的函数,可选地将某些函数锁定到序列号。
  3. VMProtect 编译并保护应用程序。
  4. 用户购买软件后,开发者(或自动系统)使用私钥生成序列号。
  5. 用户将序列号输入到应用程序中,授权系统验证序列号并解锁相应功能。

集成到应用程序

将授权系统集成到应用程序中分为两个阶段。在第一阶段,您可以使用测试模式来熟悉 API 并测试各种功能,而无需使用 VMProtect 处理应用程序。在第二阶段,您将创建受 VMProtect 保护的真实应用程序并测试其在实际环境中的运行情况。

步骤 1.1:创建测试应用程序

首先创建一个简单的控制台应用程序,包含 VMProtectSDK 头文件。将程序与 VMProtectSDK 库链接。在测试模式下,授权系统使用 INI 文件来模拟序列号的行为。

创建一个名为 "test_licensing.ini" 的文件,内容如下:

[TestLicense]
AcceptedSerialNumber=Xserialnumber

AcceptedSerialNumber 参数指定授权系统接受的序列号。参数的值可以是任意的,但必须与程序中传递的值匹配。

步骤 1.2:检查序列号

使用 VMProtectSetSerialNumber() 函数将序列号传递给授权系统。该函数返回一个位掩码,如果返回 0 则表示序列号有效:

int main(int argc, char **argv)
{
        char *serial = "Xserialnumber";
        int res = VMProtectSetSerialNumber(serial);
        print_state(res);
        return 0;
}

步骤 1.3:获取序列号信息

使用 VMProtectGetSerialNumberData() 函数获取序列号中的详细信息,包括用户名、邮箱、过期日期等:

VMProtectSerialNumberData sd = {0};
VMProtectGetSerialNumberData(&sd, sizeof(sd));
printf("User name: %ls\n", sd.wUserName);
printf("E-Mail: %ls\n", sd.wEMail);

步骤 1.4:序列号过期日期

在 INI 文件中添加过期日期:

ExpDate=20051001

格式为 YYYYMMDD。如果当前日期晚于过期日期,VMProtectSetSerialNumber() 将返回 SERIAL_STATE_FLAG_DATE_EXPIRED 标志。

步骤 1.5:运行时间限制

在 INI 文件中添加 RunningTimeLimit 参数(以分钟为单位)。如果程序运行时间超过限制,VMProtectGetSerialNumberState() 将返回 SERIAL_STATE_FLAG_RUNNING_TIME_OVER 标志。

步骤 1.6:免费升级日期

在 INI 文件中添加 MaxBuildDate 参数。如果受保护程序的编译日期晚于此日期,将返回 SERIAL_STATE_FLAG_MAX_BUILD_EXPIRED 标志。

步骤 1.7:用户名和邮箱

在 INI 文件中添加 UserName 和 EMail 参数,使用 VMProtectGetSerialNumberData() 来读取这些数据。

步骤 1.8:黑名单

在 INI 文件中添加 BlackListedSerialNumber 参数。如果当前序列号在黑名单中,将返回 SERIAL_STATE_FLAG_BLACKLISTED 标志。

步骤 1.9:硬件锁定

使用 VMProtectGetCurrentHWID() 获取硬件标识符:

int nSize = VMProtectGetCurrentHWID(NULL, 0);
char *buf = new char[nSize];
VMProtectGetCurrentHWID(buf, nSize);
printf("HWID: %s\n", buf);
delete [] buf;

在 INI 文件中设置 MyHWID(模拟当前硬件)和 KeyHWID(序列号中的硬件标识符)。如果两者不匹配,将返回 SERIAL_STATE_FLAG_BAD_HWID 标志。

重要提示:程序只有在被 VMProtect 处理后才会显示真实的硬件标识符。

步骤 1.10:用户数据

序列号可以包含最多 255 字节的任意数据。在 INI 文件中使用 UserData 参数以 HEX 格式指定数据:

UserData=010203A0B0C0D0E0

使用 VMProtectGetSerialNumberData() 读取 nUserDataLength 和 bUserData 字段获取这些数据。

步骤 2.1:创建受保护的应用程序

在第一阶段我们使用测试模式编写了几个简单应用来测试授权 API。现在在第二阶段我们将创建一个实际的应用程序,编译时不包含调试信息但启用 MAP 文件生成。

步骤 2.2:创建 VMProtect 保护项目

在 VMProtect Ultimate 中打开可执行文件。将需要保护的函数添加到项目中。然后在"许可证"区域初始化授权系统,创建 2048 位的密钥对。

步骤 2.3:首次启动受保护产品

授权系统初始化完成后,编译 VMProtect 项目并运行受保护文件。此时程序不再使用 VMProtectSDK.dll,因为授权模块已内置到程序中。在"许可证"区域生成序列号,保存到文件中,程序即可正常工作。

步骤 2.4:测试结果

测试各种场景:创建带过期日期的序列号、将序列号添加到黑名单等。验证授权系统对不同情况的响应是否正确。

步骤 2.5:将代码锁定到序列号

破解程序最常见的方式之一是定位序列号检查处及其后的条件跳转。VMProtect 允许将一个或多个函数的代码锁定到序列号,使其在没有正确序列号的情况下无法执行:

锁定到序列号

应该锁定什么?建议锁定仅在注册版本中运行的函数。由于锁定需要虚拟化,应考虑性能损失。例如,如果文本编辑器的演示版不允许保存,您可以将保存文档函数锁定到序列号。

锁定与无效序列号:当调用 VMProtectSetSerialNumber() 时,授权模块检查序列号。加密的代码片段仅在序列号完全正确时才会执行。某些限制可能在程序运行期间"触发"(例如运行时间到期),但授权模块仍会继续执行已锁定的函数,因为突然停止可能导致应用程序故障。

授权系统 API

授权系统 API 是 VMProtect API 和 SDK 的组成部分。API 允许您指定序列号并检索有关它的所有信息。

VMProtectSetSerialNumber

将序列号加载到授权系统中。调用语法:

int VMProtectSetSerialNumber(const char *SerialNumber);

输入参数 SerialNumber 必须是指向以 null 结尾的字符串的指针,包含 base-64 编码的序列号。函数返回序列号状态标志的位掩码。如果返回 0,表示序列号"正确"。

VMProtectGetSerialNumberState

返回由 VMProtectSetSerialNumber() 指定的序列号的状态标志:

int VMProtectGetSerialNumberState();

如果至少设置了一个标志,则序列号存在问题。详细的标志说明如下:

标志值说明
SERIAL_STATE_FLAG_CORRUPTED0x01授权系统已损坏。可能原因:保护项目设置不正确、破解尝试。
SERIAL_STATE_FLAG_INVALID0x02序列号不正确。授权系统无法解密序列号。
SERIAL_STATE_FLAG_BLACKLISTED0x04序列号与产品匹配,但在 VMProtect 中被列入黑名单。
SERIAL_STATE_FLAG_DATE_EXPIRED0x08序列号已过期。
SERIAL_STATE_FLAG_RUNNING_TIME_OVER0x10程序运行时间已耗尽。
SERIAL_STATE_FLAG_BAD_HWID0x20硬件标识符与序列号中的不匹配。
SERIAL_STATE_FLAG_MAX_BUILD_EXPIRED0x40序列号与受保护程序的当前版本不匹配。

VMProtectGetSerialNumberData

获取序列号的详细信息:

bool VMProtectGetSerialNumberData(VMProtectSerialNumberData *Data, int Size);

VMProtectSerialNumberData 结构体包含以下字段:

字段类型说明
nStateint密钥状态的位标志掩码。
wUserNamewchar_t[256]客户名称(UNICODE,以 null 结尾)。
wEMailwchar_t[256]客户邮箱(UNICODE,以 null 结尾)。
dtExpireVMProtectDate密钥过期日期。
dtMaxBuildVMProtectDate密钥可用的最大产品构建日期。
bRunningTimeint程序运行的最大分钟数。
nUserDataLengthunsigned char用户数据的长度。
bUserDataunsigned char[255]放入密钥的用户数据。

VMProtectDate

日期结构体:

字段类型说明
wYearunsigned short年份。
bMonthunsigned char月份,从 1 开始。
bDayunsigned char日期,从 1 开始。

VMProtectGetCurrentHWID

获取程序运行所在 PC 的硬件标识符:

int VMProtectGetCurrentHWID(char *HWID, int Size);

如果第一个参数为 NULL,函数返回存储硬件标识符所需的字节数。正确用法:

int nSize = VMProtectGetCurrentHWID(NULL, 0);
char *pBuf = new char[nSize];
VMProtectGetCurrentHWID(pBuf, nSize);
// 使用标识符
delete [] pBuf;

序列号生成器

用途

除了 VMProtect 外,其他软件也可以生成序列号。这对于自动化发送序列号是必要的。客户购买产品后,电子商务代理向供应商的网站发送 HTTP 请求,服务器上运行的生成器根据客户数据生成序列号。序列号发送给客户和供应商。供应商随后使用导入许可证对话框在 VMProtect 中手动添加序列号。

工作原理

VMProtect 的授权系统基于非对称算法,因此生成序列号需要产品的私钥。您可以在项目属性窗口中导出此密钥,并以任何合适的方式将其传递给生成器。

现有的生成器

授权系统附带三个可用的序列号生成器:DLL 版本(Windows)、.NET 版本和 PHP 版本(UNIX)。

安全建议

  • 使用 HTTPS — 如果电子商务提供商可以发送 HTTPS 请求,所有数据以加密形式传输,生成的序列号无法被截获。
  • "隐藏"您的生成器 — 确保没有人可以偶然打开生成器。不要放置外部链接,不要在网站目录或 robot.txt 中列出。
  • 验证调用者 — 检查调用者的 IP 地址是否在电子商务提供商的 IP 范围内。
  • 检查输入参数 — 确保所有必需参数都已传递且格式正确。不要对错误请求产生任何响应。
  • 添加"密码" — 在查询中指定一个附加参数作为密码,名称和值应不明显。

Windows 版本

Windows 密钥生成器是用于 x86 和 x64 平台的 DLL 文件,附带 C 语言头文件和 MSVC 兼容的 lib 文件。所有文件位于 %Examples%\Keygen\DLL 文件夹中。

生成器 API

生成器仅导出两个函数:第一个生成序列号,第二个释放第一个函数分配的内存:

VMProtectErrors __stdcall VMProtectGenerateSerialNumber(
    VMProtectProductInfo *pProductInfo,
    VMProtectSerialNumberInfo *pSerialInfo,
    char **pSerialNumber
);
void __stdcall VMProtectFreeSerialNumberMemory(char *pSerialNumber);

VMProtectSerialNumberInfo 结构体:

struct VMProtectSerialNumberInfo
{
    INT              flags;
    wchar_t *        pUserName;
    wchar_t *        pEMail;
    DWORD            dwExpDate;
    DWORD            dwMaxBuildDate;
    BYTE             nRunningTimeLimit;
    char *           pHardwareID;
    size_t           nUserDataLength;
    BYTE *           pUserData;
};

flags 字段包含以下位标志:

  • HAS_USER_NAME — 将用户名放入序列号。
  • HAS_EMAIL — 将邮箱放入序列号。
  • HAS_EXP_DATE — 序列号将在指定日期后过期。
  • HAS_MAX_BUILD_DATE — 序列号仅适用于指定日期前的版本。
  • HAS_TIME_LIMIT — 程序运行时间限制(以分钟为单位,最大 255)。
  • HAS_HARDWARE_ID — 程序仅在指定硬件上运行。
  • HAS_USER_DATA — 将自定义用户数据放入序列号。

VMProtectGenerateSerialNumber 函数返回值:

  • ALL_RIGHT — 无错误,序列号已生成。
  • UNSUPPORTED_ALGORITHM — 传递了不正确的加密算法。
  • USER_NAME_IS_TOO_LONG — UTF-8 编码的用户名超过 255 字节。
  • EMAIL_IS_TOO_LONG — UTF-8 编码的邮箱超过 255 字节。
  • SERIAL_NUMBER_TOO_LONG — 序列号太长,超出算法的位数限制。
  • BAD_PRODUCT_INFO — 第一个参数不正确或为 NULL。
  • BAD_SERIAL_NUMBER_INFO — 第二个参数不正确或为 NULL。

dwExpDate 和 dwMaxBuildDate 格式

日期字段使用特定格式:0xYYYYMMDD。使用宏 MAKEDATE(y, m, d),例如:MAKEDATE(2010, 05, 12)。

.NET 版本

.NET 版本的密钥生成器是一个包含生成序列号所需一切内容的程序集。源代码位于 %Examples%\Keygen\Net,包含 KeyGen(生成器本身)和 Usage(使用示例)两个项目。

使用步骤:

  1. 将 Usage 项目的代码作为基础,添加对 VMProtect.KeyGen.dll 的引用。
  2. 在 VMProtect 中打开"项目 | 导出密钥对"对话框,选择".NET KeyGen 参数"选项。
  3. 将导出的文本复制到应用程序中作为字符串常量。

示例代码:

try
{
    string data = @""; // 在此放入导出的数据
    Generator g = new Generator(data);
    g.UserName = "John Doe";
    g.EMail = "john@doe.com";
    g.ExpirationDate = DateTime.Now.AddMonths(1);
    g.MaxBuildDate = DateTime.Now.AddYears(1);
    g.RunningTimeLimit = 15;
    g.HardwareID = "AQIDBAgHBgU=";
    g.UserData = new byte[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
    string serial = g.Generate();
    Console.WriteLine("Serial number:\n{0}\n", serial);
}
catch (Exception ex)
{
    Console.WriteLine("Error: {0}", ex);
}

UNIX 版本

UNIX 版本的密钥生成器是一个 PHP 文件,包含序列号生成所需的所有信息。文件位于 %Examples%\Keygen\PHP。

配置生成器

在 PHP 文件开头有设置代码,由 VMProtect 自动生成,对每个产品唯一:

$exported_algorithm = "RSA";
$exported_bits = 2048;
$exported_private = "PJvj4kEpoQMIpYK+9wEt......xKeiSZgzdiln8Q==";
$exported_modulus = "rOlny/3QgZb/VmGr3CmY......I6ESAUmtQ+RBqQ==";
$exported_product_code = "oLQdGUn8kVk=";

密钥内容

$params = array(
    user_name => "John Doe",
    email => "john@doe.com",
    hwid => "vHGMdMRvGCPjWcCQ",
    expire_date => array(year => 2009, month => 10, day => 1),
    maxbuild_date => array(year => 2009, month => 10, day => 1),
    time_limit => 10,
    user_data => base64_decode("CGCvRvMWcPHGdMjQ"),
);

注意事项

  • 用户名和邮箱必须以 UTF-8 字符串传递。
  • 非对称加密是复杂的数学过程。生成器使用 gmp_powm、bi_powmod、bcpowmod 函数。如果生成时间过长,建议启用这些函数。

序列号格式

序列号结构

序列号由多个块组成。每个块以标识字节开始,指示块的内容和可能的长度。最后一个块的标识符始终为 255,包含序列号的校验和。

块格式

ID名称大小(字节)说明
0x01版本1序列号版本,必须为 1。
0x02用户名1 + NUTF-8 编码的用户名,前有 1 字节长度。最大 255 字节。
0x03邮箱1 + NUTF-8 编码的邮箱,前有 1 字节长度。最大 255 字节。
0x04硬件标识符1 + N由 VMProtectGetCurrentHWID() 返回的 base-64 字符串的解码版本。长度必须是 4 的倍数,最大 32 字节。
0x05许可证过期日期4序列号过期日期。
0x06最大运行时间1程序可运行的时间(分钟),最大 255 分钟。
0x07产品代码8由 VMProtect 创建的产品代码,此块是必需的。
0x08用户数据1 + N最多 255 字节的自定义用户数据。
0x09最大构建日期4应用程序的最大构建日期。
0xFF校验和4序列号校验和,使用 SHA-1 哈希算法计算。

日期存储格式

日期以双字存储 — 0xYYYYMMDD。高位字包含年份,低位字包含日和月。字节遵循小端序表示。

校验和计算

校验和使用 SHA-1 哈希算法计算。结果是五个 32 位字。第一个字用作序列号的校验和。注意:该字为小端序。如果 SHA-1 哈希由字符串函数生成(如在 PHP 中),则哈希的前四个字节必须反转。

序列号加密算法

授权系统的安全性基于非对称密码学算法。当前版本实现了密钥长度从 1024 到 16384 位的 RSA 算法。未来版本计划实现基于 ECC 的其他算法。

每个产品使用的算法是唯一的。用一种算法制作的密钥不能与另一种算法一起使用,这意味着在创建至少一个许可证后不允许更改算法。

RSA 算法

序列号使用 RSA 算法加密的步骤:

  1. 在序列号开头添加随机数据 — 基于 RFC2313 的方法。在密钥开头添加:00 02 NN…NN 00,其中 NN…NN 是 8 到 16 个随机非零字节。
  2. 在序列号末尾添加随机数据 — 序列号的总字节数必须等于算法密钥位数除以 8。格式为:00 02 NN..NN 00 DD..DD MM..MM。
  3. 加密 — 使用处理大数的标准过程。
  4. 打包 — 加密后的字节集被编码为 base-64,即为发送给客户的序列号。

激活系统

什么是激活?

激活是注册应用程序的两阶段过程。在第一阶段(通常在购买后立即),用户收到一个激活码,通常看起来像 XXXX-YYYY-ZZZZ。在第二阶段,客户将代码输入到应用程序中,应用程序通过互联网连接到开发者的服务器,服务器检查激活码并返回绑定到该激活码的序列号,通常锁定到用户的硬件。

为什么需要激活?

  • 短序列号 — 激活码可以解决长序列号的所有问题,同时充分利用其优势。
  • 安装控制 — 所有激活对开发者实时在线可见。每次激活都可以被监控、分析、封锁。
  • 试用期 — 您可以通过使用产品"模式"来创建限时序列号。
  • 订阅 — 类似于试用期,但需付费。您可以向用户出售在指定时间内使用程序的权利。

需要什么?

VMProtect Ultimate 提供了多个函数用于相对轻松地实现在线和离线激活。您还需要在服务器上安装 Web License Manager,并在 VMProtect 中配置保护项目。

在 VMProtect 中配置激活

要使激活 API 正常工作,需要 WebLM URL。在 VMProtect 中打开选项区域:

配置激活服务器

在"激活服务器"字段中输入地址,格式为:http://yourserver/weblm_path。这是遇到在线激活问题时首先要检查的地方。

Web License Manager 中的激活

进入 Web License Manager 并创建产品。然后将产品导出为 VMProtect 项目以配置授权和激活。设置完成后,在 WebLM 左侧面板中点击"添加新代码"链接:

WebLM 添加激活码

从上方下拉列表中选择所需产品,填写其余表单数据,然后点击"保存"按钮。您将看到可用于调试激活 API 的激活码。

WebLM 激活码

激活 API

激活 API 仅包含 4 个函数。两个用于在线激活,另外两个用于计算机无法访问互联网时的离线激活。

VMProtectActivateLicense

将激活码传递到服务器并返回此特定计算机的序列号:

int VMProtectActivateLicense(const char *code, char *serial, int size);

code 参数是从 Web License Manager 获取的激活码。serial 参数指定存放 WebLM 生成的序列号的内存块。

VMProtectDeactivateLicense

将序列号传递到服务器进行停用:

int VMProtectDeactivateLicense(const char *serial);

VMProtectGetOfflineActivationString / VMProtectGetOfflineDeactivationString

这两个函数的工作方式与前两个类似,但不尝试连接 WebLM 服务器。它们返回一个文本块,用户需要将其复制到联网计算机上,打开 WebLM 离线激活表单并粘贴:

int VMProtectGetOfflineActivationString(const char *code, char *buf, int size);
int VMProtectGetOfflineDeactivationString(const char *serial, char *buf, int size);

buf 参数应指向 1000 字节或更大的缓冲区。

可能的错误码

代码值说明
ACTIVATION_OK0激活成功。序列号已放入 serial 变量。
ACTIVATION_SMALL_BUFFER1缓冲区太小。
ACTIVATION_NO_CONNECTION2无法连接到 Web License Manager。
ACTIVATION_BAD_REPLY3激活服务器返回了意外结果。
ACTIVATION_BANNED4此激活码已被软件供应商在服务器上封禁。
ACTIVATION_CORRUPTED5激活模块自检系统出错,通常表示破解尝试。
ACTIVATION_BAD_CODE6指定的代码在激活服务器数据库中未找到。
ACTIVATION_ALREADY_USED7此代码的激活计数器已耗尽。
ACTIVATION_SERIAL_UNKNOWN8给定的序列号在服务器数据库中未找到。
ACTIVATION_EXPIRED9代码的激活期已过期。
ACTIVATION_NOT_AVAILABLE10激活/停用不可用。

提示和技巧

不要忘记为有互联网问题的用户提供离线激活方式。激活 API 不会保存它收到的序列号,也不会将其传递给授权模块 — 这应该由开发者完成。您不必在每次启动应用程序时调用激活 API,只需调用一次,从 WebLM 获取序列号,保存到适当位置,然后使用保存的副本。

使用脚本

VMProtect 内置了强大的脚本语言 LUA,极大地增强了 VMProtect 在每个保护阶段的默认保护能力。

LUA 语法与 JavaScript 非常相似,但与 JavaScript 不同,LUA 不包含显式类。尽管如此,脚本语言允许轻松实现面向对象编程机制,如类、继承和事件。脚本使用示例可在 "VMProtect/Examples/Scripts" 文件夹中找到。

重要提示:脚本编辑器不适用于 Lite 版本。

脚本类参考

VMProtect 内置的 LUA 脚本语言是面向对象的。脚本语言包括提供基本功能的标准类和提供应用程序保护功能访问的专用类。

类层次结构

Core — 用于操作 VMProtect 核心的类:

  • Watermarks / Watermark — 水印管理
  • Licenses / License — 许可证管理
  • Files / File — 文件管理
  • Folders / Folder — 文件夹管理

PEFile — 用于操作 PE 文件的类:

  • PEArchitecture — PE 架构
  • PESegments / PESegment — 段
  • PESections / PESection — 节
  • PEDirectories / PEDirectory — 目录
  • PEImports / PEImport — 导入
  • PEExports / PEExport — 导出
  • PEResources / PEResource — 资源
  • PERelocs / PEReloc — 重定位

MacFile — 用于操作 Mach-O 文件的类:

  • MacArchitecture — Mach-O 架构
  • MacSegments / MacSegment — 段
  • MacSections / MacSection — 节
  • MacCommands / MacCommand — 加载命令
  • MacSymbols / MacSymbol — 符号
  • MacImports / MacImport — 导入
  • MacExports / MacExport — 导出
  • MacRelocs / MacReloc — 重定位

其他类:

  • MapFunctions / MapFunction — 可用于保护的函数列表
  • References / Reference — 引用列表
  • IntelFunctions / IntelFunction — Intel 函数及命令
  • CommandLinks / CommandLink — 命令链接
  • FFILibrary / FFIFunction — 外部函数接口

VMProtect 全局函数

namespace vmprotect {
    Core core();
    string extractFilePath(string name);
    string extractFileName(string name);
    string extractFileExt(string name);
    string expandEnvironmentVariables(string value);
    void setEnvironmentVariable(string name, string value);
    string commandLine();
    FFILibrary openLib(string name);
}

Core 类

项目选项枚举:

enum ProjectOption {
    None, Pack, ImportProtection, MemoryProtection,
    ResourceProtection, CheckDebugger, CheckKernelDebugger,
    CheckVirtualMachine, StripFixups, StripDebugInfo, DebugMode
}

Core 类方法:

class Core {
public:
    string projectFileName();       // 返回项目文件名
    void saveProject();             // 保存项目
    string inputFileName();         // 返回源文件名
    string outputFileName();        // 返回输出文件名
    void setOutputFileName(string); // 设置输出文件名
    string watermarkName();         // 返回水印名称
    void setWatermarkName(string);  // 设置水印名称
    int options();                  // 返回项目选项
    void setOptions(int);           // 设置项目选项
    string vmSectionName();         // 返回 VM 段名称
    void setVMSectionName();        // 设置 VM 段名称
    Licenses licenses();            // 返回许可证列表
    Files files();                  // 返回文件列表
    Watermarks watermarks();        // 返回水印列表
    PEFile/MacFile inputFile();     // 返回源文件
    PEFile/MacFile outputFile();    // 返回输出文件
};

Licenses 类

class Licenses {
public:
    int keyLength();          // 返回密钥长度
    string publicExp();       // 返回公钥指数
    string privateExp();      // 返回私钥指数
    string modulus();         // 返回模数
    License item(int index);  // 返回指定索引的许可证
    int count();              // 返回许可证数量
};

class License {
public:
    string date(string format = "%c");  // 返回许可证日期
    string customerName();   // 返回客户名称
    string customerEmail();  // 返回客户邮箱
    string orderRef();       // 返回订单 ID
    string comments();       // 返回注释
    string serialNumber();   // 返回序列号
    bool blocked();          // 返回是否被封锁
    void setBlocked(bool);   // 设置封锁状态
};

PE 文件类

class PEFile {
public:
    string name();           // 返回文件名
    string format();         // 返回格式名称 "PE"
    uint64 size();           // 返回文件大小
    int count();             // 返回架构数量
    PEArchitecture item(int index);  // 返回指定索引的架构
    uint64 seek(uint64 offset);      // 设置文件位置
    uint64 tell();           // 返回文件位置
    int write(string buffer);// 写入缓冲区
};

class PEArchitecture {
public:
    string name();           // 返回架构名称
    PEFile file();           // 返回父文件
    uint64 entryPoint();     // 返回入口点
    uint64 imageBase();      // 返回基址
    OperandSize cpuAddressSize();  // 返回架构位数
    uint64 size();           // 返回架构大小
    PESegments segments();   // 返回段列表
    PESections sections();   // 返回节列表
    PEDirectories directories();  // 返回目录列表
    PEImports imports();     // 返回导入库列表
    PEExports exports();     // 返回导出函数列表
    PEResources resources(); // 返回资源列表
    PERelocs relocs();       // 返回重定位列表
    MapFunctions mapFunctions();   // 返回可保护的函数列表
    IntelFunctions functions();    // 返回受保护的函数列表
    bool addressSeek(uint64 address);  // 按地址定位
    uint64 seek(uint64 offset);  // 设置文件位置
    int write(string buffer);    // 写入缓冲区
};

Intel 函数类

编译类型:

enum CompilationType {
    Virtualization, Mutation, Ultra
};
class IntelFunctions {
public:
    IntelFunction item(int index);
    int count();
    void clear();
    IntelFunction itemByAddress(uint64 address);
    IntelFunction itemByName(string name);
    IntelFunction addByAddress(uint64 address, CompilationType type = ctVirtualization);
};

class IntelFunction {
public:
    uint64 address();
    string name();
    ObjectType type();
    IntelCommand item(int index);
    int count();
    CompilationType compilationType();
    void setCompilationType(CompilationType value);
    CommandLinks links();
    IntelCommand itemByAddress(uint64 address);
    void destroy();
    Folder folder();
    void setFolder(Folder folder);
};

FFI 库类

class FFILibrary {
public:
    string name();
    uint64 address();
    void close();
    FFIFunction getFunction(string name, ParamType ret, ParamType param1, ...);
};

class FFIFunction {
    string name();
    uint64 address();
};

内置函数

除了脚本语言的类方法和属性外,VMProtect 还向用户提供各种基本操作函数。包括处理字符串、日期和数字的通用系统函数,以及处理 VMProtect 核心和水印的专用函数:

  • string — 字符串操作
  • table — 表操作
  • math — 数学函数
  • bit32 — 位操作
  • io — 输入/输出
  • os — 操作系统
  • vmprotect — VMProtect 专用函数

脚本事件

内置脚本语言是自动化创建受保护应用程序的有效方式。在构建受保护文件的各个阶段所需的过程和函数通过 VMProtect 核心处理的特定事件进行调用。您可以为以下 5 个事件设置自定义处理程序:

OnBeforeCompilation

function OnBeforeCompilation()
end

在创建保护对象列表时调用。在此处理程序中,您可以向项目添加新过程,或修改/删除已有的过程。

OnBeforeSaveFile

function OnBeforeSaveFile()
end

在编译期间创建的所有对象写入输出文件之前调用。在此事件处理程序中,您可以更改文件及其属性(如资源列表、导出函数列表、节名称等)。

OnBeforePackFile

function OnBeforePackFile()
end

在打包受保护文件之前调用。您可以修改即将被打包的文件。仅当启用了"打包输出文件"选项时才会调用此事件。

OnAfterSaveFile

function OnAfterSaveFile()
end

在编译期间创建的所有对象写入输出文件之后调用。事件处理程序可以向输出文件添加新数据或更改之前写入的数据。

OnAfterCompilation

function OnAfterCompilation()
end

在编译项目的所有对象之后调用。在此阶段,用户可以访问已编译的项目,并可以执行任何操作,例如添加数字签名(证书)。

水印

VMProtect 提供了向受保护文件添加所有者隐藏信息的独特功能。水印是每个用户唯一的字节数组。如果水印被嵌入到受保护文件中,您始终可以确定泄露副本的所有者(例如,如果被破解的程序被分发)并采取相应措施。

水印数据库文件存储位置:

  • Windows:%ApplicationData%/VMProtect Software/VMProtect.dat
  • macOS:/Users/Shared/VMProtect Software/VMProtect.dat

"水印"对话窗口包含两个选项卡:

  • 设置 — 管理水印:添加、删除、重命名水印。每个水印包含名称和值,可以生成随机值。值中的 "?" 符号在插入到受保护文件时会被替换为随机值。
  • 搜索 — 在可执行文件或受保护应用程序的指定进程中定位水印。支持在文件中搜索和在运行中的模块中搜索。
水印设置

重要提示:水印不适用于 Lite 版本。当搜索加壳后的可执行文件中的水印时,应该在运行中的应用程序中搜索("在模块中搜索"模式),因为水印(以及代码和数据)已被打包,只有在应用程序运行时才会解包。

常见问题

通用问题

有没有办法自动加密字符串和数据数组?

在 VMProtect 中,您可以隐藏 ANSI 常量和 Unicode 常量。代码操作的所有其他数据保持不变。我们建议将所有机密信息加密存储,并在使用前直接解密。解密器本身可以被虚拟化。

有没有办法保护从多个线程调用的过程?

VMProtect 100% 多线程兼容,此类保护没有任何特殊限制。

可以将 VMProtect 与其他保护器(打包器)一起使用吗?

在 VMProtect 处理文件后使用任何其他打包器(保护器)可能会导致受保护的应用程序无法正常工作。

我应该将 VMProtectSDK32.dll/VMProtectSDK64.dll 包含在程序的安装包中吗?

这些库仅在程序的调试阶段使用(在保护之前)。使用 VMProtect 保护应用程序后,有关使用这些 DLL 的所有信息将被完全删除,因此不需要将它们包含在发布包中。

编译器消息

"Address is used by procedure" 错误

此错误意味着同一地址的命令被两个包含在受保护对象列表中的过程使用。要解决此问题,应从受保护对象列表中排除其中一个过程。

"Minimum procedure size for compilation is 5 bytes" 错误

此错误意味着过程太小,无法被保护。要解决此问题,请从受保护对象列表中排除此过程。

"The .text section allocates space required for the new section" 错误

此错误通常在保护驱动程序时发生。这意味着文件第一个节和文件头中的服务信息之间的空闲空间太小,无法创建新节。要解决此问题,请增加驱动程序源代码中的节对齐参数值并完全重建驱动程序。