760 words
4 minutes
🔐 PicoGym - C3

📂 Download encoder.
📂 Download ciphertext.

Description: This is the Custom Cyclical Cipher! Enclose the flag in our wrapper for submission. If the flag was “example” you would submit “picoCTF{example}”.
Difficulty: Medium
Author: Matt Superdock

Summary#

This challenge involves C3 (Custom Cyclical Cipher), a custom encryption algorithm that uses differential encoding with lookup tables. We’re given the encoder program and ciphertext, but need to reverse-engineer the decryption process. By understanding how the cipher works, we can write a simple reversal script to recover the original flag.

Analysis#

Challenge Files#

We have two components:

  1. convert.py - The encoder/encryption program that shows us the algorithm
  2. ciphertext - The encrypted message we need to decrypt

How the Encryption Works#

Here’s the complete encoder program:

import sys
chars = ""
from fileinput import input
for line in input():
chars += line
lookup1 = "\n \"#()*+/1:=[]abcdefghijklmnopqrstuvwxyz"
lookup2 = "ABCDEFGHIJKLMNOPQRSTabcdefghijklmnopqrst"
out = ""
prev = 0
for char in chars:
cur = lookup1.index(char)
out += lookup2[(cur - prev) % 40]
prev = cur
sys.stdout.write(out)

Breaking down the algorithm:

  1. Reading Input:

    from fileinput import input
    for line in input():
    chars += line
    • fileinput.input() reads from files passed as command-line arguments (or stdin)
    • This loops through each line in the input file
    • All lines are concatenated into the chars variable
    • So if the plaintext file has multiple lines, they’re all combined into one string
  2. The Lookup Tables:

    • lookup1 = \n "#()*+/1:=[]abcdefghijklmnopqrstuvwxyz (41 characters - valid plaintext alphabet)
    • lookup2 = ABCDEFGHIJKLMNOPQRSTabcdefghijklmnopqrst (40 characters - valid ciphertext alphabet)
  3. Differential Encoding Process:

    • For each character in the input plaintext:
      • Find its position in lookup1 (call this cur)
      • Calculate the difference from the previous position: (cur - prev) % 40
      • Use this difference as an index into lookup2 to get the encrypted character
      • Save the current position as prev for the next iteration
  4. The “Cyclical” Part:

    • The modulo 40 operation wraps differences around, creating a cyclic pattern
    • This is why it’s called the “Custom Cyclical Cipher”

Example Walkthrough#

Let’s say we’re encrypting “ab”:

  • First character ‘a’:

    • Index in lookup1: 33
    • prev = 0
    • difference = (33 - 0) % 40 = 33
    • lookup2[33] = ‘h’
    • Output: ‘h’, prev = 33
  • Second character ‘b’:

    • Index in lookup1: 34
    • prev = 33
    • difference = (34 - 33) % 40 = 1
    • lookup2[1] = ‘B’
    • Output: ‘B’, prev = 34
  • Final ciphertext: “hB”

The Encrypted Message#

DLSeGAGDgBNJDQJDCFSFnRBIDjgHoDFCFtHDgJpiHtGDmMAQFnRBJKkBAsTMrsPSDDnEFCFtIbEDtDCIbFCFtHTJDKerFldbFObFCFtLBFkBAAAPFnRBJGEkerFlcPgKkImHnIlATJDKbTbFOkdNnsgbnJRMFnRBNAFkBAAAbrcbTKAkOgFpOgFpOpkBAAAAAAAiClFGIPFnRBaKliCgClFGtIBAAAAAAAOgGEkImHnIl

This is what we need to decrypt.

Solution#

Reversing the Encryption#

Since encryption uses only addition and modulo, it’s easily reversible:

Encryption: difference = (cur - prev) % 40 → lookup2[difference]

Decryption: We reverse this:

  • Find the encrypted character in lookup2 to get the difference
  • Add the difference to prev: cur = (difference + prev) % 40
  • Look up the character in lookup1
  • Update prev for the next character

Decryption Script#

lookup1 = "\n \"#()*+/1:=[]abcdefghijklmnopqrstuvwxyz"
lookup2 = "ABCDEFGHIJKLMNOPQRSTabcdefghijklmnopqrst"
ciphertext = "DLSeGAGDgBNJDQJDCFSFnRBIDjgHoDFCFtHDgJpiHtGDmMAQFnRBJKkBAsTMrsPSDDnEFCFtIbEDtDCIbFCFtHTJDKerFldbFObFCFtLBFkBAAAPFnRBJGEkerFlcPgKkImHnIlATJDKbTbFOkdNnsgbnJRMFnRBNAFkBAAAbrcbTKAkOgFpOgFpOpkBAAAAAAAiClFGIPFnRBaKliCgClFGtIBAAAAAAAOgGEkImHnIl"
plaintext = ""
prev = 0
for char in ciphertext:
# Step 1: Find the difference from lookup2
diff = lookup2.index(char)
# Step 2: Calculate the original index
cur = (diff + prev) % 40
# Step 3: Get the plaintext character
plaintext += lookup1[cur]
# Step 4: Update prev for next iteration
prev = cur
print(plaintext)

Running the Script#

Terminal window
$ python decrypt.py ciphertext
#asciiorder
#fortychars
#selfinput
#pythontwo
chars = ""
from fileinput import input
for line in input():
chars += line
for i in range(len(chars)):
if i == b * b * b:
print chars[i] #prints
b += 1 / 1

Understanding the Decrypted Output#

The decrypted text is actually a Python script! Looking at it carefully:

  • Comments: #asciiorder, #fortychars, #selfinput, #pythontwo
  • A loop that prints characters at specific positions: where i == b * b * b

This means it prints at positions: 1³=1, 2³=8, 3³=27, 4³=64, 5³=125, 6³=216… (perfect cubes!)

Fixing the Script#

The decrypted script had some issues (using / instead of proper integer, old Python 2 syntax):

#asciiorder
#fortychars
#selfinput
#pythontwo
chars = ""
from fileinput import input
for line in input():
chars += line
b = 1
for i in range(len(chars)):
if i == b * b * b:
print(chars[i], end='') # Fixed: Python 3 syntax
b += 1

Changes made:

  1. Changed b = 1 / 1 to b = 1 (proper integer)
  2. Changed print chars[i] to print(chars[i], end='') (Python 3 syntax)
  3. Changed b += 1 / 1 to b += 1 (proper increment)

Extracting the Flag#

Running the fixed script extracts characters at cube positions:

  • Position 1 (1³): ‘a’
  • Position 8 (2³): ‘d’
  • Position 27 (3³): ‘l’
  • Position 64 (4³): ‘i’
  • Position 125 (5³): ‘b’
  • Position 216 (6³): ‘s’

The hidden flag: adlibs

Terminal window
$ cat decrypted.txt | python solve.py
adlibs
âš¡ Raikiri

🎉 Flag pwned!

Final answer: picoCTF{adlibs}

💡 TL;DR / Lesson Learned

✅ Differential Encoding - Encrypting based on differences between consecutive positions
✅ Lookup Tables - Using predefined character mappings for substitution
✅ Modular Arithmetic - The % 40 creates the cyclic behavior
✅ Stateful Cipher - The cipher maintains state (prev) affecting each character
✅ Reversible Operations - Addition and modulo are easily reversed for decryption
✅ Layered Puzzles - The plaintext itself contains another puzzle (cube position extraction)