First of all, in what language do you write this? It’s true that a crack program cannot be achieved, but you can always make it harder. A naive approach to application security means that a program can be cracked in minutes. Some tips:
If you deploy a virtual machine, this is too bad. There are not many alternatives. All popular vms (java, clr, etc.) are very simple to decompile, and no obfuscator and signature are enough.
Try to separate the programming of the user interface with the base program as much as possible. This is also a great design and will make work on the cracker more difficult from the graphical interface (for example, enter the serial window) in order to track the code in which you are actually checking.
If you compile the actual native machine code, you can always install the assembly as a release (so as not to include any debugging information), with optimization as high as possible. Also, in critical parts of your application (for example, when checking software), be sure to make it a built-in function call, so that you will not get a single point of failure. And this function is called from different places in your application.
As already mentioned, packers always add another layer of protection. And although there are many reliable solutions now, you can ultimately be identified as a false-positive virus by some anti-virus programs, and all known variants (for example, UPX) already have quite frank unpackers.
There are some anti debugging tricks that you can also find. But this is a hassle for you, because at some point you will also need to debug the release application!
Keep in mind that your priority is to make the critical part of your code as possible. Clear text strings, library calls, gui elements, etc. These are all points at which an attacker can use to track critical parts of your code.