scrambled-bytes picoMini by redpwn Solution

Published: April 2, 2026

Description

I sent my secret flag over the wires, but the bytes got all mixed up! Recover the flag from the pcapng.

Download capture.pcapng from the challenge page.

Install Wireshark and Python libraries: pip install dpkt scapy

bash
pip install dpkt scapy

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Examine the capture in Wireshark
    Observation
    I noticed the challenge description said bytes were 'mixed up' in a pcapng capture, which suggested opening the file in Wireshark first to identify the protocol and payload structure before writing any decryption code.
    Open capture.pcapng in Wireshark. The packets are UDP. Each packet carries a portion of XOR-encrypted data whose bytes have been shuffled using a seeded random number generator.
    Learn more

    pcapng (Packet Capture Next Generation) is the standard file format for network packet captures, used by Wireshark, tcpdump, and most network analysis tools. It stores raw packet data including headers and payloads for every packet captured during a network session. pcapng is the successor to the older pcap format, adding features like multiple interfaces, per-packet timestamps, and comments.

    UDP (User Datagram Protocol) is a connectionless transport protocol - unlike TCP, it has no handshaking, acknowledgment, or guaranteed ordering. Each UDP datagram is independent: they may arrive out of order or not at all. This is relevant to the challenge because the bytes are shuffled across packets, and the correct order must be reconstructed using the shuffle algorithm rather than packet sequence numbers.

    Wireshark's Protocol Hierarchy Statistics (Statistics → Protocol Hierarchy) gives an overview of what protocols are in the capture. Following a UDP stream (Analyze → Follow → UDP Stream) shows all related packets together. Wireshark can also export packet payload bytes for further analysis with external tools.

  2. Step 2
    Extract the random seed from the first packet
    Observation
    I noticed the shuffle used a seeded Python random module and that int(time()) has only second-level precision, which suggested the seed could be recovered directly from the pcapng packet timestamp rather than from any data in the payload.
    The sender's script seeded Python's random module with int(time()) at the moment of transmission. This seed is not transmitted in any packet payload - instead, you recover it from the pcapng file itself: the first packet's arrival timestamp (visible in Wireshark as Epoch Time, or extractable via dpkt as the ts value) truncated to an integer gives the seed.
    python
    python3 -c "
    import dpkt, socket
    with open('capture.pcapng','rb') as f:
        cap = dpkt.pcapng.Reader(f)
        for ts, buf in cap:
            eth = dpkt.ethernet.Ethernet(buf)
            print(eth.data.data.data[:16].hex())
            break
    "
    What didn't work first

    Tried: Reading the seed from the packet payload directly instead of the pcapng timestamp

    The payload bytes are XOR-encrypted and shuffled, so scanning them for a recognizable integer or magic value returns garbage. The seed is never serialized into any packet - it is only recoverable from the pcapng capture timestamp (the ts value dpkt exposes per-packet). The timestamp reflects when the sender called int(time()) before seeding, making it the correct source.

    Tried: Using scapy's rdpcap and pkt.time instead of dpkt's ts for the seed

    scapy exposes pkt.time as a float with microsecond precision, which matches dpkt's ts. However, scapy loads pcapng files more slowly and its layer indexing (pkt[UDP].payload) returns a Raw object, not bytes, so calling int() on it raises a TypeError. dpkt gives ts directly as a float and .data access chains cleanly, making it the more reliable choice for this extraction.

    Learn more

    dpkt is a Python library for parsing raw network packet data. It understands the Ethernet frame structure (dpkt.ethernet.Ethernet), which contains an IP packet (.data), which contains a UDP datagram (.data), which contains the application payload (.data). Each layer is accessed via the .data attribute, following the network stack from layer 2 (Ethernet) down to the application payload.

    The seed is not explicitly transmitted -- it is recoverable from the pcapng packet timestamp, because the sender used int(time()) which has only second-level precision. This is a design flaw in the key-generation scheme: using a low-entropy, externally-observable value (system time) as the PRNG seed means anyone who captures the traffic can determine the seed from the capture metadata alone, without any key material appearing in the packet payloads.

    scapy is an alternative Python packet library that is more user-friendly for interactive use but slower for bulk processing. rdpcap('file.pcapng') reads all packets, and individual layers are accessed via indexing: pkt[UDP].payload for UDP payload data. Both libraries are standard tools in network forensics.

  3. Step 3
    Reverse the shuffle
    Observation
    I noticed the Wireshark inspection revealed two stacked transformations (XOR encryption and a byte shuffle), and that Python's Mersenne Twister is fully deterministic given a seed, which suggested re-seeding with the recovered timestamp value to reconstruct and invert both operations.
    Use the recovered seed to reconstruct the same random shuffle sequence. XOR each byte with the corresponding value from the PRNG stream (successive random.randrange(256) calls using the recovered seed) to recover the ordered plaintext bytes.
    python
    python3 solve.py
    What didn't work first

    Tried: Applying the inverse shuffle before XOR-decrypting instead of XOR-decrypting first

    The sender applied XOR first and then shuffled, so the correct reversal order is un-shuffle first and then XOR. Doing it in the wrong order produces a buffer of valid-length but meaningless bytes - the PNG magic header (89 50 4E 47) does not appear at offset 0, and file flag.png reports 'data' instead of 'PNG image'. The order of inverse operations must mirror the original in reverse.

    Tried: Generating the XOR keystream with random.random() or random.randint(0, 255) instead of random.randrange(256)

    random.random() produces a float in [0.0, 1.0) rather than an integer in [0, 255], so using it as a keystream source without multiplying and truncating shifts the effective key range and desynchronizes the keystream from offset 1 onward, corrupting every byte past the first and producing an invalid PNG. Note that random.randint(0, 255) and random.randrange(256) are in fact identical in CPython (randint delegates directly to randrange), so either call works correctly.

    Learn more

    Python's random module uses a Mersenne Twister PRNG (pseudorandom number generator). Given the same seed, random.seed(n) followed by the same sequence of random calls produces identical output every time - it is entirely deterministic and reproducible. The sender seeded the PRNG with int(time()) at send time, generated a shuffle permutation and XOR stream from it, and applied both to the bytes. The attacker recovers the same seed from the pcapng timestamp and repeats this exactly to reconstruct the permutation and XOR stream, then reverses them.

    XOR decryption is the inverse of XOR encryption: plaintext XOR key = ciphertext, so ciphertext XOR key = plaintext. If the key is known (transmitted or guessable), XOR-encrypted data is trivially decrypted by XORing again with the same key. The combination of XOR and shuffle in this challenge adds two layers of obfuscation, but both are reversible once the seed is known.

    The solve script must: (1) collect payload bytes from all packets in order, (2) seed Python's random module with the integer timestamp extracted from the first packet, (3) generate the same shuffle indices and XOR values the sender used, (4) apply the inverse permutation, and (5) XOR each byte with its corresponding PRNG value. The result is the original PNG image bytes containing the flag.

  4. Step 4
    View the resulting PNG
    Observation
    I noticed the solve script output a buffer whose first bytes matched the PNG magic signature (89 50 4E 47), which confirmed the recovery was correct and that the flag was rendered visually inside the image rather than as plaintext bytes.
    The decrypted and un-shuffled bytes form a valid PNG image. Save it and open it - the flag is rendered visually inside the image.
    python
    python3 -c "
    with open('flag.png','wb') as f:
        f.write(decrypted_bytes)
    "
    bash
    eog flag.png

    Expected output

    picoCTF{n0_t1m3_t0_w4st3_5hufflin9_ar0und}
    Learn more

    Writing the recovered bytes to a file and running file flag.png confirms whether the recovery was successful - a valid PNG starts with the bytes 89 50 4E 47 0D 0A 1A 0A (the PNG magic signature). If the output is invalid, the seed extraction or shuffle reversal had an error.

    This challenge combines several disciplines: network forensics (pcap analysis), cryptography (XOR), randomness (PRNG seeding), and steganography (flag in image). Multi-discipline challenges are common at higher CTF difficulty levels - the skills required span tools and concepts from different security domains. Building fluency in each domain separately makes combined challenges approachable.

Interactive tools
  • File Magic IdentifierIdentify file types from magic numbers. Paste hex bytes or drop a file to detect PNG, JPEG, ZIP, PDF, ELF, PCAP, SQLite, and dozens of other formats.
Alternate Solution

Once you have un-shuffled the bytes and know the XOR key, use the XOR Cipher tool on this site to apply the final decryption step - paste the shuffled bytes and the key to confirm the recovered PNG magic bytes (89 50 4E 47) appear correctly.

Flag

Reveal flag

picoCTF{n0_t1m3_t0_w4st3_5hufflin9_ar0und}

When randomization is seeded with a low-entropy, externally-observable value (such as the epoch time readable from pcapng metadata), the shuffle is fully deterministic and reversible by anyone who captures the traffic.

Key takeaway

Pseudorandom number generators are deterministic: anyone who knows the seed can reproduce the entire output stream and reverse any transformation it produced. Using system time as a PRNG seed is catastrophically weak because the seed value is either observable in metadata (like a pcapng timestamp) or guessable within a narrow window. The same vulnerability appears in session token generation, cryptographic nonces, and shuffle-based obfuscation schemes whenever the seed comes from a low-entropy, externally-visible source.

Related reading

Want more picoMini by redpwn writeups?

Tools used in this challenge

What to try next