How to write a CrackMe for a CTF competition

CrackMeZ3S

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.

Download CrackMeZ3S.zip

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.

crackmes.de website

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.

Mr Burns

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.

ADS Zone Identifier

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.

Setting Alternate Data Stream on NTFS

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?).

Windows compatibility mode

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.

Dawson crying

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.

Compiler options

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

Download CrackMeZ3S.zip

About the Author

— author is interested in western philosophy, has a black belt in yoga, spends his time between watching Futurama and South Park on God knows what, apart from that he's an advocate of closed-source software and a staunch activist for high-gluten diet.