Introduction
CrackMes are executable programs designed to teach reverse engineering concepts. In this walkthrough, we will analyze [A]dvanced Keygenme, a challenge that employs various anti-analysis techniques, including anti-debugging and custom encoding schemes.
Challenge Details
- Name: [A]dvanced Keygenme
- Author: sd333221 (haxx0r)
- Release Date: February 24, 2007
- Difficulty: Medium/Advanced
Analysis
Download the Sample
You can download the sample from:
Note: Try solving this challenge yourself before reading the solution. Variable names used in this analysis are chosen for clarity.
Program Interface
Static Analysis
Using PeStudio, we can identify two important characteristics:
- Author signature:
haxx0r - Thread Local Storage (TLS): This is a common anti-debugging technique used to hinder reverse engineering efforts.
Dynamic Analysis
Let’s open the program in IDA Pro to analyze its behavior:
In the tls-callback, it calls GetTickCount and computes the value of HIGH_pp from the result:

In GetRand_CPUID_getTick, it retrieves the CPU vendor ID using cpuid (EAX=0) and uses HIGH_pp to compute a new value:

After that, it converts the result to a string using _itoa.
Function Analysis
The modify_str function contains two parts: mix_two_list and modify_cpuid_time:
In mix_two_list, it builds a list of bytes (renamed as alpha) from 0x0 to 0xFF and modifies it using another list of 13337....133371:
def mix_two_list(): nums = b"13337" alpha = [n for n in range(256)] edi = 0 for n in range(256): edi = (edi + alpha[n] + nums[n % 5]) % 256 tmp = alpha[n] alpha[n] = alpha[edi] alpha[edi] = tmp return alphaIn modify_cpuid_time, it uses the value converted from integer to string and modifies this value using the alpha list:
def modify_cpuid_time(from_cpu_and_time): alpha = mix_two_list() index = 0 edx = 0 res = [] for x in range(len(from_cpu_and_time)): index += 1 index = index & 0xFF edx = (alpha[index] + edx) & 0xFF tmp = alpha[index] alpha[index] = alpha[edx] alpha[edx] = tmp eax = (alpha[index] + tmp) & 0x800000FF if from_cpu_and_time[x] ^ alpha[eax] == 0: return res res.append(from_cpu_and_time[x] ^ alpha[eax]) return resThe modify_str function is used to encrypt/decrypt the computed value before using it.
At the end of tls-callback, it uses two methods to detect debuggers, named chckDbgr and anotherCheck. chckDbgr uses guard pages to detect debuggers, while anotherCheck uses FindWindow() and CreateFile() functions. Both tricks are explained in The Ultimate Anti-Reversing Reference by Peter Ferrie (must read!).
The CrackMe uses a custom base64 alphabet: /+9876543210zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA. If it detects a debugger, it calls the ifDebugg__ function, which reverses the alphabet:
In WinMain, it sets the result of our old friend GetRand_CPUID_getTick as the username, which is exactly the same value that we have in encrypted form.
CheckSerial Function
Let’s analyze the CheckSerial function (we are trying to write a keygen, so don’t patch it):
In the CheckSerial function, it decrypts the value obtained from tls-callback. The decrypted version of the text is the same as the username, both derived from GetRand_CPUID_getTick. After decryption, it encodes the result with base64 using a custom alphabet, encrypts the result again, and encodes it once more. The final result is saved in the esi register.
The SerialComp function receives our input as a parameter and returns a value that should match the one in esi.
Python Implementation of SerialComp
Here’s the Python reverse implementation of SerialComp:
def get_serial_number(enc2): serial__number = [0 for n in range(0x400)] i = 0 j = 0 done_1 = False done_2 = False done_3 = False
while j < len(enc2): if done_1 or done_2 or done_3: break for a in my_base64chars: if done_2: break if done_1: break for b in my_base64chars: if done_2: done_1 = True break if enc2[j] == ((my_base64chars.find(a) << 2) & 0xFF) | ((my_base64chars.find(b) >> 4) & 0xFF): serial__number[i] = a if j + 1 >= len(enc2): serial__number[i + 1] = b done_1 = True break
for c in my_base64chars: if enc2[j + 1] == ((my_base64chars.find(b) << 4) & 0xF0) | ((my_base64chars.find(c) >> 2) & 0xFF): serial__number[i + 1] = b
if j + 2 >= len(enc2): serial__number[i + 2] = c done_2 = True break
for d in my_base64chars: if enc2[j + 2] == ((my_base64chars.find(c) << 6) & 0xC0) | (my_base64chars.find(d) & 0xFF): serial__number[i + 2] = c serial__number[i + 3] = d
i += 4 j += 3
return bytearray(serial__number).strip(b'\x00').decode()Conclusion
We have everything we need to write a keygen:
import base64
my_base64chars = b"/+9876543210zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA"STANDARD_ALPHABET = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"ENCODE_TRANS = bytes.maketrans(STANDARD_ALPHABET, my_base64chars)DECODE_TRANS = bytes.maketrans(my_base64chars, STANDARD_ALPHABET)
def encode(clear): return base64.b64encode(clear).translate(ENCODE_TRANS)
def decode(encoded): return base64.b64decode(encoded).translate(DECODE_TRANS)
# Additional functions here...
if __name__ == "__main__": username = input("Username (Locked): ") from_CPUID = username.encode() enc1 = encode(bytearray(from_CPUID)) sm = modify_cpuid_time(enc1) encoded2 = encode(bytearray(sm)) serial_number = get_serial_number(encoded2) print("Serial: {}".format(serial_number))