VMProtect is a reliable tool for protecting application code from analysis and cracking, but maximum effectiveness can only be achieved if the built-in protection mechanisms are implemented correctly, without common mistakes that could compromise the entire protection system. Let’s review the key elements of building strong protection for your application.
Registration procedure
A common mistake many developers make when designing their application registration procedure is placing the entire registration key check into a separate function that returns an easily understandable value:
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;
With this approach, an intruder does not even need to understand the key verification algorithm. They can simply modify the code at the beginning of the check procedure so that it always returns a valid registration result:
function CheckRegistration(const RegNumber: String): Boolean;
begin
Result:=True;
exit;
...
end;
A much more effective approach is to integrate the registration key validation directly into the main application logic, so that the key verification algorithm cannot be separated from the calling procedure. We also recommend “blending” the application logic with the registration check procedure so that the program fails if the verification is bypassed. For the example above, this can be implemented as follows:
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;
If the CheckRegistration function is implemented in this way, an intruder will have to analyze the registration key verification code in detail in order to bypass it. If the application is protected with VMProtect, we recommend virtualizing both the CheckRegistration function and the TForm1.Button1Click procedure. To make cracking even more difficult, you can enable the “Ultra” protection mode to combine code mutation with subsequent virtualization.
Checking registration keys
Another critical mistake developers make is implementing registration key validation incorrectly. Often, the entered key is simply compared with the correct value. A cracker can easily identify the correct key value by tracing the arguments passed to the string comparison function:
var ValidRegNumber: String;
...
function CheckRegistration(const RegNumber: String): Boolean;
begin
if RegNumber=ValidRegNumber then
Result:=True
else
Result:=False;
end;
To avoid this situation, we recommend comparing key hashes instead of the actual key values. A hash function is irreversible, so a cracker cannot retrieve the original key value from the hash. As a result, they must spend significantly more time analyzing the program because multiple code fragments need to be examined, not just the registration check procedure:
var
HashOfValidRegNumber: Longint;
...
// Example of using Peter Weinberger's PJW hashing algorithm
function HashPJW(const Value: String): Longint;
var I:Integer;
G:Longint;
begin
Result:=0;
for I:=1 to Length(Value) do
begin
Result:=(Result shl 4)+Ord(Value[I]);
G:=Result and $F0000000;
if G<>0 then
Result:=(Result xor (G shr 24)) xor G;
end;
end;
function CheckRegistration(const RegNumber: String): Boolean;
begin
if HashPJW(RegNumber)=HashOfValidRegNumber then
Result:=True
else
Result:=False;
end;
...
initialization
HashOfValidRegNumber:=HashPJW(ValidRegNumber);
end.
When protecting the application with VMProtect, both the HashPJW and CheckRegistration functions should also be protected to make reverse engineering more difficult.
Saving check results
Even developers who spend a significant amount of time implementing registration procedures often fail to properly protect the result of the registration process itself. The example below uses a global variable to store and control the application’s registration state before invoking the serial number verification procedure. For an intruder, locating a global variable is extremely easy — they simply compare the data segments BEFORE and AFTER registration. Incidentally, the popular ArtMoney program uses the same principle.
var IsRegistered: Boolean;
...
procedure TForm1.Button1Click(Sender: TObject);
begin
...
if not IsRegistered then
IsRegistered:=CheckRegistration(RegNumber);
if not IsRegistered then
exit;
...
end;
To avoid this issue, we recommend storing all registration-related verification results in dynamically allocated memory. In this case, scanning data segments for modified memory blocks BEFORE and AFTER registration becomes ineffective. Here is a simple example demonstrating how to store the result in dynamically allocated memory:
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);
These are some of the simplest ways to utilize built-in protection mechanisms. Real-world implementations of registration procedures, registration key verification, and result storage are limited only by the developer’s creativity. However, understanding these common mistakes is essential in order to avoid them while designing your own protection system.