Skip to content

NoOff's LoginCrackme

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

Download / View on crackmes.one

image_1765878136470_0.png

Initial Execution

Let's run it:

*************************************
Welcome to the Secure Server!
Enter a secret passcode: HELLO
The passcode is INVALID!

Dynamic Analysis with x64dbg

Inside x64dbg we open up the executable and search for the invalid string. Our purpose is to find the condition that compares our input key to the correct key and to find the correct key this way.

image_1765879444930_0.png

We see a test al, al before a jump that leads to either the success or error message, so we set a breakpoint there to see the value during runtime.

Bypassing Length Constraints

I see there's other paths into the "INVALID" string, so I set breakpoints at all of those to see where we get a hit. We get a hit at 0x00007FF73A25175A, which comes from 0x00007FF73A251720 cmp rdi, F. F is 16 in hexadecimal. At runtime, rdi is also F. Those instructions also come after the "login successfull" messages, so let's assume for now that we are already in the fail branch.

I also noticed that sometimes when using some keys, e.g. "helloworld", we actually hit the earlier test al, al instruction. Funny. Maybe something to do with length? "helloworld" is 10 characters long.

Going further up, I keep repeating the process of setting breakpoints to discover where an instruction comes from.

image_1765881134954_0.png

I see a cmp rbp, A. 0xA is 10, and rbp matches our string length of 5 ("hello"). So it seems like our string should be length 10, as I was thinking earlier.

image_1765881740814_0.png

Just to check I want to see where our rbp gets set to 5, I would assume right after string creation. A bit before we see mov rbp,qword ptr ss:[rsp+58], and we also see in the stack that this value is set just earlier right after a call to some function which is done after cout.

Static Analysis with IDA Pro

Analyzing the Validation Function

Now we can hit our earlier test al, al, let's figure out the correct key.

Above this instruction there's a call <logincrackme.sub_7FF73A251290> so let's analyze that.

image_1765885833595_0.png

Inside function, we see a string "y@5Ft7y9xW3sgg]1Wvyj" being loaded, I assume this will suffer decryption and then each bytes gets compared to our key. The string is 20 characters long too. This function is quite complex at first glance and I wasted a bit of time trying to understand what's happening, I'm going to throw it in IDA Pro (don't ask how I have access to that wink wink ;))

image_1765886082859_0.png

Apparently this loads a bunch of xmmword into a buffer and makes v5 a pointer to this array. We loop over 20 times but only when our index is even, so at i = {0, 2, 4, ... 18}. Inside this condition, we cast a1 to a QWord if a1[3], which is our argument, is bigger than 0xF.

I'm seeing a lot of these > 0xF conditions and free() functions, seems like if our string is bigger than some value we allocate into heap, not sure, I will throw this in chatGPT later.

In the second if, we check if the character of our input key at offset v5 (which has all the xmmwords from earlier) is equal to a char of encrypted key "y@5Ft7y9xW3sgg]1Wvyj" minus 5.

C Equivalent
if (input_key[ offsets[i] ] != ( encrypted_key[i] - 5 ))

What happens if condition is true we don't care because they all use goto into return 1 instructions which would go into fail branch.

Developing the Keygen Logic

So lets look into these xmmword registers and calculate our key:

image_1765887357799_0.png

I see a bunch of numbers separated by a lot of zeros, it corresponds exactly to an int size of 4 bytes. Im going to undefine words so i can clearly see the offsets in the correct order in memory. Also need to make sure it corresponds to the order being assigned in v12. In the end the order this corresponds to: 7430 1892 5665 2981 0347.

Now lets strip all the uneven indexes: "y5tyx3g]Wy" and 7319562804. Great, no repeated indexes.

And remove 5 from the encrypted key:

Removing offset
s = "y5tyx3g]Wy"
s = ''.join(chr(ord(c) - 5) for c in s)
# Result: "t0ots.bXRt"

Then we reorder our result with the correct offset order:

Reordering
''.join(c for _, c in sorted(zip("7319562804", "t0ots.bXRt")))
# Result: Rob0ts.tXt

Final Solution

Let's try our final solution:

image_1765889678983_0.png

A note from AI

I also threw the decompiled code at Claude 4.5 Opus to try to understand what the seemingly gibberish meant (very helpful):

That's MSVC's std::string (SSO) implementation:

  • a1[3] > 0xF — Checks if string length > 15. If so, the string is heap-allocated (pointer at *a1), otherwise it's stored inline in the object itself ("Small String Optimization")
  • j_j_free(...) — Freeing the heap-allocated string buffer
  • The 0x1000 check and *(v11 - 1) stuff — MSVC debug allocator metadata, finding the real allocation start before freeing
  • *(_BYTE *)a1 = 0; a1[3] = 15; a1[2] = 0; — Resetting the string to empty state (null terminator, capacity=15, length=0)

TL;DR: It's just boilerplate for handling C++ std::string memory — not part of the actual license algorithm.