Туннель во времени. Выводим данные с компьютера через Network Time Protocol

Любая возможность незаметно обращаться к внешнему миру с хоста внутри защищенной сети — драгоценная находка для пентестера. Один из последних доступных путей — это NTP, протокол синхронизации времени на часах. Его трафик разрешен почти везде, так что он будет отличным транспортом для данных. Я покажу, как реализовать базовые клиент и сервер, которые будут использовать этот феномен, а кодить мы будем на C#.

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

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

WARNING

Создание и распространение вредоносных программ карается по статье 273 УК РФ. Ни автор, ни редакция «Хакера» не несут ответственности ни за какой вред, принесенный использованием материалов этой статьи.

Что такое NTP

NTP (Network Time Protocol) — протокол, который работает поверх UDP и используется для синхронизации локальных часов с часами на сервере точного времени. При работе в интернете точность синхронизации составляет до 10 мс, а в локальных сетях — до 0,2 мс. При этом NTP нечувствителен к задержкам канала.

Актуальная версия протокола (по данным Википедии) — 4, но мы будем использовать версию 3, которой для наших целей предостаточно.

Для максимальной точности служба обновления времени постоянно должна работать в фоновом режиме, регулярно отправляя запросы на сервер точного времени, то есть генерируя довольно много трафика. Это нам на руку, так как из-за этой особенности IDS давно не обращают внимания на трафик NTP.

За синхронизацию в Windows отвечает служба W32Time, а в Linux — демон ntpd или chronyd. Также существует более простая реализация этого протокола, известная как SNTP (Simple Network Time Protocol) — простой протокол сетевого времени. Применяют его во встраиваемых системах и устройствах, которые не требуют высокой точности, как, например, системы умного дома.

Структура пакета NTP

Структура пакета NTP описана в RFC 958 (v1), 1119 (v2), 1305 (v3) и 5905 (v4). Нас интересует версия 3, как довольно распространенная и простая, хотя ты свободно можешь пользоваться версией 4, она почти не отличается.


Для прожженных программистов на C есть псевдокод:

public struct NtpPacket
{
  public byte First8bits; // 2 бита — индикатор секунды коррекции (Leap Indicator, LI)
                          // 3 бита — версия протокола (Version Number, VN)
                          // 3 бита — режим работы (Mode)
  public byte Stratum;    // Stratum — расстояние до корневого сервера по иерархии
  public byte Poll;       // Насколько часто можно спрашивать сервер
  public byte Precision;  // Точность системных часов
  public uint RootDelay;  // Задержка сервера относительно главного источника времени
  public uint RootDisp;   // Разброс показаний часов сервера
  public uint RefID;      // ID часов
  public ulong Reference; // Последние показания часов на сервере
  public ulong Originate; // Правильное время отправки пакета клиентом (заполняет сервер)
  public ulong Receive;   // Время получения пакета сервером
  public ulong Transmit;  // Время отправки пакета с сервера клиенту
}

Теперь немного о назначении этих полей.

  • Leap indicator (LI), 2 бита — число, предупреждающее о секунде координации. Может быть от 0 до 3, где 0 — нет коррекции, 1 — последняя минута дня содержит 61 с, 2 — последняя минута дня содержит 59 с, 3 — неисправность сервера. При значении 3 полученным данным доверять не следует. Вместо этого нужно обратиться к другому серверу. Наш псевдосервер будет всегда возвращать 0.
  • Version number (VN), 2 бита — номер версии протокола NTP (1–4). Мы поставим туда 3.
  • Mode — режим работы отправителя пакета. Значение от 0 до 7, где 3 — клиент, а 4 — сервер.
  • Stratum — сколько посредников между клиентом и эталонными часами (включая сам NTP-сервер). 1 — сервер берет данные непосредственно с атомных (или других точных) часов, то есть между клиентом и часами только один посредник (сам сервер); 2 — сервер берет данные с сервера со значением Stratum 1 и так далее.
  • Poll — целое число, задающее интервал в секундах между последовательными обращениями. Клиент может указать здесь интервал, с которым он хочет отправлять запросы на сервер, а сервер — интервал, с которым он разрешает, чтобы его опрашивали.
  • Precision (точность) — число, которое сообщает точность локальных системных часов. Значение равно двоичному логарифму секунд.
  • Root delay (задержка сервера) — время, за которое показания эталонных часов доходят до сервера NTP. Задается как число секунд с фиксированной запятой.
  • Root dispersion — разброс показаний сервера.
  • RefID (идентификатор источника) — ID часов. Если поле Stratum равно единице, то RefID — имя атомных часов (четыре символа ASCII). Если текущий сервер NTP использует показания другого сервера, то в RefID записан IP-адрес этого сервера.
  • Reference — последние показания часов сервера.
  • Originate — время, когда пакет был отправлен, по версии сервера.
  • Receive — время получения запроса сервером.
  • Transmit — время отправки ответа сервера клиенту, которое заполняет клиент.
Иллюстрация из Wikipedia

В целом процесс крайне прост и понятен, если изучить картинку. Клиент посылает запрос на сервер, запоминая, когда этот запрос был отправлен. Сервер принимает пакет, запоминает и записывает в пакет время приема, заполняет время отправки и отвечает клиенту. Клиент запоминает, когда он получил ответ, и получает нечто вроде RTT (Round-Trip Time, в простонародье — пинг) до сервера. Дальше он определяет, сколько времени понадобилось пакету, чтобы дойти от сервера обратно ему (время между запросом и ответом клиента минус время обработки пакета на сервере, деленное на два).

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

Ограничения на трафик по порту UDP-123

Системы обнаружения вторжений не такие глупые, какими могут показаться, так что просто пустить трафик, например, OpenVPN по 123-му порту UDP мы не сможем, по крайней мере без риска спалиться. Соответствие RFC все же проверяется. Это можно посмотреть на примере Wireshark.

Один из NTP-пакетов, пойманных Wireshark

Придется нам заставить наши пакеты соответствовать RFC. Проще всего это сделать, назначая некоторые поля по своему усмотрению. Мы можем внедрить свои данные в поля Transmit и Originate. Последнее не вполне соответствует RFC, но так глубоко проверки обычно не добираются.

Концепт

Идея проста: мы составляем собственный «заряженный» пакет NTP и пытаемся синхронизировать время со своим сервером. Чтобы не привлекать лишнего внимания к своей передаче, на каждый запрос должен отправляться внешне валидный ответ, в котором могут быть инструкции для клиента (читай: бота).

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

Для передачи данных с клиента на сервер подходят поля Poll, Originate и Transmit. Из них Poll пригоден ограниченно, но мы на этом останавливаться не будем. Если ты задумаешь учесть его ограничение, то имей в виду, что использовать в этом поле можно только младшие три бита (как я понял из документации). Без учета этого мы можем использовать 17 байт из 48 (35% всего объема пакета) на отправку данных, что уже неплохо.

А что на прием? Сервер заполняет поля Precision, Root delay, Root dispersion, Reference, RefID, Receive и, ограниченно, Poll. На ответ сервера в этом поле распространяются такие же ограничения, как на клиента. Итого имеем 29 (28 без Poll) байт из 48 (60% пакета). Полезный объем пакета — 46 из 48 байт (96%). Оставшиеся два байта — флаги и заголовки, которые мы менять не можем без вреда для скрытности.

Реализация

Писать код и дебажить наше творение мы будем в Visual Studio. Я использую версию 2019 Community, благо она бесплатная, а скачать ее можно с сайта Microsoft.

Сервер

Как только IDE установлена, включена темная тема и любимый плей-лист, можно приступать. Для начала создадим новый проект типа «консольное приложение» (мы ведь не прячемся от юзера) с названием NtpTun_SERVER.

Создание проекта

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

WWW

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

Объявляем структуру пакета. Не смотри на странные суффиксы в названиях функций, так задумано

Уже из этого кода видно, что мы будем притворяться сервером Stratum 3. Если бы мы были Stratum 1, то нужно было бы в поле RefID указывать ID атомных часов, которых у нас нет. А список серверов первого уровня общеизвестен, и, если IP нашего псевдосервера не окажется в таких публичных списках, обман быстро будет раскрыт.

Stratum 2 не следует использовать, потому что тогда RefID должен был бы содержать IP сервера первого уровня, список которых опять же известен. А вот третий уровень позволяет указывать в RefID IP сервера второго уровня, полного списка которых нет. То есть мы сможем в RefID передавать еще четыре байта произвольных данных.

Код методов упаковки и распаковки на скриншот не поместился, к тому же нам надо разобрать его отдельно. Вот он:

public NtpPacket Parse(byte[] data)
{
  var r = new NtpPacket();
  // NTP packet is 48 bytes long
  r.First8bits = data[0];
  r.Poll = data[2];
  r.Precision = data[3];
  r.RootDelay = BitConverter.ToUInt32(data, 4);
  r.RootDisp = BitConverter.ToUInt32(data, 8);
  r.RefID = BitConverter.ToUInt32(data, 12);
  r.Reference = BitConverter.ToUInt64(data, 16);
  r.Originate = BitConverter.ToUInt64(data, 24);
  r.Receive = BitConverter.ToUInt64(data, 32);
  r.Transmit = BitConverter.ToUInt64(data, 40);
  return r;
}

Тут никаких сложностей: принимаем массив байтов и при помощи BitConverter получаем оттуда данные.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


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

  • поэтому я и использую свой сервер с защитой временем

  • Как правило в офисной сети время на рабочие компьютеры раздаёт сервер админа. А все локальные возможности синхронизации времени отключены.