Twisted Text

Задание

В убежище нашли старый терминал, который до сих пор работает. По слухам, он использовался для шифрования секретных данных. У тебя есть возможность подключиться к нему, изучить его алгоритмы и узнать, что за тайны в нем скрываются.

Решение

Подключение для выполнения данного задания происходит по Netcat.

Для выполнения задания был дан файл с исходным кодом сервиса:

import os
import socket
import random
from threading import Thread
 
alphabet = "abcdefghijklmnopqrstuvwxyz1234567890-_=+!;:?*,."
 
def generate_key():
    key_length = random.randint(5, 30)
    key = ''.join(random.choice(alphabet) for _ in range(key_length))
    return key
 
def encrypt(text, key):
    encrypted_text = []
    key_length = len(key)
    key_index = 0
 
    for char in text:
        if char in alphabet:
            char_text_index = alphabet.index(char)
            key_char = key[key_index % key_length]
            key_index_in_alphabet = alphabet.index(key_char)
 
            encrypted_index = (char_text_index + key_index_in_alphabet) % len(alphabet)
            encrypted_text.append(alphabet[encrypted_index])
 
            key_index += 1
        
        else:
            encrypted_text.append(char)
 
    return ''.join(encrypted_text)
 
def decrypt(encrypted_text, key):
    decrypted_text = []
    key_length = len(key)
    key_index = 0
 
    for char in encrypted_text:
        if char in alphabet:
            encrypted_index = alphabet.index(char)
            key_char = key[key_index % key_length]
            key_index_in_alphabet = alphabet.index(key_char)
 
            decrypted_index = (encrypted_index - key_index_in_alphabet) % len(alphabet)
            decrypted_text.append(alphabet[decrypted_index])
 
            key_index += 1
        else:
            decrypted_text.append(char)
 
    return ''.join(decrypted_text)
 
def menu(conn, key, flag):
    conn.sendall("\nWelcome. You can encrypt or decrypt some text or get the flag.\nThe encryption key changes every time you get a flag.\n".encode())
    
    while True:
        conn.sendall("1 - Encrypt\n2 - Decrypt\n3 - Get flag\n0 - Exit\n".encode())
        choice = str(conn.recv(1024).decode().strip())
 
        if choice == "1":
            conn.sendall("Enter text:\n".encode())
            text = str(conn.recv(4096).decode().strip())
 
            encrypted_text = encrypt(text.lower(), key)
            conn.sendall(f"Encrypted text: {encrypted_text}\n\n".encode())
 
        elif choice == "2":
            conn.sendall("Enter ciphertext:\n".encode())
            ciphertext = str(conn.recv(4096).decode().strip())
 
            decrypted_text = decrypt(ciphertext.lower(), key)
            conn.sendall(f"Decrypted text: {decrypted_text}\n\n".encode())
 
        elif choice == "3":
            encrypted_flag = encrypt(flag, key)
            conn.sendall(f"Your flag: {encrypted_flag}\n\n".encode())
            
            key = generate_key()
 
        elif choice == "0":
            break
 
        else:
            conn.sendall("Incorrect input!\n\n".encode())
 
def handle_client(conn, addr):
    print(f"New connection from {addr}")
 
    flag = f"ptech2024{{{os.getenv('flag')}}}\n"
 
    try:
        key = generate_key()
        menu(conn, key, flag)
 
    except Exception as e:
        print(f"Error in session with {addr}: {e}")
    
    finally:
        conn.close()
 
def server_get_connections():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('0.0.0.0', 6789))
    server_socket.listen(10)
 
    print("Server started. Listening on port 6789....")
 
    while True:
        conn, addr = server_socket.accept()
        client_thread = Thread(target=handle_client, args=(conn, addr))
        client_thread.start()
 
if __name__ == "__main__":
    server_get_connections()

После подключения пользователю доступно меню с основным функционалом сервиса:

Welcome. You can encrypt or decrypt some text or get the flag.
The encryption key changes every time you get a flag.
1 - Encrypt
2 - Decrypt
3 - Get flag
0 - Exit

В данном сервисе пользователь может зашифровать и расшифровать текст, а также получить флаг. Однако флаг выдается пользователю в зашифрованном виде:

Your flag: 7eh7*27,-{3lq!fm_znuwqg+s,7bm=or3.t_dmpq=pq4p,z,jp}

Разберем подробнее функцию шифрования. Как можно заметить, в данном задании использован шифр подстановки. Для каждого символа в шифруемом тексте происходит сдвиг индекса в алфавите путем сложения с индексом символа ключа. На основании этого, можно сделать вывод о том, что это шифр Виженера.

def encrypt(text, key):
    encrypted_text = []
    key_length = len(key)
    key_index = 0
 
    for char in text:
        if char in alphabet:
            char_text_index = alphabet.index(char)
            key_char = key[key_index % key_length]
            key_index_in_alphabet = alphabet.index(key_char)
 
            encrypted_index = (char_text_index + key_index_in_alphabet) % len(alphabet)
            encrypted_text.append(alphabet[encrypted_index])
 
            key_index += 1
        
        else:
            encrypted_text.append(char)
 
    return ''.join(encrypted_text)

Продолжим анализ кода. Шифрование текста, введенного пользователем, и флага происходит с использованием одного ключа. Однако после получения флага ключ шифрования изменяется, поэтому не получится просто расшифровать флаг с помощью соответствующей функции сервиса.

def menu(conn, key, flag):
    conn.sendall("\nWelcome. You can encrypt or decrypt some text or get the flag.\nThe encryption key changes every time you get a flag.\n".encode())
    
    while True:
        conn.sendall("1 - Encrypt\n2 - Decrypt\n3 - Get flag\n0 - Exit\n".encode())
        choice = str(conn.recv(1024).decode().strip())
 
        if choice == "1":
            conn.sendall("Enter text:\n".encode())
            text = str(conn.recv(4096).decode().strip())
 
            encrypted_text = encrypt(text.lower(), key)
            conn.sendall(f"Encrypted text: {encrypted_text}\n\n".encode())
 
        elif choice == "2":
            conn.sendall("Enter ciphertext:\n".encode())
            ciphertext = str(conn.recv(4096).decode().strip())
 
            decrypted_text = decrypt(ciphertext.lower(), key)
            conn.sendall(f"Decrypted text: {decrypted_text}\n\n".encode())
 
        elif choice == "3":
            encrypted_flag = encrypt(flag, key)
            conn.sendall(f"Your flag: {encrypted_flag}\n\n".encode())
            
            key = generate_key()

Однако зашифровав текст вида aaaaaaaaa{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} (длина должна совпадать с длиной флага), можно вычислить смешения индексов для каждого символа флага. Далее можно просто вычесть смешения символов и получить искомый флаг.

Ниже приведен код для решения данного задания:

alphabet = "abcdefghijklmnopqrstuvwxyz1234567890-_=+!;:?*,."
 
def calculate_ciphertext_offsets(cipher_text, text):
    ciphertext_offsets = []
    
    for i in range(len(cipher_text)):
        if cipher_text[i] in alphabet:
            offset = alphabet.index(cipher_text[i]) - alphabet.index(text[i])
        else:
            offset = 0
        
        ciphertext_offsets.append(offset)
    
    return ciphertext_offsets
 
def decrypt_flag(encrypted_flag, offsets):
    flag = []
 
    for i in range(len(offsets)):
        if encrypted_flag[i] in alphabet:
            flag.append(alphabet[alphabet.index(encrypted_flag[i]) - offsets[i]])
        else:
            flag.append(encrypted_flag[i])
    
    return ''.join(flag)
 
def main():
    text = input("Enter text:\n")
    cipher_text = input("Enter ciphertext:\n")
    encrypted_flag = input("Enter encrypted_flag\n")
 
    offsets = calculate_ciphertext_offsets(cipher_text, text)
    flag = decrypt_flag(encrypted_flag, offsets)
 
    print(f"Flag: {flag}")
 
main()