Description
I sent my secret flag over the wires, but the bytes got all mixed up! Recover the flag from the pcapng.
Setup
Download capture.pcapng from the challenge page.
Install Wireshark and Python libraries: pip install dpkt scapy
pip install dpkt scapySolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Examine the capture in WiresharkObservationI 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.Step 2
Extract the random seed from the first packetObservationI 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.pythonpython3 -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.dataattribute, 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].payloadfor UDP payload data. Both libraries are standard tools in network forensics.Step 3
Reverse the shuffleObservationI 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.pythonpython3 solve.pyWhat 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 withint(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, sociphertext 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.
Step 4
View the resulting PNGObservationI 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.pythonpython3 -c " with open('flag.png','wb') as f: f.write(decrypted_bytes) "basheog flag.pngExpected output
picoCTF{n0_t1m3_t0_w4st3_5hufflin9_ar0und}Learn more
Writing the recovered bytes to a file and running
file flag.pngconfirms whether the recovery was successful - a valid PNG starts with the bytes89 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.