Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Любая возможность незаметно обращаться к внешнему миру с хоста внутри защищенной сети — драгоценная находка для пентестера. Один из последних доступных путей — это NTP, протокол синхронизации времени на часах. Его трафик разрешен почти везде, так что он будет отличным транспортом для данных. Я покажу, как реализовать базовые клиент и сервер, которые будут использовать этот феномен, а кодить мы будем на C#.
Пару месяцев назад я гулял по загнивающей Германии, где по каждому удобному и не очень поводу строят туннель. И тут мне пришла идея: а не сделать ли свой туннель? В качестве протокола я выбрал 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 описана в 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; // Время отправки пакета с сервера клиенту
}
Теперь немного о назначении этих полей.
Иллюстрация из Wikipedia
В целом процесс крайне прост и понятен, если изучить картинку. Клиент посылает запрос на сервер, запоминая, когда этот запрос был отправлен. Сервер принимает пакет, запоминает и записывает в пакет время приема, заполняет время отправки и отвечает клиенту. Клиент запоминает, когда он получил ответ, и получает нечто вроде RTT (Round-Trip Time, в простонародье — пинг) до сервера. Дальше он определяет, сколько времени понадобилось пакету, чтобы дойти от сервера обратно ему (время между запросом и ответом клиента минус время обработки пакета на сервере, деленное на два).
Чтобы получить текущее время, нужно прибавить полученную задержку канала к времени отправки ответа сервером. Вот только UDP на то и UDP, что задержки могут быть случайные и непредсказуемые, так что замеры повторяются по многу раз в день, вычисляется средняя ошибка, и локальные часы корректируются.
Системы обнаружения вторжений не такие глупые, какими могут показаться, так что просто пустить трафик, например, 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
, Ref ID
, Receive
и, ограниченно, Poll
. На ответ сервера в этом поле распространяются такие же ограничения, как на клиента. Итого имеем 29 (28 без Poll
) байт из 48 (60% пакета). Полезный объем пакета — 46 из 48 байт (96%). Оставшиеся два байта — флаги и заголовки, которые мы менять не можем без вреда для скрытности.
Писать код и дебажить наше творение мы будем в Visual Studio. Я использую версию 2019 Community, благо она бесплатная, а скачать ее можно с сайта Microsoft.
Как только IDE установлена, включена темная тема и любимый плей-лист, можно приступать. Для начала создадим новый проект типа «консольное приложение» (мы ведь не прячемся от юзера) с названием NtpTun_SERVER
.
Создание проекта
Теперь нам нужна структура, описывающая пакет. Обратившись к спецификации NTP, напишем простой класс. В нем также должны быть методы упаковки пакета в массив байтов, пригодный для передачи настоящему серверу и для распаковки пришедшего ответа из массива байтов обратно в пакет.
Объявляем структуру пакета. Не смотри на странные суффиксы в названиях функций, так задумано
Уже из этого кода видно, что мы будем притворяться сервером 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 получаем оттуда данные.
Источник: xakep.ru