- Published on
PwnMe 2025 - Decode runner
Introduction
It was a really cool task that I spent almost the entire Saturday working on. It was categorized as easy, and maybe it was easy, but it was very time-consuming and required a lot of effort. On lower-ranked CTFs, this could probably have been split into 3 or 4 tasks, but here it was just one, and it was fun. This task came from this CTF.

Table of contents
- Second introduction
- Capitalized Letters
- Charabia
- Trithemius
- Chuck Norris Unary Code
- Shankar Speech Defect
- Leet
- Guitar Chords Notation
- Morbit
- Kana
- Baudot Code
- Solution
- Summary
Second introduction


Capitalized Letters
This was the simplest one. You were given a few words with capital letters, and you had to combine them and then make them lowercase at the end.
[!] big_letters
[!] cipher: Mike Oscar Tango Oscar
[!] moto
Charabia
Trim and revese.
def charabia (data):
return data[:-2][::-1]
[!] charabia
[!] oolgiit
[!] igloo
Trithemius
https://www.dcode.fr/trithemius-cipher
Chuck Norris Unary Code
https://www.dcode.fr/chuck-norris-code
Shankar Speech Defect
https://www.dcode.fr/shankar-speech-defect
Leet
Here, I only implemented the numbers, although sometimes there were combined letters as well. https://www.dcode.fr/leet-speak-1337
Guitar Chords Notation
This took the most time. It was only afterward that I realized that just 7 letters were enough. https://www.dcode.fr/guitar-chords-notation
Morbit
This wasn’t easy either. It was hard to find. Luckily, I went through all the Morse-related ciphers on Dcode. https://www.dcode.fr/morbit-cipher
Kana
This oddity was done by ppp45. Japanese Morse. https://www.dcode.fr/wabun-code
Baudot Code
It wasn’t straightforward because there were many types of it, and it looked like some binary code. But it turned out to be ITA2 MSB.
Solution
The promised source code with a partial description:
from pwn import *
import re
#context.log_level='debug'
context.update(arch='x86_64', os='linux') #o tym pamietac jak sie nie pobiera danych z pliku
context.terminal = ['wt.exe','wsl.exe'] #do wsl
#HOST="got-2ec78f10f9f4765b.deploy.phreaks.fr:443"
#ncat --ssl decoderunner-a59d507927a38509.deploy.phreaks.fr 443
HOST="decoderunner-a59d507927a38509.deploy.phreaks.fr:443"
ADDRESS,PORT=HOST.split(":")
#--------------------------shankar_speech_defect
def shankar_speech_defect(text):
# Mapa przestawienia liter
mapping = {
'A': 'D', 'B': 'F', 'C': 'G', 'D': 'H', 'E': 'J',
'F': 'K', 'G': 'L', 'H': 'M', 'I': 'N', 'J': 'U',
'K': 'O', 'L': 'P', 'M': 'Q', 'N': 'R', 'O': 'S',
'P': 'T', 'Q': 'I', 'R': 'V', 'S': 'W', 'T': 'X',
'U': 'Y', 'V': 'Z', 'W': 'B', 'X': 'A', 'Y': 'C',
'Z': 'E'
}
# Przekształć tekst
result = []
for char in text.upper():
if char in mapping:
result.append(mapping[char])
else:
result.append(char) # Zachowaj znaki, które nie są w mapie (np. spacje, znaki specjalne)
return ''.join(result).lower()
#--------------------------Baudot ITA2 (MSB)t
# Pełna mapa Baudot ITA2 (MSB)
def decode_baudot_msb(encoded_str):
# Definicje zestawu znaków Baudot ITA2 (tylko litery)
letters = {
'00000': None, # NULL
'00001': 'T',
'00010': '\r', # Carriage Return
'00011': 'O',
'00100': ' ',
'00101': 'H',
'00110': 'N',
'00111': 'M',
'01000': '\n', # Line Feed
'01001': 'L',
'01010': 'R',
'01011': 'G',
'01100': 'I',
'01101': 'P',
'01110': 'C',
'01111': 'V',
'10000': 'E',
'10001': 'Z',
'10010': 'D',
'10011': 'B',
'10100': 'S',
'10101': 'Y',
'10110': 'F',
'10111': 'X',
'11000': 'A',
'11001': 'W',
'11010': 'J',
'11011': 'FIG', # Figure Shift (ignorujemy)
'11100': 'U',
'11101': 'Q',
'11110': 'K',
'11111': 'LTR' # Letter Shift (ignorujemy)
}
# Funkcja do odwracania bitów (MSB -> LSB)
def reverse_bits(code):
return code[::-1]
# Dekodowanie
decoded_text = []
for code in encoded_str.split():
# Odwracamy bity (MSB -> LSB)
reversed_code = reverse_bits(code)
# Dekodujemy znak
if reversed_code in letters:
decoded_text.append(letters[reversed_code])
return ''.join(decoded_text).lower()
#--------------------------big letter
def big_letter(data):
# Usuwamy "cipher:" i białe znaki z początku i końca
data = data.replace("cipher:", "").strip()
# Rozdzielamy ciąg na słowa i wybieramy pierwsze litery każdego słowa
words = data.split()
big_letters = [word[0] for word in words if word[0].isupper()]
return ''.join(big_letters).lower()
#--------------------------charabia
def charabia (data):
return data[:-2][::-1]
#--------------------------leet
def leet_decoder(cipher):
"""
Dekoduje wiadomość zapisaną w stylu 1337 (leet), zamieniając tylko cyfry na litery.
:param cipher: Zaszyfrowana wiadomość w stylu 1337.
:return: Zdekodowany tekst.
"""
# Słownik zamiany cyfr na litery
leet_to_text = {
'0': 'o',
'1': 'l',
'3': 'e',
'4': 'a',
'5': 's',
'7': 't',
'9': 'g',
'2': 'z',
'6': 'g',
'8': 'b',
#"|<" - not implemented
}
# Zamiana każdego znaku w cipher na odpowiadającą mu literę (jeśli jest w słowniku)
decoded_text = ''.join([leet_to_text.get(char, char) for char in cipher])
return decoded_text.lower()
#--------------------------chuck_norris_unary
def chuck_norris_unary_to_ascii(code):
# Funkcja do dekodowania Chuck Norris Unary Code do kodu binarnego
def decode_to_binary(code):
groups = code.split(' ')
binary_bits = []
i = 0
while i < len(groups):
bit_group = groups[i]
count_group = groups[i + 1] if i + 1 < len(groups) else ''
if bit_group == '0':
bit = '1'
elif bit_group == '00':
bit = '0'
else:
raise ValueError("Nieprawidłowa grupa bitowa: " + bit_group)
count = len(count_group)
binary_bits.append(bit * count)
i += 2
return ''.join(binary_bits)
# Funkcja do konwersji kodu binarnego na tekst ASCII (7-bitowy)
def binary_to_ascii(binary_code):
ascii_text = ''
for i in range(0, len(binary_code), 7):
byte = binary_code[i:i+7]
if len(byte) < 7:
byte = byte.ljust(7, '0') # Dopełnij zerami, jeśli potrzeba
ascii_text += chr(int(byte, 2)) # Konwertuj na znak ASCII
return ascii_text
# Zdekoduj Chuck Norris Unary Code do kodu binarnego
binary_output = decode_to_binary(code)
# Przetłumacz kod binarny na tekst ASCII
ascii_output = binary_to_ascii(binary_output)
return ascii_output
#--------------------------decrypt_trithemius_plus_3
def decrypt_trithemius_plus_3(ciphertext):
result = ""
shift = 3 # Początkowe przesunięcie
for char in ciphertext:
if char.isalpha():
# Określamy przesunięcie dla małych i dużych liter
if char.islower():
start = ord('a')
else:
start = ord('A')
# Obliczamy nową literę z uwzględnieniem zawijania
new_char = chr(start + (ord(char) - start - shift) % 26)
result += new_char
shift += 1 # Zwiększamy przesunięcie o 1 dla kolejnej litery
else:
# Jeśli to nie litera, dodajemy znak bez zmian
result += char
return result
#--------------------------Kana (from ppp45)
def kana_decrypt(ciphertext):
kana = {
".-": "i",
"-.-": "wa",
".-..-": "wi",
"-.-.-": "sa",
".-.-": "ro",
".-..": "ka",
"..--": "no",
"-.-..": "ki",
"-...": "ha",
"--": "yo",
".-...": "o",
"-..--": "yu",
"-.-.": "ni",
"-.": "ta",
"...-": "ku",
"-...-": "me",
"-..": "ho",
"---": "re",
".--": "ya",
"..-.-": "mi",
".": "he",
"---.": "so",
"-..-": "ma",
"--.-.": "shi",
"..-..": "to",
".--.": "tsu",
"-.--": "ke",
".--..": "we",
"..-.": "chi",
"--.-": "ne",
"--..": "fu",
"--..-": "hi",
"--.": "ri",
".-.": "na",
"----": "ko",
"-..-.": "mo",
"....": "nu",
"...": "ra",
"-.---": "e",
".---.": "se",
"-.--.": "ru",
"-": "mu",
".-.--": "te",
"---.-": "su",
".---": "wo",
"..-": "u",
"--.--": "a",
".-.-.": "n",
}
res = ""
for e in ciphertext.split():
res += kana.get(e, e)
return res
#--------------------------Morbit
# Definiujemy słowniki dla kodów Morse'a
morseDict = {'.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D', '.': 'E', '..-.': 'F', '--.': 'G', '....': 'H', '..': 'I', '.---': 'J', '-.-': 'K', '.-..': 'L', '--': 'M', '-.': 'N', '---': 'O', '.--.': 'P', '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T', '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X', '-.--': 'Y', '--..': 'Z'}
alphaDict = {'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.', 'F': '..-.', 'G': '--.', 'H': '....', 'I': '..', 'J': '.---', 'K': '-.-', 'L': '.-..', 'M': '--', 'N': '-.', 'O': '---', 'P': '.--.', 'Q': '--.-', 'R': '.-.', 'S': '...', 'T': '-', 'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-', 'Y': '-.--', 'Z': '--..'}
def convertFromMorse(msg, table):
ret = []
valid = True
msgAry = msg.split('x')
for ch in msgAry:
if ch in table: ret.append(table[ch])
elif len(ch) == 0: ret.append(' ')
else: valid = False
if valid:
ret = ''.join(ret).strip()
if ' ' in ret: valid = False
return valid, ret
# Ładujemy tabelę Morbit
def loadMorbitTable():
return ['..', '.-', '.x', '-.', '--', '-x', 'x.', 'x-', 'xx']
# Funkcja do deszyfrowania Morbit
def decryptMorbit(msg, morbitTable, key):
ret = ''
for num in msg:
ret += morbitTable[key.index(int(num))]
return ret
# Konwertujemy klucz "AZERTYUIO" na numer
def keyToNumber(key):
sorted_key = sorted(key)
key_number = []
for char in key:
key_number.append(sorted_key.index(char) + 1)
return key_number
# Główna funkcja dekodująca
def decodeMorbit(ciphertext, key):
morbitTable = loadMorbitTable()
key_number = keyToNumber(key)
morse_code = decryptMorbit(ciphertext, morbitTable, key_number)
valid, plaintext = convertFromMorse(morse_code, morseDict)
if valid:
return plaintext.lower()
else:
return "Nie udało się zdekodować wiadomości."
#--------------------------guitar_chords
def guitar_chords(input_string):
# Mapa wartości na akordy
chord_map = {
'x02220': 'A',
'x24442': 'B',
'x32010': 'C',
'xx0232': 'D',
'022100': 'E',
'133211': 'F',
'320003': 'G'
}
# Podziel ciąg znaków na pojedyncze wartości
values = input_string.strip().split()
# Przetwórz każdą wartość i zbierz odpowiadające litery
result = []
for value in values:
chord = chord_map.get(value, '?') # Domyślna wartość '?', jeśli wartość nie jest w mapie
result.append(chord)
# Połącz litery w jeden ciąg znaków
return ''.join(result).lower()
counter=0
p = remote(ADDRESS,PORT,ssl=True)
p.recvuntil(b'Good luck!')
empty=p.recv()
for i in range (100):
cipher=p.recv().strip()
counter+=1
warn(f"counter: {counter}")
#guitar-chords-notation
if b'Hendrix' in cipher:
pattern = r"cipher:\s*([^\n]+)"
match = re.search(pattern, cipher.decode())
cipher_text = match.group(1).strip()
result=guitar_chords(cipher_text)
warn("guitar-chords-notation")
warn(cipher_text)
warn(result)
p.sendline(result.encode())
#Morbit
if b'A code based on pairs of dots and dashes.' in cipher:
pattern = r"cipher:\s*([^\n]+)"
match = re.search(pattern, cipher.decode())
cipher_text = match.group(1).strip()
key = "AZERTYUIO"
result=decoded_message = decodeMorbit(cipher_text, key)
warn("morbit")
warn(cipher_text)
warn(result)
p.sendline(result.encode())
#kana
if b'It looks like Morse code' in cipher:
pattern = r"cipher:\s*([^\n]+)"
match = re.search(pattern, cipher.decode())
cipher_text = match.group(1).strip()
result=kana_decrypt(cipher_text)
warn("kana")
warn(cipher_text)
warn(result)
p.sendline(result.encode())
#chuck_norris_unary
if b'He can snap his toes' in cipher:
pattern = r"cipher:\s*([^\n]+)"
match = re.search(pattern, cipher.decode())
cipher_text = match.group(1).strip()
result=chuck_norris_unary_to_ascii(cipher_text)
warn ("chuck_norris_unary")
warn(cipher_text)
warn(result)
p.sendline(result.encode())
#trithemius
if b'Born in 1462 in Germany' in cipher:
pattern = r"cipher:\s*([^\n]+)"
match = re.search(pattern, cipher.decode())
cipher_text = match.group(1).strip()
result=decrypt_trithemius_plus_3(cipher_text)
warn("trithemius")
warn(cipher_text)
warn(result)
p.sendline(result.encode())
#leet
if b'1337 ...' in cipher:
pattern = r"cipher:\s*([^\n]+)"
match = re.search(pattern, cipher.decode())
cipher_text = match.group(1).strip()
result=leet_decoder(cipher_text)
warn("1337")
warn(cipher_text)
warn(result)
p.sendline(result.encode())
#charabia?
if b'what is this charabia' in cipher:
pattern = r"cipher:\s*([^\n]+)"
match = re.search(pattern, cipher.decode())
cipher_text = match.group(1).strip()
result=charabia(cipher_text)
warn("charabia")
warn(cipher_text)
warn(result)
p.sendline(result.encode())
#shankar_speech (thanx for ppp45)
if b'slumdog millionaire' in cipher:
pattern = r"cipher:\s*([^\n]+)"
match = re.search(pattern, cipher.decode())
cipher_text = match.group(1).strip()
result=shankar_speech_defect(cipher_text)
warn("shankar_speech")
warn(cipher_text)
warn(result)
p.sendline(result.encode())
#decode_baudot_msb
if b'CTF 150 years' in cipher:
pattern = r"cipher:\s*([01\s]+)"
match = re.search(pattern, cipher.decode())
cipher_text = match.group(1).strip()
result=decode_baudot_msb(cipher_text)
warn("decode_baudot_msb")
warn(cipher_text)
warn(result)
p.sendline(result.encode())
#first_bit_letters
if cipher.startswith(b'cipher:'):
# Dekodujemy bajty na string i wywołujemy funkcję big_letter
result = big_letter(cipher.decode('utf-8'))
warn("big_letters")
warn(cipher)
warn(result)
p.sendline(result.encode())
warn("-----------------")
p.interactive()
Summary
The task was fantastic. I don’t regret spending my Saturday on a single task. I learned a lot of new ciphers.
PWNME{d4m_y0U_4r3_F4st!_4nd_y0u_kn0w_y0ur_c1ph3rs!}