team-logo
Published on

PwnMe 2025 - Decode runner

Authors

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.

alt text

Table of contents

Second introduction

alt text The idea was fairly simple. You connected to the console, and within 3 seconds, you had to decode a ciphers 100 times. Simple, right? For a bot, yes. Of course, you first had to figure out what those ciphers were. So, some time passed before I understood what was what. Luckily, almost all of them, if not all, were available on dcode.fr. But that became clear later when we were searching for some exotic ciphers like Morse code in numbers. Yes, not all the ciphers were straightforward, and on top of that, they had to be implemented. Fortunately, there were hints for them. If it weren’t for ChatGPT and DeepSeek, I probably wouldn’t have made it. ppp45 helped me too. In the past, this task would probably have had just one cipher, but here there were about 10. I won’t describe each one, I’ll just leave a link to dcode for them. For the simpler ones that I did myself, I’ll give a short description. And of course, at the end, I’ll include the source code.
alt text

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!}