Заголовок обрывается на полуслове, и тем не менее вы всё отлично поняли. И ведь хватило нескольких букв и контекста, чтобы восстановить недостающее слово. Похожий принцип работает и с зашифрованным сетевым трафиком: чтобы делать выводы о происходящем внутри соединения, не всегда нужно видеть содержимое. Поэтому защищенный шифрованием трафик можно анализировать, не расшифровывая. И начать такой анализ стоит с самой простой характеристики — размера сообщений.

Итак, поехали!


Дальше вы узнаете:

  • что может рассказать о сетевом трафике размер его пакетов;

  • почему исправление уязвимости, раскрывающей длину пароля SSH создает точный индикатор способа аутентификации;

  • как на практике создать детектор метода аутентификации SSH при помощи IDS Suricata.

Казалось бы, зачем способ аутентификации в SSH отлавливать, да еще и окольными путями вроде анализа размера зашифрованных пакетов? Дело здесь не в праздном любопытстве аналитика, а в важной работе по обогащению и корреляции с другими событиями. Пароль — это, как правило, интерактив с пользователем: составь карту хостов с этим событием за период времени и наблюдай, как злоумышленник перемещался и где аутентифицировался в сети. Или от обратного: если хост подключился по ключу, то это, скорее всего, уже не первое его подключение, можешь воспринимать как шум автоматики. Поэтому аналитикам полезно фиксировать то, как прошла удаленная аутентификация на сетевых узлах.

В контексте расследования вам обязательно пригодится понимание метода аутентификации в SSH. Особенно это полезно, если есть проблема отсутствия событий с некоторых устройств, которая может быть компенсирована инспекцией в зашифрованном трафике с помощью анализа метаданных (длин пакетов, периодики и временных интервалов). Но не будем сильно забегать вперед — напротив, для начала обратимся к истории, как говорится, всё уже когда-то было.

P. S. Исторический контекст для этой статьи мне помог собрать Егор Ефремов — исследователь Музея криптографии и научный куратор выставки «Ключ к доверию. Безопасность в эпоху высоких технологий». Спасибо ему за интересные факты и помощь в работе над материалом!

Телеграмма Циммермана: анализ метаданных за век до того, как это стало мейнстримом

Думаете, что на метаданные зашифрованных сообщений начали обращать внимание только в цифровую эпоху? А вот и нет. Первый нашумевший случай, когда форма послания (еще до вскрытия его содержания) стала признаком надвигающейся беды, произошел в начале прошлого века, в ходе военного и информационного противостояния Германии и Великобритании. Понимаю, что вы ждете технических деталей и экспериментов, но никак не урока истории, но этот эпизод довольно ярко иллюстрирует важность темы. Так что ненадолго перенесемся в аналоговую эпоху, а уже потом снова вернемся к нашим цифровым пакетам и сообщениям из SSH.

Подозрительно длинная телеграмма

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

17 января 1917 года из Берлина в Вашингтон одна за другой ушли две зашифрованные телеграммы: первая — из 856 кодовых групп от 3 до 5 цифр, вторая — всего из 153. Эти послания были перехвачены и легли на стол Уильяму Монтгомери, аналитику знаменитого криптографического отдела британского Адмиралтейства, так называемой «Комнаты 40» (Room 40). Он сразу насторожился: два подряд сообщения отправлены в спешке, да еще и одно из них неожиданно длинное.

Здесь стоит пояснить, что отправка телеграмм оплачивалась по количеству слов или символов. Поэтому в викторианской Великобритании для удешевления связи придумали так называемые кодовые книги ABC. Изначально ими пользовались в основном торговцы в своей деловой переписке.

Что такое кодовые книги АВС

Книги ABC представляли собой своеобразные словари, где популярным словам, словосочетаниям, устойчивым выражениям, отдельным буквам или цифрам соответствовали короткие кодовые слова или группы символов (часто числа — трех-, четырех- или пятизначные). Каждому числу от 0 до 88 849 соответствовало слово естественного языка.

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

Желающие же не только сэкономить, но и понадежнее защитить информацию от посторонних глаз, иногда шифровали послания с помощью обычных кодовых книг, потом перекодировали шифровки через ABC в краткий набор, казалось бы, бессвязных слов. 

Однако вышеупомянутые отправители из Берлина не стали ничего перекодировать — отправка телеграммы обошлась в 85 долларов, что с учетом инфляции эквивалентно примерно 2,5 тыс. долларов в 2026 году. Дороговато для обычного послания родственникам или торговым партнерам, не находите? Немцы попросту не хотели тратить лишнее время на дополнительное шифрование и даже не подозревали, что скорость и объем сообщения сыграют роковую роль.

О подозрительных телеграммах сразу доложили адмиралу сэру Уильяму Холлу, руководителю британской военно-морской разведки.

«Немецкий гамбит» Циммермана

Монтгомери вместе со своим коллегой (криптоаналитиком) Найджелом де Гри частично расшифровали первую телеграмму. В ней министр иностранных дел Германии Артур Циммерман поручал своему послу в США графу фон Бернсторфу переслать депешу немецкому представителю в Мексике фон Экхардту. Ключевой месседж гласил:

«Мы намерены начать с 1 февраля неограниченную подводную войну. Несмотря на это, мы попытаемся удержать США в состоянии нейтралитета…». 

Тем самым немцы пытались одним выстрелом убить двух зайцев: спровоцировать Мексику напасть на США, чтобы отвлечь Дядюшку Сэма от европейского театра, и заставить сконцентрировать свои силы на южной границе. В обмен на это Германия обещала Мексике помочь вернуть потерянные территории — Техас, Аризону, Нью-Мехико и другие южные штаты. США же будет попросту не до борьбы с Германией, и они хотя бы на время останутся нейтральными, даже несмотря на немецкие атаки на американские, английские и французские торговые суда. В результате получится навредить морской торговле американцев с союзниками, не опасаясь расплаты.

Справка

К слову, двумя годами ранее, в 1915 году, немецкая подлодка потопила шедший из Нью-Йорка в Ливерпуль пассажирский лайнер «Лузитания». Это вылилось в международный скандал и стало первым звоночком к вступлению США в войну на стороне Антанты. 

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

Вот только Вашингтон не спешил объявлять войну на основании данных иностранной разведки и потребовал подтверждения. Британцам пришлось раскрыть США факт взлома немецкой кодовой книги, а для широкой публики изобрели версию о краже сообщения агентом в Мексике. Германия так и не заподозрила, что ее шифры вскрыли. Точку в спорах поставил сам автор: Циммерман публично признал факт отправки телеграммы и подтвердил ее содержание. 6 апреля 1917 года США официально объявили войну Германии.

В итоге хитроумный «немецкий гамбит» провалился. Мексика так и не откликнулась на антиамериканский призыв Германии и осталась в стороне. Идти на прямое боестолкновение без союзников у границ США Берлин не решался.

Подробнее об истории телеграммы Циммермана и ее последствиях можно узнать, посетив инсталляцию в Музее криптографии.

А мы, пожалуй, оставим большую политику и посмотрим на техническую (криптографическую) сторону вопроса: перед нами еще одно яркое доказательство того, что канал утечки может существовать и без доступа к содержимому посланий — через их признаки (объем, момент отправки, очередность писем и т. д.)

От аналоговых книг до эфемерных SSH-ключей

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

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

Например, в работе «Timing Analysis of Keystrokes and Timing Attacks on SSH», опубликованной в 2001 году, было наглядно показано, что зашифрованный интерактивный сеанс выдает длину пароля и интервалы между нажатиями клавиш. Это был пример пассивной атаки на уязвимость работающего соединения. Спустя двадцать лет уже активная Terrapin (CVE-2023-48795) продемонстрировала еще одну угрозу.

Особенности SSH-протокола

Защищенный транспорт SSH на этапе установления соединения разбивает поток на записи, каждая из которых несет одно сообщение. Перед отправкой после прохождения определенного этапа запись шифруется. Шифрование работает поверх бинарного формата сообщений SSH: у каждого служебного сообщения фиксированная структура и, следовательно, предсказуемая длина шифротекста.

Отсюда вытекает несколько свойств, существенных для последующего анализа трафика.

На базе этих свойств строится практическая схема анализа. Составляется таблица «тип сообщения → длина открытого текста → длина шифртекста» для важных служебных сообщений, по которым дальше будем синхронизироваться, чтобы найти те пакеты, на которых будем строить детект.  

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

Методы аутентификации SSH

С важностью размера SSH-пакетов для исследования в общих чертах разобрались — продолжим теоретическую часть, перейдем к методам аутентификации.  

Итак, взглянем на различные методы аутентификации. Каждая из них оставляет уникальный след в виде определенной последовательности длин пакетов.

Что вообще разрешает протокол?

Стандарт допускает несколько методов аутентификации:

  • пароль — password (Password Authentication Method), RFC 4252;

  • открытый ключ — publickey (Public Key Authentication Method), RFC 4252. Единственный обязательный к реализации метод;

  • интерактивный диалог «запрос-ответ» — keyboard-interactive (Generic Message Exchange Authentication), RFC 4256;

  • доверие к машине-источнику — hostbased (Host-Based Authentication), RFC 4252;

  • внешние механизмы вроде Kerberos — gssapi-with-mic (GSS-API Authentication), RFC 4462.

Конфигурация сервера определяет, какие из способов аутентификации доступны в конкретном соединении — это и есть политика аутентификации.

Не зная по умолчанию этих правил, клиент начинает с заведомо неуспешной попытки. Ему надо сперва получить перечень допустимых методов аутентификации. Сервер отвечает отказом на вход, одновременно перечисляя разрешенные механизмы, например, publickey, password. Дальше клиент перебирает доступные ему методы из этого перечня в произвольном для себя порядке. Так, в типичной конфигурации OpenSSH при наличии ключа и разрешения сервера до ввода пароля дело может вовсе не дойти.

Эталонные данные: наблюдения MITM

Теперь нужно собрать эталоны сообщений, по которым мы сможем проверить все теоретические прогнозы и выявить порядок следования длин пакетов в соединении. Это та самая таблица «тип сообщения → длина открытого текста», о которой шла речь выше. 

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

Описание стенда 

Трафик мы будем снимать через инструмент ssh-mitm, доступный на GitHub. Утилита умеет принимать входящие SSH-соединения от клиентов и устанавливать соединения с одним целевым сервером. По пути к серверу сообщения выводятся в консоль в незашифрованном виде. По умолчанию инструмент выводит только лог состояния, однако после небольшой доработки можно получить и hex‑dump. 

Анализ запускается следующей командной строкой, в которой прокси слушает на локальном порту 10022 и переадресует подключение на целевой сервер 10.0.180.221:

ssh-mitm server --listen-port  10022 --remote-host 10.0.180.221

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

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

Суть приема HalfPass

Соединение доводится до момента приглашения ввести пароль, а потом обрывается самим исследователем. Таким образом, в записи остается пролог входа без парольной части. Последний пойманный пакет отмечает границу, после которой начинается ввод пароля и собственно область детектирования. 

Общий пролог

Посмотрим, что объединяет обозначенные ранее способы аутентификации. На схеме ниже представлены две ветки аутентификации проходящие уже под шифрованием, то есть после сообщения newkeys.

Слева ветка аутентификации по ключу, справа по паролю, и они… одинаковы! Да, и это нормально.

Важные отличия методов начинаются уже после ответа «Failure» в сообщении USER AUTH RESP. Всего в этой фазе 7 сообщений. 

Схема передачи сообщений между клиентом и сервером на общем этапе аутентификации 
Схема передачи сообщений между клиентом и сервером на общем этапе аутентификации 

До фазы аутентификации соединение с сервером проходит несколько обязательных шагов: 

  1. Клиент и сервер обмениваются открытыми сообщениями kexinit: каждая сторона присылает список поддерживаемых алгоритмов обмена ключами, шифров, MAC и сжатия. 

  2. Направляются сообщения самого обмена ключами kex, в которых стороны передают данные, необходимые для вычисления общего секрета. 

  3. Каждая сторона отправляет закрытое сообщение newkeys. Это означает, что теперь канал считается зашифрованным. Словом, город засыпает, и просыпается мафия включается шифрование. 

Содержание реальных длин сообщений пролога после передачи newkeys, доступно для контент-анализа, так как MITM атака ретранслирует эти сообщения между узлами.

Тип сообщения       

Код  

Длина

Комментарий

newkeys                  

21

1 

последнее открытое сообщение

ext-info                  

7

252

список поддерживаемых расширений протокола

service-request           

5

17

запрос аутентификации, сервис ssh-userauth

service-accept

6

17

ответ на запрос сервиса ssh-userauth

userauth-request

50

35

метод none (проба), клиент запрашивает методы

userauth-failure

51

24

сервер перечисляет методы: publickey, password

На всякий случай поясню, что число в колонке «Код» — это тип SSH-сообщения из бинарного протокола.

Формат сообщений SSH

Чтобы корректно интерпретировать доступные параметры и понимать открытые сообщения, необходимо знать базовый формат данных, используемый в SSH. Согласно стандарту (RFC 4251, §5), все многобайтные числа в протоколе передаются в формате big-endian (сетевой порядок). В свою очередь, длина строк определяется предварительным отдельным 4-байтным полем перед string. В таблице ниже собраны структуры сообщений в соответствии с базовыми форматами данных.

Напомню, что зашифрованная фаза начинается после newkeys: сервер объявляет поддерживаемые расширения (server-sig-algs, publickey-hostbound, ping), в том числе список алгоритмов подписи, которые готов принять. Это сообщение довольно крупное (252 байта нагрузки) и стабильное как по размеру, так и по положению в стриме.

Далее следует пара service-request / service-accept длины 17 байт каждый. С их помощью запрашивается и подтверждается сервис ssh-userauth (механизм аутентификации). 

После запроса сервиса выполняется проба методом none. Она возвращает ответ userauth‑failure со списком разрешенных методов: publickey и password. Опираясь на этот список, клиент выбирает следующий шаг и предлагает ключ, и только уже в случае неудачи вводит пароль. 

Расшифрованная часть пролога до момента аутентификации
Расшифрованная часть пролога до момента аутентификации

Вход по ключу

Рассмотрим подробнее первый из двух возможных сценариев — когда сервер принимает предъявленный ключ и успешно аутентифицирует клиента. Продолжение общего пролога в этом случае выглядит так:

Тип сообщения

Код

Длина

Комментарий

userauth-request

50 

111 

метод publickey, ssh-ed25519, без подписи

userauth-pk-ok

60

71

сервер подтверждает алгоритм и ключ

userauth-request

50

198

метод publickey, флаг 01, + подпись

userauth-success

52

1

вход выполнен

Клиент отправляет запрос аутентификации по ключу, содержащий блоб публичной части ключа в виде компактной точки на кривой Эдвардса еd25519. Тот же самый блоб в неизменном виде лежит в authorized_keys на сервере.

Предъявление клиентом публичной части ключа, флаг подписи не установлен
Предъявление клиентом публичной части ключа, флаг подписи не установлен

Сервер подтверждает приемлемость открытого ключа для данного пользователя сообщением userauth-pk-ok (код 60), содержащим эхо публичного ключа клиента. Тем самым полезная нагрузка увеличивается до 71 байта. 

Получив подтверждение на вход по ключу, клиент повторяет userauth-request тем же методом publickey, но теперь с флагом 01 (подпись приложена). Клиент вычисляет подпись закрытым ключом над конкатенацией идентификатора сессии и тела запроса. Для ssh-ed25519 подпись занимает фиксированные 64 байта. 

В итоге нагрузка растет со 111 байт на шаге предъявления до 198, на подписи: все прежние поля (имя пользователя, сервис, метод, алгоритм, блоб ключа) остаются неизменными, добавляется лишь поле подписи — 87 байт, из которых 64 приходятся на саму ed25519-подпись, остальное на ее обрамление (имя типа ssh-ed25519 и префиксы длины). 

Проверив подпись, сервер завершает вход однобайтовым сообщением userauth-success (код 52). В нем нет полезной нагрузки — только байт типа сообщения. В стриме это самый короткий пакет фазы аутентификации. Он отправляется однократно и означает прохождение всех требуемых этапов аутентификации. Словом, вход состоялся. 

После одобрения предъявленного публичного ключа сервером, клиент выставляет флаг подписи на повторном запросе
После одобрения предъявленного публичного ключа сервером, клиент выставляет флаг подписи на повторном запросе

Проверки ключа опираются на два файла (authorized_keys и known_hosts), каждый из которых находится на своей стороне (клиент/сервер) и выполняет определенную задачу. 

Authorized_keys хранится на сервере, в домашнем каталоге пользователя, и содержит открытые ключи для входа под этой учетной записью. Сервер сверяется с файлом: если ключ в списке — вход по нему возможен, если нет — получите отказ. 

В свою очередь, known_hosts находится на клиенте. Этот файл хранит открытые ключи хостов, к которым клиент уже подключался. При соединении сервер доказывает владение своим хостовым ключом, клиент же сверяет его с записью в known_hosts. Совпадение подтверждает подлинность сервера. Если запись отсутствует — значит, это первое подключение, и клиент запрашивает подтверждение.

Отдельный разговор — когда запись есть, но ключ в ней не совпадает с предъявленным. Для клиента это сигнал тревоги (адрес раньше отвечал одним ключом, теперь — другим), и он отказывается соединяться и выдает предупреждение «REMOTE HOST IDENTIFICATION HAS CHANGED». Вход разблокируется только удалением старой записи. Такое случается, к примеру, в результате подмены, или когда сервер переустановили и хостовые ключи перегенерировали.

Вход по паролю

Перейдем ко второму сценарию — когда сервер отклоняет клиентский ключ и запрашивает пароль. Продолжение общего пролога в этом случае выглядит так:

Тип сообщения

Код

Длина

Комментарий

userauth-request

50

111

метод publickey, ssh-ed25519, без подписи

userauth-failure

51

24

ключ отклонен, снова «publickey, password»

userauth-request

50

52

метод password, пароль «P@ssw0rd»

userauth-success

52

1

вход выполнен

Предъявление клиентом публичной части ключа, флаг подписи не установлен
Предъявление клиентом публичной части ключа, флаг подписи не установлен

На сообщение с ключом сервер отвечает userauth-failure (код 51). Здесь мы видим такое же сообщение отказа с аналогичной нагрузкой в 24 байта, что и в прологе после пробы методом none. Сервер снова перечисляет допустимые методы аутентификации (publickey, password). Причина отказа в том, что сервер не нашел предъявленный открытый ключ среди разрешенных для этого пользователя.

Следом идет userauth-request методом password (код 50). В теле сообщения передаются:

  • имена пользователя и сервиса,

  • строка метода password,

  • строка пароля. 

Нагрузка этого запроса до шифрования не фиксирована и зависит от длины пароля. Например, для 8-байтового «P@ssw0rd» она составляет 52 байта. Его длина в реальном зашифрованном трафике — предмет отдельного разговора, к которому мы вернемся чуть позже.

Завершает вход однобайтный userauth-success (код 52).

Аутентификация по паролю содержит три сообщения, в одном из которых непосредственно сам пароль
Аутентификация по паролю содержит три сообщения, в одном из которых непосредственно сам пароль

На этой стадии, как и в ключевой ветке аутентификации, к схеме пролога добавляются еще четыре сообщения.

Аутентификация добавляет к схеме пролога еще четыре сообщения независимо от метода
Аутентификация добавляет к схеме пролога еще четыре сообщения независимо от метода

Последовательность сообщений зависит от метода.

При входе по ключу — это offer → PK-OK → signature → success, при входе по паролю — offer → failure → password → success.

В обоих случаях от момента отправки newkeys до успешной аутентификации проходит восемь зашифрованных сообщений, но их длины и состав у двух методов различаются — именно это различие и детектируется.

Влияние шифронаборов на наблюдаемую длину пакета

Захват через MITM дал нам эталон — последовательность открытых SSH-сообщений фазы аутентификации и их длины: от полезной нагрузки до шифрования. Давайте замкнем эту цепочку, связав длину открытого текста с той, что занимает этот же пакет в зашифрованном канале.

Формат записи и дополнительные байты в пакете

Чтобы понять, как шифронабор влияет на итоговую длину, разберемся в базовом формате бинарного пакета SSH и в том, какие служебные поля и паддинг к нему добавляются.

По RFC 4253 (§6) каждое SSH-сообщение упаковано в бинарный пакет, состоящий из четырех частей:

  1. 4-байтное поле длины;

  2. 1-байтное поле длины паддинга;

  3. полезная нагрузка;

  4. случайный паддинг.

Паддинг подбирается так, чтобы шифруемая область была кратна размеру блока шифра (16 байт у AES, 8 у 3DES и ChaCha20-Poly1305), при минимуме паддинга 4 байта. Поверх зашифрованного пакета добавляется имитовставка или AEAD-тег. Итоговая длина — это длина полезной нагрузки, округленная вверх до блока шифра, плюс постоянная служебная надбавка набора.

Разберем это на примере набора AES128-CTR с имитовставкой HMAC-SHA1 (режим Encrypt-and-MAC). 

Здесь 4-байтное поле длины входит в шифруемую, выравниваемую по блоку область, потом сверху приписывается 20-байтный HMAC-SHA1. Для нагрузки payload_len байт длина будет вычислена по формуле:

wire_len = ⌈(payload_len + 9) / 16⌉ × 16 + 20

где 9 = 4 (поле длины) + 1 (поле длины паддинга) + 4 (минимальный паддинг); 16 — размер блока; 20 — длина HMAC-SHA1. Так, для userauth-success (сообщение длиной в один байт) формула дает 36 байт, а для предъявления ключа (сообщение длины 111 байт) — 148 байт.

Обращение с полем длины в разных наборах

Разные наборы шифров обрабатывают служебные поля и паддинг по-своему — отсюда вариации наблюдаемой длины полезной нагрузки TCP-пакета. Наборы различаются размером блока, размером имитовставки и тем, как обходятся с полем длины:

  1. AES128-CTR / AES128-CBC + HMAC-SHA1 (Encrypt-and-MAC). Поле длины зашифровано и входит в выравниваемую по 16 байтам область, сверху — 20-байтный HMAC. Режимы CTR и CBC по длинам неотличимы.

  2. ChaCha20-Poly1305 (AEAD). 4-байтное поле длины шифруется отдельным ключом. Область padlen + payload + padding выравнивается по 8 байтам, в конце — 16-байтный тег Poly1305.

  3. AES128-GCM (AEAD, RFC 5647). 4-байтное поле длины не шифруется и передается открытым текстом. Область padlen + payload + padding выравнивается кратно 16 байтам, в конце — 16-байтный GCM-тег.

Паддинг парольного запроса

После релиза OpenSSH 2024 года в протоколе появились механизмы выравнивания длин пакетов аутентификации: клиент дополняет парольный запрос паддингом до кратной величины, чтобы его длина больше не выдавала длину пароля.

И вот тут кроется поучительный разворот. Эта мера действительно закрыла утечку длины пароля — но, выровняв все короткие пароли в пакет одного и того же размера, она превратила переменную величину в константу. Прежде по длине парольного запроса можно было оценить длину пароля, а теперь парольный запрос стал узнаваемым пакетом фиксированного размера, чуть ниже я покажу, как это отразилось в коде. Но прежде оцените «красоту» момента: харденинг не убрал побочный канал, а сместил его — вектор наблюдения сузился с «определить длину пароля» до «определить способ аутентификации». Исправление одной утечки многократно усилило возможность другой, — отличить по зашифрованному трафику вход по ключу от входа по паролю.

Если взглянуть на всё это философски, защита SSH обернулась против самой себя. Клиент OpenSSH намеренно дополняет парольный запрос паддингом, чтобы длина пакета не выдавала размер пароля. Вот только эта мера делает парольный запрос одинаковым по длине для всех паролей нормальной длины. Таким образом, механизм, задуманный как контрмера против анализа длин, сам демаскировал признак аутентификации по паролю. Это, пожалуй, классический случай, когда одно чинишь — другое ломаешь.

Клиент OpenSSH дополняет парольный запрос userauth-request_pass паддингом, скрывая длину пароля. В коде клиента (sshconnect2.c) в ветке парольной аутентификации выполняется следующий вызов:

if ((r = sshpkt_add_padding(ssh, 64)) != 0 ||
    (r = sshpkt_send(ssh)) != 0)
        goto out;

Далее в упаковщике пакетов (packet.c) он доводит запись до значения, кратного 64 байтам:

if (state->extra_pad) {
        state->extra_pad =
            roundup(state->extra_pad, block_size);
        pad = state->extra_pad -
            ((len + padlen) % state->extra_pad);
        ...
        padlen += pad;
        state->extra_pad = 0;
}

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

Длина пароля квантуется 64-байтными ступенями: пароли до 63 символов всегда дают одну длину пакета: 148 байт. А пароли, содержащие 64 символа и более, переводят пакет на следующую ступень — 212 байт. Один символ на границе 63/64 смещает длину на целый квант. 

Механизм защиты длины пароля в SSH полностью демаскирует аутентификацию. Все пароли до 63 знаков формируют пакет одинаковой точной длины — 148 байт. Это ключевой момент, который учитывается при детектировании, чтобы выделить аутентификационный пакет SSH среди общего потока данных.

Разметка фазы аутентификации по трем наборам шифров для 8-байтного пароля, выровненного паддингом

SSH-сообщение

Длина открытого текста

AES128-CTR/ GCM

ChaCha20-Poly1305

userauth-request

52

148

148

userauth-success

1

36

28

Финальный userauth-success — один из самых коротких пакетов сессии: 36 байт под AES-наборами и 28 под ChaCha20-Poly1305. Сам по себе он лишь сообщает об успешной аутентификации, не раскрывая ее метод. Признаком входа по паролю он становится только в связке, то есть если ему предшествует 148-байтный парольный запрос от клиента.

От наблюдений к детектированию: выбор инструментария

Разметка длин из предыдущего раздела — это модель того, какие пакеты и какой длины ожидаются в фазе аутентификации. Чтобы превратить ее в работающий детектор, нужен инструмент, который сможет применить эту модель к трафику.

Инструмент: Suricata IDS/IPS

Задача решается на любом движке сетевого детектирования, умеющем работать с длинами пакетов, их порядком и состоянием потока, — выбор довольно широкий. Я выбрала Suricata (открытый инструмент IDS-/IPS-инспекции) по нескольким практическим причинам. Во-первых, он просто устанавливается и хорошо документирован. Во-вторых, Suricata располагает понятным синтаксисом для написания сигнатур и содержит все нужные нам ключевые слова и механики — сопоставление по длине и содержимому, флаги и счетчики состояния в границах потока.

Suricata работает в двух режимах:

  • потоковый — слушает сетевой интерфейс и разбирает трафик на лету;

  • разбор файла — читает заранее снятый pcap и прогоняет по нему тот же набор правил.

Разберемся с установкой, запуском и условиями, которые нужно соблюсти при работе.

Установка и запуск

В этом кейсе я работала на Suricata 7.0.16. Для Ubuntu — установка из репозитория проекта:

sudo add-apt-repository ppa:oisf/suricata-stable

sudo apt update && sudo apt install suricata

При разработке правил рабочий режим — разбор захвата, то есть переигрывание записанного дампа трафика:

suricata -c /etc/suricata/suricata.yaml -S ssh_pass_detect.rules -r capture.pcap -l ./out -k none

Кратко по ключам

-с — конфигурация через кастомный файл настроек;

-S — подключает файл правил (только его, минуя системные);

-r — входной pcap;

-l — каталог логов (в нем появляется fast.log со срабатываниями);

-k — отключает проверку контрольных сумм.

В дампах, снятых на хосте-отправителе, контрольные суммы пакетов часто «пустые» из-за разгрузки вычислений на сетевую карту. Без -k none такие пакеты Suricata отбрасывает, и до правил они не доходят.

Обязательное условие: включаем инспекцию SSH после рукопожатия

Метод анализа длин работает с пакетами, которые идут после рукопожатия, поэтому Suricata нужно настроить так, чтобы она инспектировала их полезную нагрузку. По умолчанию, увидев newkeys, парсер считает сессию перешедшей в зашифрованную фазу и дальше ее содержимое уже не разбирает (по документации). 

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

Для Suricata 8.0 и новее достаточно перевести обработку зашифрованного SSH в режим полной инспекции:

app-layer:
  protocols:
    ssh:
      encryption-handling: full

При этом SSH-парсер остается включенным, а Suricata разбирает поток целиком, включая фазу аутентификации, — и правила по длинам получают свои пакеты.

Для Suricata ниже 8.0 отдельной опции encryption-handling для SSH нет, поэтому инспекция возвращается отключением самого SSH-парсера — в yaml-конфигурации или ключом командной строки:

--set app-layer.protocols.ssh.enabled=no

После этого Suricata ведет попакетную инспекцию потока, и пакеты фазы аутентификации доходят до правил. 

Стратегия детектирования

Стратегия детектирования в нашем случае базируется на трех условиях.

Фиксированные и прозрачные границы фазы. Начало фазы аутентификации задает клиентский NEWKEYS, как последнее передаваемое в открытом виде сообщение с постоянным телом [00 00 00 0c 0a 15 …]. Его можно отловить точным сопоставлением по содержимому. Это и есть наша точка отсчета, открывающая окно детектирования. 

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

Однозначная длина опорных записей парольной ветки. Парольный запрос — 148 байт. Финальный userauth-success — 28 или 36 байт. В окне детектирования на определенных позициях должны находиться пакеты определенной длины.

Выбор ключевых слов

Нам понадобится набор ключевых слов Suricata — каждое участвует в построении правил и связки их в цепочку.  

  • dsize — длина полезной нагрузки TCP-пакета. Потребуется в правилах, детектирующих пакет парольного запроса и ответ сервера;

  • content — поиск контента по шестнадцатеричным байтам. Пригодится для определения начала окна детектирования;

  • flowbits — флаги в пределах потока. Они необходимы, чтобы правила срабатывали в нужном контексте окна;

  • flowint — счетчик в пределах потока, который нужен для формирования цепочки правил;

  • stream_size — ограничитель глубины проверки, который позволит удержать инспекцию в пределах фазы аутентификации, отсекая остальной трафик сессии.

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

Набор сигнатур для детектирования аутентификации по паролю

Общие направления обозначили, ключевые слова подобрали — перейдем к реализации. Сигнатуры работают на уровне tcp-pkt, то есть по отдельным пакетам, без реассемблинга потока и без подавления сообщений в лог (noalert), так быстрее и писать, и матчить. Потом можно расставить дополнительные проверки на состояние потока TCP.

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

Весь набор правил сведен к четырем сигнатурам, и он работает для любых шифронаборов сразу — AES-GCM, AES-CTR, AES-CBC, ChaCha20-Poly1305, неважно. Это стало возможным благодаря изучению кода, в результате которого стало ясно, что userauth-request с паролем под любым набором дает всегда 148 байт, а userauth-success укладывается в узкий диапазон от 28 до 36 байт. Поэтому отдельных правил под каждый набор не требуется. 

Открытие окна аутентификации (sid:1)

Служебная сигнатура для детектирования начала фазы аутентификации
Служебная сигнатура для детектирования начала фазы аутентификации

Обнаружив тело клиентского NEWKEYS в начале пакета (startswith) в пределах первых 10 КБ потока, первая сигнатура выставляет флоу-бит ssh_new_key и обнуляет счетчик ssh_pkt. Так задается точка отсчета, от которой нумеруются все последующие пакеты фазы. Это единственная сигнатура, работающая на поиск контента: NEWKEYS — последнее сообщение с доступным открытым текстом.

Сигнатура-счетчик пакетов в окне аутентификации SSH-соединения
Сигнатура-счетчик пакетов в окне аутентификации SSH-соединения

Вторая сигнатура увеличивает счетчик ssh_pkt на каждом пакете стрима в обе стороны (<>) с полезной нагрузкой больше одного байта. Условие dsize:>1 отсекает пакеты без полезной нагрузки, а content:! с телом NEWKEYS исключает из счета сам якорный пакет. Таким образом нумерация начинается с первого пакета зашифрованной фазы аутентификации.

Сигнатура для выявления в трафике пакета, содержащего пароль
Сигнатура для выявления в трафике пакета, содержащего пароль

Третья сигнатура ловит клиентский пакет с зашифрованным паролем (to_server) длиной 148 байт шестым по счету (ssh_pkt==6) и выставляет флаг sshpasspkt.

Правило не опирается на содержимое — только на размер полезной нагрузки TCP-пакета и его позицию.

Длина 148 здесь общая для всех наборов: парольный запрос дает одну и ту же длину под AES-GCM, CTR, CBC и ChaCha20.

Сигнатура для детектирования успешной аутентификации
Сигнатура для детектирования успешной аутентификации

Финальная, четвертая, сигнатура детектирует сообщение сервера userauth-success — событие, завершающее весь каркас.

Причем срабатывает она в заранее заданном контексте: стоит на позиции ssh_pkt==7 и приходит при уже выставленном флаге sshpasspkt. Длина задана диапазоном dsize:27<>37, который покрывает userauth-success под всеми наборами.

Условие на алерт здесь ключевое. Успех засчитывается, только если ему предшествовал парольный запрос, отмеченный sid:3. Сработав, сигнатура снимает оба флага (sshpasspkt и ssh_new_key) и закрывает окно инспекции для потока.

В готовом наборе служебные сигнатуры (открытие окна, счетчик, маркер пароля) работают, не создавая события — алерт порождает только сигнатура успеха. Но на этапе отладки их лучше делать видимыми, то есть писать без noalert (как на скринах выше), чтобы в логе отображались все срабатывания. Всё потому, что видимый лог — отличный способ проследить, что происходит внутри потока: открылось ли окно аутентификации, как считает счетчик, на какой позиции сработал маркер пароля.  

Отладка сигнатур

Для отладки нужен воспроизводимый материал. Поэтому мы сначала снимаем корпус — множество дампов трафика, покрывающих разные шифронаборы и оба метода входа (по паролю и по ключу) уже без MITM.

Запускаем Suricata из командной строки в режиме разбора файла. Не забываем отключить SSH-парсер:

suricata -c suricata.yaml -S ssh_pass_detect.rules -r capture.pcap -l ./out  -k none --set app-layer.protocols.ssh.enabled=no

По завершении в каталоге ./out появляются несколько логов, из них нам важны два. Первый — fast.log. Это компактная строка на каждое срабатывание (время, sid, адреса, сообщение). По нему видно, какие сигнатуры и в каком порядке сработали.

Результат работы Suricata на тестовом отладочном наборе сигнатур
Результат работы Suricata на тестовом отладочном наборе сигнатур

Разбор fast.log. В обработанном логе зеленым выделены адреса и направления, желтым — номера сработавших сигнатур, красным — приоритет (P:3), голубым — сообщение сигнатуры. По нему прослеживается вся цепочка.

Первым срабатывает sid:1 на клиентском NEWKEYS. В результате счетчик обнуляется, и окно аутентификации открывается. Дальше на каждом пакете фазы, в обе стороны, срабатывает sid:2 и наращивает ssh_pkt на единицу. На позиции ssh_pkt==6 клиентский парольный запрос (dsize:148) срабатывает sid:3 и выставляет флаг sshpasspkt. Наконец, на позиции ssh_pkt==7 серверный userauth-success вызывает финальную сигнатуру sid:4 — вход по паролю обнаружен.

О порядке срабатывания. Suricata загружает сигнатуры не в порядке их записи в файле, а сортирует по использованию flow-переменных: сигнатуры, выставляющие флаг (set), ставятся перед теми, кто его читает (isset). Приоритет — «write > write+read > read > без flowbits» (docs: Rule Processing; реализация — detect-engine-sigorder.c). Благодаря этому sid:1 (выставляет ssh_new_key) гарантированно отрабатывает до читающих его sid:2–4.

Для счётчика flowint есть тонкость. На пакете с паролем работают два правила: sid:3 читает позицию (flowint:ssh_pkt,==,6), а sid:2 её инкрементирует (flowint:ssh_pkt,+,1). По факту sid:3 считывает ssh_pkt до того, как sid:2 добавит на этом пакете единицу, — поэтому пакет, седьмой по ручному счёту, проверяется при ssh_pkt==6. Это поведение подтверждено на нашем наборе эмпирически (видно в fast.log: sid:3 срабатывает перед вторым sid:2), но строгой гарантии для flowint-читателей спецификация не даёт — при изменении набора или версии порядок стоит перепроверять.

Второй лог — это alert-debug.log. Он представляет собой подробный лог с полным контекстом каждого срабатывания: значения флагов и счетчиков пакетов, содержимое пакета, сработавшие правила.

Подробный лог срабатывания финального правила, alert-debug.log 
Подробный лог срабатывания финального правила, alert-debug.log 

Проверка по alert-debug.log. Подробный лог который подтверждает срабатывание всех правил с noalert. В частности sid:4, посмотрим, направление to_client указано как TRUE, значение счетчика ssh_pkt — 7, длина полезной нагрузки (PAYLOAD LEN) — 36 байт. В теле пакета также видно открытое поле длины 00 00 00 10 — это длина userauth-success сообщения зашифрованного AES-GCM. Все правильно, ожидания подтверждаются.

Такой метод отладки полезен не только для текущего набора. Тот же прием (снять корпус, включить видимый лог, прочитать распределение срабатываний) позволяет собирать сигнатуры и под другие наблюдаемые сценарии. Например, неуспешный ввод пароля, когда на стенде задается конкретное число попыток (одна или три), и можно наблюдать, как чередование запросов и отказов ложится в счетчик.

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

Ограничение набора сигнатур

Позиции, на которых сигнатуры ждут парольный запрос и сообщение об успехе, рассчитаны на типовую раскладку фазы аутентификации, где клиент сначала предлагает ключ: проба none, ответ сервера, предложение ключа, отказ по нему, затем парольный запрос и успех. Счетчик ssh_pkt нумерует пакеты в этом порядке, и правила привязаны к конкретным его значениям.

Ситуация меняется, если offer-обмена в фазе нет. Это может произойти, например, когда сервер не разрешает вход по ключу. То есть в ответе на пробу методом none он возвращает список доступных методов и, если publickey в нем отсутствует (сервер настроен на PubkeyAuthentication no или Authentication Methods password), клиент ключ вовсе не предлагает. В результате из фазы выпадают два сообщения — предложение ключа и отказ по нему, — и всё, что идет за ними, смещается на несколько позиций ближе к началу.

Это всё, что возьму я с собой

Кейс с аутентификацией мы разобрали — но самое ценное в нем даже не сам детект, а приемы, которые останутся с вами и тогда, когда SSH-пароли будут уже не актуальны.

Основные приемы

Счетчик пакетов как адрес. Служебная сигнатура, инкрементирующая flowint на каждом пакете потока, дает попакетную нумерацию. Так к любому пакету можно обратиться по его порядковому номеру в обмене — и привязать проверку именно к той позиции, где ожидается нужное сообщение.

Флаги как память потока. Связка правил через flowbits превращает набор сигнатур в конечный автомат. Одно правило открывает окно, другое отмечает пройденную стадию, третье проверяет эту отметку и закрывает окно. Так собирается логика, отслеживающая фазы любых протоколов.

Длина пакета как опорный признак. Сопоставление размера зашифрованной полезной нагрузки с длиной открытого сообщения протокола. В зашифрованных и/или кастомных протоколах это часто единственная стабильная величина для сигнатуры — и привычка строить детект от длин сообщений выручает далеко за пределами этого кейса.

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

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

Что до предложенного в статье набора сигнатур, лишь отмечу, что он создан для оптимальных сетевых условий. В других ситуациях его, возможно, придется чуть-чуть донастроить. Однако считаю, что важнее самого набора — приемы, которые можно перенести на работу с любым протоколом. Ведь у множества инструментов и вредоносных программ есть свои кастомные сетевые протоколы, метод можно применять для их детектирования.


Вместо заключения

Надеюсь, что, пройдя вместе со мной по тонкой ниточке от начала до конца, вы убедились, что на телеграмме Циммермана, что на живом трафике SSH, не расшифровав ни слова/байта, можно и нужно находить что-то очень важное для нас. Причем для этого достаточно базовых возможностей. Ничего принципиально нового я не открыла: в криптоанализе этому посвящено целое направление. Если тема вас зацепила, копайте в сторону атак по побочным каналам (side-channel attacks), там вас ждет много интересного.

Ну и напоследок — та самая мысль, к которой мы сегодня не раз возвращались. Зашифрованный канал скрывает содержание сообщения, но не метаданные. А значит, там, где содержание скрыто, размер всё равно имеет значение — и умение его прочитать однажды вам пригодится. Надеюсь, вы забрали это с собой. ❤️

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