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:
- Base-16 encoding: The flag is converted to binary and encoded using only 16 alphabet characters
- 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:
$ file new_caesar.pynew_caesar.py: Python script, ASCII text executableHere’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 numbersALPHABET = 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:
- Takes each character from the flag
- Converts it to 8 binary digits (its ASCII value in binary)
- Splits those 8 digits into two groups of 4 digits each
- Treats each group as a number (0-15)
- Maps each number to a letter (a=0, b=1, c=2… p=15)
- 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 letterg - Last 4 bits:
0001= 1 in decimal → maps to letterb - 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:
- Encodes the flag using
b16_encode - For each character in the encoded message, shifts it by the key
- 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:
- Try all 16 possible keys
- For each key, decrypt the message
- Decode the base-16 result back to plaintext
- 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 challengeencrypted = "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 :
$ python solve.pyKey: 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 - theassertstatements 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