Quantum Scrambler picoCTF 2025 Solution

Published: April 2, 2025

Description

"Quantum Scrambler" is nothing more than a deterministic shuffle of a list of hex bytes. Capture the remote output, re-run the loops in reverse, and you recover the original flag.

Connect with nc and save the shuffled output to a file (the server only scrambles the list and prints it).

Download quantum_scrambler.py so you can study the scramble function and confirm no actual encryption took place.

Sanity-check the saved blob: the last line should be a nested Python list literal that looks like [[['0x70', '0x69'], ['0x63', '0x6f']], [...]], not raw binary. Earlier lines are the server's banner and prompts, which is why the solver takes only splitlines()[-1].

bash
nc verbal-sleep.picoctf.net <PORT_FROM_INSTANCE> > result
bash
wget https://challenge-files.picoctf.net/c_verbal_sleep/27d1d27147deac5835e1ef9633cf1858c89bf32b14e2f4fbac72b6ca093f6d27/quantum_scrambler.py
bash
tail -1 result

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
Reading the "encryption" source and replaying it backwards is a recurring CTF pattern. Python for CTF covers the parsing primitives, and the broader CTF Encodings cheatsheet catalogues the cosmetic transformations like this one that masquerade as crypto.
  1. Step 1
    Understand the scramble routine
    Observation
    I noticed the challenge provided the full source of quantum_scrambler.py and the scramble function only pops and appends sublists without changing any byte values, which suggested that no actual encryption occurred and the original bytes could be recovered by reversing the reordering.
    The provided code repeatedly pops elements and appends prefixes, but never modifies the underlying data. It simply reorders sublists, meaning the plaintext bytes are still present.
    Learn more

    Security through obscurity is the antipattern this challenge demonstrates. The "Quantum Scrambler" name sounds sophisticated, but examining the source reveals it is purely a permutation - a reordering of data with no cryptographic key, no mixing of values, and no information loss. Every original byte is present in the output; only their arrangement changed.

    True encryption transforms data in a way that is computationally infeasible to reverse without the key. Permutation-only schemes fail this test because the number of possible arrangements is finite (factorial of input length), and more importantly, in this challenge the permutation algorithm is deterministic and provided in source. Anyone who reads quantum_scrambler.py can reverse it instantly.

    This is a common mistake in amateur cryptography: building elaborate pipelines of bit shifts, rotations, and shuffles that look complex but lack true randomness or key material. Claude Shannon's formal definition of confusion (substitution that hides the relationship between key and ciphertext) and diffusion (spreading plaintext influence across many ciphertext bits) are the properties that distinguish real ciphers from shuffles like this one.

  2. Step 2
    Iterate through the nested lists
    Observation
    I noticed the server output was a deeply nested Python list literal and that the scramble code uses A[i-1].append(A[:i-2]) to embed a growing copy of prior elements as a nested sublist at each step, which suggested that only items at non-list positions of each top-level sublist represent original bytes and that descending recursively would produce duplicates.
    Parse the saved result with ast.literal_eval (the server prints a banner before the data, so take only the final line via splitlines()[-1]). The scramble function builds each new element by appending a slice of all prior elements as a nested sublist, so the full tree contains many duplicate copies of earlier hex values. A naive recursive DFS that descends into every sublist will therefore emit each value multiple times, producing a corrupted result. The correct approach is to iterate only one level deep: for each top-level sublist, collect items that are strings and skip items that are lists (those are the embedded copies). This visits every original byte exactly once in the order the flag was written.
    python
    # Use ast.literal_eval, NOT eval (server output is untrusted):
    python3 - <<'PY'
    import ast
    with open('result') as f:
        data = ast.literal_eval(f.read().strip().splitlines()[-1])
    out = []
    for sublist in data:
        for item in sublist:
            if not isinstance(item, list):
                out.append(item)
    print(''.join(chr(int(c, 16)) for c in out))
    PY

    Expected output

    picoCTF{python_is_weird9ece...}
    What didn't work first

    Tried: Use a recursive DFS to collect all strings from every level of the nested list.

    Because the scramble embeds a growing snapshot of prior elements as a nested sublist at each step, a recursive traversal re-visits those embedded copies and emits each hex value multiple times. A 13-character flag expands to 65 or more characters with repeated prefix segments, producing garbled output instead of the flag. Iterating only one level deep and skipping list items (not descending into them) visits each original byte exactly once.

    Tried: Call eval() instead of ast.literal_eval() to parse the server output.

    eval() executes arbitrary Python expressions, not just literals, so it would run any code injected into the server response. In a CTF context the server output here is benign, but ast.literal_eval() is strictly safer and restricts parsing to Python literals (strings, numbers, lists, dicts, tuples). Using eval() on untrusted input is a vulnerability pattern, not a valid parsing strategy.

    Learn more

    Using Python's eval() to parse the output is a quick CTF trick because the server's output is a valid Python literal (a nested list of strings). In production code, eval is dangerous - it executes arbitrary Python, so it should never be used on untrusted input. The safe alternative is ast.literal_eval(), which parses only Python literals (strings, numbers, lists, dicts, tuples) without executing arbitrary expressions.

    The scramble function contains the line A[i-1].append(A[:i-2]), which splices a copy of all previously processed elements into the current sublist as a nested list. This means each iteration buries a growing snapshot of earlier data deeper in the structure. A recursive DFS would follow those nested copies and re-emit every hex byte it finds inside them, so a 13-character flag would produce 65 or more decoded characters with repeated prefix segments. Recognizing that the original data lives exactly at the non-list positions of each top-level sublist, and skipping the nested list items entirely, is the key insight.

    In real reverse engineering, you often encounter proprietary serialization formats or obfuscated data structures that must be parsed before the payload can be analyzed. Fluency with Python for rapid data structure manipulation - list comprehensions, slicing, zip, map - is invaluable for quickly prototyping parsers and decoders during CTF competitions.

  3. Step 3
    Decode to ASCII
    Observation
    I noticed that each collected element was a 0x-prefixed hex string such as '0x70' or '0x69', which are standard ASCII byte values, suggesting that chr(int(c, 16)) on each string would reconstruct the original flag characters.
    The same chr(int(c, 16)) line at the bottom of the script is the hex-to-ASCII conversion - each chunk like '0x70' becomes 'p'. Joining them prints the picoCTF flag; nothing further to do.
    Learn more

    Hexadecimal representation of bytes is ubiquitous in low-level security work. Each byte (0 to 255) maps to a two-digit hex value (0x00 to 0xff). The int(chunk[2:], 16) idiom strips the 0x prefix and converts the remaining hex digits to an integer in base 16. chr() then maps the integer to its Unicode character; for values 32 to 126 (printable ASCII) this is the familiar character set.

    The inverse operations are equally important: hex(ord('A')) gives 0x41, and '{:02x}'.format(65) gives '41'. These conversions appear constantly in cryptography, binary exploitation, and network protocol analysis. Python's bytes type and its .hex() / bytes.fromhex() methods are often more ergonomic for bulk conversions than character-by-character loops.

    The lesson here is that adding visual complexity to data (wrapping bytes in 0x-prefixed hex strings and nesting them in lists) does not add security. Any transformation that is purely cosmetic and reversible without a key provides zero cryptographic protection. Recognizing "fake encryption" quickly is a valuable CTF skill that also translates to evaluating real-world security claims about data "obfuscation" products.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
Alternate Solution

Once you have reassembled the hex byte sequence, use the Binary to Hex tool on this site to convert the hex chunks to ASCII characters one by one - or paste the entire hex string to decode the flag without writing any additional Python.

Flag

Reveal flag

picoCTF{python_is_weird9ece...}

The key insight is that the scramble only permutes bytes; the flag values survive intact at the non-list positions of each top-level sublist. Skip nested list items to avoid duplicates from embedded copies.

Key takeaway

A keyless deterministic permutation is not encryption; anyone who reads the source can reverse it, because there is no secret parameter an adversary lacks. Real encryption requires key material the attacker cannot observe, and Shannon's formal properties of confusion and diffusion are the measures that distinguish a genuine cipher from an elaborate shuffle like this one.

Related reading

Want more picoCTF 2025 writeups?

Useful tools for Reverse Engineering

What to try next