NoOff’s LoginCrackme.exe — Full Solve Writeup
Crackmes.one ID: 666732aee7b35c09bb266b1c. x86-64 PE, MSVC-compiled, std::cin-based passcode check.
1. Target Overview
A console app that prompts for a secret passcode and validates it against data baked into .rdata. The validation lives in a single function, sub_7FF7B4E51290, called from main after the input is read via std::cin.
2. Static Recon
Key strings and symbols recovered from .rdata:
"Enter a secret passcode: ","LOGIN SUCCESSFUL!!!","The passcode is INVALID!"— straightforward prompt/outcome strings, used to locatemain's branch points.
- Five 16-byte arrays (
xmmword_7FF7B4E54500,_54510,_54520,_54530,_54540) referenced insidesub_7FF7B4E51290— these turned out to be an index/permutation table. - One 16-byte array + one 4-byte array (
xmmword_7FF7B4E543F8,dword_7FF7B4E54408) — a 20-byte "pool" of reference characters, copied verbatim into a heap-allocatedstd::stringat the top of the validation function.
3. Recovering the Permutation Table
IDA renders xmmword operands as single 128-bit hex integers with leading zeros stripped, which is easy to misread by hand — a first manual transcription attempt produced garbage. The reliable approach: treat each value as a little-endian 128-bit integer and split it into four 32-bit words.
vals = [
"30000000400000007",
"1000000080000000900000002",
"2000000090000000800000001",
"5000000060000000600000005",
"7000000040000000300000000",
]
for v in vals:
n = int(v, 16)
b = n.to_bytes(16, 'little')
dwords = [int.from_bytes(b[j:j+4], 'little') for j in range(0, 16, 4)]
print(dwords)This gave four-dword groups per xmmword,
[7, 4, 3, 0]
[2, 9, 8, 1]
[1, 8, 9, 2]
[5, 6, 6, 5]
[0, 3, 4, 7]but their .rdata address order is not the order the function actually uses. The disassembly copies them into a stack buffer in a different sequence:
movdqa xmm0, xmmword_7FF7B4E54500 ; -> var_88 (first)
movdqa xmm1, xmmword_7FF7B4E54520 ; -> var_78 (second)
movdqa xmm0, xmmword_7FF7B4E54530 ; -> var_68 (third)
movdqa xmm1, xmmword_7FF7B4E54510 ; -> var_58 (fourth)
movdqa xmm0, xmmword_7FF7B4E54540 ; -> var_48 (fifth)The real, as-used 20-element table, built from that copy order, is:
[7,4,3,0, 1,8,9,2, 5,6,6,5, 2,9,8,1, 0,3,4,7]An earlier pass that assumed straight address order (54500,54510,54520,54530,54540) produced a different, wrong table, and wasted a round of guessing on four plausible-but-incorrect candidate passcodes. Lesson: never infer struct/array layout from static address order when a stack copy is involved — always check the actual copy sequence in the disassembly.
4. Recovering the Pool String
The 20 raw bytes from xmmword_7FF7B4E543F8 + dword_7FF7B4E54408, decoded little-endian:
hi = 0x315D6767733357783979377446354079
lo = 0x6A797657
raw_pool_bytes = hi.to_bytes(16, 'little') + lo.to_bytes(4, 'little')Important: these bytes are not decoded (-5'd) by the function ahead of time — they're copied as-is into a std::string, and the -5 subtraction happens live inside the comparison loop, once per byte, as needed.
5. The Validation Loop
Hex-Rays decompilation of sub_7FF7B4E51290 :
(cleaned up)
v3 = 0; // loop counter i
v4 = v2; // pointer into pool string (raw bytes)
v5 = (int *)v12; // pointer into the 20-element table
do {
if (v3 % 2 != 1) { // <-- only EVEN i is ever checked
v6 = a1;
if (a1[3] > 0xF) v6 = *a1; // SSO-vs-heap dispatch for input string
if (*((char *)v6 + *v5) != *v4 - 5) {
// mismatch -> cleanup, return 1 (fail)
}
}
++v3; ++v5; ++v4;
} while (v3 < 20);
// loop completed without mismatch -> return 0 (success)The critical detail, easy to miss on a first asm read: the i % 2 gate means odd-indexed loop iterations never execute a comparison at all. Only the 10 even-i iterations matter, and table[i] for those even positions is [7,3,1,9,5,6,2,8,0,4] — a full permutation of 0..9.
Get Root_Fabric’s stories in your inbox
Join Medium for free to get updates from this writer.
So the actual constraint set is just:
for i in {0,2,4,6,8,10,12,14,16,18}:
input[table[i]] == raw_pool_bytes[i] - 56. The Length Trap
The original task description (supplied at the start of this session) stated the passcode was a strict 20 characters. That assumption drove a lot of wasted effort trying to fill 10 “unconstrained” trailing characters with printable filler — all of which the real binary rejected.
main's decompilation revealed the actual gate, which runs before the validation function is ever called:
if ( v11 != 10 ) // v11 = length of the line read from std::cin
{
// print "The passcode is INVALID!" and exit
}
...
v26 = 10; // length field of the string object built for sub_7FF7B4E51290
v19 = sub_7FF7B4E51290(&v25);The real passcode length is 10, not 20. The 20-iteration loop inside the validation function still runs to i < 20, but with only a 10-character string, positions 10–19 of the table's even-i lookups never land anywhere meaningful for the length that's actually passed in — the loop bound of 20 is simply larger than it needs to be (dead iterations past the string's real content), not a hint about input length.
7. Solving the 10 Constraints
table = [7,4,3,0, 1,8,9,2, 5,6,6,5, 2,9,8,1, 0,3,4,7]
hi = 0x315D6767733357783979377446354079
lo = 0x6A797657
raw_pool_bytes = hi.to_bytes(16, 'little') + lo.to_bytes(4, 'little')
passcode = [None] * 10
for i in range(0, 20, 2):
target_index = table[i]
if target_index < 10:
passcode[target_index] = chr(raw_pool_bytes[i] - 5)
print(''.join(passcode))Result: Rob0ts.tXt — a "robots.txt" pun, thematically fitting for a "Secure Server" login prompt.
8. Verification
Confirmed against the live binary:
9. Retrospective — What Went Wrong Along the Way
- Manual xmmword transcription is unreliable. IDA’s leading-zero-stripped hex display caused a hand-counting error on the first table read; scripting the byte-order conversion fixed it immediately.
- Address order ≠ usage order. The five xmmwords are stored sequentially in
.rdata, but the function copies them into a local buffer in a non-sequential order. Assuming the two matched produced a table that was subtly wrong in exactly two of five 4-element blocks — wrong enough to break the solve, right enough to look plausible. - A structurally-similar older script (for a different, related crackme) suggested an even/odd input-character split. That pattern didn’t apply here; the real asymmetry was an even/odd loop-iteration skip, a different mechanism that happened to also produce 10 “active” constraints, which made the wrong model superficially plausible until tested.
- Trusting the stated length constraint (20 chars) over the binary’s actual behavior cost an extra round of failed live tests.
main's own length check (== 10) was the ground truth and only surfaced oncemainitself was decompiled, not just the validation subroutine.
The net lesson: for crackmes with any kind of nontrivial data layout, decompiled pseudocode of every function in the call chain (not just the “interesting” one) is worth getting before committing to a candidate solution — static data reading and partial disassembly are good for forming hypotheses, but asm/pseudocode of the full path is what actually confirms them.







