Однажды столкнулся с непонятной ошибкой в установлении 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):


#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
5234f60f846bb1d5f059c70e75434be6    <-- Тут начинается первый ключ шифрования
89f41113ef56e2bf69253fad6a30ab5f
a449204f52f64a0265fd5744a9489f41
cefd95a1d642830e9cf9cdce13c55245
270cf7d367ddc6b56eb1ba749be40e42    <-- Тут начинается первый ключ для HMAC
80cdf7cfbad178348a0e057f1fdc87f0
e5ba84717475b868a7fd617fee8c561c
b4575d983534ef71dd8aaa48a53ed469
0ae85faf4522f7defd89ba373a0d22ee    <-- Тут начинается второй ключ шифрования
f9356e487fdfa0796b0b1fc393fd3ab5
52db7a1ed691ddd50eeb7be2cc32d912
52df93987f4878ec42a12e3a7cda5a40
b1aa850a05821b300359a796313cbcec    <-- Тут начинается второй ключ для HMAC
4076483ec7692708c32d323ed080beba
60c58d9281fb5d27c688ac271d3b6d15
1695093291fb788dbbfefb8b7c7f9bef
-----END OpenVPN Static key V1-----

Фактически каждые 4 строчки с HEX — это 512 битный ключ. 1-ый и 3-ий — ключи для шифрования, из каждого в примере будут использоваться только первые 128 бит (остальные 384 бита никак не используются), 2-ой и 4-ый — ключи для HMAC, из каждого будут использоваться первые 160 бит. Другими словами:


key_encrypt_0 = unhexlify("5234f60f846bb1d5f059c70e75434be689f41113ef56e2bf69253fad6a30ab5fa449204f52f64a0265fd5744a9489f41cefd95a1d642830e9cf9cdce13c55245")[:16]
key_hmac_0 = unhexlify("270cf7d367ddc6b56eb1ba749be40e4280cdf7cfbad178348a0e057f1fdc87f0e5ba84717475b868a7fd617fee8c561cb4575d983534ef71dd8aaa48a53ed469")[:20]
key_encrypt_1 = unhexlify("0ae85faf4522f7defd89ba373a0d22eef9356e487fdfa0796b0b1fc393fd3ab552db7a1ed691ddd50eeb7be2cc32d91252df93987f4878ec42a12e3a7cda5a40")[:16]
key_hmac_1 = unhexlify("b1aa850a05821b300359a796313cbcec4076483ec7692708c32d323ed080beba60c58d9281fb5d27c688ac271d3b6d151695093291fb788dbbfefb8b7c7f9bef")[:20]

Для тех, кто не знаком с Python

unhexlify просто превращает HEX строку в массив байт.
Индекс [:16] значит что мы берём первые 16 элементов (байт) от последовательности.
Индекс [16:] значит что мы берём всё, кроме первых 16 байт от последовательности.
Индекс [16:20] значит что мы берём первые 20 байт и отбрасываем первые 16 из них.


Файлы настроек при работе через UDP в режиме static key с использованием обеих пар ключей (каждое направление шифруется своей парой):


# Первый хост
dev tun
ifconfig 192.168.1.1 192.168.1.2
remote 192.168.0.2
secret psk.key 0

# Второй хост
dev tun
ifconfig 192.168.1.2 192.168.1.1
remote 192.168.0.1
secret psk.key 1

Где 192.168.0.1 и 192.168.0.2 — адреса физических интерфейсов машин, а 192.168.1.1 и 192.168.1.2 — соответствующие им адреса внутри туннеля.


Опцию remote достаточно указать только в одном из конфигурационных файлов. В таком случае хост, которому remote не задана, будет просто ждать входящего соединения (в данном случае UDP пакета).


Препарируем


Формат передаваемого пакета такой:


[Len (2 байта)] | HMAC (20 байт) | IV (8 байт) | packet ID (4 байта) | timestamp (4 байта) | packet payload

Len — длина всего пакета, кроме первых двух байт; используется только при работе через TCP. При работе через UDP длина пакета OpenVPN рассчитывается из длины UDP пакета, а поля Len в OpenVPN пакете нет.
HMAC — HMAC-SHA1 от всего, что идёт после HMAC (IV и зашифрованные данные)
IV — вектор инициализации для режима CBC.
Всё, что идёт после IV, передаётся зашифрованным.
Packet ID — номер пакета. Он включён сюда для защиты от повторов, и не используется для организации гарантированной доставки данных.
timestamp — так же используется для защиты от повторов.
packet payload — данные, которые несёт пакет. Это либо служебная информация OpenVPN, либо сам инкапсулированный трафик.


Что такое HMAC

HMAC(key, message) — функция на основе криптографической хэш-функции (в данном случае на основе SHA1), которая генерирует код аутентификации — строку бит, служащую подписью для сообщения. Имея секретный ключ, сообщение и код аутентификации любой может проверить правильность кода аутентификации. Если код правильный — значит сообщение (в нашем случае пакет) не менялось с того момента как его подписал кто-то, у кого есть такой же ключ. Т.е. для проверки HMAC, в отличии от ЭЦП, обе стороны должны иметь один и тот же секретный ключ.


Расшифровываем служебные пакеты


Теперь обещанный wireshark (вопреки утверждению на странице диссектора, OpenVPN в режиме static key он не понимает):



Возьмём первый пакет и попробуем его расшифровать. Т.к. VPN настроен на работу через UDP, то поля Len нет, пакет начинается сразу с HMAC. Попробуем посчитать чему должен равняться HMAC от этого пакета без поля HMAC:


packet = unhexlify("7c0a45b025b3225c9a60051699fc87bae44dec9026e792a7ff3842aabd7cc77b439564b42002e5e327cba04c620cd80ca92c60139e2345a2c5d99b0f")
print("HMAC = %s" % HMAC(key_hmac_0, packet[20:], 'sha1').hexdigest())

HMAC = 7c0a45b025b3225c9a60051699fc87bae44dec90

Видим, что рассчитанный нами HMAC точно совпадает с HMAC, вставленным в пакет. Значит аутентификация пакета пройдена, а мы на правильном пути! Теперь пора его расшифровать.


iv = packet[20:28]
encrypted_part = packet[28:]
decrypted = Blowfish.new(key_encrypt_0, mode=Blowfish.MODE_CBC, IV=iv).decrypt(encrypted_part)
print("Открытый текст: %s" % hexlify(decrypted).decode())

Открытый текст: 00000001 58069a45 287f346bd4ef7a812d56b8d3afc5459c00 07070707070707

Первые 4 байта — это номер пакета. Всё правильно, пакет мы взяли первый. Вторые 4 байта — временная метка. Семь байт 0x07 в конце — это паддинг (байты, добавленные при шифровании для того, что бы длина сообщения была кратна длине блока), он правильный, отбрасываем.


packet_id = decrypted[:4]
timestamp = decrypted[4:8]
decrypted_data = decrypted[8:-decrypted[-1]]  # Убираем packet_id и timestamp из начала и паддинг с конца
print("ID пакета: %s" % hexlify(packet_id).decode())
print("Временная метка: %s" % hexlify(timestamp).decode())
print("Содержимое пакета: %s" % hexlify(decrypted_data).decode())

ID пакета: 00000001
Временная метка: 58069a45
Содержимое пакета: 287f346bd4ef7a812d56b8d3afc5459c00

На первый взгляд похоже на то, что передаётся 16-байтная случайная строка и нулевой байт. Т.к. OpenVPN — проект с открытым кодом, лезем в код и в файле occ.c находим ответ, частично подтверждающий гипотезу.


287f346bd4ef7a812d56b8d3afc5459c — это константа occ_magic — признак того, что пакет является управляющим пакетом OpenVPN. После неё следует признак типа управляющего пакета, в данном случае это байт 0x00 == OCC_REQUEST. В пакете с OCC_REQUEST больше ничего не должно передаваться.


Теперь расшифруем второй пакет:



packet = unhexlify("91861a4479c376d3013cdcd7f3e657ac093048cdb3a40775fc5804f68842ef06288a9343acb298562aaa4c92c10aae2213441619c3df0d8b89a0f13f5dd69d1eaa0e22667938b68e38a1238e39b76b889b7b6e5dbadd8f3a264f6fc9bb9c225de387d5d17914b2e2853d7fbb706da429f596721ab394febe7e52875183f0e9e10ec16042398c4116c2069ef18f06da45b55581a9997d1f54def143b053226727934404be44379a0172d1b618bbf1b676b60f57889e734af6b54291d07d9c7e13573cc81e")
# Так как в конфигурационном файле в опции 'secret' указан необязательный параметр 'direction', то трафик в эту сторону шифруется второй парой ключей.
key_hmac = key_hmac_1
key_encrypt = key_encrypt_1

print("HMAC = %s" % HMAC(key_hmac, packet[20:], 'sha1').hexdigest())
iv = packet[20:28]
encrypted_part = packet[28:]
decrypted = Blowfish.new(key_encrypt, mode=Blowfish.MODE_CBC, IV=iv).decrypt(encrypted_part)
print("Открытый текст: %s" % hexlify(decrypted).decode())
packet_id = decrypted[:4]
timestamp = decrypted[4:8]
decrypted_data = decrypted[8:-decrypted[-1]]  # Убираем packet_id и timestamp из начала и паддинг с конца
print("ID пакета: %s" % hexlify(packet_id).decode())
print("Временная метка: %s" % hexlify(timestamp).decode())
print("Содержимое пакета HEX: %s" % hexlify(decrypted_data).decode())
print("Содержимое пакета: %s" % decrypted_data)

HMAC = 91861a4479c376d3013cdcd7f3e657ac093048cd
Открытый текст: 0000000158069a45287f346bd4ef7a812d56b8d3afc5459c0156342c6465762d747970652074756e2c6c696e6b2d6d747520313534342c74756e2d6d747520313530302c70726f746f2055445076342c6966636f6e666967203139322e3136382e312e31203139322e3136382e312e322c6b657964697220312c6369706865722042462d4342432c6175746820534841312c6b657973697a65203132382c73656372657400030303
ID пакета: 00000001
Временная метка: 58069a45
Содержимое пакета HEX: 287f346bd4ef7a812d56b8d3afc5459c0156342c6465762d747970652074756e2c6c696e6b2d6d747520313534342c74756e2d6d747520313530302c70726f746f2055445076342c6966636f6e666967203139322e3136382e312e31203139322e3136382e312e322c6b657964697220312c6369706865722042462d4342432c6175746820534841312c6b657973697a65203132382c73656372657400
Содержимое пакета в ASCII: b'(\x7f4k\xd4\xefz\x81-V\xb8\xd3\xaf\xc5E\x9c\x01V4,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\x00'

Видим, что первые 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 на него. Расшифровываем их аналогичным образом:



HMAC = fc52f6981ccb4d7c2a3c951bd1ce8e882f9155da
ID пакета: 00000002
Временная метка: 58069a45
Содержимое пакета HEX: 287f346bd4ef7a812d56b8d3afc5459c00
Содержимое пакета: b'(\x7f4k\xd4\xefz\x81-V\xb8\xd3\xaf\xc5E\x9c\x00'


HMAC = e4987efec6d4625e89ba26b69b0d8f54ed391c3c
ID пакета: 00000002
Временная метка: 58069a45
Содержимое пакета HEX: 287f346bd4ef7a812d56b8d3afc5459c0156342c6465762d747970652074756e2c6c696e6b2d6d747520313534342c74756e2d6d747520313530302c70726f746f2055445076342c6966636f6e666967203139322e3136382e312e32203139322e3136382e312e312c6b657964697220302c6369706865722042462d4342432c6175746820534841312c6b657973697a65203132382c73656372657400
Содержимое пакета: b'(\x7f4k\xd4\xefz\x81-V\xb8\xd3\xaf\xc5E\x9c\x01V4,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\x00'

После получения настроек удалённого хоста локальный хост проверяет соответствие своих и чужих настроек и выдаёт предупреждение если есть несоответствия. В данном случае видим, что настройки 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


Расшифровываем инкапсулированный трафик


Следующий пакет в этом дампе трафика несёт пользовательские данные. Расшифровываем их так же, как и раньше:



HMAC = efe4850d41f0e4a7535f13310685d74c2722a0da
ID пакета: 00000003
Временная метка: 58069a45
Содержимое пакета HEX: 4500005420874000400196cec0a80102c0a80101080066ed1a1300014d9a0658000000005a390a0000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637

Тут нет ни occ_magic, ни ping_magic (другая магическая константа равная 2a187bf3641eb4cb07ed2d0a981fc748), значит это пакет с данными. На самом деле 0x45 — это первый байт IPv4 пакета без опциональных полей заголовка. Видим, что это ровно тот же IPv4 пакет с ping-request, что и вышел из tun интерфейса (в hex на картинке IPv4 начинается со 2-ой строчки):



Ответный пакет:



HMAC = a0a0108cd173e8829b91e8b34bd5cac2ab8a9c0b
ID пакета: 00000003
Временная метка: 58069a45
Содержимое пакета HEX: 450000547f00000040017855c0a80101c0a8010200006eed1a1300014d9a0658000000005a390a0000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637

Видно, что это ровно тот же IPv4 с ping-reply, который был отправлен в туннель:



Замечания


OpenVPN Ping


Есть ещё ping пакеты — служебные пакеты, которые OpenVPN посылает что бы подтвердить наличие соединения если прошло слишком много времени с момента отправки последнего пакета другому хосту (настраивается опциями keepalive, inactive и ping*). В отличии от обычного icmp ping, OpenVPN не отвечает на полученный пакет, а просто запоминает что соединение живо.


nmap vs OpenVPN


UDP порт, на котором слушает 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 в следующей части.

Комментарии (4)


  1. alex_scorp
    31.10.2017 21:23

    Очень жду продолжения!


  1. alexander_8901
    31.10.2017 22:31

    Круто! Познавательно! Сам использую в работе OpenVPN (мосты между офисами, правда у нас TLS режим, так что очень жду следующей части).


  1. badfiles
    01.11.2017 11:24

    А как wireshark разберет трафик, если вы пишете, что он


    выглядит случайным, т.е. там нет сигнатур, по которым можно было бы отнести эти пакеты к OpenVPN


    1. Nokta_strigo Автор
      01.11.2017 16:29

      Отнести пакеты к OpenVPN — это одна проблема и Wireshark для этого сигнатуры не использует. На скриншотах видно, что к он относит пакеты к OpenVPN (т.к. я поднимал всё на стандартном порту) и вызывает соответствующий диссектор. Если бы порт был нестандартный, то можно было бы вручную щёлкнуть «Decode as...» и выбрать OpenVPN.
      А вот диссектор уже разобрать структуру пакета для PSK режима не смог.