准备工程
让我们看一个非常简单的应用程序,它只包含一个表单 (Form1)、一个文本元素 (Edit1) 和一个按钮 (Button1)。该应用程序的工作原理如下:当单击 Button1 时,应用程序检查输入的密码是否正确并显示相应的消息。
使用非常简单的算法检查密码:在第一步中,我们将其转换为数字形式,然后计算除以 17 的余数。如果输入密码的数字除以 17 的余数等于 13,则表示密码正确。。Delphi 上的密码检查程序实现如下所示:
function TForm1.CheckPassword: Boolean; begin Result:=(StrToIntDef(Edit1.Text, 0) mod 17=13); end; procedure TForm1.Button1Click(Sender: TObject); begin if CheckPassword then MessageDlg('密码正确', mtInformation, [mbOK], 0) else begin MessageDlg('密码不正确', mtError, [mbOK], 0); Edit1.SetFocus; end; end;
可以通过三种方式选择要保护的程序和功能:
- 使用由编译器创建的 MAP 文件 以及程序的可执行文件。MAP 文件包含有关应用程序的所有过程和功能的名称和地址的所有必要信息。如果使用 MAP 文件,您可以通过名称选择要保护的过程和函数。使用 MAP 文件,每次重新编译工程时,VMProtect 都会自动确定过程和函数的新地址。
- 使用插入到应用程序源代码中的标记 。 标记是 VMProtect 用来确定受保护片段边界的特殊标记。 此外,VMProtect 支持具有预定义编译类型的标记。 当您只想保护函数或过程的一部分时,使用标记是有意义的。 使用标记允许您指定将进一步放置要保护的字符串常量的代码部分。
- 通过可执行文件中受保护过程的地址。与上述两种方式相比,这种方式使用起来不太方便。每次修改和重新编译应用程序时,您都必须再次指定所有地址。对于没有可用源代码的应用程序,建议使用这种类型的保护。
使用 MAP 文件来定义受保护代码的边界还有一个显着的优势。值得多回顾一下。几乎任何具有局部变量或使用堆栈来保存寄存器和/或中间计算结果的过程或函数,在编译过程或函数的开头和结尾都有相应的序言和结尾:
push ebp \ mov ebp, esp \ 序言 push 00 / push ebx / ... pop ebx \ pop ecx \ 结尾 pop ebp / ret /
由于现今的编译器的工作方式,代码标记从不包含函数的序言和尾声。即使开始和结束之间的 CheckPassword 函数的整个代码都包含在标记中。对于破解者来说,修改函数的序言就足够了,这样虚拟化的代码就永远不会被执行。对于 CheckPassword 函数,可以按如下方式完成:
mov eax, 1 ret
重要提示:
如果使用 MAP 文件为虚拟化选择代码片段,则序言和结尾也会被虚拟化,从而显着提高受保护程序的防破解能力。此外,如果一个虚拟化函数是从另一个虚拟化函数调用的,它们之间的控制转移实际上并不跳转到被调用函数的地址(在这种情况下,调用只是简单地跳转到虚拟机解释器字节码中的另一个地址)。这也加强了对程序的保护,因为破解者对 EntryPoint 入口点所做的所有修改都变得毫无用处。使用虚拟化函数时,只有在从未受保护或变异的代码片段调用受保护函数时,才会将控制权转移到虚拟化函数的 EntryPoint 入口点。