- PVSM.RU - https://www.pvsm.ru -

Источник: polymerh.
На Хабре достаточно статей про передачу данных через протокол ICMP. Чего говорить, шесть лет назад я сам писал про стеганографию [1] в IP-пакетах и «пингах». Но кажется, самое время вернуться к этой теме и предложить неочевидные методы.
Если вам кажется, что тема передачи данных в ICMP уже исчерпана и я не смогу вас удивить, то предлагаю извлечь данные из дампа [2] сетевого трафика до прочтения статьи. То, что будет дальше, может ввести в недоумение.
При подготовке статьи я задался вопросом «Насколько скрытно и быстро можно передавать данные в ICMP?» Поэтому в статье будет только сетевая стеганография.
Дисклеймер: статья написана исключительно в академических целях.
Протокол ICMP (Internet Control Message Protocol) входит в стек TCP/IP и используется для передачи служебных сообщений между узлами сети. Вот, что говорит Wikipedia:
В основном ICMP используется для передачи сообщений об ошибках и других исключительных ситуациях, возникших при передаче данных, например, запрашиваемая услуга недоступна или хост или маршрутизатор не отвечают. Также на ICMP возлагаются некоторые сервисные функции (services).
Опытный пользователь ПК в явном виде сталкивается с тремя типами ICMP-пакетов.
Самые «неприметные» — это эхо-запросы. Посмотрим на их строение.

Нам доступны несколько полей.
Передача полезных данных в блоке «Данные» — это самый очевидный и самый явный способ.
Плюсы
Минусы
Этот способ передачи данных хорош в условиях, когда TCP и UDP по каким-то причинам недоступны, а на ICMP не наложили серьезных ограничений. Скрытой передачей данных тут и не пахнет. Поэтому обратимся к «нормальному» ICMP-трафику.

Чтобы стать нормальным пакетом, надо думать как нормальный пакет. Посмотрим на изменяемые поля ICMP.
На поверку оказывается, что идентификатор ICMP может измениться при прохождении через NAT. При этом его изменение вызывает пересчет контрольной суммы. Это значит, что мы не можем передавать данные в идентификаторе или контрольной сумме.

Совершенно не подозрительно! Особенно нуль-терминатор в конце. :)
Номер последовательности увеличивается на единицу с каждым пакетом, какие-то другие манипуляции могут быть пропущены на сетевом оборудовании, но в дампе трафика такие фокусы легко обнаружить.
Посмотрим в блок данных. Что по умолчанию передается в эхо-запросах, если пользователь запускает утилиту ping(1) на своем компьютере? Я нашел пару вариантов.
Утилита из набора iputils использует прекрасное решение, когда в начало ICMP-данных сохраняется временная метка отправки пакета. Так как эхо-ответ обязан содержать данные из эхо-запроса, то время круговой задержки (round trip time, RTT) можно рассчитать как разницу текущего времени и времени, записанного в данных эхо-ответа. Такой подход, конечно, упрощает разработку, так как можно не хранить время отправки эхо-запросов.
Оптимизация с отправкой времени в теле пакета — это старый прием. Как минимум, в первом коммите набора iputils на GitHub [3] от 2002 года (был импорт из другого источника) уже используется. Поэтому анализаторы трафика, в частности, Wireshark, подсвечивают метку времени.

Метка времени — это данные? Wireshark с вами не согласен.
Как видно на скриншоте, Wireshark вынес метку времени в заголовок ICMP-сообщения и вычел ее размер из блока Data, хотя RFC 792 [4] ничего такого не регламентирует. Такое «удобство» можно использовать в своих целях. Рассмотрим структуру данных timeval, которая описывается в системном вызове gettimeofday(2) [5]:
/* Описание в документации */
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 не выделяется как ошибка или предупреждение.
Сам по себе пакет «из будущего» — не проблема. Это может быть некорректная настройка времени у отправителя или перехватчика. Но «прыгающее» из будущего в прошлое время может навести на мысли о передаче сообщения. Более того, передача английского текста будет заметна в шестнадцатеричном редакторе.
Плюсы
Минусы
Фиксированность местоположения передаваемых данных меня сильно огорчала, так как это упрощает анализ. Более того, передача массива одинаковых данных, например нулей, будет выглядеть как отправка эхо-запросов промежутками в одну секунду с точностью до микросекунды. Без погрешности. Это потенциально возможно, но подозрительно.
Можно ли передать данные где-то «вне» пакета?
У 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);
Дополнительная задержка между пакетами должна быть заметна, но насколько? Пришла пора закодить [6] и проверить — можно использовать Python и Scapy, но я пошел по пути С. Для отправки и получения ICMP-пакетов нужно открывать «сырой» (raw) сокет, а для этого нужны права суперпользователя.

Слева — «чистые» пакеты, справа — с данными.
Даже «чистые» эхо-запросы отправляют не строго раз в секунду, а с задержкой до трех миллисекунд. У эхо-запросов с данными задержка существенно больше. Впрочем, это сейчас хорошо видно, когда мы знаем, куда смотреть, а рядом для сравнения есть «нормальный» дамп.
Плюсы
Минусы
Да, это не очень быстрый способ передачи сообщений, но в случае доступности ICMP вполне скрытный.
Если передавать необходимо текст, то можно использовать компактную кодировку. Например, только заглавные буквы алфавита и пробел: 34 символа для русского алфавита. А если алфавит отсортировать с учетом частотности [7] символов, то отклонение множества пакетов будет в рамках погрешности и заметить передачу будет сложнее.

Действительно, зачем? Источник [8].
Как и обозначалось в начале статьи, этот способ стеганографии носит скорее академический характер. Я протестировал описанный способ на своих друзьях и заинтересованных коллегах, но переданное сообщение никто не смог разгадать. Выборка, конечно, небольшая, но как есть.
Незадолго до публикации статьи я выложил дамп для изучения в качестве тизера в моем Telegram-канале [9]. Там я пишу маленькие познавательные тексты по темам прошлых или будущих статей.
Допустим, что этот метод стеганографии кому-то потребуется. Есть ряд нерешенных вопросов.
Протокол ICMP не дает никаких гарантий. Пакет может быть потерян, пакеты могут прийти в неверном порядке. При некоторых условиях эту проблему можно игнорировать. При подготовке статьи я отправил 5 000 эхо-запросов на адрес австралийского зеркала Debian. Круговая задержка была всего 300-350 мс, но ни один пакет не был потерян и перепутан.
Проблема неправильного порядка решается вниманием к полю «номер в последовательности» в заголовке ICMP, а потеря пакетов решается избыточным кодированием [10]. Здесь можно использовать оператор XOR для резервирования N байт дополнительным одним байтом или коды Рида-Соломона, если совсем скучно.
Описанный в статье способ — это однонаправленный канал связи. Приемник данных обязан скопировать данные из эхо-запроса в эхо-ответ. Модификация данных эхо-запроса будет определена как «битый» пакет, что само по себе привлекает внимание. Использовать задержку между отправками эхо-ответов будет затруднительно из-за джиттера при пересылке пакетов.
Сперва я задумывался о каких-то способах различия между эхо-запросами с полезной нагрузкой и «обычными пингами». Я специально оставил сниффер входящих ICMP-пакетов на 78 часов. За это время я определил, что мой сервер «пинговали» 1 255 адресов, в большинстве случаев — реже одного раза в минуту с одного адреса.
Также у пакетов «интернет-пингователей» были необычные поля. Например, ICMP SEQ, «номер в последовательности», — не единица, произвольное число. Кроме того, у большинства размер полезной нагрузки — 8 байт (размер IP-пакета — 60 байт), которые Wireshark не разбирает в метку времени, но это временная метка в наносекундах.
Так что для распознавания «своих» передатчику достаточно отправить пару десятков эхо-запросов и поддерживать корректное поле «номер в последовательности».
Как вам мое стеганографическое безумие? Попробуйте свои силы и «разгадайте» дамп [2]. Первый, кто отправит ответ в комментариях, получит мерч Selectel.
Автор: Владимир
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/selectel/391185
Ссылки в тексте:
[1] стеганографию: https://habr.com/ru/articles/413851/
[2] из дампа: https://slc.tl/sbpx8
[3] в первом коммите набора iputils на GitHub: https://github.com/iputils/iputils/blob/33370345c7d8c217b51c13b0e2864b64b53d5f96
[4] RFC 792: https://datatracker.ietf.org/doc/html/rfc792%23:~:text=Echo%2520or%2520Echo%2520Reply%2520Message
[5] gettimeofday(2): https://www.opennet.ru/man.shtml?topic=gettimeofday%26russian=2
[6] закодить: https://github.com/Firemoon777/icmp-stegano
[7] частотности: https://ru.wikipedia.org/wiki/%25D0%25A7%25D0%25B0%25D1%2581%25D1%2582%25D0%25BE%25D1%2582%25D0%25BD%25D0%25BE%25D1%2581%25D1%2582%25D1%258C
[8] Источник: https://pikabu.ru/story/potomu_chto_mozhem_1768485
[9] Telegram-канале: https://t.me/%2BVzpLr5pam-MxODEy
[10] избыточным кодированием: https://habr.com/ru/companies/yandex/articles/510050/
[11] Источник: https://habr.com/ru/companies/selectel/articles/809599/?utm_source=habrahabr&utm_medium=rss&utm_campaign=809599
Нажмите здесь для печати.