So I've been looking for random files to reverse engineer and crack, where I stumbled upon this file. Looking at it, it looks like it requires a password that has to be entered in order for the program to work. So let's launch a new session on IDA Pro and get started!
Step 1: Finding the failure path
First, by searching for the string "ACCESS DENIED" that gets std::cout'd to the display, we find the function responsible for that print, which is at address .text:0000000140017ED0. From outside it's just random bullshit that prints the access denied message — no useful logic there, just a dead-end print statement. Our goal isn't this function itself, it's whatever calls it, since that's where the actual pass/fail decision gets made. Our real target is the function that compares the password or gives ACCESS GRANTED.

Step 2: Finding the flag string
So let's scroll up a bit and voilà, we found the flag string sitting in plain text in .rdata: "FLAG: CRACKME{X0R_CR4CK3D}". No encryption, no obfuscation on the flag itself — it's stored as a raw readable string. Looking at it, seems like it just displays the flag after getting access with the correct password, meaning this string only gets referenced from the "success" branch of the program. That makes it a perfect anchor point — if we find what code touches this string, we find the success path.
So let's go to the function that calls the flag, using IDA's cross-reference feature, which is DATA XREF: sub_140017CD0+1BA up. This tells us exactly which function and offset references the string, letting us jump straight there instead of manually scrolling through the whole binary. Where we see this:
.text:0000000140017E8A lea rdx, aFlagCrackmeX0r ; "FLAG: CRACKME{X0R_CR4CK3D}" .text:0000000140017E91 mov rcx, cs:?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::ostream std::cout .text:0000000140017E98 call sub_1400110C8 .text:0000000140017E9D lea rdx, sub_140011055

The most important line here is call sub_1400110C8, which tells us it's most likely calling sub_1400110C8 to print the FLAG — this is basically just a wrapped std::cout << call under the hood, the compiler's mangled C++ naming makes it look scarier than it is. This whole block's only job is: load the flag string into a register, load cout into another, call the print function. Nothing computational happening here, it's purely cosmetic output — the "you win" screen.
Step 3: Tracing back to the password entry
From there we go up a little bit to find where the password logic is, which is under the "ENTER PASSWORD" function — identifiable by another string reference, this time to "[+] Enter password: ". Looking at it, we see the program:
Prints the password prompt. Reads user input via std::cin into a local buffer (var_128). Takes that buffer and calls sub_1400012DA, passing the input as an argument via rdx/rcx.
This is most likely the target that compares the password with the actual one — the return value (al, a single byte) is almost certainly a boolean pass/fail flag that later code branches on to decide whether to print ACCESS GRANTED or ACCESS DENIED.
Step 4: Decompiling the check function
So we go into sub_1400012DA, hit F5 to get the decompiled (Hex-Rays) version instead of trying to read raw assembly by hand, and find the password logic:
sub_1400116D6(&unk_14002D103); qmemcpy(correct_pass, "8'dffb", 6); correct_pass[32] = 85; compare_pass = sub_140011604(pass_input); if ( compare_pass == 6 ) { for ( j = 0; j < 6; ++j ) { v10 = correct_pass[j] ^ 0x55; v11 = v10; compare_pass = (unsigned int)*(char *)sub_140011028(pass_input, j); if ( v11 != (_DWORD)compare_pass ) { LOBYTE(compare_pass) = 0; goto LABEL_12; } compare_pass = (unsigned int)(j + 1); } LOBYTE(compare_pass) = 1; } else { LOBYTE(compare_pass) = 0; } LABEL_12: v4 = compare_pass; sub_1400115BE(v6, &unk_140023030); return v4;

Step 5: Breaking down the logic
This is a simple XOR obfuscation scheme, not real encryption — no key derivation, no rounds, just a static single-byte XOR mask applied per character. Let's go through it piece by piece:
qmemcpy(correct_pass, "8'dffb", 6); copies the 6 raw obfuscated bytes 8'dffb into a local buffer (v8, which I'm calling correct_pass for clarity). These 6 bytes are the encoded password, sitting statically in the binary — anyone dumping strings from this file would see 8'dffb and think it's meaningless garbage, which is exactly the point of the obfuscation.
compare_pass = sub_140011604(pass_input); is a length check, functionally equivalent to strlen(pass_input). It measures how many characters you typed.
if ( compare_pass == 6 ) is a hard gate: if your input isn't exactly 6 characters long, it skips straight to failure without even bothering to check individual characters. This is a nice early-exit for us reversers too — it tells us immediately the password is fixed-length, 6 characters.
Inside the loop: v10 = correct_pass[j] ^ 0x55; is the actual decoding step. Each of the 6 stored bytes gets XORed against the constant 0x55 to reveal the real character.
compare_pass = (unsigned int)*(char *)sub_140011028(pass_input, j); grabs the character you typed at position j (basically pass_input[j]).
if ( v11 != (_DWORD)compare_pass ) compares the decoded real character against your typed character. Any single mismatch sets the result to 0 (fail) and jumps straight to LABEL_12, skipping the rest of the loop — meaning it doesn't even bother checking the remaining characters once one fails.
If the loop completes all 6 characters without any mismatch, LOBYTE(compare_pass) = 1; sets the return value to 1 (success).
Step 6: Manually decoding the password
Since we know the algorithm — stored_byte XOR 0x55 = real_character — we can decode all 6 bytes of "8'dffb" by hand:
stored char ASCII/hex XOR 0x55 binary check decoded char 8 0x38 0x38 ^ 0x55 00111000 ^ 01010101 = 01101101 0x6D = m ' 0x27 0x27 ^ 0x55 00100111 ^ 01010101 = 01110010 0x72 = r d 0x64 0x64 ^ 0x55 01100100 ^ 01010101 = 00110001 0x31 = 1 f 0x66 0x66 ^ 0x55 01100110 ^ 01010101 = 00110011 0x33 = 3 f 0x66 0x66 ^ 0x55 01100110 ^ 01010101 = 00110011 0x33 = 3 b 0x62 0x62 ^ 0x55 01100010 ^ 01010101 = 00110111 0x37 = 7
Reading the decoded column top to bottom gives us the password: mr1337.
Conclusion
Typing mr1337 into the password prompt satisfies both checks — the length check (== 6) and the byte-by-byte XOR comparison — triggering the success path, which calls the print function for sub_1400110C8 with the flag string loaded in rdx, displaying FLAG: CRACKME{X0R_CR4CK3D} on screen. And like that, we solved this simple file — no dynamic analysis or debugger stepping needed, pure static analysis from string references down to a manual XOR decode.
