Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C++
Article

A simple software key useful to protect software components

Rate me:
Please Sign up or sign in to vote.
4.92/5 (59 votes)
29 Nov 200418 min read 359.9K   9.3K   306   62
This article shows a way to implement a base software key that could be useful for protecting software components.

Introduction

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.

Background

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 f and 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.

Figure 1 - Flow diagram for our software key

Inside the code

Core class

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”.

class CSoftwareKey
{
public:

    static RETVALUE RetrieveMACAddress(
            BYTE            pMACaddress[MAC_DIM]);

    static RETVALUE ComputeMachineCode(
            const BYTE      pMACaddress[MAC_DIM], 
            BYTE            pMachineCode[MACHINE_CODE_DIM]); 

    static RETVALUE ComputeSoftwareKey(
            const BYTE      pMachineCode[MACHINE_CODE_DIM], 
            BYTE            pSoftwareKey[SOFTWAREKEY_DIM]); 

    static RETVALUE VerifySoftwareKey(
            const char*      pSoftwareKeyString, 
            bool*            pIsValid);

    static RETVALUE GetSoftwareKeyStringFromIniFile(
            const char*       pFilePath, 
            char**            pSoftwareKeyString);

    static RETVALUE Buffer2String(
            const BYTE*        pBuffer, 
            const unsigned int pBufferSize, 
            char**             pString);

    static RETVALUE String2Buffer(
            const char*        pString,
            BYTE**             pBuffer, 
            unsigned int*      pBufferSize);
};

The method 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 UuidCreate and 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.

The method 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.

The method 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.

The method 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”.

The method GetSoftwareKeyStringFromIniFile retrieves the software key installed in the “.ini” file.

The method 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”.

The method 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 Buffer2String and 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 CSoftwareKey:

  • GetSoftwareKeyStringFromIniFile;
  • VerifySoftwareKey.

The method VerifySoftwareKey internally uses the following methods:

  • RetrieveMACAddress;
  • ComputeMachineCode;
  • ComputeSoftwareKey;
  • Buffer2String.

Customer tool - “softwarekey_customertool.exe”

The customer uses this tool in order to generate a machine code.

This tool uses the following methods of CSoftwareKey:

  • RetrieveMACAddress;
  • ComputeMachineCode;
  • Buffer2String.

The usage is:

softwarekey_customertool    -g

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 CSoftwareKey:

  • String2Buffer;
  • ComputeSoftwareKey;
  • Buffer2String.

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.

ASM
; 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
00401335  RETN

; 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
0040134D  RETN

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:

  • Using an assembler-level debugger, we determine the hex pattern (i.e., sequence of bytes) representing the instruction we would like to change. In our case, the hex pattern belonging to "JNZ SHORT" is “0x75”. It is useful to consider the bytes around as well. So, we consider the pattern “0x6A 0x00 0x75 0x1B 0x68 0xAC”.

    As we’ll see later, this will help us in searching in the executable file the "right" bytes to change. It is important that the tool we are using allows us to view the hex dump of the executable file corresponding to every assembler instruction;

  • Now, we can open the file “protected_comp.exe” with a binary editor (F.Y.I., MS Visual Studio) as a binary file;
  • After that, we search for the binary path “0x6A 0x00 0x75 0x1B 0x68 0xAC” (note that the byte “0x75” correspond to the JNZ SHORT instruction, while the remaining, useful for simplifying the research, are the bytes around this instruction …);
  • Then we can replace the byte “0x75” with “0xEB” (note that “0xEB” is the numerical value for JMP SHORT) and can save this tampered file as “protected_comp_patched.exe”;
  • Now, running this executable with a wrong software key installed in file "protected_comp.ini", we’ll see a message box showing “correct software key installed” instead of the expected one showing "wrong software key".

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 f and g: these techniques try to complicate functions f and 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 f and 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 f and 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)

where 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); // this is the function "f"

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 f and 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 f).

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 g.

On the other side, if he knew the function f and its parameters p1,...,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 p1,...,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 p1,...,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.

Hardware key

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.

Limitations

  • For the sake of simplicity, we have used simple permutations for functions f and 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

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.

History

  • November 2004 – First version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Italy Italy
I'm graduated in computer science and I'm working as a software analyst and programmer in the field of medical information technology.

Comments and Discussions

 
QuestionAwesome! Pin
Member 119546574-Sep-15 15:50
Member 119546574-Sep-15 15:50 
GeneralMy vote of 5 Pin
Rupesh Baikar17-Oct-12 9:39
Rupesh Baikar17-Oct-12 9:39 
GeneralMy vote of 5 Pin
apandey214-Sep-12 20:29
apandey214-Sep-12 20:29 
Good one
QuestionVery Urgent Help Pin
Rohit Sinha5418-Sep-09 1:11
Rohit Sinha5418-Sep-09 1:11 
GeneralProtect DLL Pin
tuanpm26-Aug-07 23:30
tuanpm26-Aug-07 23:30 
GeneralProblem with Windows Vista Business Pin
mrtrantuan™13-Jul-07 18:49
mrtrantuan™13-Jul-07 18:49 
GeneralRe: Try PELock instead Pin
Bartosz Wójcik19-Dec-07 4:29
Bartosz Wójcik19-Dec-07 4:29 
GeneralMemory leaks... Pin
JKJKJK22-Dec-05 16:11
JKJKJK22-Dec-05 16:11 
GeneralAnother way to protect your program Pin
-asm-26-Jul-05 1:17
suss-asm-26-Jul-05 1:17 
GeneralBiometrics... Pin
M i s t e r L i s t e r20-Jan-05 3:57
M i s t e r L i s t e r20-Jan-05 3:57 
QuestionHow to avoid IF (a = b) ... Pin
burek1237-Jan-05 6:56
burek1237-Jan-05 6:56 
QuestionWhat about ASProtect ? Pin
Defenestration5-Jan-05 9:34
Defenestration5-Jan-05 9:34 
AnswerRe: What about ASProtect ? Pin
Bartosz Wójcik19-Dec-07 6:36
Bartosz Wójcik19-Dec-07 6:36 
Generalmy five cents Pin
fafasoft31-Dec-04 22:41
fafasoft31-Dec-04 22:41 
GeneralInternet validation of license keys works Pin
Jon Person30-Dec-04 21:37
Jon Person30-Dec-04 21:37 
GeneralRe: Internet validation of license keys works Pin
Ganti427022-Apr-09 18:44
Ganti427022-Apr-09 18:44 
GeneralBad news, fellow programmers, C++ protection will be cracked in one evening. No matter what. ;-) PinPopular
Krzysztof Wojdon29-Dec-04 23:32
Krzysztof Wojdon29-Dec-04 23:32 
GeneralRe: Bad news, fellow programmers, C++ protection will be cracked in one evening. No matter what. ;-) Pin
Manuele Sicuteri30-Dec-04 2:37
Manuele Sicuteri30-Dec-04 2:37 
GeneralRe: Bad news, fellow programmers, C++ protection will be cracked in one evening. No matter what. ;-) Pin
Krzysztof Wojdon30-Dec-04 8:50
Krzysztof Wojdon30-Dec-04 8:50 
GeneralRe: Bad news, fellow programmers, C++ protection will be cracked in one evening. No matter what. ;-) Pin
Anonymous31-Dec-04 6:22
Anonymous31-Dec-04 6:22 
GeneralRe: Bad news, fellow programmers, C++ protection will be cracked in one evening. No matter what. ;-) Pin
Marek Grzenkowicz30-Dec-04 4:18
Marek Grzenkowicz30-Dec-04 4:18 
GeneralRe: Bad news, fellow programmers, C++ protection will be cracked in one evening. No matter what. ;-) Pin
Tiger11-Jan-05 10:21
professionalTiger11-Jan-05 10:21 
GeneralRe: Bad news, fellow programmers, C++ protection will be cracked in one evening. No matter what. ;-) Pin
pocjoc30-Jan-05 23:04
pocjoc30-Jan-05 23:04 
GeneralRe: Bad news, fellow programmers, C++ protection will be cracked in one evening. No matter what. ;-) Pin
Krzysztof Wojdon31-Jan-05 11:31
Krzysztof Wojdon31-Jan-05 11:31 
GeneralRe: Bad news, fellow programmers, C++ protection will be cracked in one evening. No matter what. ;-) Pin
pocjoc1-Feb-05 0:10
pocjoc1-Feb-05 0:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.