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. 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, "

\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!