![image](https://habrastorage.org/getpro/habr/post_images/165/f97/0d5/165f970d56cf6ab7221de1a2a864f10b.jpg)
Вряд ли можно представить мир современных сетевых технологий без DPI (deep packet inspection – глубокий анализ пакетов). На нём держатся системы обнаружения сетевых атак, львиная доля политик безопасности корпоративных сетей, шейпинг и блокировка пользовательского трафика оператором связи – да-да, чтобы выполнять требования Роскомнадзора, средства DPI обязан иметь каждый провайдер.
И всё-таки, при всей своей востребованности, DPI имеет некоторые недостатки. Главный из них в том, что средствам DPI необходимо видеть полезную нагрузку анализируемых пакетов. А что делать, когда клиент использует шифрование? Или, например, если у нас нет DPI здесь и сейчас, но в перспективе потребуется проводить какой-то анализ текущего по сети трафика – тогда нам остаётся только сохранять всю полезную нагрузку для последующего анализа, что очень неудобно.
В данной статье я хочу предложить альтернативный способ решения одной из главных задач DPI – определения протокола прикладного уровня – на основе очень маленького количества информации, при этом не сверяясь со списком широко известных портов (well-known ports) и не глядя в полезную нагрузку пакетов. Вообще.
Подход к решению
Во-первых, определимся с объектом классификации — для какой сущности мы будем определять протокол прикладного уровня?
В средствах DPI, как правило, объектом классификации выступает поток трафика транспортного уровня – это совокупность IP-пакетов, у которых совпадает протокол транспортного уровня, а также неупорядоченная пара endpoint-ов: <(ip источника, порт источника), (ip назначения, порт назначения)>. Мы тоже будем работать именно с такими потоками.
Идея, которая лежит в основе предлагаемого метода, заключается в том, что разные приложения, пользующиеся разными протоколами, также генерируют потоки транспортного уровня с разными статистическими характеристиками. Если аккуратно и ёмко определить набор статистических метрик потока, то по значениям этих метрик можно будет с высокой точностью предсказывать, какое приложение сгенерировало данный поток, и, соответственно, какой протокол прикладного уровня этим потоком переносится.
Прежде, чем определять эти метрики, введём парочку определений:
- Клиент – инициатор TCP-соединения либо отправитель первой UDP-дейтаграммы потока, в зависимости от протокола транспортного уровня;
- Сервер – принимающая сторона TCP-соединения либо адресат первой UDP-дейтаграммы потока, в зависимости от протокола транспортного уровня.
- Порция данных – совокупность полезной нагрузки прикладного уровня, которая передавалась от одной стороны другой (от клиента к серверу или наоборот), и при этом не прерывалась полезной нагрузкой с другой стороны.
Последнее определение требует некоторого пояснения, так что мой внутренний Микеланджело спешит на помощь:
![image](https://habrastorage.org/getpro/habr/post_images/563/a43/09e/563a4309ed398b7b933bd266868367c9.jpg)
В данном примере после TCP-рукопожатия клиент начинает передавать полезную нагрузку – следовательно, начинается порция данных со стороны клиента. Пока сервер не посылает никакой полезной нагрузки в ответ, а всего лишь шлёт ACK, порция данных со стороны клиента продолжается. Когда сервер ощущает необходимость передать какую-то нагрузку прикладного уровня, порция данных клиента заканчивается, и начинается порция данных сервера. Как несложно понять, вся передача полезной нагрузки представляет собой чередование порций данных то с одной, то с другой стороны. При очень интенсивном обмене данных с обеих сторон порции данных могут вырождаться в отдельные IP-пакеты.
Статистические метрики потока
Все наши статистические характеристики потока будут плясать вокруг четырёх рядов чисел:
- Последовательность размеров сегментов транспортного уровня (TCP или UDP), отправленных со стороны клиента;
- Последовательность размеров сегментов транспортного уровня, отправленных со стороны сервера;
- Последовательность размеров порций данных, отправленных со стороны клиента;
- Последовательность размеров порций данных, отправленных со стороны сервера;
Для представленного на рисунке выше короткого примера это ряды будут иметь следующие значения:
- Размеры сегментов со стороны клиента: [220, 520]
- Размеры сегментов со стороны сервера: [720, 420]
- Размеры порций данных со стороны клиента: [700]
- Размеры порций данных со стороны сервера: [1100]
Эти 4 ряда чисел (как мы увидим впоследствии) очень хорошо характеризуют поток данных, и на их основе можно достаточно точно угадать протокол прикладного уровня.
Включив фантазию, сформулируем статистические характеристики потока данных, отталкиваясь от этих 4 рядов чисел:
- Средний размер пакета со стороны клиента
- Стандартное отклонение размера пакета со стороны клиента
- Средний размер пакета со стороны сервера
- Стандартное отклонение размера пакета со стороны сервера
- Средний размер порции данных со стороны клиента
- Стандартное отклонение размера порции данных со стороны клиента
- Средний размер порции данных со стороны сервера
- Стандартное отклонение размера порции данных со стороны сервера
- Среднее число пакетов на порцию данных со стороны клиента
- Среднее число пакетов на порцию данных со стороны сервера
- КПД клиента – количество переданной нагрузки прикладного уровня, делённое на общее количество переданной нагрузки прикладного и транспортного уровня
- КПД сервера
- Соотношение байт – во сколько раз клиент передал больше байт, чем сервер
- Соотношение полезной нагрузки – во сколько раз клиент передал больше байт, чем сервер
- Соотношение пакетов – во сколько раз клиент передал больше пакетов, чем сервер
- Общее количество переданных байт со стороны клиента
- Общее количество переданной нагрузки прикладного уровня со стороны клиента
- Общее количество переданных сегментов транспортного уровня со стороны клиента
- Общее количество переданных порций данных со стороны клиента
- Общее количество переданных байт со стороны сервера
- Общее количество переданной нагрузки прикладного уровня со стороны сервера
- Общее количество переданных сегментов транспортного уровня со стороны сервера
- Общее количество переданных порций данных со стороны сервера
- Размер первого сегмента транспортного уровня со стороны клиента
- Размер второго сегмента транспортного уровня со стороны клиента
- Размер первого сегмента транспортного уровня со стороны сервера
- Размер второго сегмента транспортного уровня со стороны сервера
- Размер первой порции данных со стороны клиента
- Размер второй порции данных со стороны клиента
- Размер первой порции данных со стороны сервера
- Размер второй порции данных со стороны сервера
- Тип протокола транспортного уровня (0 — UDP, 1 — TCP)
Теперь надо пояснить несколько моментов.
Во-первых, признаки вида «общее количество Х» сами по себе нестабильны, потому что их значение зависит от того, как долго мы наблюдаем за потоком. Чем больше наблюдаем, тем, естественно, больше будет передано и байт, и прикладной нагрузки, и сегментов, и порций данных. Чтобы добавить определённости в эти показатели, мы в будущем будем рассматривать не весь поток целиком от начала наблюдения и до конца, а некоторый срез каждого потока по первым N сегментам транспортного уровня. При рассчёте всех перечисленных метрик будем использовать только первые N сегментов.
Во-вторых, может быть непонятно, зачем нам в показателях размер первого и второго сегмента и порции данных с каждой стороны. Как показано в одной из научных работ на эту тему[1] (ссылка в источниках), размеры первых нескольких IP-пакетов могут нести много информации об используемом протоколе. Так что лишними эти показатели не будут.
Теперь определимся, как мы будем пытаться угадать протокол прикладного уровня конкретного потока, имея на руках рассчитанные статистические метрики. Здесь нам поможет машинное обучение. Рассматриваемая задача – это в своей классической формулировке задача классификации объектов на несколько классов.
У каждого объекта целых 32 характеристики, причём релевантность каждой из них имеющейся у объекта метке класса на данном этапе исследования остаётся под вопросом. Поэтому разумно будет выбрать популярный алгоритм машинного обучения «Случайный Лес» («Random Forest»), поскольку он слабо чувствителен к шумам и корреляции признаков (а некоторые наши статистические метрики, скорее всего, сильно коррелируют между собой).
Данный алгоритм работает по принципу «обучение с учителем». Это значит, что нам нужна некоторая выборка объектов, для которой уже известны метки классов. Эту выборку мы разделим в пропорции 1 к 2 на обучающую и проверочную. На обучающей выборке мы проведём обучение модели (в нашем случае обучение заключается в построении набора решающих деревьев), а на проверочной будем оценивать, насколько хорошо модель справляется с поставленной задачей.
Время жатвы
С методологией проведения эксперимента теперь всё понятно, осталось найти сам трафик, характеристики которого будем рассчитывать.
Никакой провайдер, конечно, не пустит нас в свою сеть собирать трафик – кто там знает, какие данные мы будем собирать? Обойдёмся кустарными условиями. В течение нескольких дней я проводил захват трафика своего собственного компьютера, попутно делая обычные дела – сидел в ВК, смотрел видео на Youtube, сделал несколько звонков в Скайпе, скачал парочку торрентов. В результате получилось более 3 Гб захваченного трафика.
Теперь вспомним, что кроме самого трафика нам также нужно для каждого захваченного потока достоверно определить переносимый им протокол прикладного уровня. И вот тут нам помогут средства DPI, конкретно – библиотека nDPI. В комплекте с этой библиотекой поставляется пример анализатора трафика под названием ndpiReader – он нам и пригодится. Приятный бонус заключается в том, что это приложение само за нас разделит все наши сырые дампы трафика на потоки транспортного уровня, так что нам этим даже не придётся заниматься.
Но всё-таки покопаться в недрах pcap-файлов будет нужно. Для этого используем библиотеку dpkt. К огромному сожалению, её не портировали на третью версию Python, так что скрипт обработки pcap-файлов напишем на второй версии языка.
Собирая всё выше изложенные воедино, напишем скрипт, который преобразовывает PCAP-файлы в таблицу признаков и экспортирует её в формат CSV. Ссылка на репозиторий с этим скриптом в конце статьи.
What time is it? Classification time!
Да-да, наконец-то мы добрались до того, ради чего всё затевалось!
Но перед тем, как с корабля на бал запихивать всю таблицу признаков в первый попавшийся Random Forest, рассмотрим данные немного внимательнее. Вот список прикладных протоколов и количество потоков каждого протокола, которое имеется в нашей выборке:
![image](https://habrastorage.org/getpro/habr/post_images/eee/6c0/db7/eee6c0db7dd9b9818888e1d80eaea468.jpg)
Первое, что хочется сделать – слить весь трафик SSL_No_Cert в SSL, потому что это по сути одно и то же.
Второе – выкинуть трафик Apple, NTP, Unencrypted_Jabber и Unknown. У первых трёх протоколов слишком мало потоков, чтобы проводить классификацию, а под Unknown может скрываться всё что угодно.
Теперь разделим наши данные на обучающую и проверочную выборки. Получим такой расклад:
![image](https://habrastorage.org/getpro/habr/post_images/8d7/b5d/f05/8d7b5df056230023f7eff6a6f7c0c424.jpg)
В качестве параметров алгоритма Random Forest после нескольких экспериментов были выбраны следующие значения:
- Число деревьев: 27
- Критерий: энтропия
- Максимальная глубина дерева: 9
Вспомним, что некоторые наши статистические характеристики обязывают нас фиксировать объем рассматриваемого трафика для каждого потока. Для начала возьмём срез трафика побольше – 1000 первых сегментов в каждом потоке. Проверим, можно ли вообще предсказать по нашим статистическим метрикам протокол прикладного уровня. Барабанная дробь!
![image](https://habrastorage.org/getpro/habr/post_images/533/f0c/a1d/533f0ca1dbacde6e349b9b58b9283e61.jpg)
Выше представлена таблица точности и полноты предсказаний по каждому классу. Точность для класса K – это доля предсказаний вида «объект X принадлежит классу K», которые оказались верными. Полнота для класса K – количество объектов X, которые распознаны классификатором как принадлежащие классу K, делённое на общее количество принадлежащих классу K объектов. Ф-мера – среднее гармоническое полноты и точности.
Ну и чуть более простая таблица:
![image](https://habrastorage.org/getpro/habr/post_images/578/492/289/578492289029b4cedce4e5195248a968.jpg)
Здесь в каждой ячейке указано число, которое означает количество случаев, когда поток трафика, принадлежащий указанному в заголовке строки классу, был классифицирован как принадлежащий указанному в заголовке столбца классу. То есть, числа на главной диагонали – правильные классификации, числа вне её – разного рода ошибки.
В целом, кажется, что классификатор справляется со своей работой неплохо. За исключением редких ошибок, все предсказания оказываются верны.
Алгоритм машинного обучения «Случайный Лес» хорош ещё и тем, что он может дать нам информацию о том, какие признаки оказались более полезными, чем другие. Из любопытства взглянем на самые важные признаки:
![image](https://habrastorage.org/getpro/habr/post_images/2e3/952/465/2e39524657934396ccf9ada6eb250061.jpg)
Топ стабильно занимают признаки, завязанные на статистические характеристики клиентской стороны. Отсюда можно сделать вывод, что именно клиенты разных приложений отличаются между собой больше всего, а серверы – меньше.
От сервера затесался только размер второго пакета. Это легко объясняется тем, что по размеру второго пакета можно очень просто выделить SSL из всего остального трафика, ведь во втором пакете сервера передаётся сертификат.
Приятно удивляет то, что тип протокола транспортного уровня входит всего лишь в ТОП-10 самых важных признаков – хотя, казалось бы, очень много информации в этом признаке содержится! А значит, в наших статистических метриках информации намного больше.
Ещё хочется отметить, что при изменении семени генератора псевдослучайных чисел топ самых важных признаков несколько меняется. Общим остаётся о, что завязанные на клиента метрики стабильно вверху, но их порядок меняется. А протокол транспортного уровня прыгает между первым и десятым местом.
Теперь самое интересное. До этого мы брали срез по первым 1000 сегментам каждого потока, и на основе этого среза рассчитывали статистические метрики. Возникает закономерный вопрос: а насколько можно уменьшить этот срез и всё равно получать хорошие показатели классификации?
Ответ обескураживает. Можно опускать количество сегментов в каждом потоке до… трёх. Не включая TCP handshake, разумеется, ведь он никакой информации не несёт.
Ниже представлены значения полноты и точности для каждого класса, таблица реальных и предсказанных классов и топ характеристик при построении и проверке модели всего лишь на первых трёх сегментах транспортного уровня (для TCP это на самом деле 6 сегментов, три первые отбрасываются).
![image](https://habrastorage.org/getpro/habr/post_images/448/bd3/e56/448bd3e569e29577f96146b7d45f3d5b.jpg)
![image](https://habrastorage.org/getpro/habr/post_images/1ab/7c3/84b/1ab7c384b43c210ad67e975dd7c8f34e.jpg)
![image](https://habrastorage.org/getpro/habr/post_images/ec4/8de/8c6/ec48de8c6dfca86ffee36e1922f59660.jpg)
Внимательный читатель может заметить, что суммарное количество записей в выборке стало меньше по сравнению с прошлым случаем. Дело в том, что для расчёта большинства характеристик нужно, чтобы была хотя бы одна порция данных со стороны клиента и хотя бы одна со стороны сервера. Если выясняется, что в первых 3 сегментах передавал только клиент или только сервер, такой поток отбрасывается ещё на этапе составления таблицы признаков.
Результат получился неожиданный тем, что с резким уменьшением количества сегментов в каждом потоке качество классификации не ухудшилось, а даже несколько улучшилось. Связать это можно с тем, что именно первые переданные сегменты несут наибольшую информацию о типе приложения. Поэтому при уменьшении количества сегментов наши статистические метрики начинают нести информацию только о первых сегментах, а не обо всех, и качество классификации несколько увеличивается.
Заключение
В данной статье я наглядно продемонстрировал, как можно производить достаточно качественный анализ трафика с целью определения протокола прикладного уровня без средств DPI. Что характерно, можно достаточно точно определить прикладной протокол уже по первым нескольким переданным сегментам транспортного уровня.
Весь разработанный код выложен в репозитории на github. В readme репозитория дана инструкция, как вы можете повторить полученные в статье результаты. В папке csv находятся таблицы признаков, полученные после обработки собранных PCAP-файлов. А вот сами файлы PCAP вы там не найдёте – прошу прощения, но конфиденциальность моих данных прежде всего.
В качестве наглядной иллюстрации работы классификатора в реальном времени я разработал несложное приложение на PyQt4. И даже записал видео с демонстрацией его работы.
Для чего можно применять изложенный подход? Например, для идентификации используемых протоколов прикладного уровня в тех условиях, когда полезная нагрузка зашифрована (привет, паранойя). Или для превентивного сбора статистики для анализа на случай, если такой анализ потребуется – ведь для определения протокола прикладного уровня достаточно иметь всего 256 байт информации на каждый поток (32 числа с плавающей точкой).
Благодарности
Хочу поблагодарить Даниила Прохорова и Надежду Трофлянину, с которыми мы занимались разработкой этой идеи, собирали трафик и потом выступали на VII Молодёжном Научном Форуме МТУСИ.
Источники
Неправильно было бы не указать научные работы, из которых мы почерпнули вдохновение и несколько хороших идей: