perplexed

Published: April 2, 2025

Description

A stripped 64-bit ELF asks for a password and only prints "Wrong :(" when you guess incorrectly. Reverse the bitwise `check` function to reconstruct the expected bytes and feed them back to the program.

Grab the binary, mark it executable, and run it once to see the password prompt.

Load the executable into Ghidra (or IDA/Hopper) and inspect `main`, which forwards user input to `check`.

wget https://challenge-files.picoctf.net/c_verbal_sleep/2326718ce11c5c89056a46fce49a5e46ab80e02d551d87744306ae43a4767e06/perplexed
chmod +x perplexed && ./perplexed

Solution

  1. Step 1Analyze the check routine
    Decompiling `check` reveals a 0x17-byte array `local_58` and two nested loops that compare each bit of the user input to each bit of `local_58`. The function also requires an exact 27-byte password (`strlen(input) == 0x1b`).
    Learn more

    Static reverse engineering is the process of analyzing a compiled binary without executing it. Tools like Ghidra (free, NSA-developed), IDA Pro (industry standard), and Binary Ninja disassemble machine code and use heuristics to reconstruct higher-level pseudocode. The decompiler output isn't perfect C, but it reveals the structure of loops, conditionals, and data accesses well enough to understand the algorithm.

    The local_58 variable is a stack-allocated array - the name comes from its offset relative to the stack frame base pointer. Ghidra names stack variables by their offset automatically. Recognizing common patterns like "loop over every bit of a fixed array and compare to input bits" is a core reversing skill developed through practice with progressively harder challenges.

    When a binary is stripped (compiled without debug symbols), function names like check are replaced with addresses. Ghidra still finds function boundaries through prologue/epilogue pattern recognition, but you must rename them yourself as you understand their purpose. Symbols can sometimes be recovered by matching code patterns against known library versions - a technique called FLIRT (Fast Library Identification and Recognition Technology) in IDA.

  2. Step 2Recreate the bit logic in Python
    Copy the literal values from `local_58` into a Python list and reproduce the nested loops: for every bit that is set in `local_58`, set the bit in an accumulator byte and append the byte whenever eight bits have been processed. This mirrors what `check` does internally, except you build the string yourself.
    python3 - <<'PY'
    local_58 = [-0x1f, -0x59, 0x1e, -8, ord('u'), ord('#'), ord('{'), ord('a'), -0x47, -99, -4, ord('Z'), ord('['), -0x21, ord('i'), 0xd2, -2, 0x1b, -0x13, -0xc, -0x13, ord('g'), -0xc]
    flag = []
    local_20 = 0
    local_2c = 0
    for value in local_58:
        for bit in range(8):
            if local_20 == 0:
                local_20 = 1
            local_30 = 1 << (7 - bit)
            local_34 = 1 << (7 - local_20)
            if value & local_30:
                local_2c |= local_34
            local_20 += 1
            if local_20 == 8:
                flag.append(chr(local_2c))
                local_20 = 0
                local_2c = 0
    print(''.join(flag))
    PY
    Learn more

    Bit manipulation is a favourite obfuscation technique in CTF reverse engineering challenges. By operating on individual bits rather than whole bytes or characters, the author makes it harder to immediately recognize the algorithm from the decompiled output. Common bit operations include XOR masking, bit rotation (ROL/ROR), interleaving bit fields from two values, and permuting individual bit positions.

    The key insight for this challenge is that the check function is deterministic - given the same local_58 array (which is hardcoded in the binary), it always accepts exactly one password. Rather than guessing the password, you replay the same logic in Python to compute what the password must be. This "emulate the validator" approach works whenever the comparison function has no randomness and the key material is embedded in the binary.

    Python is ideal for this kind of reconstruction because its integers are arbitrary precision (no overflow surprises), it handles signed/unsigned values transparently when you use & 0xFF or ctypes.c_int8, and list comprehensions make bit manipulation loops compact. Alternatively, tools like angr (a symbolic execution engine) can automatically find inputs that satisfy a binary's constraints without manual analysis - useful for more complex validation logic.

  3. Step 3Submit the recovered password
    Running the script prints the picoCTF flag in plaintext. Paste it back into the program to see "Correct!! :D" and submit the same string as the challenge answer.
    Learn more

    This final step validates your understanding of the algorithm. If the script's output triggers "Correct!! :D" when fed to the binary, you've successfully reversed the bit permutation. If not, the most common errors are off-by-one mistakes in the bit indexing, incorrect handling of signed vs. unsigned bytes from the Ghidra decompilation, or mis-transcribed constants from the local_58 array.

    A useful debugging technique is to run the binary under GDB and set a breakpoint inside check to observe what value the loop is building on each iteration, then compare that to your Python output at each step. The pwndbg and GEF GDB plugins add rich visualization of registers, stack frames, and memory - making dynamic analysis much more comfortable than vanilla GDB.

    For competitive CTF play, the "reversed algorithm" technique generalizes broadly: if you can understand what transformations a binary applies to your input before comparing it to a target, you can invert those transformations on the target to recover the expected input. This works for XOR ciphers, custom hash functions, encoding schemes, and many other challenge types.

Alternate Solution

Use the Bit Shift Calculator on this site to trace through the individual bit operations from the decompiled check routine - step through the 1 << (7 - bit) shifts and OR assignments to verify your Python reconstruction is producing the correct bit pattern.

Flag

picoCTF{0n3_bi7_4t_a_...}

The encoded bytes in `local_58` already contain the flag, so no bruteforce is required once you mirror the loop in a higher-level language.

Want more picoCTF 2025 writeups?

Useful tools for Reverse Engineering

Related reading

What to try next