Источник: polymerh.
На Хабре достаточно статей про передачу данных через протокол ICMP. Чего говорить, шесть лет назад я сам писал про стеганографию в IP-пакетах и «пингах». Но кажется, самое время вернуться к этой теме и предложить неочевидные методы.
Если вам кажется, что тема передачи данных в ICMP уже исчерпана и я не смогу вас удивить, то предлагаю извлечь данные из дампа сетевого трафика до прочтения статьи. То, что будет дальше, может ввести в недоумение.
При подготовке статьи я задался вопросом «Насколько скрытно и быстро можно передавать данные в ICMP?» Поэтому в статье будет только сетевая стеганография.
Дисклеймер: статья написана исключительно в академических целях.
Основы передачи данных
Протокол ICMP (Internet Control Message Protocol) входит в стек TCP/IP и используется для передачи служебных сообщений между узлами сети. Вот, что говорит Wikipedia:
В основном ICMP используется для передачи сообщений об ошибках и других исключительных ситуациях, возникших при передаче данных, например, запрашиваемая услуга недоступна или хост или маршрутизатор не отвечают. Также на ICMP возлагаются некоторые сервисные функции (services).
Опытный пользователь ПК в явном виде сталкивается с тремя типами ICMP-пакетов.
- Эхо-запрос. «Пинги» утилиты ping(1), которые используют для проверки достижимости узла в сети.
- Эхо-ответ. Это пакеты, которые отправляют (или не отправляют) узлы сети в ответ на эхо-запросы.
- Время жизни пакета (TTL) истекло. Такие пакеты используются в утилите traceroute(1) для определения промежуточных узлов сети. Утилита отправляет пакет с TTL равным N. N-ый узел в сети отметит, что пакет «все» и отправит соответствующий ICMP-пакет.
Самые «неприметные» — это эхо-запросы. Посмотрим на их строение.
Нам доступны несколько полей.
- Тип: 8 (эхо-запрос). Неизменяемое поле.
- Код: 0. Неизменяемое поле.
- Контрольная сумма. Вычисляемое поле.
- Идентификатор. Номер, который можно грубо назвать «сессией» пингов.
- Номер в последовательности. Порядковый номер в рамках «сессии».
- Данные.
Передача полезных данных в блоке «Данные» — это самый очевидный и самый явный способ.
Плюсы
- Один ICMP-пакет может содержать до 65535 байт информации за вычетом заголовков IP и ICMP (ограничение IP-пакетов).
- Все сделано за нас: аргумент -p утилиты ping позволяет указать данные для передачи в эхо-запросах, а аргумент -s — указать размер данных.
Минусы
- Без шифрования данных факт передачи очевиден.
- Пакеты легко обнаружить из-за большого размера.
- Различные реализации ICMP-тоннелей используют «магические» числа для разделения обычных «пингов» и полезных данных.
Этот способ передачи данных хорош в условиях, когда TCP и UDP по каким-то причинам недоступны, а на ICMP не наложили серьезных ограничений. Скрытой передачей данных тут и не пахнет. Поэтому обратимся к «нормальному» ICMP-трафику.
«Нормальные» пакеты
Чтобы стать нормальным пакетом, надо думать как нормальный пакет. Посмотрим на изменяемые поля ICMP.
- Контрольная сумма. Вычисляемое поле. Можем управлять его значением, изменяя значения остальных полей пакета.
- Идентификатор. 16-битное число, одинаковое в рамках одного «пингования».
- Номер в последовательности. Порядковый номер начинается с 1 и увеличивается с каждым пакетом.
- Данные. Обязательно рассмотрим чуть позже.
На поверку оказывается, что идентификатор ICMP может измениться при прохождении через NAT. При этом его изменение вызывает пересчет контрольной суммы. Это значит, что мы не можем передавать данные в идентификаторе или контрольной сумме.
Совершенно не подозрительно! Особенно нуль-терминатор в конце. :)
Номер последовательности увеличивается на единицу с каждым пакетом, какие-то другие манипуляции могут быть пропущены на сетевом оборудовании, но в дампе трафика такие фокусы легко обнаружить.
Посмотрим в блок данных. Что по умолчанию передается в эхо-запросах, если пользователь запускает утилиту ping(1) на своем компьютере? Я нашел пару вариантов.
- Windows. 32 байта. Полезная нагрузка — английский алфавит без букв Y и Z, циклически заполняющий доступное пространство.
- macOS, Linux (ping from iputils). 56 байт. i-тый байт полезной нагрузки вычисляется по формуле i % 256. Дополнительно первые 8 или 16 байт, в зависимости от разрядности ОС, занимает структура timeval.
Утилита из набора iputils использует прекрасное решение, когда в начало ICMP-данных сохраняется временная метка отправки пакета. Так как эхо-ответ обязан содержать данные из эхо-запроса, то время круговой задержки (round trip time, RTT) можно рассчитать как разницу текущего времени и времени, записанного в данных эхо-ответа. Такой подход, конечно, упрощает разработку, так как можно не хранить время отправки эхо-запросов.
Оптимизация с отправкой времени в теле пакета — это старый прием. Как минимум, в первом коммите набора iputils на GitHub от 2002 года (был импорт из другого источника) уже используется. Поэтому анализаторы трафика, в частности, Wireshark, подсвечивают метку времени.
Метка времени — это данные? Wireshark с вами не согласен.
Как видно на скриншоте, Wireshark вынес метку времени в заголовок ICMP-сообщения и вычел ее размер из блока Data, хотя RFC 792 ничего такого не регламентирует. Такое «удобство» можно использовать в своих целях. Рассмотрим структуру данных timeval, которая описывается в системном вызове gettimeofday(2):
/* Описание в документации */
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
/* Описание в sys/time.h */
/* A time value that is accurate to the nearest
microsecond but also has a range of years. */
struct timeval
{
#ifdef __USE_TIME_BITS64
__time64_t tv_sec; /* Seconds. */
__suseconds64_t tv_usec; /* Microseconds. */
#else
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
#endif
Видим, что поле tv_sec хранит количество секунд с начала эпохи, то есть с 00:00:00 1 января 1970 года, а tv_usec содержит дополнительную точность в микросекундах. Международная система единиц (СИ) говорит, что в одной секунде миллион микросекунд:
999999 = 0xF423F
Это значит, что в микросекундах мы можем хранить до двух байт полезной информации. Сделать это можно так:
struct timeval* t = (struct timeval*)icmp_packet.payload;
gettimeofday(t, NULL);
t->tv_usec = (t->tv_usec - t->tv_usec & 0xFFFF) + data;
Два младших байта искажают метку времени и могут добавить до 65 мс. Например, вот так:
Метка времени: 2024-04-24 14:00:00.000000
Данные: 0xFFFF, в десятичной системе счисления 65535.
Метка времени в пакете: 2024-04-24 14:00:00.065535
Пакет отправлен в: 2024-04-24 14:00:00.001000
Пакет перехвачен снифером в: 2024-04-24 14:00:00.002000
Метка пакета в будущем!
Отрицательное относительное время в Wireshark не выделяется как ошибка или предупреждение.
Сам по себе пакет «из будущего» — не проблема. Это может быть некорректная настройка времени у отправителя или перехватчика. Но «прыгающее» из будущего в прошлое время может навести на мысли о передаче сообщения. Более того, передача английского текста будет заметна в шестнадцатеричном редакторе.
Плюсы
- В пакете нет магических констант или чего-то, что выдает передачу данных
- Особенности микросекундной точности хорошо маскируют неповторяющиеся данные.
Минусы
- Необходимо отслеживать, чтобы пакет не оказался «из будущего».
- Положение передаваемых данных фиксировано. В моем случае — байты 0x33 и 0x34.
- Скорость передачи: 2 байта в секунду.
Фиксированность местоположения передаваемых данных меня сильно огорчала, так как это упрощает анализ. Более того, передача массива одинаковых данных, например нулей, будет выглядеть как отправка эхо-запросов промежутками в одну секунду с точностью до микросекунды. Без погрешности. Это потенциально возможно, но подозрительно.
Можно ли передать данные где-то «вне» пакета?
Скрытый параметр ICMP
У ICMP есть «скрытый» параметр, который используется при работе, но никак не появляется в заголовках, — интервал. Эхо-запросы отправляются с определенным интервалом. Это совершенно ожидаемое поведение, так как ICMP используется для мониторинга доступности. Закодируем в этом интервале сообщение!
По умолчанию для утилиты ping(1) интервал между эхо-запросами составляет одну секунду. Добавим некоторое количество миллисекунд равное, например, пересылаемому байту. Так как сеть по умолчанию считается ненадежной, то подгоняем метку времени в ICMP-пакете под вычисленное значение и плюс-минус вовремя отправляем пакет.
/* Добавляем байт данных в виде миллисекунд */
tv.tv_usec += ((uint8_t)message[i]) * 1000;
/* Добавляем секунду и возможный перенос из микросекунд */
tv.tv_sec += 1 + tv.tv_usec / 1000000;
/* Убираем переполнение, если оно есть */
tv.tv_usec = tv.tv_usec % 1000000;
/* Опционально: модифицируем микросекунды, чтобы выглядело натурально */
tv.tv_usec = ((tv.tv_usec / 1000) * 1000) + (rand() % 1000);
Теперь байт данных — это разница временных меток между пакетами. При этом постоянно изменяются три байта микросекунд, что может создать ложный след. Особенно, если цель — найти данные непосредственно в ICMP.
/* Указатели на структуры timeval в первом и втором пакете соответственно */
struct timeval* x, *y;
/* Считаем разницу:
* 1. Секунды вычитаем сразу, чтобы избежать переполнения.
* 2. Конвертируем секунды (п.1) в миллисекунды.
* 3. Конвертируем микросекунды в миллисекунды, отбрасывая остаток от деления
* 4. Добавляем миллисекунды второго пакета к остатку из секунд,
* вычитаем миллисекунды первого пакета.
*/
uint8_t data = (y->tv_sec - x->tv_sec - 1) * 1000 + (y->tv_usec / 1000) - (x->tv_usec / 1000);
Дополнительная задержка между пакетами должна быть заметна, но насколько? Пришла пора закодить и проверить — можно использовать Python и Scapy, но я пошел по пути С. Для отправки и получения ICMP-пакетов нужно открывать «сырой» (raw) сокет, а для этого нужны права суперпользователя.
Слева — «чистые» пакеты, справа — с данными.
Даже «чистые» эхо-запросы отправляют не строго раз в секунду, а с задержкой до трех миллисекунд. У эхо-запросов с данными задержка существенно больше. Впрочем, это сейчас хорошо видно, когда мы знаем, куда смотреть, а рядом для сравнения есть «нормальный» дамп.
Плюсы
- Данные не появляются в пакетах в явном виде, а сам пакет выглядит обычным.
- Обнаружение таких пакетов требует обращения к истории прошлых ICMP-пакетов с того же адреса. Это кажется достаточно ресурсоемкой задачей для автоматизации.
Минусы
- Скорость передачи — байт в секунду.
- Потеря одного пакета приводит к потере двух байтов.
Да, это не очень быстрый способ передачи сообщений, но в случае доступности ICMP вполне скрытный.
Если передавать необходимо текст, то можно использовать компактную кодировку. Например, только заглавные буквы алфавита и пробел: 34 символа для русского алфавита. А если алфавит отсортировать с учетом частотности символов, то отклонение множества пакетов будет в рамках погрешности и заметить передачу будет сложнее.
Где это применимо
Действительно, зачем? Источник.
Как и обозначалось в начале статьи, этот способ стеганографии носит скорее академический характер. Я протестировал описанный способ на своих друзьях и заинтересованных коллегах, но переданное сообщение никто не смог разгадать. Выборка, конечно, небольшая, но как есть.
Незадолго до публикации статьи я выложил дамп для изучения в качестве тизера в моем Telegram-канале. Там я пишу маленькие познавательные тексты по темам прошлых или будущих статей.
Допустим, что этот метод стеганографии кому-то потребуется. Есть ряд нерешенных вопросов.
Гарантия доставки
Протокол ICMP не дает никаких гарантий. Пакет может быть потерян, пакеты могут прийти в неверном порядке. При некоторых условиях эту проблему можно игнорировать. При подготовке статьи я отправил 5 000 эхо-запросов на адрес австралийского зеркала Debian. Круговая задержка была всего 300-350 мс, но ни один пакет не был потерян и перепутан.
Проблема неправильного порядка решается вниманием к полю «номер в последовательности» в заголовке ICMP, а потеря пакетов решается избыточным кодированием. Здесь можно использовать оператор XOR для резервирования N байт дополнительным одним байтом или коды Рида-Соломона, если совсем скучно.
Однонаправленность
Описанный в статье способ — это однонаправленный канал связи. Приемник данных обязан скопировать данные из эхо-запроса в эхо-ответ. Модификация данных эхо-запроса будет определена как «битый» пакет, что само по себе привлекает внимание. Использовать задержку между отправками эхо-ответов будет затруднительно из-за джиттера при пересылке пакетов.
Система «свой-чужой»
Сперва я задумывался о каких-то способах различия между эхо-запросами с полезной нагрузкой и «обычными пингами». Я специально оставил сниффер входящих ICMP-пакетов на 78 часов. За это время я определил, что мой сервер «пинговали» 1 255 адресов, в большинстве случаев — реже одного раза в минуту с одного адреса.
Также у пакетов «интернет-пингователей» были необычные поля. Например, ICMP SEQ, «номер в последовательности», — не единица, произвольное число. Кроме того, у большинства размер полезной нагрузки — 8 байт (размер IP-пакета — 60 байт), которые Wireshark не разбирает в метку времени, но это временная метка в наносекундах.
Так что для распознавания «своих» передатчику достаточно отправить пару десятков эхо-запросов и поддерживать корректное поле «номер в последовательности».
Как вам мое стеганографическое безумие? Попробуйте свои силы и «разгадайте» дамп. Первый, кто отправит ответ в комментариях, получит мерч Selectel.