Sitemap
Root_Fabric

Root_Fabric

NoOff’s LoginCrackme.exe — Full Solve Writeup

6 min readOct 25, 2026

--

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 locate main's branch points.
Press enter or click to view image in full size
  • Five 16-byte arrays (xmmword_7FF7B4E54500, _54510, _54520, _54530, _54540) referenced inside sub_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-allocated std::string at 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:

Press enter or click to view image in full size
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] - 5

6. 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 once main itself 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.

--

--