Sitemap
Martin Atanasov

Martin Atanasov

Aspiring Junior SOC Analyst | Bridging my knowledge gaps one blog post at a time.

My First Crackme: Reverse-Engineering Writeup

10 min read4 days ago

--

I’m new to reverse engineering. My goal is malware reverse engineering, but I figured I should learn to read assembly on something small and forgiving before pointing a debugger at anything that bites back. So I started with crackmes.

This is the first one I’ve ever solved, and I’m writing it up while the confusion is still fresh. I got stuck in plenty of places, and I’ll show those too, because the stuck parts are where the learning happened.

What is a crackme, and what is a keygen?

A crackme is a small program written on purpose to be reversed. It’s a legal, safe puzzle: there’s some kind of check (a password, a serial number), and your job is to get past it or work out how it works.

A keygenme is a crackme with a specific goal: you don’t just get past the check, you write a keygen, a small program that generates valid serials for any input. Instead of beating one lock, you work out how the lock was built and make keys for it.

This one, by zart https://crackmes.one/crackme/5ab77f5633c5d40ad448c277, asks for a name and a serial. Type the right serial for your name and it says you’re correct. Otherwise it says you failed.

The idea behind the solution

The key insight is that the program doesn’t store the correct serial anywhere. It calculates it from your name every time, then compares the result to what you typed.

That gives you two ways in:

  1. Catch it at the comparison. The program has to compute the right answer before it can compare, so the value will be sitting in a register. Stop the program there and read it. No understanding needed.
  2. Understand the calculation and reimplement it. Work out exactly how the name becomes a serial and write code that does the same. Now you can make a serial for any name.

I’ll show both. The first is faster and teaches you how to find the check. The second is the actual point of a keygenme, and it taught me far more. I’ll use x32dbg running inside a FlareVM virtual machine, and by the end you’ll have a working keygen.

While working through this crackme, I got lost and confused more than once. It’s rated level 1, and that’s true if you only want the serial: you can just read it at the comparison. But understanding how the serial is actually generated, and translating that assembly into a working keygen, was a real challenge for my first ever crackme. Let’s begin now!

First contact

Run the program before you open a debugger. It asks for a name (any name) and a serial (which we don’t know). With a wrong serial it prints Serial failed check!. With the right one it prints Serial checked out - now right a keygen.

Press enter or click to view image in full size

So the program has a pass/fail branch, and whatever decides it is what we’re after.

Navigating the compiler noise: string references

C++ compiled code is noisy, with a lot of library calls around very little real logic. A trick that cuts through it is to start from the strings:

Right-click in the CPU/assembly pane → Search For → Current Region → String References

Press enter or click to view image in full size

Double-click one of the two result strings. It takes you to the code that prints it, and right above it you’ll find the code that decided to print it.

Press enter or click to view image in full size

Method 1: the fast way

Here is what sits right before those two messages:

0040123C  call 401000          ; computes something, returns it in EAX
00401241 cmp eax,ebx ; compare it to ... EBX
00401244 jne 40124D ; not equal -> jump to "Serial failed check!"
00401246 push 407028 ; "Serial checked out - now right a keygen."

A comparison followed by a conditional jump is a decision point. If EAX and EBX are not equal, the program jumps to the failure message. Otherwise it falls through to the success message.

Press enter or click to view image in full size

Set a breakpoint on the instruction right after the compare (00401243, the pop ecx) and press Run (F9). Switch to the crackme window, enter a name and any serial. I used the name jon and the serial 12345. Then press Enter

The debugger stops at your breakpoint, right before the program prints pass or fail. Now look at the two registers:

Press enter or click to view image in full size

Registers show in hex, so use your calculator’s programmer mode:

  • EBX = 00003039, which is 12345 in decimal. That’s the serial I typed.
  • EAX = 7685C1FE, which is 1988477438 in decimal. That’s the serial the program calculated for jon.
Press enter or click to view image in full size

Press F8 and you’ll see the jump is taken, because 12345 ≠ 1988477438, and you land on Serial failed check!. (Look at the EIP with blue background)

Press enter or click to view image in full size

Restart, enter jon again, and this time type 1988477438.

Press enter or click to view image in full size

You land on Serial checked out.

Press enter or click to view image in full size

Important: that number is only valid for the name jon. Change the name and EAX changes. We beat one lock with one key, but we haven’t learned how the locks are made. That’s what Method 2 is for.

Method 2: understand it and write a keygen

Now we know what to expect: some code turns the name into a number in EAX. This time we start at the beginning of the program to get the full picture. The most interesting parts are:

  1. How the program reads your input, and how it separates the name from the serial.
  2. What it does to the name.
  3. The math that turns the processed name into the serial.

I’ll cover only the instructions that matter. Going line by line would bury the idea.

Part A: the setup and the phase flag

Find the banner string (keygenme[v1] - [zort]) in the string references and go there, or set a breakpoint at 0040116A.

Press enter or click to view image in full size

Restart with Ctrl+F2, press Run once, and you’ll stop at the start of the program. The first instructions print the banner through cout, which is the C++ output stream.

Press enter or click to view image in full size

A few lines later, the program initializes three variables to zero:

00401191  mov [ebp-4],ebx      ; EBX is 0 here
00401194 mov [ebp-14],ebx ; counter = 0
00401197 mov [ebp-1C],ebx ; phase flag = 0
0040119A call _kbhit ; the input loop starts here

EBX is zero at this point, so all three variables start at 0. Compilers don’t name variables, so we work out their meaning from how they’re used. This is the main skill of the whole exercise:

Part B: one loop, two jobs

The program has a single input loop, from 0040119A onward. It reads one key at a time:

Press enter or click to view image in full size
0040119A  call _kbhit          ; is a key waiting?
004011A0 test eax,eax
004011A2 je 40119A ; no -> keep waiting
004011A4 call _getch ; read the key/letter into EAX (and AL)
004011AA cmp [ebp-1C],0 ; which phase are we in?
004011AE mov [ebp-18],al ; save the key/letter for example "j"
004011B1 jne 401211 ; flag != 0 -> serial code
004011B3 cmp al,0D ; is it Enter?
Press enter or click to view image in full size

How do we know [ebp-1C] is a phase flag? By reading every instruction that touches it:

Press enter or click to view image in full size

It starts at 0, is checked on every keypress, and flips to 1 exactly when the program asks for the serial. That’s a “which phase am I in” flag.

Get Martin Atanasov’s stories in your inbox

Join Medium for free to get updates from this writer.

If the flag is 0 and Enter is pressed, two things can happen. If the counter [ebp-14] is still 0, the name is empty and it prints You must enter a name!. Otherwise it prints Serial: and sets the flag to 1. Any other key is a name character, which we handle in the next part.

Part C: the trick, name stretching

This is the only “hard” part in the crackme. For every name character, the program runs:

004011FD  push dword ptr [ebp-18]    ; arg 2: the character
00401200 lea ecx,[ebp-30] ; this = address of the name string
00401203 push dword ptr [ebp-14] ; arg 1: the count
00401206 call std::string::append ; name.append(count, ch)
0040120C inc dword ptr [ebp-14] ; counter++

In C++ terms this is name.append(count, ch), which means “add ch to the end of the string, count times.” The counter serves as both the repeat count and the character’s index. So after it runs for all characters in the name input for the name jon:

Character i is repeated i times. The first character is always dropped(since i=0), and the original name is never stored. Only onn is kept.

Two assembly details worth explaining:

  • lea ecx,[ebp-30] is the compiler passing a hidden this pointer. MSVC passes the object a method belongs to in ECX. lea loads the address of the string, like &name. Whenever you see lea ecx,[...] just before a call, it’s almost always a C++ method call on that object.
  • The pushes are in reverse order. The count is pushed second and the character first, because the arguments go on the stack right to left.

Part D: reading the serial

Once the flag is 1, the loop jumps to the serial branch at 00401211. Each digit goes through:

Press enter or click to view image in full size
00401228  lea eax,[ebx+ebx*4]        ; eax = ebx * 5
0040122B movsx ecx,byte [ebp-18] ; ecx = the key pressed
0040122F lea ebx,[ecx+eax*2-30] ; ebx = key + (ebx*5)*2 - 0x30

Combined, this is ebx = ebx*10 + (key - '0'). Keys arrive as ASCII, so '1' is 0x31, and subtracting 0x30 gives the digit. Multiplying by 10 shifts the number one decimal place to make room. Typing 1, 2, 3 gives 1, 12, 123. That’s why EBX was 0x3039 (12345) in Method 1.

Press enter or click to view image in full size
Keep in mind the screenshot above is for the FIRST iteration of the serial number

When the program finishes buidling the EBX it goes inside 401000, the function sets up its working values:

00401005  xor eax,eax           ; var3 = 0   (the running serial)
0040100F mov edi,43BA ; var1 = 0x43BA (the multiplier)
00401009 mov edx,[ecx+8] ; edx = length of the string
00401017 mov ebx,[ecx+4] ; ebx = pointer to the characters

Set a breakpoint on 0040102A and look at the register’s values before doing the calculations of the serial:

Press enter or click to view image in full size

The serial constructor loop itself is three instructions that matter:

Press enter or click to view image in full size
0040102A  imul eax,edi          ; t3 = t3 * t1
0040102D add eax,ecx ; t3 = t3 + character
0040102F imul edi,edi,6E6FA ; t1 = t1 * 0x6E6FA

with the character fetch, inc esi and cmp esi,edx / jb around them. In plain English: each pass scales up the running total, adds the next character, then changes the multiplier. It’s the same shape as the digit parser (x*10 + digit), except the multiplier changes every pass.

On the first pass, EAX is 0, so 0 * 0x43BA = 0. The multiply does nothing yet. The author wrote one uniform loop instead of special-casing the first character.

Here is the trace for jon, whose stretched name is onn:

Run the program till you reach the last letter in your name, and you will see EAX changing after every iteration:

Press enter or click to view image in full size

This was on the third run (2nd/last “o”), now the serial is calculated into the EAX register. The final 0x7685C1FE is 1988477438, exactly what we read from EAX in Method 1.

Part F: the comparison

After ret, EAX holds the serial and the program does the cmp eax,ebx from Method 1. It compares two integers: what the math produced, and what you typed. Equal means success.

Press enter or click to view image in full size

Writing the keygen

I decided to write it in Python, because it is easier for me:

python

def serial(name):
stretched = "".join(ch * i for i, ch in enumerate(name))
var1, var3 = 0x43BA, 0
for ch in stretched:
var3 = (var3 * var1 + ord(ch)) & 0xFFFFFFFF
var1 = (var1 * 0x6E6FA) & 0xFFFFFFFF
return var3
print(serial("jon"))

What went wrong along the way

  • I stared at the wrong address. I looked at the string object and saw nothing. The object is only a header, and the text lives behind the pointer at +4.
  • EAX meant two different things. Before the call to 401000 it held the argument pointer, and after it, the serial. Same register, different meaning at different moments.
  • Registers are scratch space. The string’s state doesn’t live in any register. It lives in the object at [ebp-30] for the whole function. Registers get overwritten constantly.

I hope you found this writeup helpful! I plan to publish many more crackme walkthroughs while continuing to refine my explanations so everything is as clear and easy to follow as possible.

--

--