If you have read my entry on Copy protection schemes and have published one or more pieces of shareware, you may be feeling pretty good about yourself. After all, one of the conclusions of that piece is that it’s feasible to protect shareware quite well against pirating.
However, as many people have found, just because something is possible does not necessarily mean that it’s easy to do, and implementing excellent protection against pirates is indeed very hard. Moving a step in the right direction and taking your software from laughably easy to somewhat tricky is straightforward, though: you just need to know some of what the hacker looks for and then take it away.
Most protection mechanism fail because the author has no idea what a hacker looks for in order to circumvent a copy protection mechanism. The result is often a design that looks solid to the author but has one or more of the following characteristics, all of which make life too easy for the hacker:
- A message dialog is displayed in the unregistered version to remind users of the need to register,
- To register, the user has to type in something. After he hits Enter, a message dialog is displayed to say “Thank you for registering” or “Invalid registration code”,
- Strings like “Registered to…” and “Unregistered version” are embedded in your source code,
- The registration “key” is a simple function of the user name, and the function is included in the code,
- Memory used for licence key validation is not cleared.
All of these can be summarized simply: you give away information that makes life easy for the hacker. Let’s look at each in turn to see why they are bad, and what to do about it.
Security by obscurity
First, humour me in an important digression. You may have heard of the term Security by obscurity and know that it – at least in the context of cryptography – os a Very Bad Thing that normally leads to algorithms that are easy to break. The gist of it is that if you have to keep your algorithms secure in order to protect your encryption mechanism, then it’s almost certainly no good. Good ones like the newly chosen AES (Advanced Encryption Standard) and the now obsolete DES standard are both public, have undergone lots of scrutine, and are/were considered very, very secure.
In this case, it is important to realize that the problem we are faced with in protecting software is substantively different. In cryptography, we need to hide information from people without the right key; people that have the right key can see the entire message. In software protection, people with the right key (in the form of a registration code, for example) must be able to access all of the functions of the software but should not necessarily know the ins and outs of how the registration process works.
In protecting software, unfortunately we have to rely on security by obscurity to some extent, rather than avoid it.
How the hacker operates
If the code is unobscured in one of th 4 ways listed earlier, the hacker has an easy time circumventing your protection mechanism. Here is how – and beware, we’re about to get a bit technical.
A debugger is a program that can “load” another program before it starts, look at it in machine code and control its execution in various ways. We’ll look at a few examples in a moment.
If you clicked the links, you may have noticed that both debuggers mentioned are “text mode”, i.e. they look like old-fashioned DOS programs. In spite of the way they look, they are actually fully fledged 32-bit Windows programs – and text mode is very desirable for a debugger because it then does not interfere with the graphics display of the application being debugged.
Stopping execution with a breakpoint
The most common message box is implemented by Windows, by an API called MessageBox, and your program may use this same API. In fact, most do because it’s an easy way to display a simply Yes/No/Cancel dialog.
What the hacker can do in this case is simple: use the debugger to find the address of this API and place a breakpoint on it. A breakpoint on a particular address causes the program to stop when it reaches the address – or, in other words, when the program would otherwise have displayed a message box, it is stopped and control is returned to the debugger.
When this happens, it is a simple matter for the hacker to use the stack to backtrace and see where the program decided to call the MessageBox API. In the debugger, the machine code will typically look something like this:
mov byte ptr ,1 call 400312 jz @1 push 405611 push 451122 call MessageBox mov byte ptr ,0 @1:
The details of this is obscure to the uninitiated, but it’s actually quite simple. First, a value at some address is set to 1. Then, a call is made to some address, presumably one that checks whether the program is registered. If it is, control is transferred to the
@1 label – if not, the MessageBox is shown and the byte value that was first is set to 1 is now set to 0.
With this knowledge, the hacker can do several things. The obvious thing in this example is the value that is either 0 or 1. It seems a good guess that this is a Boolean variable that in the source code may be called “Registered”, and always just setting it to 1 may be enough to “register” the software.
Alternatively, the hacker could investigate the routine that checks if the software is registered to figure out what it does and make a key for himself, or at perhaps even a key generator.
This little example should demonstrate why it is a bad idea to show a message dialog when the software is unregistered, whether it’s during startup or after the user types in an invalid username/key combination. In my limited experience, more than half of all shareware does this and is easy to hack as a result.
Plain text strings and Data points
If you avoid the easy MessageBox hack, chances are that some vital strings can be found unencrypted in memory. While the hacker can no longer place a breakpoint to stop execution when you display an error message, he can use the debugger to search the program for strings that relate to registration, such as “Unregistered version”, which you might display in the title bar or about box.
String data is normally found in the “data segment” of a program and are clustered together. Once the hacker identifies the location of a couple of strings, he probably has most of the ones used in the program and can use a data point to do his deed. A data point can be used to stop the program when a particular memory address is accessed – a function that can be very useful indeed.
The hacker placed a data point on the address of a couple of likely-looking strings, runs the program and shows the About box, for example. The program will now stop and control return to the debugger – using almost the same technique as above, he can now figure out how you determine whether the software is registered or not, and so can circumvent it.
The morale is that you need to encrypt or at least lightly scramble these strings so they cannot easily be searched for!
Validating the key
Most shareware uses an algorithm that calculates a valid “serial number” from the user name. This means that when the user enters his name and his registration key, it is easy to calculate the correct key for the name given and thus check if the entered one is correct. The code might look something like this:
Name = GetUserInput Key = GetUserInput if CalculateKey(Name) = Key then Registered = True
Unfortunately, this is about the worst thing you can do: it makes the hacker’s life nice and easy. When the hacker finds this piece of code, he doesn’t need to figure out how
CalculateKey works: he can read the correct key for the user name he entered from memory. This means that he does not need to do anything other than run the software in the debugger, enter a user name and an invalid key, and the software will tell him what the key should be for it to be accepted!
In my experience, almost all shareware (and some commercial software such as Perforce) uses a mechanism like the one above, to their own detriment.
Without resorting to strong public/private key cryptography, one-way hash functions and encrypted code (which is hard to implement in a way that is actually secure), it is not possible to solve this problem in a secure way. However, it is easy to implement something that at least is better than the standard very weak scheme in that it does not reveal a “correct” key when it is run.
Clear the memory!
A common mistake is to not clear the memory used when validating the key or when calculating string values to display (for example, when displaying an Unregistered message in the about box).
The effect of this is that the hacker can stop the program during execution, or even wait until after it has terminated, and perform a successful string search even if you have carefully encoded all of your strings. Note that in most languages that have a
String type, setting the string to an empty value will not clear it. For example, in Delphi:
var Key: ShortString; begin Key := GetUserInput; // Perform validation of key ... // The following does not clear the key string from memory Key := ''; // The following clears it: fillchar(Key, SizeOf(Key), 0); ...
In one piece of software I looked at, the user-entered key and the correct key (calculated by the program) could be found right next to each other in memory after the program terminated. This is a good example of a case where a few extra lines of source code would have made the software much harder to hack.
(I sent the company an email about this 2 years ago, which they after some arguing accepted gave a correct picture of their protection mechanism. They have still not taken the advice though, and the software will still gladly calculate a key for any hacker who can be bothered to run it…)
Designing and implementing a mechanism for shareware that makes it impossible for hackers to create a fake key, or “crack” the software, is very hard. However, making the job of hacking the software less than a trivial exercise is relatively simple, and is something every shareware author should do.
If you feel a key registration and validation mechanism is appropriate for your software, the least you can do is make sure the effort you put into it is not wasted by following the advice given here.