875 words
4 minutes
🔐 PicoGym - New Caesar

📂 Download challenge file.

Description: We found a brand new type of encryption, can you break the secret code (Wrap with picoCTF{}) fegdeogdgecoeocgcgchcfcffccfca
Difficulty: Medium
Author: madStacks

Summary#

This challenge involves breaking a “New Caesar” cipher that combines two encryption techniques:

  1. Base-16 encoding: The flag is converted to binary and encoded using only 16 alphabet characters
  2. Shift cipher: Each encoded character is shifted by a secret key (which is only 1 character long)

Since the key is single character, we can brute-force all 16 possible keys to find the original flag.

Analysis#

We are provided with a Python file called new_caesar.py:

Terminal window
$ file new_caesar.py
new_caesar.py: Python script, ASCII text executable

Here’s the complete code:

import string
LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]
def b16_encode(plain):
enc = ""
for c in plain:
binary = "{0:08b}".format(ord(c))
enc += ALPHABET[int(binary[:4], 2)]
enc += ALPHABET[int(binary[4:], 2)]
return enc
def shift(c, k):
t1 = ord(c) - LOWERCASE_OFFSET
t2 = ord(k) - LOWERCASE_OFFSET
return ALPHABET[(t1 + t2) % len(ALPHABET)]
flag = "redacted"
key = "redacted"
assert all([k in ALPHABET for k in key])
assert len(key) == 1
b16 = b16_encode(flag)
enc = ""
for i, c in enumerate(b16):
enc += shift(c, key[i % len(key)])
print(enc)

Understanding the Encryption#

Step 1: Setting up the variables

  • LOWERCASE_OFFSET = ord("a") → This is 97, used to convert letters to numbers
  • ALPHABET = string.ascii_lowercase[:16] → This creates a reduced alphabet: a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p (only 16 characters)

Step 2: The b16_encode function

This function converts plaintext to a special base-16 format. Here’s what it does step-by-step:

  1. Takes each character from the flag
  2. Converts it to 8 binary digits (its ASCII value in binary)
  3. Splits those 8 digits into two groups of 4 digits each
  4. Treats each group as a number (0-15)
  5. Maps each number to a letter (a=0, b=1, c=2… p=15)
  6. Combines these two letters to encode one character

Example: Encoding the letter ‘a’

  • ASCII value: 97
  • Binary form: 01100001
  • First 4 bits: 0110 = 6 in decimal → maps to letter g
  • Last 4 bits: 0001 = 1 in decimal → maps to letter b
  • Final encoded result: gb (represents the original ‘a’)

Step 3: The shift function

This function applies a simple shift cipher (like the traditional Caesar cipher):

  • Takes an encoded character and a key character
  • Converts each to a number (a=0, b=1, c=2… p=15)
  • Adds the two numbers together
  • Uses modulo 16 to wrap around if needed (so it stays in the a-p range)
  • Converts the result back to a letter

Example: Shifting ‘g’ with key ‘c’

  • ‘g’ = 6, ‘c’ = 2
  • 6 + 2 = 8
  • 8 maps to letter ‘i’
  • Result: ‘i’

Step 4: Encryption process The algorithm:

  1. Encodes the flag using b16_encode
  2. For each character in the encoded message, shifts it by the key
  3. Since len(key) == 1, the same single key character is used for all positions

Key Weakness#

The assertion assert len(key) == 1 tells us the key is only 1 character long. This means there are only 16 possible keys (a through p). We can brute-force all of them!

Solution#

Since we know:

  • The encrypted message: fegdeogdgecoeocgcgchcfcffccfca
  • The key is single character from alphabet {a-p}
  • We need to reverse the process

We’ll:

  1. Try all 16 possible keys
  2. For each key, decrypt the message
  3. Decode the base-16 result back to plaintext
  4. Find which result looks like a readable flag

Creating the Decryption Script#

My solution:

import string
LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]
def b16_decode(enc):
"""Converts base-16 encoded text back to plaintext"""
dec = ""
for i in range(0, len(enc), 2):
# Get two characters from the encoded message
c1 = ALPHABET.index(enc[i])
c2 = ALPHABET.index(enc[i + 1])
# Combine the two 4-bit values back into 8 bits
byte_value = (c1 << 4) + c2
# Convert back to ASCII character
dec += chr(byte_value)
return dec
def unshift(c, k):
"""Reverses the shift operation"""
t1 = ord(c) - LOWERCASE_OFFSET
t2 = ord(k) - LOWERCASE_OFFSET
# Subtract instead of add, and use modulo to handle negative numbers
return ALPHABET[(t1 - t2) % len(ALPHABET)]
def decrypt(ciphertext, key):
dec = ""
for i, c in enumerate(ciphertext):
# Reverse the shift
dec += unshift(c, key[i % len(key)])
return b16_decode(dec)
# The encrypted message from the challenge
encrypted = "fegdeogdgecoeocgcgchcfcffccfca"
# Try all 16 possible keys (a through p)
for key_char in ALPHABET:
try:
result = decrypt(encrypted, key_char)
print(f"Key: {key_char} → picoCTF{{{result}}}")
except:
print(f"Key: {key_char} → Invalid result")

Output#

Most will produce unreadable gibberish, but one will show readable text that makes sense :

Terminal window
$ python solve.py
Key: a → picoCTF{TcNcd.N&&'%%R% }
Key: b → picoCTF{CR=RS=A}
Key: c → picoCTF{2A,AB,0}
Key: d → picoCTF{!01ûóôòò/òý}
Key: e → picoCTF{/ê ââãáááì}
Key: f → picoCTF{ùÙùÑÑÒÐÐ ÈèÀÀÁÏÏüÏÊ}
Key: g → picoCTF{þýÐýþ¿Ðç翾¾ê¾¹}
Key: h → picoCTF{íüÃ, üý·Ã, ¿¿°¾¾ë¾¹}
Key: i → picoCTF{ÜëÆëì¦Æ®®¯­­Ú­¨}
Key: j → picoCTF{ËÚµÚÛµÉ}
Key: k → picoCTF{ºÉ¤Éʤ¸}
Key: l → picoCTF{©¸¸¹s{{|zz§zu}
Key: m → picoCTF{§§¨bjjkiiid}
Key: n → picoCTF{qQqYYZXXXS}
Key: o → picoCTF{v`@`HHIGGtGB}
Key: p → picoCTF{et_tu_77866c61}

Finding the Correct Flag#

Looking at all 16 results, only one contains readable words and valid characters:

picoCTF{et_tu_77866c61}

âš¡ Raikiri

🎉 Flag pwned!


💡 TL;DR / Lesson Learned

✅ Base-16 encoding converts data using only 16 symbols (0-15, represented as a-p)
✅ Single-character keys are extremely weak - only 16 possibilities to brute-force
✅ Combining two weak ciphers doesn’t create strong encryption - both layers must be broken
✅ Always read the code for clues - the assert statements revealed the key length was just 1
✅ Brute-force is valid when key space is small - with 16 possibilities, trying them all is practical
✅ Look for readable output - the correct flag stands out from gibberish immediately