步骤2.5:将代码锁定到序列号
破解程序最常见的方法之一是定位检查序列号的位置以及紧随其后的条件跳转。如果序列号正确,则程序的执行以一种方式进行,如果不正确,则以另一种方式执行。破解者找到了这个跳转并将其替换为 “正确” 方式的跳转。让我们使用这种技术 “破解” 我们的测试程序。当然,直接在源代码中。让我们 “关闭” 我们的条件跳转:
char *serial = read_serial("serial.txt"); int res = VMProtectSetSerialNumber(serial); delete [] serial; if (false && res) {
现在,我们的程序接受任何序列号并正常工作。当然,如果文件是用 VMProtect 保护的,即使是经验丰富的破解者也会像我们一样花费几个月的时间来定位和修改条件跳转。并且考虑到程序在不同条件下多次检查序列号,即使是这样简单的检查也是相当安全的。但让我们更进一步。
将代码锁定到序列号
重要提示! VMProtect 的演示版对处理函数的数量有限制:只处理一个函数。所以如果您使用演示版,您应该只在工程中包含 foo() 函数,否则 VMProtect 的演示版可以选择 main() 函数并且锁定到一个序列号将不起作用。
VMProtect 的授权系统允许您将一个或多个函数的代码锁定在一个序列号上,这样,如果没有提供正确的序列号,它们就不能工作。函数的主体被虚拟化,然后被加密,只能用正确的序列号解密。这意味着,即使破解者发现并修复了序列号检查中的条件跳转,锁定在序列号上的函数仍然无法工作。让我们来试试这个。在 "函数 "部分选择 foo() 函数,并在右侧面板将 "锁定到序列号 "选项改为 "是"。
然后,保护应用程序。因为,我们已经 “破解” 了它,将任意文本放入 serial.txt 文件并运行应用程序。控制台中出现以下文本:
C:\test>dummy_app.vmp.exe 序列号正确,调用 foo()
这意味着,破解者 “修复” 了条件跳转,程序以 “正确” 的方式运行。但是当调用 foo() 函数时,程序会显示一条消息:
由于我们将 foo() 函数锁定到序列号,而破解者却没有序列号,因此试图解密该函数的代码就导致了故障,无法继续执行程序。按下 “确定” 时,程序关闭,控制台中永远不会显示 “完成” 消息。
什么应该锁定到序列号?
将仅应在程序的注册版本中运行的函数锁定到序列号是有意义的。由于锁定需要虚拟化,因此您应该考虑到一些性能损失。例如,如果文本编辑器不允许在演示版中保存结果,您可以将保存文档函数锁定为序列号。如果这个函数在运行过程中调用了其他函数,也不必锁定它们,因为没有主函数它们就没有任何用处。
您还应该记住,在没有序列号的情况下调用锁定函数会导致程序关闭,而没有机会保存工作结果。这就是为什么您应该彻底测试应用程序以确保它不会在试用模式下调用此类函数。在上面的示例中,文本编辑器必须在演示模式下禁用 “保存” 命令,并且也不会对 Ctrl+S 快捷键做出反应。当然,它也不应该要求在退出时保存文档。如果您不注意这一点,用户可能会对您的 “错误” 演示版感到失望。
锁定序列号和无效的序列号
当调用 VMProtectSetSerialNumber() 函数时,授权模块会检查传递给该函数的序列号。仅当序列号在检查时绝对正确时才会执行加密的代码片段 – 未列入黑名单、具有正确的硬件标识符、未过期等。在这种情况下,所有加密过程都会执行,直到应用程序关闭或再次调用 VMProtectSetSerialNumber() 函数。
在程序执行过程中可能会 “触发” 一些限制:例如,程序的运行时间可能到期或序列号到期日期到来。在这种情况下,授权模块仍然会加密并执行锁定到序列号的函数。之所以如此,是因为受保护的应用程序很难检测到这些限制触发的时刻并相应地更改行为(冻结相应的菜单项等)。如果授权模块突然停止执行锁定到序列号的代码片段,这很可能导致应用程序故障。这就是为什么在设置序列号时做出决定,并选择相应的执行模式。