Skip to content

Fatmike's Crackme #1

Crackme Information
  • Difficulty: 3
  • Rating: 5
  • Platform: Windows
  • Language: C/C++

Download / View on crackmes.one

image_1765916595694_0.png

UPX Packed, and I know of 2 ways to unpack it:

Initial Assessment & Unpacking

PS C:\Users\yvesb\Desktop\crackmes > upx -d .\Crackme#1.exe -o Crackme#1-Unpacked.exe
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2025
UPX 5.0.2       Markus Oberhumer, Laszlo Molnar & John Reiser   Jul 20th 2025

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
    186880 <-     38400   20.55%    win32/pe     Crackme#1-Unpacked.exe

Unpacked 1 file.

Just run with -d flag and you're done.

Steps:

  1. Find OEP After loading in x32dbg we see that at entry point we pushad, which is expected since after unpacking we want the original state of the registers to be retrieved.

    image_1765917662506_0.png

    This is also useful because since values are going on the stack, they won't be modified until the program eventually does popad later. So we set a HW Access BP at where EBP register was pushed to, and press run.

  2. Fix IAT and Dump after unpack Break point gets hit, we jump to OEP successfully, same one as the upx -d command from method 1, but we fail at reconstructing IAT, and when we dump the executable doesn't launch anymore :(

  3. Give up.....

Dynamic Analysis in x32dbg

Let's keep going. When we run the executable we see the following:

image_1766000130333_0.png

Screenshot shows us entering serial key "Hello" and getting a Try again message.

As popup message seems like a standard MessageBox, I pause execution in x32dbg exactly at the point we get the message (before clicking OK), and set a breakpoint there. We seem to be stuck in a NtWaitUserMessage from module win32u.dll.

Call stack allows us to see a call to MessageBoxA earlier in execution so let's Follow Dump to the address MessageBoxA returns to and set a BP.

image_1766000817206_0.png

We are presented with the following:

image_1766002192505_0.png

Evidently we see our partially consumed string there "ELLO".

Static Analysis

We find the beginning of the function (Shift+Home), throw executable into IDA, and go to that address.

image_1766000084161_0.png

This seems to be XORing our key with some values in memory and then verifying if some function call is equal to a number. The function inside seemed a bit complicated and Gemini Pro 3.0 immediately identified the function as a CRC32 hashing function. From what I know it is pretty much impossible to reverse a CRC32 unless we have a pre-made image, and even if we did have one of those rainbow tables, our input would still get XORed with random bytes, so we need to find another approach.

Bytes in memory being stored: ^69440f0c-5915-46c9-b449-665442012a30 09 32 09 4B DB 2D 65 1B 16 DF 2E 65 D2 5E 99 D7 2B 73 D2 74 AF E3 23 72

image_1766068034333_0.png

I noticed that on success condition, we return 1 instead of 0. Going back, I see that IDA incorrectly decompiled the code too.

image_1766004750057_0.png

If we return 0 in license function, we will actually not jump on the jz function and go to lpBaseAddress_!!!! But there's only NOP instructions there...

We saw earlier that if we find a string that XORed with the stored bytes results in the correct CRC32 hash we will hit the valid branch. We also see that the correct branch: - Gets our current process id; - Opens a process with VM operation and write access rights

image_1766005251410_0.png - Writes 0x18 bytes starting at lpBaseAddress_ with lpBuffer

lpBuffer is the starting address of all those bytes that get written into in the license check function.

image_1766005668133_0.png

So maybe the lpbuffer at the end needs to be a call to MessageBox in the same way our fail branch does it?

We can find the opcodes corresponding to the fail branch:

image_1766009454423_0.png

If the message box displays a success message, then the string must be stored somewhere in memory. We search for success messages in the strings view and find a sneaky one I had missed before.

image_1766011083613_0.png

Based on this we can guess the success branch will be:

  • 6A 40 // push 40h
  • 68 28 B0 40 00 // push Caption
  • 68 38 B0 40 00 // push Text (guessed)
  • FF 35 F0 B4 40 00 // push hWnd
  • FF 15 DC 90 40 00 // call MessageBoxA

Notice also, this corresponds to exactly 24 bytes which is exactly how many bytes the loop writes to lpBuffer! :)

Key Generation

target = bytes.fromhex("6A 40 68 28 B0 40 00 68 38 B0 40 00 FF 35 F0 B4 40 00 FF 15 DC 90 40 00".replace(" ", ""))
key = bytes.fromhex("09 32 09 4B DB 2D 65 1B 16 DF 2E 65 D2 5E 99 D7 2B 73 D2 74 AF E3 23 72".replace(" ", ""))
answer = ''.join(chr(t ^ k) for t, k in zip(target, key))
print(answer)
# crackmes.one-kicks-asscr

We try inputting the key and get an error. Looking back at the licence function decompiled code IDA we see that not only does it check CRC32 but also if a var n22 is equal 22. Since our XOR function XORs 24 times and also loops over if our input is too short, and also because the end of the string corresponds to the beginning ("cr"), we try the Serial "crackmes.one-kicks-ass" and SUCCESS!!! This took way too long for a 3.0 difficulty crackme :(

image_1766068631555_0.png

Password

crackmes.one-kicks-ass