Description
Every block of three characters in the message was permuted: positions (0,1,2) became (2,0,1). Undo the pattern to read the plaintext flag.
Setup
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.
wget https://artifacts.picoctf.net/c/191/message.txtpython3 - <<'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))
PYSolution
Walk me through it- Step 1Deduce the permutationBecause 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].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.
- Step 2Automate the swapLoop 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{...}.Learn more
Python's
range(0, len(data), 3)generates indices0, 3, 6, 9, ...- stepping through the string in chunks of 3. Slicingdata[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) == 3check follows option (1): incomplete blocks are skipped. If a future variant pads with a different sentinel character, swap the check forblock.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.
Flag
picoCTF{7R4N5P051N6_15_3XP3N51V3_56E6...}
Any language works; just be careful with the final block if the length isn’t divisible by three.