With the growing popularity of CTF (capture the flag) competitions, and the excellent performance of Polish teams like Dragon Sector in this area, I thought it would be interesting to demonstrate the construction of a simple CrackMe, using some creative techniques which make it difficult to crack and analyse.
If you have ever been curious about reverse engineering, entered a CTF competition, or wanted to create your own CrackMe and drive other contestants crazy, this article is for you.
Familiar with the territory? Stop reading, and try to capture the flag!
If you already have some reversing skills, and would like to try your hand at the CrackMe I am about to describe in detail, put this article aside, get the compiled executable, and try to find the flag! Once you've made your best attempt at it, you can come back to the article and compare what you discovered against the full story. This CrackMe has a medium level of difficulty.
To run the CrackMe executable you may need the Visual C++ Redistributable Packages for Visual Studio 2013.
Got the CrackMe? Don't cheat by reading any further – get cracking! ;)
Otherwise, if you want to learn how to build your own CrackMe, I invite you to keep reading...
What's a CrackMe?
You may be familiar with sites like HackThisSite.org, where you are challenged to find weaknesses or security holes in web pages or other software systems to obtain a hidden message.
A CrackMe is simply a computer program which is created specifically so that people can try to bypass its security mechanisms and obtain the correct password or serial number. CrackMes were popular well before CTF contests became popular. In this way, coders could use creative approaches to software protection to compete against crackers who would attempt to break these protections.
The site crackmes.de which is over 20 years old (!) and has almost 3000 files in its archive, hosts both CrackMes and tutorials about them. The site is still active and new CrackMes are added all the time.
What are the different types of CrackMe?
If you want to get specific, CrackMe programs are traditionally divided into a few categories, depending on the author's intended goal.
These include:
- CrackMe – the goal is to generate a serial number, licence file, or username/password combination. Modifying the file is against the rules, and traditionally, CrackMes are not protected against modifications to the binary file.
- KeygenMe – as the name suggests, the goal is to create a key generator. This differs from a regular CrackMe in that interesting cryptographic algorithms generally need to be used, and knowledge about cryptography and encryption algorithms is necessary to create a keygen. Often, values of the BIGNUM data type are used, as well as algorithms such as ECC, RSA or DSA, which can make it necessary to apply brute-force to break known keys.
- ReverseMe – the most complicated form of CrackMe. The goal may be to, e.g. force the program to display a message, like “Thank you for registering.” ReverseMes go further than just using sophisticated cryptographic algorithms to protect the application from analysis; they employ many techniques to make it difficult to modify the application file, because this is the most common method used to reach the desired goal (e.g. changing the behaviour of certain functions in the program).
- UnpackMe – a slightly different form of CrackMe, where you are given a file which is compressed, protected, or obfuscated with a custom-made or commercial exe-packer or exe-protector. The aim is to unpack the file, in other words, to recover the original form of the executable. Most often this involves rebuilding the import table, recovering the original (compiled) code, and rebuilding the executable file structure, so the file can run without a protection layer. In the case of “homebrew” protection methods, this can be a fun and interesting challenge, but if commercial-grade protections are used, this kind of reversing can be pretty hardcore.
The goal of our CrackMe
In CTF competitions, the goal of a CrackMe is usually to obtain a hidden “flag”. The goal of our CrackMe will be to guess and enter the right access keys, after which the flag will be revealed. Each key will be entered in a different way, to provide varied entertainment for those who will try to figure out the keys.
Each key will have a simple means of verification, so as not to make the exercise too complicated.
Operating system and programming language
Our CrackMe will be created in Windows 10 (but will run without a problem on older Windows versions such as Windows 7). We will use the C++ language compiled to native x86 code. We will use a few interesting features of the Windows API, which are perhaps not well known. The UNICODE encoding is utilised in this CrackMe, which may cause a bit of difficulty with several sorts of reversing tools.
TLS Callbacks
First up, we'll use an obscure mechanism called a TLS Callback. It's connected to the functioning of the Thread Local Storage mechanism, which allows different threads of an application to refer to their own copies of global variables. For instance, in C++ we can declare a variable as thread-local with a special attribute:
__declspec(thread) int value;
In this case, each thread of the application will possess its own copy of this variable. Changes to this variable by one thread will not be observed by other threads.
TLS Callbacks are one part of the TLS mechanism. They are a bit like the entry points of DLLs – namely DllMain()
. Windows calls functions which are declared as TLS Callbacks to inform the application of newly loaded libraries or newly created threads being attached to the process. This is much like how DllMain() is repeatedly called, with one small difference: when this mechanism is used by an EXE, the code will be executed before the application's entry point.
This difference is key, because in theory it allows us to secretly run some code which is unlikely to be noticed without using the right debugger features.
TLS Callbacks have existed since Windows XP, although their operation has varied slightly over different Windows versions (some event types are not supported on all versions). They are used by certain software protection systems to set up some anti-debug features before the actual application code is started.
In our CrackMe we will take advantage of TLS Callbacks to check for the presence of a debugger.
///////////////////////////////////////////////////////////////////////////////
//
// The TLS callback mechanism allows code to be executed prior to
// the launch of a program's entry point; this is one place where
// we can hide the initialisation of a couple of things
//
// details about implementing this in C++:
// https://stackoverflow.com/questions/14538159/about-tls-callback-in-windows
//
///////////////////////////////////////////////////////////////////////////////
void NTAPI TlsCallback(PVOID DllHandle, DWORD dwReason, PVOID)
{
// ensure the reason for calling the callback is that the application
// process has been attached, i.e. the application has been launched
// exactly the same as in the DllMain() in DLL libraries
if (dwReason != DLL_PROCESS_ATTACH)
{
return;
}
// check the heap flags - in the case of a debugged application
// they are different to an application started normally
// in case a debugger is detected, stop the application
// at this point
__asm
{
mov eax, dword ptr fs:[30h]
test dword ptr [eax + 68h], HEAP_REALLOC_IN_PLACE_ONLY or HEAP_TAIL_CHECKING_ENABLED or HEAP_FREE_CHECKING_ENABLED
je _no_debugger
_sleep_well_my_angel:
push 1000000
call Sleep
jmp _sleep_well_my_angel
_no_debugger:
}
}
If we start the CrackMe with a debugger like OllyDbg v2 without any plugins hiding its presence, this TLS Callback code will detect the debugger and block the application from loading any further. It will look like the application has hung.
Key verification
The different key checking procedures will operate in separate threads. Multi-threaded operation always poses an obstacle in debugging an application – sometimes a large obstacle. Each key-verification function will in turn create a thread for the next function.
//
// table of addresses of successive key verification functions
// the pointers in this table will be encrypted, and decrypted
// only at the moment when they are ready to be executed
//
// we will store the address adjusted 100 bytes forward
// this will cause a hiccup in every disassembler, since this will
// be treated as a function pointer
// for further entertainment we can add extra dummy entries to this table
//
#define ENCRYPTED_PTR(x, y) reinterpret_cast<PVOID>(reinterpret_cast<DWORD>(&x) + y)
PVOID lpKeyProc[KEYS_COUNT] = {
ENCRYPTED_PTR(Key0, 100),
ENCRYPTED_PTR(Key1, 100),
ENCRYPTED_PTR(Key2, 100),
ENCRYPTED_PTR(Key3, 100),
ENCRYPTED_PTR(Key4, 100),
ENCRYPTED_PTR(Key5, 100),
};
SpeedStart('C');
//
// create 5 EVENT objects, which will serve as markers
// of the validity of the access keys
// also, encrypt the pointers to the functions which
// check the validity of the keys
//
for (int i = 0; i < KEYS_COUNT; i++)
{
hEvents[i] = CreateEvent(nullptr, TRUE, FALSE, nullptr);
lpKeyProc[i] = static_cast<LPTHREAD_START_ROUTINE>(EncodePointer(reinterpret_cast<PVOID>(reinterpret_cast<DWORD>(lpKeyProc[i]) - 100)));
}
//
// fire up the first thread which will pretend to verify the serial number
// it will start successive threads which will run successive procedures
// to verify access keys
//
hThreads[0] = CreateThread(nullptr, 0, static_cast<LPTHREAD_START_ROUTINE>(DecodePointer(lpKeyProc[0])), lpKeyProc, 0, &dwThreadIds[0]);
SpeedEnd('C');
// wait for all threads to be initialised (in case someone tries to skip something)
// the threads are started in a chain reaction, so their handles will not all
// be generated yet, and so we can't use WaitForMultipleObjects()
for (int i = 0; i < _countof(hThreads); i++)
{
while (hThreads[i] == nullptr)
{
OutputDebugString(_T("What's up, Doc?"));
}
}
// wait for all threads to finish working
WaitForMultipleObjects(_countof(hThreads), hThreads, TRUE, INFINITE);
After verifying an access key, we will use the event system to record which keys were correctly entered.
Key 0 – fake key
How are we going to input our first key? CrackMes often prompt the user for a serial number or password directly, so let's start with this idea. In our CrackMe, we will ask for a password, carefully check its validity and record the result, only to ignore it in the final verification phase.
This will be the one key that the CrackMe will ask to be entered in the console, so it will be the most obvious. Yet this key will simply be a red herring. It won't matter whether it is correct or incorrect.
In order to draw an attacker into our little trick, we will use a very common (but outdated) hash technique based on the MD5 algorithm. We will compare the hashed key with the hardcoded hash of the word “fake”.
The hash for this short word can be easily found in tables of precalculated hashes for dictionary words and letter combinations (known as rainbow tables) or by using a password cracker like John the Ripper or hashcat.
///////////////////////////////////////////////////////////////////////////////
//
// Fake key - to waste an attacker's time ;)
//
///////////////////////////////////////////////////////////////////////////////
DWORD WINAPI Key0(LPTHREAD_START_ROUTINE lpKeyProc[])
{
// start up the next thread (chain reaction style)
hThreads[1] = CreateThread(nullptr, 0, static_cast<LPTHREAD_START_ROUTINE>(DecodePointer(lpKeyProc[1])), lpKeyProc, 0, &dwThreadIds[1]);
_tprintf(_T("Enter the secret key: "));
// read the password as an ANSI string (so that it's not too difficult
// for an attacker to find the password e.g. using rainbow tables.
// We'll do them a favour by choosing ANSI over UNICODE)
gets_s(szPassword, sizeof(szPassword));
// start measuring time here so that gets_s() doesn't
// artificially extend the time
SpeedStart('0');
if (strlen(szPassword) > 0)
{
// encrypted with https://www.stringencrypt.com (v1.1.0) [C/C++]
// szFakeHash = "144C9DEFAC04969C7BFAD8EFAA8EA194"
unsigned char szFakeHash[33];
szFakeHash[2] = 0xA8; szFakeHash[0] = 0xCD; szFakeHash[10] = 0xBC; szFakeHash[30] = 0x28;
szFakeHash[16] = 0x0A; szFakeHash[13] = 0x0D; szFakeHash[29] = 0x76; szFakeHash[14] = 0x30;
szFakeHash[12] = 0x01; szFakeHash[32] = 0xEC; szFakeHash[3] = 0xCE; szFakeHash[31] = 0x3B;
szFakeHash[15] = 0x48; szFakeHash[1] = 0x33; szFakeHash[25] = 0x27; szFakeHash[27] = 0xD9;
szFakeHash[9] = 0x5F; szFakeHash[17] = 0x93; szFakeHash[24] = 0x8B; szFakeHash[7] = 0x9C;
szFakeHash[26] = 0x5A; szFakeHash[23] = 0x24; szFakeHash[18] = 0x66; szFakeHash[19] = 0x06;
szFakeHash[5] = 0xC1; szFakeHash[28] = 0x69; szFakeHash[21] = 0xF8; szFakeHash[20] = 0x9D;
szFakeHash[4] = 0xFC; szFakeHash[22] = 0x44; szFakeHash[6] = 0xFF; szFakeHash[11] = 0x42;
szFakeHash[8] = 0x83;
for (unsigned int GpjcO = 0, qeVjl; GpjcO < 33; GpjcO++)
{
qeVjl = szFakeHash[GpjcO];
qeVjl = (((qeVjl & 0xFF) >> 2) | (qeVjl << 6)) & 0xFF;
qeVjl += GpjcO;
qeVjl = (((qeVjl & 0xFF) >> 5) | (qeVjl << 3)) & 0xFF;
qeVjl ^= 0xF7;
qeVjl = ~qeVjl;
qeVjl ^= GpjcO;
qeVjl--;
qeVjl = ~qeVjl;
qeVjl -= 0xDF;
qeVjl = ((qeVjl << 6) | ((qeVjl & 0xFF) >> 2)) & 0xFF;
qeVjl--;
qeVjl ^= 0x76;
qeVjl += 0xF0;
qeVjl -= GpjcO;
qeVjl ^= GpjcO;
qeVjl = ~qeVjl;
qeVjl += GpjcO;
qeVjl = (((qeVjl & 0xFF) >> 2) | (qeVjl << 6)) & 0xFF;
qeVjl += 0x2C;
qeVjl = ((qeVjl << 4) | ((qeVjl & 0xFF) >> 4)) & 0xFF;
qeVjl -= 0xFF;
qeVjl = ((qeVjl << 1) | ((qeVjl & 0xFF) >> 7)) & 0xFF;
qeVjl = ~qeVjl;
qeVjl++;
qeVjl = (((qeVjl & 0xFF) >> 4) | (qeVjl << 4)) & 0xFF;
qeVjl -= 0xEF;
qeVjl = (((qeVjl & 0xFF) >> 2) | (qeVjl << 6)) & 0xFF;
qeVjl -= 0xF7;
qeVjl = (((qeVjl & 0xFF) >> 3) | (qeVjl << 5)) & 0xFF;
qeVjl -= 0x48;
qeVjl = ~qeVjl;
qeVjl -= GpjcO;
qeVjl ^= GpjcO;
qeVjl += 0xE6;
qeVjl ^= 0xB4;
qeVjl -= 0x9D;
qeVjl = ~qeVjl;
qeVjl--;
qeVjl ^= GpjcO;
qeVjl += 0x17;
qeVjl ^= 0x55;
qeVjl += GpjcO;
qeVjl += 0xB3;
qeVjl = (((qeVjl & 0xFF) >> 3) | (qeVjl << 5)) & 0xFF;
qeVjl -= 0xCE;
qeVjl = ~qeVjl;
qeVjl += 0x9B;
qeVjl ^= 0x71;
qeVjl--;
qeVjl = ((qeVjl << 7) | ((qeVjl & 0xFF) >> 1)) & 0xFF;
szFakeHash[GpjcO] = qeVjl;
}
// compare with the hash of the word "fake" (https://www.pelock.com/products/hash-calculator)
if (CheckMD5(szPassword, strlen(szPassword), reinterpret_cast<char *>(szFakeHash)) == TRUE)
{
SetEvent(hEvents[0]);
}
}
SpeedEnd('0');
return 0;
}
It's worth remembering that prolonging the duration of code analysis is one of the best methods to discourage potential attackers. Although “red herrings” are easy to bypass in theory, we should not discount them for this reason, because in practice they can be very effective. An attacker is likely to become bored or frustrated when he or she discovers that a whole lot of work was done for nothing, and this can only work in our favour.
By the way, protection systems for games are often not intended to be “unbreakable” but are designed for the sole purpose of extending the time during which the game publisher can sell many copies of the game following the game's release. In such cases, the “breaking” of the protection and headlines in the media proclaiming this are illusory – crackers and pirates proclaim victory, filled with satisfaction, singing tunes of “everything can be broken”, patting each other on the back, not even realising who has really won.
Key 1 – environment variables
Our next key will be gathered from an environment variable, which must be set up e.g. by using the environment variable editor. To make this trickier we will use a standard Windows environment variable – “PROCESSOR_ARCHITECTURE”, but with a minor typo (“S” instead of “SS”), that is, “PROCESOR_ARCHITECTURE”.
The correct value for this variable will be the one that is seen on 64-bit systems, except with a space at the end, namely “AMD64 ”.
If someone lists the environment variables, e.g. by issuing the “set” command, they will certainly see this value, but they may not notice the trailing space. In Windows 10, environment variables can be changed with the simple command:
set PROCESOR_ARCHITECTURE=AMD64 ← space at the end
or through the environment variable editor, which can be launched by pressing Win+R and typing “sysdm.cpl”.
Key 2 – hidden ADS key
The NTFS filesystem allows programs to save additional “streams” in files. This feature is called Alternate Data Streams and can be used to hide additional data in files that is invisible in Windows Explorer. This feature is used by web browsers to keep track of where downloaded files originate from. When a file is downloaded from the Internet, the additional stream “:ZoneIdentifier” is attached to the file, recording the zone from which the file was downloaded. This is why you get those annoying warning messages when you try to run a program that was downloaded from the internet.
The ADS mechanism is also used by malware to hide data “in plain sight”. Still, it's an interesting method to use in a CrackMe and we'll make use of it to hide our next key.
We will search for it in the CrackMe file itself; in the stream “CrackMeZ3S.exe:Z3S.txt” to be specific.
///////////////////////////////////////////////////////////////////////////////
//
// Key 2 - checking ADS
//
///////////////////////////////////////////////////////////////////////////////
DWORD WINAPI Key2(LPTHREAD_START_ROUTINE lpKeyProc[])
{
SpeedStart('2');
// start up the next thread (chain reaction style)
hThreads[3] = CreateThread(nullptr, 0, static_cast<LPTHREAD_START_ROUTINE>(DecodePointer(lpKeyProc[3])), lpKeyProc, 0, &dwThreadIds[3]);
TCHAR wszPath[512] = { 0 };
// get the path to the CrackMe executable
GetModuleFileName(GetModuleHandle(nullptr), wszPath, sizeof(wszPath));
// add the ADS suffix
_tcscat_s(wszPath, _countof(wszPath), _T(":Z3S.txt"));
// open the stream "CrackMeZ3S.exe:Z3S.txt"
HANDLE hFile = CreateFile(wszPath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
SpeedEnd('2');
// check if open was successful
if (hFile == INVALID_HANDLE_VALUE)
{
return 0;
}
// find the file size
DWORD dwFileSize = GetFileSize(hFile, nullptr);
// ensure that it will fit in the buffer
if (dwFileSize > sizeof(szADS))
{
CloseHandle(hFile);
return 0;
}
DWORD dwReadBytes = 0;
// read the contents of the secret stream
if (ReadFile(hFile, &szADS, dwFileSize, &dwReadBytes, nullptr) == FALSE || dwReadBytes != dwFileSize)
{
CloseHandle(hFile);
return 0;
}
CloseHandle(hFile);
char szTemp[sizeof(szADS)];
strcpy_s(szTemp, _countof(szTemp), szADS);
// reverse the string
_strrev(szTemp);
if (strcmp(szTemp, "\n\r70.6102") == 0)
{
// set the flag which indicates the ADS key was verified
SetEvent(hEvents[2]);
}
return 0;
}
The required value of the key can be set in a command window, by running the command:
echo 2016.07> CrackMeZ3S.exe:Z3S.txt
and to check if the stream was successfully created, run the command:
dir /r
It is important to note that there should be no space before the “>”. It is easy to add a space by mistake and this will result in an invalid key.
Key 3 – Clipboard
The next key will be obtained from the Windows clipboard. The CrackMe will require a specific text value to be stored there. And I'm not talking about a bank account number! ;)
///////////////////////////////////////////////////////////////////////////////
//
// Key 3 - checking the clipboard
//
///////////////////////////////////////////////////////////////////////////////
DWORD WINAPI Key3(LPTHREAD_START_ROUTINE lpKeyProc[])
{
SpeedStart('3');
// start up the next thread (chain reaction style)
hThreads[4] = CreateThread(nullptr, 0, static_cast<LPTHREAD_START_ROUTINE>(DecodePointer(lpKeyProc[4])), lpKeyProc, 0, &dwThreadIds[4]);
// open the clipboard
if (OpenClipboard(nullptr) == TRUE)
{
// get a handle to the data in CF_TEXT format
HANDLE hData = GetClipboardData(CF_TEXT);
// was any data obtained?
if (hData != nullptr)
{
// lock memory
char *pszText = static_cast<char *>(GlobalLock(hData));
if (pszText != nullptr)
{
// hehe ;)
if (strcmp(pszText, "Boom Boom - Lip Lock - Song") == 0)
{
// copy the clipboard contents to a global variable
strcpy_s(szClipboard, sizeof(szClipboard), pszText);
// set the flag for this key
SetEvent(hEvents[3]);
}
}
GlobalUnlock(hData);
CloseClipboard();
}
}
SpeedEnd('3');
return 0;
}
In this case, setting up the key is pretty self-explanatory. Simply Ctrl-C and you're done!
Key 4 – checking compatibility mode
Windows allows applications to be run in “compatibility mode” that behaves like an older version of Windows. This is for applications which do not operate properly under new versions of the operating system. We will use this setting as our next key, checking if the application is run in Windows Vista mode.
///////////////////////////////////////////////////////////////////////////////
//
// Key 4 - checking compatibility mode
//
///////////////////////////////////////////////////////////////////////////////
DWORD WINAPI Key4(LPTHREAD_START_ROUTINE lpKeyProc[])
{
SpeedStart('4');
// start up the next thread (chain reaction style)
hThreads[5] = CreateThread(nullptr, 0, static_cast<LPTHREAD_START_ROUTINE>(DecodePointer(lpKeyProc[5])), lpKeyProc, 0, &dwThreadIds[5]);
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
// the GetVersionEx() function has been deprecated,
// but for our CrackMe it'll do fine
#pragma warning(disable : 4996)
GetVersionEx(&osvi);
// the numbering will match Windows Vista and Windows Server 2008
// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexa
if (osvi.dwMajorVersion == 6 && osvi.dwMinorVersion == 0)
{
// set the flag indicating the compatibility mode is set correctly
SetEvent(hEvents[4]);
}
SpeedEnd('4');
return 0;
}
This check is pretty inconspicuous since it looks like a simple check identifying the Windows version. In order for this key to be accepted, one can either change the compatibility mode of “CrackMeZ3S.exe” in the file properties, or run the program on Windows Vista (does anyone still use it?).
If anyone wants to attack this CrackMe on something older than Windows Vista, they will need to either patch the code manually, or set up a hook for the GetVersionEx()
function and emulate the expected values of the Windows version numbers, so that they will indicate Windows Vista.
Key 5 – intercepting Ctrl-C
In our CrackMe we'll set up a handler function for the Ctrl-C console event, which normally terminates console applications. We will detect whether the user entered the Ctrl-C combination in the course of execution of the CrackMe.
///////////////////////////////////////////////////////////////////////////////
//
// handler for the Ctrl-C shortcut
//
///////////////////////////////////////////////////////////////////////////////
BOOL CtrlHandler(DWORD fdwCtrlType)
{
switch (fdwCtrlType)
{
case CTRL_C_EVENT:
// set the flag which indicates the user pressed Ctrl-C
SetEvent(hEvents[5]);
return TRUE;
}
return FALSE;
}
///////////////////////////////////////////////////////////////////////////////
//
// Key 5 - check whether the user has pressed Ctrl-C
//
///////////////////////////////////////////////////////////////////////////////
DWORD WINAPI Key5(LPTHREAD_START_ROUTINE lpKeyProc[])
{
SpeedStart('5');
// set up Ctrl-C handler
SetConsoleCtrlHandler(reinterpret_cast<PHANDLER_ROUTINE>(CtrlHandler), TRUE);
SpeedEnd('5');
return 0;
}
Pressing Ctrl-C will both set the flag of this access key, and will cause the CrackMe program to finish up.
All the keys are set up – what next?
If all the key verification threads terminate and all the keys have been detected, and the user presses Ctrl-C, the “flag” that the attacker is hoping to capture will be built from individual letters of the keys.
///////////////////////////////////////////////////////////////////////////////
//
// Verifies the correctness of all the keys, and generates
// a flag from individual letters of the keys
//
// The correct flag:
//
// "PELock v2.0"
// 01234567890
//
///////////////////////////////////////////////////////////////////////////////
DWORD WINAPI Check(DWORD Param)
{
SpeedStart('C');
// Key 0 - fake key
if (WaitForSingleObject(hEvents[0], 1) == WAIT_OBJECT_0)
{
// misleading writes - the characters in this password
// will not be used (we're writing them past the end
// of the buffer)
wszFlag[16] = TCHAR(szPassword[4]);
wszFlag[12] = TCHAR(szPassword[1]);
#ifdef _DEBUG
_tprintf(_T("[i] key 0 - OK\n"));
#endif
}
// Key 1 - environment variables
if (WaitForSingleObject(hEvents[1], 1) == WAIT_OBJECT_0)
{
// "PELock[ ]v2.0" - "AMD64[ ]"
wszFlag[6] = wszEnvrionmentVariable[5];
#ifdef _DEBUG
_tprintf(_T("[i] key 1 - OK\n"));
#endif
}
// Key 2 - ADS
if (WaitForSingleObject(hEvents[2], 1) == WAIT_OBJECT_0)
{
// "PELock v[2].[0]" - "[2][0]16.07"
wszFlag[8] = TCHAR(szADS[0]);
wszFlag[10] = TCHAR(szADS[1]);
wszFlag[9] = TCHAR(szADS[4]);
#ifdef _DEBUG
_tprintf(_T("[i] key 2 - OK\n"));
#endif
}
// Key 3 - clipboard contents
if (WaitForSingleObject(hEvents[3], 1) == WAIT_OBJECT_0)
{
// "Boom Boom - Lip Lock - Song"
wszFlag[4] = TCHAR(szClipboard[18]);
wszFlag[3] = TCHAR(szClipboard[17]);
wszFlag[2] = TCHAR(szClipboard[16]);
wszFlag[5] = TCHAR(szClipboard[19]);
#ifdef _DEBUG
_tprintf(_T("[i] key 3 - OK\n"));
#endif
}
// Key 4 - pressing Ctrl-C
if (WaitForSingleObject(hEvents[4], 1) == WAIT_OBJECT_0)
{
// missing letter
wszFlag[7] = TCHAR('v');
#ifdef _DEBUG
_tprintf(_T("[i] key 4 - OK\n"));
#endif
}
// Key 5 - system version matching Windows Vista
if (WaitForSingleObject(hEvents[5], 1) == WAIT_OBJECT_0)
{
// letter 'P' = 0x4A + 6
wszFlag[0] = TCHAR(0x4A + osvi.dwMajorVersion);
// letter 'E' = 0x45 - 0
wszFlag[1] = TCHAR(0x45 - osvi.dwMinorVersion);
#ifdef _DEBUG
_tprintf(_T("[i] key 5 - OK\n"));
#endif
}
SpeedEnd('C');
return 0;
}
Before displaying a victory message containing the flag, we will check it by verifying its cryptographic hash with an additional “salt”:
//
// calculate MD5 from the flag string and salt
// (in order to thwart brute-force attacks)
// the point of this is to guard against situations
// where somebody bypasses some of the defences
// (e.g. by manually setting up the EVENTs)
//
TCHAR wszFlagSalty[128];
_stprintf_s(wszFlagSalty, _T("#flag4poprawna %s \n123458s3cr3t _+=-=-="), wszFlag);
// calculate the hash from a TCHAR string; the result is an ANSI string
BOOL bValidFlag = CheckMD5(wszFlagSalty, _tcslen(wszFlagSalty) * sizeof(TCHAR), "4ED28DA4AAE4F2D58BF52EB0FE09F40B");
SpeedEnd('V');
if (bValidFlag == TRUE)
{
This is done to ensure that the supplied keys were valid and that, for instance, the code was not modified in a debugger to skip earlier sections and simply reach this code fragment.
Antidebugging
Could you really call our program a proper CrackMe without employing any defence against debugging? Our CrackMe won't be lacking in this department. We could use one of the popular methods of detecting debuggers based on WinAPI functions like, e.g., IsDebuggerPresent()
, but their popularity and the widespread knowledge about them means we'd be defeated before we start! Besides, I have seen IsDebuggerPresent() so many times that I just want to cry whenever I see it.
Detecting when our program is run in a debugger
We're going to add some code to our CrackMe which detects popular tools used to analyse software as it runs, namely debuggers. Debuggers allow compiled applications to be traced without access to their source code. They display the code of compiled applications in the form of assembly instructions, allowing these instructions to be stepped through one by one. Debuggers also allow stopping the application when it reaches a specified instruction (known as a breakpoint) or when a particular system function is called, for example when the application will want to display a window with the message “Your key is incorrect” using, say, the MessageBox()
function.
We will take advantage of the simple fact that when a program is being debugged, no matter which debugger, it runs significantly slower, since the debugging mechanism slows down the execution of all instructions.
Where does this slowness come from? Take a look at a standard debugger loop based on WinAPI functions, and you'll see how much is going on! Additionally, the user of a debugger slows things down much further, seeing as he or she will execute a few instructions, check a few register values, look at some documentation, and in this way create delays of seconds instead of microseconds.
We will obtain the time taken to execute designated sections of code, and assuming the CrackMe is not being run in a PC emulator on a Commodore 64 or Atari then there's no chance that executing a handful of instructions would take anywhere close to 5 seconds, but someone tracing the code in a debugger will easily spend much more time doing so.
///////////////////////////////////////////////////////////////////////////////
//
// gets the start time - this function MUST be inline to prevent
// someone simply patching the function in one place
//
///////////////////////////////////////////////////////////////////////////////
void __forceinline SpeedStart(int iSpeedStructIndex)
{
QueryPerformanceFrequency(&Speed[iSpeedStructIndex].Frequency);
QueryPerformanceCounter(&Speed[iSpeedStructIndex].StartingTime);
}
///////////////////////////////////////////////////////////////////////////////
//
// gets the end time and checks whether execution time
// exceeds the specified limit
//
///////////////////////////////////////////////////////////////////////////////
void __forceinline SpeedEnd(int iSpeedStructIndex, int iMaxTimeInSeconds = 5)
{
QueryPerformanceCounter(&Speed[iSpeedStructIndex].EndingTime);
Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart = Speed[iSpeedStructIndex].EndingTime.QuadPart - Speed[iSpeedStructIndex].StartingTime.QuadPart;
//Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart *= 1000000;
Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart /= Speed[iSpeedStructIndex].Frequency.QuadPart;
// check whether the time limit was exceeded
if (Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart > iMaxTimeInSeconds)
{
#ifdef _DEBUG
_tprintf(_T("[!] the limit of %i seconds was exceeded for index %c, execution time %llu"), iMaxTimeInSeconds, iSpeedStructIndex, Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart);
#endif
// in case of the time limit being exceeded, no error is
// displayed, but we will corrupt the internal structure of
// the CrackMe, which will cause the CrackMe to
// malfunction or simply hang at some point
// randomly decide whether to corrupt something or not
#define LOTTO_CRASH ((rand() & 6) == 0)
// decide whether to erase a thread handle
if (LOTTO_CRASH) hThreads[rand() % _countof(hThreads)] = nullptr;
// decide whether to erase an event handle
if (LOTTO_CRASH) hEvents[rand() % _countof(hEvents)] = reinterpret_cast<HANDLE>(rand());
// decide whether to reset an event (the indicator of a valid access key)
if (LOTTO_CRASH) ResetEvent(hEvents[rand() % _countof(hEvents)]);
// randomly fill text buffers
if (LOTTO_CRASH) memset(wszEnvrionmentVariable, _countof(wszEnvrionmentVariable) * sizeof(TCHAR), rand());
if (LOTTO_CRASH) memset(szADS, sizeof(szADS), rand());
if (LOTTO_CRASH) memset(szClipboard, sizeof(szClipboard), rand());
if (LOTTO_CRASH) memset(szPassword, sizeof(szPassword), rand());
if (LOTTO_CRASH) memset(wszFlag, _countof(wszFlag) * sizeof(TCHAR), rand());
// evil asm trick ;), corrupt the stack pointer
// this is guaranteed to cause the application to crash
if (LOTTO_CRASH) __asm inc esp
}
}
If we detect these long execution times, we won't display any messages about it to the user. That would be the worst thing we could do, as it would give the attacker a clear indicator of where the problem lies. Instead, we will randomly corrupt internal data buffers and individual EVENTs which indicate validated keys. Because of this, even if the correct access keys are supplied, the correct flag will not be generated if the CrackMe is run in a debugger.
This type of protection can be bypassed by employing a debugger plugin or creating a hook for the functions which determine the time, which can give the application falsified timing results.
Compiler and linker options
Despite the fact that the CrackMe is written in C++, and not in assembly language, we can further spice up this challenge by using appropriate compiler and linker options. In our CrackMe, we will apply address-space layout randomisation (ASLR), which will result in our executable file being relocated by default, and each time it is launched, Windows will load it to a different base address in memory.
Attackers can forget about placing breakpoints at fixed virtual addresses in their debugger! Every time the program is launched, the code will be in a different memory region, and function addresses obtained through disassembly will be useless. In other words, it will be a bit of a pain in the rear.
A smart attacker could however remove the relocation information and cause the EXE image to always be loaded to the same base address. For this reason we will set the base address to 0. This is rarely done, however in the case of ASLR Microsoft recommends setting the base address to 0. Strangely, their compiler does not implement this by default.
How would someone bypass even this protection to make their life easier, not only in the case of this CrackMe, but in the analysis of other applications? First the EXE file would have to be relocated to any valid base address, for instance the default address 0x400000, and then the relocation information, or the ALSR flag in the PE (Portable Executable) file header would need to be removed.
Conclusion
As you can see, creative methods for key verification are plentiful; many of them hide in little-used features of Windows or obsolete WinAPI functions. They can successfully be employed as elements in all types of CrackMe. Why don't you share some of your own ideas? Leave a comment outlining interesting methods you'd like to use or have seen in other CrackMes.
The password to the sources is “CrackMeZ3S”.