Challenge Info

Field Details
Platform crackmes.one
Author Kryptos
Language C/C++
Architecture x86-64
Difficulty 2.0
Date 2026-04-16

Tools Used

Tool Purpose
Detect-It-Easy Binary identification
PEStudio More Binary identification
PowerShell Fingerprinting
floss extract strings
IDA Free Debugging and analysis

Static Analysis

Binary Identification

Key Value Tool used
SHA1 9B3253DDEE0908C0AA69B0FFE99C4AA79423FEC3 PowerShell (Get-FileHash)
SHA256 F5E0D7EEF306E78B0C937D735931D73F542470449B94BE15597E437E1E27A1AA PowerShell (Get-FileHash)
name crackmepls.exe explorer.exe
language C/C++ Detect It Easy
compiler MSVC Detect It Easy
type Windows PE, 64-bit, console Detect It Easy / PEStudio
pdb path C:\Users\Kryptos\source\repos\crackme-lvl1\x64\Release\crackme-lvl1.pdb PEStudio

This binary is a Windows PE 64-bit executable compiled using MSVC. The binary requests a username and a password, after which it performs a hashing function based on the username to generate an expected password. The binary then compares the provided password with the hash generated password. If they match, the binary prints “Access granted”, else it prints “Access denied”.

Prerequisites for success

The crackme has been successfully cracked, if the hashing algorithm is being well understood and if a script can be written that calculates the password based on the username to consistently “guess” the correct password for each corresponding user.


Reconnaissance

The first stage usually involves finding out what kind of metadata / strings the file contains.

Strings

floss.exe crackmepls.exe

From this command (which extracts strings from a binary), the following can be deducted:

  • WinAPI Functions: QueryPerformanceCounter, GetCurrentThreadId, GetSystemTimeAsFileTime, InitializeSListHead, SetUnhandledExceptionFilter, GetModuleHandleW;
  • Imported DLLs: MSVCP140.dll, VCRUNTIME140.dll;
  • Miscellaneous: User:, Pass:, Access granted, Access denied.

If this would’ve been malware, a nice resource to cross reference WinAPI calls against would be MalAPI.io.

The miscellaneous strings already give a clear picture of where this CrackMe is heading:

...
User:
Pass:
Access granted
Access denied
...
memcmp
memcpy
strlen
...

Decompilation

Loading the binary into IDA and running the autoanalysis, IDA provides a graph view of the binary.

F5 opens the Pseudocode Window that reveals a disassembled version of the code.

Note: Personally, I find the pseudocode window useful to get a general idea of how a function works. But when debugging the application, I tend to prefer looking at the assembly

Identifying the main function

Going back to the graph view and pressing space to go to the linear view. Here the main function can be found at address 0x140001290. If the main address is different, That’s likely a byproduct with the Address Space Layout Randomization (ASLR).

Note: Default 64-bit PE executables their base address tends to be 0x140000000. At runtime, ASLR slides the binary to a random base. To configure this in IDA, go to:

edit -> Segments -> Rebase program -> Set value to 0x140000000

Flow of the binary

Following the main function, the C++ functions are being called:

  • std::cout: To print “User: “ (0x1400012EC);
  • std::cin: To save the string of the user (0x1400012FD);
  • std::cout: To print “Pass: “ (0x140001310);
  • std::cin: To save the string of the password (0x140001321);

Hashing Algorithm

The actual hashing functions starts at 0x140001350. Certain operations are performed on the username to calculate what the password would be.

hashing_function:
    lea     rax, [rsp+0xC8+username_input]   ; rax = stack buffer (SSO)
    cmp     rbp, 0xFh                        ; is string len <= 15?
    cmova   rax, rbx                         ; if len > 15, use heap ptr (rbx) instead
    movsx   ecx, byte ptr [rax+rdx]          ; ecx = (signed) char[i]
    lea     eax, [rdx+1]                     ; eax = i + 1
    imul    eax, ecx                         ; eax = (i + 1) * char[i]
    add     eax, r8d                         ; eax = hash + (i+1) * char[i]
    lea     r8d, ds:0[rax*8]                 ; r8d = eax * 8
    xor     r8d, eax                         ; r8d = (eax * 8) ^ eax
    inc     rdx                              ; i++
    cmp     rdx, r9                          ; i < len?
    jb      short hasing_function            ; go to hashing_function

Once the hashing algorithm is completed, the calculated value (hash) is stored in r8d:

imul edx, r8d, 0x539 ; edx = r8d * 1337
xor edx, 0x5A5A ; edx ^= 23130

The final hash is stored in edx.

hash_to_string

Once the hash is calculated, it’s only a decimal value. To compare it against the “Pass” provided by the user, the decimal value must be converted to a string. That happens within this function called at 0x14000138F.

Verify Password

After the “hashed” password, has been converted to a string, will the comparison start. This starts directly after the has_function beind called at 0x140001394:

mov r13, [rsp+0xC8+var_48]          ; load capacity of the user-provided password (likely 0xF)
lea rdx, [rsp+0xC8+password_input]  ; rdx will hold the address of the string buffer (on the stack)
mov rdi, [rsp+0xC8+password_input]  ; moves the existing heap pointer into rdi (in case it is needed in cmova)
cmp r13, 0xF
cmova rdx, rdi  ; if the previous comparison was > 15 (0xF), move the value of rdi into rdx
mov r8, [rsp+0xC8+Size] ; Moves size of provided password into r8 

This snippet explains a check to see if the characters provided as the password exceed the length of 15 (0xF), if this is the case, rdx will point to heap memory where the password lives and use that instead of the stack buffer. This pattern will be once more repeated for the hash calculated based on the username that was converted into a string.

mov r15, [rsp+0xC8+var_88]  ; loading capacity of actual password into r15
lea rdx, [rsp+0xC8+Buf1]    ; rdx will hold the address of the string buffer
mov rsi, [rsp+0xC8+Buf1]    ; moves the existing heap pointer into rdi
cmp r15, 0xF    
cmova rcx, rsi              ; if > 15 (0xF), move rsi into rcx

After the length of both passwords have been identified, they will be compared. If the lengths of the passwords differ, it’s not worth it comparing them to each other anyway:

cmp r8, [rsp+0xC8+var_50]           ; Verify lengths of passwords are equal
jz short passwords_are_same_length  ; If previous comparison == true, go to this function
xor r14b, r14b                      ; if not true, xor r14b with itself (resulting in 0)
jmp short after_memcmp              ; jump to code executed after the memcmp (skips memcmp call)

If the passwords were the same length, a next verification will be done to verify they weren’t a length of 0

passwords_are_same_length:
    test r8, r8                     ; is r8 == 0?
    jnz short compare_passwords     ; if not, go here
    mov r14b, 1                     ; if yes, set r14b to 1
    jmp short after_memcmp          ; skip memcmp and go to this function

If the passwords weren’t a length of 0, they will be compared with memcmp:

compare_passwords:
    call memcmp     ; Obvious what happens here
    test eax, eax   ; check if eax is 0? (memcmp returns the result into eax, if password1 == password2? eax = 0 else eax != 0)
    setz r14b       ; if eax is zero, comparison was successful and r14b will hold the value 1

This concludes how the password is verified, later in the code it’ll show:

test r14b, r14b                 ; is r14b 0?
lea rdx, aAccessGranted         ; move into rdx a pointer to this string
jnz short print_result  ; go to the function that prints the string in rdx
lea rdx, aAccessDenied          ; if r14b is 0, load this instead.

print_result:

Which is responsible for printing the “Access granted” string, or if the password was incorrect, it will print “Access denied”.


Password Generator

Knowing how this hash function operates, it is now possible to write a password generator to always create the right password for each username.

I decided to write the generator in Rust, because I already had my IDE for Rust open anyway.

fn main() {
    let text = "alcidius";

    let mut r8 = 0;
    for i in 0..text.len() {
        let cx = text.as_bytes()[i];
        let mut ax = i + 1;
        ax *= (cx as usize);
        ax += r8;
        r8 = ax * 8;
        r8 = r8 ^ ax;
    }
    let dx: i32 = (((r8 * 1337) ^ 23130) & 0xFFFFFFFF) as i32;

    println!("dx: {}", dx);
}

Let’s go over it line by line:

  1. Settings the username (text) as “alcidius”, as well as setting r8 to 0;
  2. Introduce a for loop goes from 0 until the length of the text, in this case 7;
  3. Introduce a constant variable cx, this variable represents the current character of text[i];
  4. Introduce a mutable variable ax and setting it to the iterator (i) + 1;
  5. Multiplying ax by itself and the ascii value the character variable cx holds (e.g., a = 0x61, l = 0x6C, etc.);
  6. Adding the value of r8 to ax, which in the first iteration will be 0;
  7. Setting the value of r8 equal to the current value of ax multiplied by 8;
  8. Setting the value of r8 equal to r8 xor’d by ax;
  9. Going back to the beginning. Every iteration, r8 will become bigger;
  10. After the for loop, the variable dx is introduced that first performs a multiplication on r8 by 1337 or as noted in the assembly code: (0x)539h. After the multiplication, the value will be once more xor’d by 23130 or as noted in the assembly code: (0x)5A5A, followed by a bitwise AND operation.
  11. That’s all there is to the calculation of the hash. It will be printed to the screen at that’ll be the password for the username.

Example

Let’s go over the password generator manually

Iteration cx ax+1 ax*=cx ax+=r8 r8=ax*8 r8^=ax
0 97 1 97 97 776 873
1 108 2 216 1089 8712 9801
2 99 3 297 10098 80784 72930
3 105 4 420 73350 586800 649910
4 100 5 500 650410 5203280 4622842
5 105 6 630 4623472 36987776 41086960
6 117 7 819 41087779 328702232 300247611
7 115 8 920 300248531 2401988248 2664301387
r8 = 2664301387;
result = ((r8 * 1337) ^ 23130) & 0xFFFFFFFF;
result = (3562170954419 ^ 23130) & 0xFFFFFFFF;
result = 3562170968297 & 0xFFFFFFFF;
result = 1643079913;

Conclusion

Despite it being a beginner difficulty crackme, the binary’s hash algorithm was rather challenging at first to undertake. Only using dynamic debugging and writing along in my notebook was I able to fully understand the workings of the hashing algorithm. Once the algorithm was well understood, was writing the password generator rather straightforward. The hash is fully deterministic, meaning the correct password can always be computed given any username.

I hope this writeup was in any way informative. Feel free to reach out if anything is unclear or if you spot a mistake. Thanks for reading!