Masscan — быстрый сетевой сканер, который хорошо подходит для сканирования большого диапазона IP-адресов и портов. Мы немного доработали его, адаптировав под свои нужды.
Больше всего неудобств оригинала было связано с невозможностью собирать баннеры с HTTPS-серверов. А что такое современный веб без HTTPS? Особо ничего не насканируешь. Это и смотивировало нас изменить masscan. Как обычно бывает, одна маленькая доработка переросла в другую, обнаружились баги. Теперь мы хотим поделиться нашими наработками с сообществом. Все изменения, о которых пойдет речь, уже доступны в нашем репозитории на GitHub.
Зачем нам нужны сетевые сканеры
Сетевой сканер — один из универсальных инструментов исследований в области кибербезопасности. С его помощью мы решаем такие задачи, как анализ периметра, сканирование уязвимостей, поиск фишинга и утечек, обнаружение C&C, сбор информации о хосте.
Устройство masscan
Прежде чем говорить о кастомной версии, разберемся, как устроен оригинальный masscan. Если вы с ним уже хорошо знакомы, возможно, вам будет интересна подборка полезных опций сканера. Или сразу листайте до раздела с нашими доработками.
Проект masscan небольшой, написанный, на наш взгляд, аккуратно и логично. Порадовало обилие комментариев — даже недоработки и костыли явно помечены в коде:
Логически код можно поделить на несколько частей:
реализация прикладных протоколов,
реализация TCP-стека,
потоки обработки и отправки пакетов,
реализация форматов вывода,
чтение сырых пакетов.
Рассмотрим некоторые из них подробно.
Реализация прикладных протоколов
В основе masscan лежит модульная концепция. И чтобы сканер поддерживал любой протокол, нужно всего лишь зарегистрировать соответствующую структуру и потом не пропустить все места, где нужно прописать ее использование (хе-хе):
/**
* A registration structure for various TCP stream protocols
* like HTTP, SSL, and SSH
*/
struct ProtocolParserStream {
const char *name;
unsigned port;
const void *hello;
size_t hello_length;
unsigned ctrl_flags;
int (*selftest)(void);
void *(*init)(struct Banner1 *b);
void (*parse)(
const struct Banner1 *banner1,
void *banner1_private,
struct ProtocolState *stream_state,
const unsigned char *px, size_t length,
struct BannerOutput *banout,
struct InteractiveData *more);
void (*cleanup)(struct ProtocolState *stream_state);
void (*transmit_hello)(const struct Banner1 *banner1, struct InteractiveData *more);
Вот небольшое описание структуры.
Имя протокола и стандартный порт несут только информационную нагрузку. Поле сtrl_flags
нигде не используется.
Функция init
— инициализация протокола, parse
— метод, отвечающий за обработку входящего потока данных и генерирующий ответные сообщения, cleanup
— функция очистки для соединения.
Для генерации приветственного пакета, если сервер сам не отправил что-то первым, используется функция transmit_hello,
а если она не указана — данные из поля hello.
Функцию, тестирующую функциональность, можно указать в selftest.
Через этот механизм, например, реализована возможность писать обработчики на Lua (опция ‑‑script
). Однако у нас так и не дошли руки проверить, действительно ли она работает. Мы наткнулись на такую особенность masscan: большинство интересных параметров не описаны в документации, а сама она раскидана частично пересекающимися кусками по разным местам. Часть флагов можно найти только в исходниках (main-conf.c) Опция ‑‑script
— одна из таких, а некоторые другие, нужные и забавные, мы собрали в разделе «Полезные опции оригинального masscan».
Реализация TCP-стека
Одна из причин, почему masscan такой быстрый и может обрабатывать много соединений параллельно, — это собственная реализация TCP-стека. Она занимает около 1000 строк кода в файле proto-tcp.c.
Потоки обработки и отправки пакетов
Masscan быстрый и при этом однопоточный. Если быть точнее, он использует по два потока на каждый сетевой интерфейс, один из которых — поток на обработку входящих пакетов. Но обычно никто не пользуется возможностью запуска на нескольких интерфейсах одновременно.
/*
* Start all the threads
*/
for (index=0; index<masscan->nic_count; index++) {
struct ThreadPair *parms = &parms_array[index];
/*
* Start the scanning thread.
* THIS IS WHERE THE PROGRAM STARTS SPEWING OUT PACKETS AT A HIGH
* RATE OF SPEED.
*/
parms->thread_handle_xmit = pixie_begin_thread(transmit_thread, 0, parms);
/*
* Start the MATCHING receive thread. Transmit and receive threads
* come in matching pairs.
*/
parms->thread_handle_recv = pixie_begin_thread(receive_thread, 0, parms);
}
Один поток выполняет такие задачи:
Считывает сырые данные из сетевого интерфейса.
Обрабатывает их, прогоняя через собственный TCP-стек и обработчики прикладных протоколов.
Формирует нужные данные для отправки.
Складывает их в очередь
transmit_queue.
Другой поток забирает подготовленные для отправки сообщения из transmit_queue
и записывает их в сетевой интерфейс (рис. 1). Если отправка сообщений из очереди не превышает лимит, то формируются и отправляются SYN-пакеты для следующих целей сканирования.
Реализация форматов вывода
Эта часть концептуально схожа с модульной реализацией протоколов: здесь тоже есть структура OutputType
, в которой прописываются основные функции сериализации. Радует обилие всевозможных форматов вывода: кастомный бинарный, и современный NDJSON, и противный XML, и grep-абельный. Есть даже возможность сохранять данные в Redis. Напишите в комментариях, кто пробовал :)
Некоторые форматы совместимы (или, как пишет автор masscan, вдохновлены ими) с аналогичными утилитами, такими как nmap и unicornscan.
Чтение сырых пакетов
Masscan предоставляет возможность работать с сетевым адаптером через библиотеки PCAP или PFRING, а также читать данные из PCAP-дампа. В файле rawsock.c находится несколько функций, которые абстрагируют основной код от конкретных интерфейсов.
Чтобы выбрать PFRING,
нужно воспользоваться опцией ‑‑pfring,
а чтобы включить чтение из дампа — указать префикс file:
у адаптера.
Полезные опции оригинального masscan
Рассмотрим несколько интересных и полезных возможностей оригинального masscan, про которые редко пишут.
Опция | Описание | Комментарий |
| Справка | Даже вместе эти опции выдают очень мало полезного. Документация тоже содержит неполную информацию и раскидана по разным файлам: README.md, man, FAQ. Еще есть небольшой HOWTO по применению сканера совместно с AFL (American Fuzzy Lop). Если хочется узнать обо всех возможностях, то полный список параметров можно найти только в исходниках (main-conf.c) |
| Поддержка | Гигабайтные файлы построчного |
| Возможность сохранять результаты | А почему бы и нет? :) Если вы не работали с этим инструментом, почитать о нем можно тут |
| Поддержка IPv6 | Тут все понятно, но будет интересно почитать в комментариях о реальных кейсах использования. В голову приходит сканирование локальной сети или только небольшого диапазона какой-то конкретной страны, полученного через BGP |
| Кастомизация HTTP-запроса | При создании HTTP-запроса можно изменить любые его части под свои задачи: метод, URI, версию, заголовки, тело |
| Сканирование протоколов на нестандартных портах | Если masscan не получил приветственного пакета от цели, по умолчанию он кидает запрос первым, выбирая протокол с опорой на порт цели. Но иногда хочется посканировать HTTP на каком-нибудь нестандартном порте |
| Пауза | Masscan умеет аккуратно останавливаться и продолжать работу с того места, где закончил. По команде |
| Ротация файла с результатами | Данных в результатах может быть очень много, а эта опция позволяет указать максимальный размер файла, по достижении которого результаты начнут записываться в следующий файл |
| Горизонтальное | Masscan в псевдослучайном порядке выбирает цели из сканируемого диапазона. Если нужно запускать masscan на нескольких машинах в рамках одного диапазона, то добиться такого же случайного распределения даже между машинами можно, воспользовавшись этой опцией |
| Сканирование N | Эта опция пришла из |
| Скрипты на Lua | Есть сомнение, что опция работает, но сама возможность интересная. Кто использует, отпишитесь, может, у вас есть занятные примеры |
| Поиск некоторых известных уязвимостей | Про корректность и работоспособность не можем ничего сказать, так как данный механизм детектирования уязвимостей костылями разбросан по всему коду и конфликтует со многими другими опциями, а в реальных задачах применять его не приходилось |
Напомним один важный момент, на которые все натыкаются: masscan, скорее всего, не будет работать, если просто запустить его для сбора баннеров. Об этом написано в документации, но кто ее читает? Поскольку masscan использует собственный сетевой стек, то ОС ничего не знает о наших соединениях и очень удивляется, когда в ответ на SYN-запрос от сканера ей приходит пакет (SYN,
ACK
) откуда-то из сети. А дальше, в зависимости от типа и настроек ОС и межсетевого экрана, ОС кидает ICMP- или RST-пакет, что крайне негативно сказывается на результатах. Так что нужно читать документацию и учитывать этот момент.
Что мы изменили в masscan
Добавили поддержку HTTPS
Интернет нынче sекурный, уже даже самые отсталые мошенники отказались от HTTP без шифрования. Поэтому без поддержки HTTPS неудобно, с этой фичей куда проще решать исследовательские задачи, такие как поиск C&C-серверов и фишинга. Есть и другие инструменты помимо masscan, но работают они медленнее. Мы же хотели иметь универсальный инструмент, который охватит HTTPS и останется быстрым.
В первую очередь требовалось реализовать полноценный SSL. То, что есть в оригинальном masscan, — это возможность послать статически зашитый приветственный пакет, получить и обработать сертификат сервера. Наша версия умеет устанавливать и поддерживать SSL-соединение, анализировать содержимое вложенных протоколов, то есть она может собирать HTTP-баннеры c HTTPS-серверов.
Вот как мы этого добились. Добавили в исходный код новый протокол прикладного уровня, а в качестве имплементации самого SSL взяли стандартное решение — OpenSSL. Тут потребовалась небольшая доработка, и в кастомном сканере структура, описывающая протокол прикладного уровня, стала выглядеть так:
/* A registration structure for various TCP stream protocols
* like HTTP, SSL, and SSH */
struct ProtocolParserStream {
const char *name;
enum ApplicationProtocol proto;
bool is_dynamic;
const void *hello;
size_t hello_length;
unsigned ctrl_flags;
int (*selftest)(void);
void *(*init)(struct Banner1 *b);
void (*cleanup)(struct Banner1 *b);
void (*transmit_init)(const struct Banner1 *banner1,
struct ProtocolState *stream_state,
struct ResendPayload *resend_payload,
struct BannerOutput *banout, struct KeyOutput **keyout);
void (*transmit_parse)(const struct Banner1 *banner1, void *banner1_private,
struct ProtocolState *stream_state,
struct ResendPayload *resend_payload,
const unsigned char *px, size_t length,
struct BannerOutput *banout,
struct SignOutput *signout, struct KeyOutput **keyout,
struct InteractiveData *more);
void (*transmit_cleanup)(const struct Banner1 *banner1,
struct ProtocolState *stream_state,
struct ResendPayload *resend_payload);
void (*transmit_hello)(const struct Banner1 *banner1,
struct ProtocolState *stream_state,
struct ResendPayload *resend_payload,
struct BannerOutput *banout, struct KeyOutput **keyout,
struct InteractiveData *more);
Мы добавили обработчики для деинициализации протокола, инициализации соединения и расширили набор их параметров. В результате появилась возможность обрабатывать вложенные протоколы. Также удалось более корректно реализовать смену обработчика прикладного протокола. Она необходима, если обрабатывать данные текущим протоколом невозможно или если такой механизм заложен в сам протокол, например при использовании STARTTLS.
Дальше начались проблемы с производительностью и потерей пакетов. SSL — это тяжело для процессора. Был вариант попробовать что-то более быстрое, чем OpenSSL, но мы пошли в сторону обработки входящих пакетов в несколько потоков в рамках одного сетевого интерфейса. После реализации пайплайн обработки пакетов стал выглядеть так:
Поток th_recv_read
необходим для считывания данных из сетевого интерфейса независимо от скорости обработки самих данных. Очередь q_recv_pb
позволяет определять случаи, когда скорость отправки данных слишком большая и не дает вовремя обработать входящие пакеты. Поток th_recv_sched
раскидывает сообщения на основе хеша от исходящего и входящего IP-адресов и портов по потокам th_recv_hdl_*,
чтобы одно и то же соединение попадало в один обработчик. Опции, связанные с данной функциональностью:‑‑num‑handle‑threads
— количество потоков обработчиков, ‑‑tranquility
— автоматическое снижение скорости отправки пакетов при недостаточно быстрой обработке входящих пакетов.
Поддержка HTTPS включается параметром ‑‑dynamic‑ssl,
а с помощью ‑‑output‑filename‑ssl‑keys
можно сохранять мастер-ключи.
Еще можно заметить небольшое «косметическое» улучшение — имена у потоков. В нашей версии стало понятно, на какие потоки уходят ресурсы:
Улучшили качество кода
В masscan было обнаружено много странных мест и ошибок. Например, приведение времени к тикам выглядело так:
#define TICKS_PER_SECOND (16384ULL)
#define TICKS_FROM_SECS(secs) ((secs)16384ULL)
#define TICKS_FROM_USECS(usecs) ((usecs)/16384ULL)
#define TICKS_FROM_TV(secs,usecs) (TICKS_FROM_SECS(secs)+TICKS_FROM_USECS(usecs))
Сетевые TCP-соединения зачастую обрабатывались некорректно, из-за чего происходили разрывы соединений и лишние повторные отправки:
Также были обнаружены ошибки в работе с памятью, в том числе утечки. Многое удалось исправить, но не все. Например, при сканировании /0:80
наблюдается утечка нескольких диапазонов по 2 байта.
За обнаружение ошибок спасибо коллегам, которые внимательно пользовались нашими наработками, статическим анализаторам (GCC, Clang и VS), UB и memory-санитайзерам. Отдельно хочется упомянуть PVS-Studio: ребята бесподобны — по качеству и удобству им нет равных.
Добавили сборку под разные ОС
Для закрепления результатов мы написали сборку и проверку под Windows, Linux и MacOS с помощью GitHub Actions.
Пайплайн сборки выглядит так (рис. 4):
проверка форматирования,
статическая проверка Clang-анализатором,
сборка в дебаге с санитайзерами и запуск встроенных тестов,
сборка и отправка данных в сервисы SonarCloud и CodeQL.
Скачать скомпилированные бинарники можно из артефактов сборки или релиза:
Добавили еще несколько возможностей
Вот оставшиеся менее значимые вещи, которые появились в нашей версии:
‑‑regex(‑‑regex‑only‑banners)
— фильтрация сообщений на уровне данных в TCP. Регулярное выражение применяется на содержимом каждого TCP-пакета. Если сработает регулярное выражение, то информация о соединении будет в результатах.‑‑dynamic‑set‑host
— проставление заголовкаhost
в HTTP-запрос. В качестве значения берется IP-адрес сканируемой цели.Вывод сработок внутренних сигнатур на протоколы masscan в результат.
Опция для указания URI в HTTP-запросах. Позже мы убрали ее, так как автор оригинального masccan сам добавил аналогичную функциональность. Семейство опций
‑‑http‑*.