Introduction
Some years ago my now wife and I launched an IRC based trivia game site. I did the programming, (game client and matching game-aware chat client) and she did the web-site. Together we did the human related admin. That meant, among other things, selecting (or coaxing :) ) volunteer game hosts to write and run games.
IRC Networks - a matter of trust
IRC networks, like much of the internet, attract their fair share of nut-cases; people who will hide behind the 'anonymity' of an IP address and be as disruptive as possible. So it becomes necessary to invest game hosts with the power to gag, kick or ban disruptive players.
This is achieved by connecting to the IRC network and supplying the appropriate password. (Appropriate because there are differing levels of authority on most IRC networks, where the level of authority is specified by the password used). Most chat clients facilitate this by expecting the user to type a command such as /join #chatroom password
. The chat server then assigns the appropriate level of authority based upon the password supplied.
Our IRC network was a little different. Most IRC networks have many chatrooms covering various topics where each chatroom has it's own little 'clique' of hosts and it's own rules governing conduct. Our server, given that it was dedicated to gaming, had a more restricted selection of chat rooms and consistent rules of conduct applying to all chat rooms. More importantly, each host on the 'classic' IRC network knows the hosting password(s) for each chatroom in which they are a host. This is a major weakness. If the password escapes into the wild (and such an escape need not be malicious, it can be as simple as mistyping the /join
keyword) it can take a major effort of co-ordination to change the password. All legitimate users must be identified and updated. There is also almost always a lingering suspicion that password leakage was deliberate.
Avoiding the need for trust
I decided to design a model for my chat client where the host had no need to know the hosting password(s). To achieve this I needed to come up with an encryption scheme that met the following goals.
- No passwords should be saved anywhere in plain text.
- It should be difficult to reverse engineer.
- It should be time limited. In other words, the encrypted data used by the chat client should require periodic updates.
- It should be able to detect if someone set the PC clock back in time.
- Last but not least: It should be something I could code, debug and test. I'm not a cryptographer!
The code and utility I present here is based on the code I used in the game software. Note however that it's not exactly the same code and I certainly won't be presenting the same encryption keys. (The site is still live as of writing even though I'm no longer involved in it).
Abstraction
Let's now abstract away from IRC networks and take a higher level view. Expressed again, differently, these are my goals.
- Encrypt some text of interest to the application which should be obscured.
- Make it difficult to decrypt without knowledge of the specifics.
- Set a time limit including the ability to detect if the PC clock has been set backwards in time.
For the sake of convenience for myself I added a fourth requirement. The encrypted entity should be an MFC archive. This meant that the encrypted entity had to be a
CObject
derived object that implemented the
DECLARE_SERIAL
and
IMPLEMENT_SERIAL
macros. I make no apology to thee or thine for using the MFC serialisation mechanism. It allows me to easily save and reload complex data structures.
The CObject
derived object is serialised into a memory archive which is then encrypted and written to storage. Loading reverses the process: the encryped entity is loaded into a memory archive, decrypted and then serialised into an object instance you supply.
The scheme
The scheme I came up with is a triple
XOR
of the serialised block of memory against three different encryption keys. In addition the encoded entity contains two timestamps. The first timestamp is when the entire encoded entity expires. The second timestamp is the last time we decoded the entity. This implies that each time the entity is decoded we update it by rewriting the second timestamp.
The second timestamp (the burn date) is used to protect the first timestamp (the expiry date). If the PC clock time is earlier than the burn date then we know something is fishy.
Aha you say! The burn date changes each time we run the program. If I know there's a timestamp that says when we last decoded the entity then I can decode the burn date and, more importantly, rewrite it, and defeat the ability to determine that the PC clock has been set back in time.
Well, yes, you could, if you could find where the burn date is. So I added another variable. The very first value written to the encoded entity is the lower 32 bits of the value returned by a call to QueryPerformanceCounter()
. Then, after the triple XOR
of the entity against the three encryption keys I do a final XOR
of the remainder of the entity (excluding the initial 32 bits) using that initial 32 bits. Thus, after each decoding of the entity the entire entity (as written back to it's storage) changes its byte values. It's not easy to detect that the only substantial change was the burn date when the entire entity changes.
Simple but effective. If you know that the first 32 bits of the encoded entity is the first level encryption key then you can always decode the remainder of the entity back to a standard state.
Does that get you very far? Not really. You still have a block of apparently random binary values.
Aha you say! The exe file I'm cracking must contain the decryption keys. I have an implementation of the UNIX strings
utility handy, which will let me dump all the strings contained in the exe. Surely the decryption keys must be in there somewhere.
Well yes, they are. But as I've already demonstrated, I'm a devious bastard. The encryption keys are also encrypted into the exe. For this to be difficult to crack I use a random sequence of characters as a meta key
. These characters are used as a decryption key to decrypt the keys that will be used to decrypt the encrypted entity in storage (after it's been decrypted using the timestamp). Holy crypt!!!
I've provided a simple dialog based utility (MetaKeyGen) to create meta encryption keys given a meta key
and a desired key
. The result is a C code string, representing the desired key
encoded by the meta key
, which you cut and paste into your source file.
As a final safety measure (yeah I know I'm paranoid but have you ever had to rescue an IRC network taken over by hostile users? I haven't either, maybe because of this paranoia) the classes presented here store only the encrypted entity. Each time a caller requests data the data is returned after all internal buffers have been overwritten with random values and freed. I can't prevent a debugger seeing the contents of process memory but I can make it more difficult for a cracker. Paranoia carries a performance penalty but it's all done in memory so it's not that bad a penalty. The random value I use is the least significant byte of the return value from a call to rand()
. Thus, each time the program is run the temporary storage it needs is overwritten with different values.
Do note that the expiry date and burn dates referred to earlier are in fact members of the CObject
derived class and it's your objects responsiblity to determine appropriate action if they are found to be invalid.
The classes
CEncryptedData
is the class that stores the encrypted entity. The encrypted entity is simply a block of
BYTE
S that contain the text to be protected. The class provides constructors to Load, and Save functions to act on both the registry or a disk file and it also implements the
QueryPerformanceCounter()
obfuscation.
class CEncryptedData
{
friend class CDataDecrypter;
public:
CEncryptedData(LPCTSTR szFileName);
CEncryptedData(HKEY baseKey, LPCTSTR szPath, LPCTSTR szKey);
~CEncryptedData();
BYTE *Data(DWORD& dwLen);
void Attach(BYTE *pbData);
private:
void Save();
void Obfuscate();
BYTE *m_pbData;
DWORD m_dwLen;
HKEY m_baseKey;
CString m_csPath,
m_csKey;
};
The
Data()
function returns a pointer to an encrypted block of
BYTE
S that still needs to be decrypted using the triple
XOR
scheme.
CDataDecrypter
is a class that provides the core functionality of the en/de crypter.
class CDataDecrypter
{
public:
CDataDecrypter();
void Decrypt(CEncryptedData *pbData, LPCTSTR szMetaKey, CObject *pObj);
void Encrypt(CEncryptedData*& pbData, LPCTSTR szMetaKey, CObject *pObj);
private:
void Scramble(BYTE *pbSrc, BYTE *pbDest, LPCTSTR szKey, int iLen);
static BYTE key0[];
static BYTE key1[];
static BYTE key2[];
};
The
Decrypt()
function takes a pointer to the encrypted entity (after it's been de-obfuscated), a
meta key
that is used to decode the three encrypted keys, and a pointer to a serialisable object derived from
CObject
that contains your data. Debug builds of the class
assert
that the object pointer passed is in fact serialisable.
key0
, key1
and key2
are the BYTE
arrays created by MetaKeyGen
and are compiled into your exe file.
CEncryptedData
doesn't require that the storage exists at the time the class is instantiated. It does the 'right thing' by failing benignly. This is the mechanism by which you create a new instance of the storage. Call the appropriate CEncryptedData
constructor with the name of the desired storage, even though the storage does not yet exist, and then call CDataDecrypter::Encrypt
to encrypt your object and write it to the desired storage.
The classes here overwrite internal buffers before returning. However they can't overwrite your decoded object. It's up to you to manage the lifetime of your decoded object. Decode the object, use it's data, and destroy it (and overwrite memory) as soon as possible after the decoding. Your CObject
derived object should at least zero all allocated memory in it's destructor before freeing the memory.
The classes are not threadsafe. This only matters if one thread is attempting to decrypt your encrypted entity at the same moment another thread is attempting to do a save of the same entity. The Save()
function updates the first 32 bits of the encrypted entity and obfuscates it, writes it to storage and then obfuscates it a second time to reverse the effect of the first obfuscation.
Bad things to do when using this scheme
- Don't imagine that this scheme can provide copy protection. It can't. At best it can be used to sign a copy of some software with a unique ID that identifies a particular licensee. If suddenly your software escapes into the wild through warez this scheme can be used to identify the leaker. Of course this only works if you distribute a unique encrypted entity with each copy that identifies the recipient of that copy.
- It's tempting to add validation functions to the class so your code can verify a correct decryption. But if you do that then you're providing an attempting cracker with a way of validating his/her success at cracking your scheme. After all, your code is making a call into some unknown function and testing the result. It then branches to code that pops up a message box with a message that decryption failed. Crackers look for those strings in an exe and work out where they're referenced. From there it's a short step to breaking your protection scheme wide open. Within the scenario for which I developed theses classes a validation check isn't necessary. At worst what comes back is an invalid date or the wrong password.
- At first it seems to make sense to validate licensed software during application startup. A naive check would decrypt the encrypted entity via a call from
CWinApp::InitInstance()
. Bad idea! Remember that if you're trying to protect your software you are also trying to defeat crackers. It's not that difficult to locate the entry point of an exe file and single step, using a debugger, through the startup code.
- For the same reasons, it's not a great idea to try and present information from the encrypted entity in an About box. Any developer worth his salt can locate the invocation of the About box and reverse engineer from that point.
How do I get around the last two points? You could set a timer during application startup to send a validation command via PostMessage()
to your main message loop. Sometime after application startup the app validates itself and acts appropriately if validation fails. Similarly, in your About box OnInitDialog()
procedure you could post yourself a message or set a timer to update UI elements some time after the WM_INITDIALOG
message is processed. But best, perhaps, is not to validate your encrypted entity until you need the information encrypted within it.
Conclusions
Is my scheme unbreakable? Of course not! Only a fool imagines he can create something that someone else can't break. But in the five years this scheme has been in use we've never had a security break on our IRC network. This could, of course, merely be due to the fact that no one has tried. I'll never know.
What use is it outside the scenario for which I designed it? I honestly don't know. It could be used to protect FTP passwords for, for example, a blog client. Or it could be used to uniquely sign a copy of some software licensed to a specific person. Whatever, it can stand as a monument to my paranoia :) I do however use these classes in the next article I'm writing.