transposition-trial picoCTF 2022 Solution

Published: July 20, 2023

Description

Every block of three characters in the message was rotated: each plaintext triple [a, b, c] was scrambled to [b, c, a]. Undo the pattern to read the plaintext flag.

Download message.txt and note that “The flag” appears if you reorder each triple of characters.

Write a short script (Python shown below) to iterate through the text in steps of three and rearrange the characters back to their original order.

bash
wget https://artifacts.picoctf.net/c/191/message.txt
python
python3 - <<'PY'
flag = []
with open('message.txt') as f:
    data = f.read()
for i in range(0, len(data), 3):
    block = data[i:i+3]
    if len(block) == 3:
        flag.append(block[2] + block[0] + block[1])
print(''.join(flag))
PY

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
For a wider tour of classical and ad-hoc encodings (Caesar, ROT, base-N, transposition variants, XOR), see the CTF Encodings reference.
  1. Step 1
    Deduce the permutation
    Observation
    I noticed the challenge description stated each plaintext triple [a, b, c] was scrambled to [b, c, a], and that the message begins with readable English, which suggested using the known-plaintext prefix 'The' to confirm and invert the fixed cyclic permutation.
    Because the first word should read "The", you can infer the mapping. Each plaintext chunk [a b c] was scrambled to [b c a]. To recover the original, read each ciphertext block back in the order positions 2, 0, 1, which gives [a b c].
    What didn't work first

    Tried: Running frequency analysis on the ciphertext to find letter substitutions.

    Frequency analysis only works on substitution ciphers where each plaintext letter maps to a fixed ciphertext letter. In a transposition cipher, no letters are changed - only their positions are shuffled - so letter frequencies are identical to normal English and frequency tables give no useful information. The correct approach is positional: figure out which index in each ciphertext triple holds which original letter.

    Tried: Trying ROT13 or Caesar shifts on the ciphertext hoping to decode it.

    ROT13 and Caesar are substitution ciphers that shift character codes, but the characters in this message are already unchanged from the plaintext. Applying a shift produces garbled output because the problem is ordering, not encoding. Noticing that the first three letters are a recognizable anagram of a common English word like 'The' immediately identifies this as a transposition problem, not a substitution one.

    Learn more

    A transposition cipher rearranges characters according to a fixed rule without changing what the characters are. In this case, every 3-character plaintext block [a, b, c] was permuted to [b, c, a] (a cyclic left rotation). To recover the original, apply the inverse: from each ciphertext block take position 2, then 0, then 1, which reassembles [a, b, c].

    Known-plaintext reasoning makes the mapping easy to determine: since English text likely starts with "The", the ciphertext must start with those same letters in scrambled order. Trying different permutations of "T", "h", "e" until the result reads "The" immediately reveals the mapping. This is why knowing any plaintext makes breaking transposition ciphers trivial.

    Block transposition was used historically in columnar transposition ciphers, where text was written into a grid and columns were read out in a specified order. The ADFGVX cipher used by Germany in WWI combined a substitution step with columnar transposition - French cryptanalyst Georges Painvin broke it under significant time pressure during the war.

  2. Step 2
    Automate the swap
    Observation
    I noticed the permutation was consistent across every 3-character block in the file, which suggested a simple Python loop stepping through the ciphertext in increments of 3 and indexing each block as block[2] + block[0] + block[1] would fully reconstruct the plaintext.
    Loop through the ciphertext in steps of 3, reassemble each block as block[2] + block[0] + block[1], and concatenate the results. The final sentence ends with picoCTF{...}.
    What didn't work first

    Tried: Using the wrong index order such as block[1] + block[2] + block[0] or block[0] + block[2] + block[1] to reassemble each triple.

    There are six possible permutations of three indices, and only one undoes the original scramble. Trying an incorrect order produces readable-looking but wrong text - sometimes still English words by coincidence - which is misleading. The reliable way to find the right order is to test each permutation on the first three characters and check whether the result spells the known plaintext prefix 'The'.

    Tried: Looping with range(0, len(data)) and processing every character individually instead of in blocks of 3.

    Iterating one character at a time discards the block structure entirely - there is nothing to swap because each block[2], block[0], block[1] reference needs a 3-character slice. The output is either an index error or the ciphertext printed unchanged. The step size in range() must match the cipher's block size, which the challenge description gives as 3.

    Learn more

    Python's range(0, len(data), 3) generates indices 0, 3, 6, 9, ... - stepping through the string in chunks of 3. Slicing data[i:i+3] extracts each block. This is the standard Python idiom for processing fixed-width records in a string or byte array.

    What picoCTF does for non-divisible-by-3 messages. The 2022 challenge ships a message whose length is exactly a multiple of 3, so the edge case never fires in this specific instance. The reference encoder (publicly available with the challenge sources) right-pads with spaces before permuting. That means if you ever see a partial trailing block in a related challenge, the safe options are: (1) drop the trailing remainder if it is whitespace (typical for picoCTF), or (2) preserve the remainder unmodified by appending data[3 * (len(data) // 3):] to the output. Try option (1) first; fall back to (2) if it mangles the flag.

    The Python script's if len(block) == 3 check follows option (1): incomplete blocks are skipped. If a future variant pads with a different sentinel character, swap the check for block.ljust(3)[2] + block.ljust(3)[0] + block.ljust(3)[1] and strip the padding from the final string.

    In modern cryptography, block ciphers like AES also operate on fixed-size blocks (128 bits for AES), and the handling of messages that don't divide evenly into blocks requires padding schemes (PKCS#7 is most common). Improperly implemented padding led to real vulnerabilities like padding oracle attacks against CBC mode encryption.

Interactive tools
  • Rail Fence CipherEncrypt or decrypt rail fence (zigzag) transposition ciphers. Brute-force across rail counts and offsets to find the right setting fast.

Flag

Reveal flag

picoCTF{7R4N5P051N6_15_3XP3N51V3_56E6...}

Any language works; just be careful with the final block if the length isn’t divisible by three.

Key takeaway

Transposition ciphers rearrange characters according to a fixed permutation without substituting them, so every character in the ciphertext also appears in the plaintext. Any known plaintext, including predictable words like 'The' at the start of English prose or a flag format like 'picoCTF{' anywhere in the message, immediately reveals the permutation key because the same characters must be present in both. Columnar transposition was used in real wartime ciphers but was consistently broken through known-plaintext analysis, and the same weakness applies to any fixed positional rearrangement regardless of block size.

Related reading

Want more picoCTF 2022 writeups?

Tools used in this challenge

What to try next