Description
A classic Crackme. Find the password, get the flag! Binary can be downloaded here. Crack the Binary file locally and recover the password. Use the same password on the server to get the flag! Access the server using nc titan.picoctf.net 56916
Setup
Download crackme100 and load it into Ghidra.
Install gdb for dynamic analysis.
wget https://artifacts.picoctf.net/c_titan/83/crackme100 && \
chmod +x crackme100sudo apt install gdbSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Understand the mangling loop in GhidraObservationI noticed the challenge description called it a 'classic Crackme' and provided a downloadable binary with no source, which pointed directly to static reverse engineering in Ghidra as the first step to understand what the binary does to user input before comparing it.In Ghidra, rename variables so the logic reads cleanly (local_c is the outer counter i, local_10 is the inner counter j, local_a8 is the input buffer). The loop runs three rounds. Each round, for every character j, it computes intermediate values from the index j and the constants 0x55/0x33/0xF, adds (input[j] - 'a'), then reduces the result modulo 26 back into the lowercase range. After three rounds it calls memcmp against the stored secret string and prints the flag on a match.Learn more
A crackme is a program designed to be reverse-engineered. The goal is to recover the correct password without source. Ghidra's decompiler is the key tool: after importing the binary and running analysis, open
mainin the decompiler pane. Renaming variables makes the three-round transformation readable.The binary reads up to 50 characters of user input into
local_a8, then runs a three-iteration loop that applies the same index-dependent modular arithmetic to each byte. After the loop it compares the transformed buffer against the stored secret string withmemcmp. The important subtlety: the value the binary compares against is not the password you type, it is the password after three rounds of mangling. The constants printed near the top ofmain(the movabs values like0x676d76727970786c) are the bytes of that already-mangled target, not the password itself.Step 2
Break in gdb and read the real comparison stringObservationI noticed Ghidra showed a series of movabs instructions storing a 50-character target and a memcmp call, which suggested the easiest way to extract the exact comparison string was to set a gdb breakpoint after all movabs stores completed and read the buffer directly from memory rather than decoding each hex immediate by hand.Disassemble main with objdump to find the address right after the constant stores. A breakpoint at 0x4011e8 lands after every movabs has populated the secret buffer. Run, then info locals reveals the comparison string lxpyrvmgduiprervmoqkvfqrblqpvqueeuzmpqgycirxthsjaw. That 50-character string is the mangled target the input must equal after three rounds.bashobjdump -D crackme100 | lessbashgdb crackme100bashbreak *0x4011e8bashrunbashinfo localsThe buffer prints aslxpyrvmgduiprervmoqkvfqrblqpvqueeuzmpqgycirxthsjaw. Copy that string; the next step reverses the three rounds to recover the password that produces it.What didn't work first
Tried: Copying the movabs hex immediates from Ghidra and converting them to ASCII to use as the password directly
The movabs values store the already-mangled target string, not the original password. Decoding them gives lxpyrvmgduiprervmoqkvfqrblqpvqueeuzmpqgycirxthsjaw, which the program will then mangle a second time before comparing - producing a mismatch. The gdb breakpoint approach reads the buffer after all movabs stores complete, which is the correct target, but the target still needs to be reversed through three rounds before it can be typed as input.
Tried: Setting the gdb breakpoint at main instead of 0x4011e8, then calling info locals immediately
At the entry of main the movabs instructions have not yet executed, so the comparison buffer contains uninitialized stack garbage rather than the secret string. The breakpoint must land after all constant stores are complete (around 0x4011e8) but before the program reads user input, otherwise the buffer will not reflect the correct mangled target.
Learn more
gdb (the GNU Debugger) lets you pause a running binary and inspect memory and registers. Here it sidesteps reverse-engineering the constant layout by hand: instead of decoding each movabs value into ASCII, you let the program populate the buffer itself, then read it out. Use
objdump -Dto read the disassembly and pick an address after the last movabs but beforesetvbufis called, so the secret buffer is fully written but the program has not yet started its real work.Setting
break *0x4011e8stops execution at that instruction address,runexecutes up to the breakpoint, andinfo localsdumps the current stack variables, where the comparison string is visible. This dynamic-analysis shortcut is often faster and less error-prone than statically reconstructing data from movabs immediates.Step 3
Reverse the three rounds in PythonObservationI noticed the Ghidra decompilation showed a three-iteration outer loop applying index-dependent modular arithmetic using constants 0x55, 0x33, and 0xF, and since the offset depended only on the character index j and not on the input itself, the transformation was invertible by subtracting the same offset modulo 26 three times.Recreate the per-character math from Ghidra, but invert the addition: instead of adding the index-derived offset to (input - 'a'), subtract it from (cipher - 'a') modulo 26. Loop the same three times over the captured comparison string, and the recovered password falls out.pythonpython3 - <<'PY' c = "lxpyrvmgduiprervmoqkvfqrblqpvqueeuzmpqgycirxthsjaw" p = ['' for _ in c] for i in range(3): for j in range(len(c)): v7 = (85 & (j % 255)) + (85 & ((j % 255) >> 1)) v6 = (v7 & 51) + (51 & (v7 >> 2)) x = (ord(c[j]) - 97) % 26 y = (x - ((v6 & 15) + (15 & (v6 >> 4)))) % 26 p[j] = chr(y + 97) c = ''.join(p) print(c) PYExpected output
lumsopgxaocglvijjikbpwhfvchdmeipbotdjhxmwzilkvguun
What didn't work first
Tried: Running the inversion only once instead of three times, because the loop counter in Ghidra could be misread as a single-pass check
The forward transformation applies the same per-character offset three times in sequence, so the ciphertext is three rounds deep. Inverting only once recovers the intermediate state after two forward rounds, not the original input. Submitting that intermediate value to the server fails because the server's binary will apply all three forward rounds to it and compare against the stored target, producing a different result than the stored secret.
Tried: Applying the inverse in the same order as the forward pass (j=0 to 49, round 0 to 2) rather than iterating rounds in the same outer-inner structure
Because the offset depends only on the index j and the round constants (not on other characters), the round order and character order within a round can be applied independently. However, if you reverse the outer loop direction (trying round 2 first then round 1 then round 0), the per-character subtraction still uses the same offset so the output is unchanged. The critical point is that each round must use the output of the previous inverse round as its input - passing the original ciphertext to all three inverse rounds simultaneously gives three independent incorrect outputs rather than the chained inversion.
Learn more
The constants 0x55 (85), 0x33 (51), and 0xF (15) are bit masks. The original loop computes an offset from the character index j: it masks
j % 255with 85 and adds a shifted copy, masks that with 51 and adds a shifted copy, then masks with 15. That offset is added to(input[j] - 'a')and reduced modulo 26. Because the offset depends only on j (not on the input), it is the same on every pass, so undoing the round is a simple subtraction modulo 26.The script reproduces the offset exactly (
v7,v6, and the final(v6 & 15) + (15 & (v6 >> 4))term), then subtracts it from each mangled byte. Repeating the inverse three times mirrors the three forward rounds, recovering the lowercase password the program expects.Step 4
Submit remotelyObservationI noticed the challenge description said to use the recovered password on the remote server at titan.picoctf.net to get the real flag, which meant the local crackme only validated the technique while the actual flag required sending the same password over netcat.Test the recovered password locally against crackme100 first; if it prints the sample flag, the recovery is correct. Then connect to the server and enter the same password to receive the real flag.bashnc titan.picoctf.net 56916Learn more
netcat (
nc) provides a raw TCP connection to the server. You type directly to the server process's stdin and see its stdout in your terminal. The pattern of cracking locally then submitting remotely is common in reversing challenges: the remote service runs the same binary but returns the real flag only after correct input, keeping the flag out of the downloadable binary.From a security perspective, this challenge illustrates why storing a verifiable password as a reversibly-transformed string in a binary is not secure. Any determined attacker with the binary can recover the password through static analysis plus a short script, or simply by reading the comparison buffer at runtime. Real applications should use a cryptographic hash (bcrypt, Argon2, scrypt) where reversing the transformation is computationally infeasible.
Flag
Reveal flag
picoCTF{s0lv3_angry_symb0ls_150f...}
Entering the recovered password on the remote instance prints the flag.