Препарируем OpenVPN. Часть 1. Статические ключи
Однажды столкнулся с непонятной ошибкой в установлении OpenVPN соединения и ощутил нехватку понимания того, как он устроен внутри. Под катом рассказ о том, как устроена криптографическая часть протокола, как это всё выглядит в реальности (т.е. в Wireshark) и как заглянуть внутрь VPN, т.е. расшифровать трафик руками (при наличии ключей, конечно ;-) ). В этой части рассмотрим только режим со статическими ключами.
Режимы работы
Сам OpenVPN без плагинов поддерживает 2 варианта установления соединения:
- PSK (PreSharedKey или static key mode) – с симметричным ключом, который генерируется и записывается на все машины в процессе настройки VPN. В первой части статьи рассматривается только этот режим.
- TLS – основанный на инфраструктуре открытых ключей (PKI). В начале устанавливается TLS соединение с аутентификацией обеих сторон по сертификатам, через которое производится обмен ключевым материалом. Из этого ключевого материала генерируются симметричные ключи и трафик внутри VPN шифруется уже этими ключами так же как в режиме PSK. Внутри TLS канала пользовательского трафика нет – исключительно служебная информация самого OpenVPN. Планирую написать вторую часть про препарирование OpenVPN в этом режиме.
Подробнее про PSK режим
Режим PSK включается опцией secret, первым параметром которой является имя файла со статическими ключами, которые генерируются на этапе настройки VPN. В файле находятся 2 пары 512 битных случайных ключей (в каждой паре ключ для шифрования и ключ для HMAC), записанные один за другим (без заголовков и прочего) и закодированные в HEX. Одинаковый файл с ключами должен быть на всех компьютерах, соединяемых VPN. Если для опции secret не задать второй необязательный параметр — direction, то будет использоваться только первая пара ключей, т. е. в обе стороны трафик будет подписываться и шифроваться одинаковыми ключами.
В режиме static key может использоваться только CBC и No-crypto форматы пакетов с данными (data channel crypto format). Дальше рассматриваем формат CBC, т.к. No-crypto не так интересен.
По умолчанию для шифрования используется BlowFish в режиме CBC c 128 битным ключом, а для HMAC используется SHA1 и 160 битный ключ. В качестве ключей используются соответственно первые 128 и 160 бит от 512 битных ключей из файла со статическим ключом.
Так как вся ключевая информация и настройки задаются заранее, то, если адрес удалённого хоста (опция remote) задан, инкапсулированный трафик может передаваться сразу после запуска OpenVPN ещё до обмена управляющими пакетами (о них будет ниже).
Тестовый стенд
Для тестов использовался следующий ключ (генерируется командой openvpn --genkey --secret psk.key):
Фактически каждые 4 строчки с HEX — это 512 битный ключ. 1-ый и 3-ий — ключи для шифрования, из каждого в примере будут использоваться только первые 128 бит (остальные 384 бита никак не используются), 2-ой и 4-ый — ключи для HMAC, из каждого будут использоваться первые 160 бит. Другими словами:
unhexlify просто превращает HEX строку в массив байт. Индекс [:16] значит что мы берём первые 16 элементов (байт) от последовательности. Индекс [16:] значит что мы берём всё, кроме первых 16 байт от последовательности. Индекс [16:20] значит что мы берём первые 20 байт и отбрасываем первые 16 из них.
Файлы настроек при работе через UDP в режиме static key с использованием обеих пар ключей (каждое направление шифруется своей парой):
Где 192.168.0.1 и 192.168.0.2 — адреса физических интерфейсов машин, а 192.168.1.1 и 192.168.1.2 — соответствующие им адреса внутри туннеля.
Опцию remote достаточно указать только в одном из конфигурационных файлов. В таком случае хост, которому remote не задана, будет просто ждать входящего соединения (в данном случае UDP пакета).
Препарируем
Формат передаваемого пакета такой:
Len — длина всего пакета, кроме первых двух байт; используется только при работе через TCP. При работе через UDP длина пакета OpenVPN рассчитывается из длины UDP пакета, а поля Len в OpenVPN пакете нет. HMAC — HMAC-SHA1 от всего, что идёт после HMAC (IV и зашифрованные данные) IV — вектор инициализации для режима CBC. Всё, что идёт после IV, передаётся зашифрованным. Packet ID — номер пакета. Он включён сюда для защиты от повторов, и не используется для организации гарантированной доставки данных. timestamp — так же используется для защиты от повторов. packet payload — данные, которые несёт пакет. Это либо служебная информация OpenVPN, либо сам инкапсулированный трафик.
HMAC(key, message) — функция на основе криптографической хэш-функции (в данном случае на основе SHA1), которая генерирует код аутентификации — строку бит, служащую подписью для сообщения. Имея секретный ключ, сообщение и код аутентификации любой может проверить правильность кода аутентификации. Если код правильный — значит сообщение (в нашем случае пакет) не менялось с того момента как его подписал кто-то, у кого есть такой же ключ. Т.е. для проверки HMAC, в отличии от ЭЦП, обе стороны должны иметь один и тот же секретный ключ.
Расшифровываем служебные пакетыТеперь обещанный wireshark (вопреки утверждению на странице диссектора, OpenVPN в режиме static key он не понимает):
Возьмём первый пакет и попробуем его расшифровать. Т.к. VPN настроен на работу через UDP, то поля Len нет, пакет начинается сразу с HMAC. Попробуем посчитать чему должен равняться HMAC от этого пакета без поля HMAC:
Видим, что рассчитанный нами HMAC точно совпадает с HMAC, вставленным в пакет. Значит аутентификация пакета пройдена, а мы на правильном пути! Теперь пора его расшифровать.
Первые 4 байта — это номер пакета. Всё правильно, пакет мы взяли первый. Вторые 4 байта — временная метка. Семь байт 0x07 в конце — это паддинг (байты, добавленные при шифровании для того, что бы длина сообщения была кратна длине блока), он правильный, отбрасываем.
На первый взгляд похоже на то, что передаётся 16-байтная случайная строка и нулевой байт. Т.к. OpenVPN — проект с открытым кодом, лезем в код и в файле occ.c находим ответ, частично подтверждающий гипотезу.
287f346bd4ef7a812d56b8d3afc5459c — это константа occ_magic — признак того, что пакет является управляющим пакетом OpenVPN. После неё следует признак типа управляющего пакета, в данном случае это байт 0x00 == OCC_REQUEST. В пакете с OCC_REQUEST больше ничего не должно передаваться.
Теперь расшифруем второй пакет:
Видим, что первые 16 байт этого пакета — occ_magic, а следущий байт (тип управляющего пакета) 0x01 == OCC_REPLY. В пакете OCC_REPLY после типа пакета передаётся си-строка с параметрами. В данном случае передаётся такой текст: V4,dev-type tun,link-mtu 1544,tun-mtu 1500,proto UDPv4,ifconfig 192.168.1.1 192.168.1.2,keydir 1,cipher BF-CBC,auth SHA1,keysize 128,secret. Эту же строку можно найти в логе OpenVPN.
Следующие два пакета — OCC_REQUEST от второго хоста первому и OCC_REPLY на него. Расшифровываем их аналогичным образом:
После получения настроек удалённого хоста локальный хост проверяет соответствие своих и чужих настроек и выдаёт предупреждение если есть несоответствия. В данном случае видим, что настройки VPN на обеих сторонах совпадают с точностью до направления:
V4,dev-type tun,link-mtu 1544,tun-mtu 1500,proto UDPv4,ifconfig 192.168.1.1 192.168.1.2,keydir 1,cipher BF-CBC,auth SHA1,keysize 128,secret V4,dev-type tun,link-mtu 1544,tun-mtu 1500,proto UDPv4,ifconfig 192.168.1.2 192.168.1.1,keydir 0,cipher BF-CBC,auth SHA1,keysize 128,secret
Расшифровываем инкапсулированный трафикСледующий пакет в этом дампе трафика несёт пользовательские данные. Расшифровываем их так же, как и раньше:
Тут нет ни occ_magic, ни ping_magic (другая магическая константа равная 2a187bf3641eb4cb07ed2d0a981fc748), значит это пакет с данными. На самом деле 0x45 — это первый байт IPv4 пакета без опциональных полей заголовка. Видим, что это ровно тот же IPv4 пакет с ping-request, что и вышел из tun интерфейса (в hex на картинке IPv4 начинается со 2-ой строчки):
Видно, что это ровно тот же IPv4 с ping-reply, который был отправлен в туннель:
Замечания
OpenVPN PingЕсть ещё ping пакеты — служебные пакеты, которые OpenVPN посылает что бы подтвердить наличие соединения если прошло слишком много времени с момента отправки последнего пакета другому хосту (настраивается опциями keepalive, inactive и ping*). В отличии от обычного icmp ping, OpenVPN не отвечает на полученный пакет, а просто запоминает что соединение живо.
nmap vs OpenVPNUDP порт, на котором слушает OpenVPN в режиме PSK (а так же в режиме TLS с включённой опцией TLS-AUTH), для всех, кто не знает секретный ключ, будет выглядеть как будто все пакеты на нём дропаются. nmap покажет статус этого порта как "open|filtered". В логах OpenVPN (при достаточно высоком значении опции verb) при сканировании порта будут записи "Authenticate/Decrypt packet error: packet HMAC authentication failed".
Обнаружение трафика OpenVPNВ режиме PSK при работе через UDP всё содержимое пакетов OpenVPN для стороннего наблюдателя (у которого нет секретных ключей) будут выглядеть случайным, т.е. там нет сигнатур, по которым можно было бы отнести эти пакеты к OpenVPN. Однако выбиваться из образа случайных данных будет длина пакетов — она зависит от используемой хэш-функции и меняется с шагом, равным размеру блока используемого шифра, а также наличие служебных пакетов фиксированной длины (OCC_REQUEST и OpenVPN PING пакеты).
При работе через TCP кроме того в начале каждого OpenVPN пакета будет незашифрованное 2-х байтное поле длины пакета.
Заключение
Видно, что в режиме PSK всё устроено очень просто. Здесь даже толком нет разделения клиент/сервер.
В режиме TLS установление соединения проходит намного сложней, но зато он позволяет использовать инфраструктуру открытых ключей, обеспечивает регулярную смену сессионных ключей и реализует Perfect forward secrecy. Но о режиме TLS в следующей части.