In this article, we’ll show a way to implement a simple software key that could be useful for protecting software components (e.g., EXE, DLL, COM, etc.) against misuse and for keeping track of installations.
We want to highlight that this is just an example. We are aware that it is quite simple by-passing the protection offered by the software key presented in this article.
However, we think that this article could be useful in allowing us to better understand the mechanisms underlain a software key.
By the way, in the following sections, we'll mention how to by-pass this protection and some ideas that could be useful to make attackers work harder.
In order to explain the background idea, we need to describe the context in which our software key could be useful.
Let’s suppose we have a software product encapsulated in an executable (EXE) with a policy fee based on installations. Note that the following notes are suitable for other types of components as well (e.g., DLL, COM, etc.). Let’s assume we would like to control how many installations our customers do. The software key we propose in this article, with the mentioned limitations, allows us to get the above intention. In particular, it allows us to grant to the customer the right to use our component (EXE) only on a certain machine. If he tries to install our executable on a different machine, the component won’t run.
How it works
Let's suppose we are the vendor of a certain component “protected_comp.exe”. In order to use our component, the customer needs to generate a “machine code” using the tool “softwarekey_customertool.exe” we provide to him. Then he needs to send this “machine code” to us. This “machine code” can be viewed as a signature of the machine in which the tool “softwarekey_customertool.exe” has been run and in which the “protected_comp.exe” will be executed.
Substantially, this signature is obtained using the MAC (Medium Access Control) address (i.e., usually an Ethernet address) as a seed. We can view the machine code generation as the application of a function
f to the MAC address:
MachineCode = f (MACaddress)
Note that this process can be quite easily encapsulated in a web page, for instance, making the console application “softwarekey_customertool.exe” an Active X.
Using this “machine code”, we are able to create a “software key” (also known as “license number”) using the tool “softwarekey_vendortool.exe”. This tool, getting as input the “machine code”, will produce a “software key” (i.e., a “licence number”) that will work only for the machine on which the “machine code” has been generated. We can view the software key generation as the application of a function
g to the machine code:
SoftwareKey = g (MachineCode)
Note: In the code provided with this article, functions
g are just simple permutations of the MAC address. In order to improve protection, more complex algorithms should be used. Moreover, it is possible to use other data than the MAC address as a seed (F.Y.I., CPU identifier, Windows serial number, etc.), but our choice seems to be easier and quite general. We will introduce some possible techniques in the following sections.
After generating the “software key”, we need to send it to the customer. On receiving the “software key”, the customer has to install it in the file “protected_comp.ini”. At this point, the customer can use our component “protected_comp.exe”. For simplicity, we save the software key in an “ini” file. It is possible to save it in the registry. Furthermore, it is possible to create a tool that automates the software key installation.
The following picture tries to illustrate the steps needed to produce and install a software key valid for a certain machine A.
Inside the code
In this section, we give a brief description of the class
CSoftwareKey whose methods will be used in “protected_comp.exe”, “softwarekey_vendortool.exe” and “softwarekey_customertool.exe”.
static RETVALUE RetrieveMACAddress(
static RETVALUE ComputeMachineCode(
const BYTE pMACaddress[MAC_DIM],
static RETVALUE ComputeSoftwareKey(
const BYTE pMachineCode[MACHINE_CODE_DIM],
static RETVALUE VerifySoftwareKey(
const char* pSoftwareKeyString,
static RETVALUE GetSoftwareKeyStringFromIniFile(
const char* pFilePath,
static RETVALUE Buffer2String(
const BYTE* pBuffer,
const unsigned int pBufferSize,
static RETVALUE String2Buffer(
const char* pString,
unsigned int* pBufferSize);
RetrieveMACAddress retrieves the MAC. For doing that, if the operating system is Windows 2000, ME or XP, we have used the API
UuidCreateSequential; otherwise, if the operating system is NT, we have used
CoCreateGuid. This is due to the fact that, in Windows XP/2000, the
UuidCreate function internally called by
CoCreateGuid, generates for security reasons an UUID that cannot be traced to the Ethernet/token ring address of the computer on which it was generated. However, as MSDN library states, in Windows XP/2000, the API
UuidCreateSequential returns a UUID that is a function of the MAC. We could have used
UuidCreateSequential for all operating systems, but unfortunately, it is not always present in Windows NT, depending on the service pack that has been installed.
Note: in the code related to NT, we didn't check the service pack version installed. This implies that this function doesn't work properly in Windows NT, if a certain service pack has been installed.
Another problem arises on computers without any network card. APIs
UuidCreateSequential will return a constant UUID. This means that the same machine code will be generated for two different machines both without a network card. A possibility to solve this problem, and in general to improve the protection offered by the software key, is to generate a machine code starting from a combination of values instead of starting from the MAC address only. This approach will be detailed later.
ComputeMachineCode generates the machine code starting from the MAC address. It is the implementation of the above introduced function
f. The created machine code is just a simple permutation of the MAC address.
ComputeSoftwareKey generates the software key using the machine code as a seed. It is the implementation of the above introduced function
g. The created software key is just a simple permutation of the machine code.
VerifySoftwareKey checks whether the software key installed in the “.ini” file is valid. It computes a software key starting from the MAC address, and checks it against the software key saved in the file “protected_comp.ini”.
GetSoftwareKeyStringFromIniFile retrieves the software key installed in the “.ini” file.
Buffer2String converts a buffer, i.e., a sequence of bytes, into a string. This is useful for producing a printable version of the machine code and the software key. In this way, they can be easily delivered, for instance, inside the text of an e-mail, without requiring the use of MIME. In our example, we translate each byte into a triplet of decimal numeric digits. For instance, the byte 0xCD will be translated in the numerical decimal digit triplet (i.e., a string) “205”.
String2Buffer converts a string in a sequence of bytes. Note that the function assumes that each byte is represented as a triplet of numerical decimal digits in the string. For instance, the numerical digit triplet “056” will be translated into the byte “0x38”.
Note: In the above methods
String2Buffer, we could have used the “base64” algorithm, but for simplicity, we have chosen the above-described method. Summarizing, the “base64” algorithm represents each three bytes of the input buffer as an output string of four printable characters of a given alphabet. In this way, the dimension of the resulting string is smaller than the string produced by our algorithm. In fact, our algorithm generates nine characters (decimal digit characters) for each three bytes. A possible improvement that would allow us to generate a smaller output string, is to convert each byte into a couple of hexadecimal digit characters. In that case, the byte 0xCD will be translated in "CD". Therefore, for each three bytes in the input stream, we could obtain six hexadecimal characters. However, since our machine code and software key are very short and this is just an example, we won't use these more powerful algorithms (although it should be quite simple to encapsulate them in the above two methods).
In the following sections, we give a brief description of the way in which the above class will be used in the executables presented in this article.
Protected component - “protected_comp.exe”
It is a simulation of the software component to be protected. It assumes that the software key, obtained by the vendor, is installed in the file “protected_comp.ini”. This simple console application will show a message box indicating if a valid software key has been installed or not.
This component uses the following methods of
VerifySoftwareKey internally uses the following methods:
Customer tool - “softwarekey_customertool.exe”
The customer uses this tool in order to generate a machine code.
This tool uses the following methods of
The usage is:
Vendor tool - “softwarekey_vendortool.exe”
The vendor uses this tool in order to generate a software key matching the given machine code. This tool uses the following methods of
The usage is:
softwarekey_vendortool -v machinecode
Hints to by-pass the software key
In this section, we give just some hints to bypass the software key protection. We can open the executable file “protected_comp.exe” with a disassembler or a debugger, with the goal of searching for the places in which the API for showing a message box is called. In particular, we look for a message box displaying the message "wrong software key" hoping to find in around it the code that verifies the installed software key.
As our example is quite trivial, and most importantly, as we have written it , we easily find the code that verifies the installed software key. In the following snippet, we show a dump of this code, with some comments, excerpted using an assembler-level debugger.
; Call of method CSoftwareKey::GetSoftwareKeyStringFromIniFile(...)
004012E0 SUB ESP,8
004012E3 LEA EAX,DWORD PTR SS:[ESP+4]
004012E7 MOV DWORD PTR SS:[ESP+4],0
004012EF PUSH EAX
004012F0 PUSH 004070B4
004012F5 CALL 004011A0
; Call of method CSoftwareKey::VerifySoftwareKey(...)
004012FA MOV EDX,DWORD PTR SS:[ESP+C]
004012FE LEA ECX,DWORD PTR SS:[ESP+B]
00401302 PUSH ECX
00401303 PUSH EDX
00401304 MOV BYTE PTR SS:[ESP+13],0
00401309 CALL 00401030
0040130E MOV AL,BYTE PTR SS:[ESP+13]
00401312 ADD ESP,10
00401315 TEST AL,AL
00401317 PUSH 0 ; Style = MB_OK|MB_APPLMODAL
; Hex pattern of the following instruction and some context is
; (....0x6A 0x00 0x75 0x1B 0x68 0xAC....).
; This information should be made accessible by the debugger.
00401319 JNZ SHORT 00401336
; Call of function MessageBoxA contained in user32.dll
0040131B PUSH 004070AC ; Title = "error"
00401320 PUSH 00407098 ; Text = "wrong software key"
00401325 PUSH 0 ; hOwner = NULL
00401327 CALL DWORD PTR DS:[<&USER32.MessageBoxA>] ; MessageBoxA
0040132D MOV EAX,1
00401332 ADD ESP,8
; Call of function MessageBoxA contained in user32.dll
00401336 PUSH 00407094 ; Title = "ok"
0040133B PUSH 00407074 ; Text = "correct software key installed"
00401340 PUSH 0 ; hOwner = NULL
00401342 CALL DWORD PTR DS:[<&USER32.MessageBoxA>] ; MessageBoxA
00401348 XOR EAX,EAX
0040134A ADD ESP,8
Having a little knowledge of assembler, we can easily understand that the instruction contained at address “00401319” verifies the software key. In fact, at address "00401336", we can see the code that shows the message box "correct software key installed". Now, we can replace this instruction (the conditional jump instruction "
JNZ SHORT 00401336") with an unconditional jump “
JMP SHORT 00401336”. Doing that, we can easily bypass the protection.
In more detail, in order to remove the software key protection, a possible way is:
Note that, in this simple example, it has been enough changing just one byte in the executable to bypass the software key protection. In the following section, we’ll show some possible approaches to make this work harder. A ready to use copy of “protected_comp_patched.exe” is available for download in “softwarekey_demo.zip”.
Hints to improve the software key
In this section, we point up just a few ideas that could be useful to improve the protection offered by the software key presented in this article. The aim is just to provide some useful notes. All the approaches presented here can be bypassed. They can only augment the effort needed by an attacker.
These approaches can be divided in two groups detailed in the following paragraphs:
- Code obfuscation and tampering detection: these techniques try either to complicate the work of determining the "right place" in the code to modify or to detect code tampering;
- Enforce functions
g: these techniques try to complicate functions
g. The aim here is to make harder the work of determining the software key starting from a well-known machine code.
Code obfuscation and tampering detection
In order to bypass the protection offered by our software key, in the previous paragraph we have modified the code. The suggestions we provide in this paragraph try either to complicate the work of determining the "right place" in the code to modify or to detect code tampering.
- There are coding techniques that allow making the assembly (both data and code) as tricky as possible to read and manipulate. It could be useful to forget every good programming paradigm, as object oriented programming, data encapsulation, and so on. Rowly speaking, we can say that the more our code looks like "spaghetti code", the best it is. The purpose of doing that is to force our compiler to produce a tricky machine code as well. The issue has been studied in detail and has been formalized under the name of "code obfuscation";
- It is a good idea to keep the code that verifies the software key as away as possible from the code that signals to the user the presence of a wrong key. For instance, in a previous paragraph, we have easily recognized the byte to tamper as the conditional instruction was next to the one showing the message box indicating that a wrong key was installed. Further, we can add several checks instead of just one;
- Instead of using a message box for signaling that a wrong software key has been installed, in that case, we can force some bugs in the code. In this way, the program won't run correctly and it will be more difficult for the user to understand the point of code to tamper;
- An approach useful to discover tampering of code or data is to compute and verify checksums for data and code that could be tampered;
- In order to disorientate an attacker, we can modify some parts of the code dynamically (F.Y.I., inserting software key verifications).
A combination of these techniques (and others) can make the work of individuating and modifying the assembly code that manages the software key more difficult.
Enforce functions f and g
As said before, in the code provided with this article, functions
g are just simple permutations. In order to improve protection, more complex algorithms should be used. The suggestions we provide in this paragraph try to complicate these functions.
A first trivial solution, valid both for
g, is to use a bigger size (i.e., number of bytes) for the machine code and the software key.
In order to enforce function
f, we can generate a machine code starting from a combination of the subsequent values instead of using only the MAC address:
- Processor ID (usually accessible using a specific machine code instruction);
- Windows serial number (accessible in the registry, usually saved in the key with path “\\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ProductID”);
- Hard disk volume ID (in some cases, accessible using API
GetVolumeInformation or specific interrupt address);
- Other “quite” stable system information.
This means making the machine code computation a function of the above-mentioned parameters:
MachineCode = f (MACaddress, ProcessorID, WindowsSerialNumber, …)
The use of this information could be a heavy task as their retrieval could be different with different hardware devices or operating systems.
In order to enforce the function
g, we can use public key cryptography. A possible solution is drawn in the following. In that case, for each customer's machine, the vendor generates a pair of keys <"PrivateKey", "PublicKey">. Then, the vendor sends to the customer a package containing "softwarekey_customertool.exe", "protected_comp.exe", "protected_comp.ini" and "PublicKey". As usual, customer generates its machine code using "softwarekey_customertool.exe" and sends it to the vendor.
MachineCode = f (MACaddress,...)
On receiving the machine code, the vendor generates the software key encrypting the machine code using "PrivateKey". Now we have:
SoftwareKey = g (MachineCode) = Encrypt(PrivateKey, MachineCode)
Encrypt is a function that encrypts
MachineCode using the key
PrivateKey. At this point, the vendor sends the software key to the customer, who installs it in "protected_comp.ini". For checking the installed software key, "protected_comp.exe" will perform some code described by the following pseudocode.
sk = CSoftwareKey::GetSoftwareKeyFromIniFile(...);
mc = Decrypt(PublikKey, sk);
mac = CSoftwareKey::RetrieveMACAddress(...);
mc1 = CSoftwareKey::ComputeMachineCode(mac);
if (mc != mc1)
"wrong software key"
In this way, we have made the task of discovering the function
g as complex as the public cryptography can. Note that the strength of encryption is related to the difficulty of discovering the key, which in turn depends on both the cipher suite used and the length of the key.
Also note that with the suggested approach, "PrivateKey" is not necessary on customer side.
Note: Let's drop some observations about the ways a customer can follow to bypass the protection offered by our software key. These thoughts should help us in understanding how it is important to use complex functions for
g. Let's suppose the customer has obtained a valid software key for a machine A and wants to use "protected_comp.exe" on a machine B for which he doesn't dispose of a valid software key. In our scenario, customer has the tool for generating machine codes (substantially, he has an implementation of the function
On one side, if he knew the function
g, he could (obviously) generate software keys by himself for whatever machine he wants. Therefore, it is straightforward the need of hiding
On the other side, if he knew the function
f and its parameters
pn , he would tamper them on a machine B (if it is possible) in such a way that the machine code for B, computed using
f is the same as the one generated for a machine A (for which he has a valid software key). For instance, if we use only the MAC address as parameter for
f, moving the network adapter on a machine B should be enough to make the software key obtained for A valid for machine B as well. Therefore, it is important to chose
pn in such a way we can get "a more reliable as possible" signature of the machine. Considering what is mentioned above, it seems that using a complex function for
f could be useful at least for hiding the set of parameters
pn used in the computation of the machine code.
Alternatives to software key
In the following section, we point up just a few well-known alternative approaches to the use of a software key.
Remote code execution
The idea is to execute the code of the application on vendor side as much as possible. The major drawbacks are:
- it requires an active connection. Note that in some situations this could be a problem;
- it needs to assure integrity and confidentiality of the data transferred between customer and vendor site;
- it requires strong server on the vendor site in which executes part of the application.
The idea is to keep some part of the code needed to execute the component inside the hardware key (in the worst case, only the software key). The major drawback is that, since they are not for free, they are not be suitable for simple or "personal" components.
- For the sake of simplicity, we have used simple permutations for functions
g. These algorithms should be made more complex. For the same reason, we have used short dimensions (in bytes) for machine code and software key. In a real scenario, they should be augmented. As said before, a better solution can be obtained using cryptography. Further, function
f depends only on MAC address. It could be a good idea to make it depending on other values such as processor ID, Windows serial number, etc.;
- For the sake of clearness, we haven’t used any of the above mentioned techniques for improving software key protection;
- We have not tested the method “
CSoftwareKey::RetrieveMACAddress” in machines with more than one network cards (usually, Ethernet cards) or with particular operating system configurations (e.g., clustering). In such cases, if some problems arise, the hidden API “
GetAdaptersInfo” contained in “IPHLPAPI.DLL” could be useful;
- As stated before, in the code related to NT of
CSoftwareKey::RetrieveMACAddress method, we've not checked the service pack version installed. This implies that this function won't work properly in Windows NT, if a certain service pack has been installed;
- In the attached code, a lot of error situations are there; also where they are envisioned are not management.
The download “softwarekey.zip” contains source code and executables ready to use. Unzipping this file, you can find the following directories:
- "protected_comp" contains source code for the component “protected_comp.exe”;
- "softwarekey_customertool" contains source code for the tool "softwarekey_customertool.exe”;
- "softwarekey_vendortool" contains source code for the tool "softwarekey_vendortool.exe”;
- "softwarekey_demo” contains executables ready to use.
- November 2004 – First version.