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);
}

Один поток выполняет такие задачи:

  1. Считывает сырые данные из сетевого интерфейса.

  2. Обрабатывает их, прогоняя через собственный TCP-стек и обработчики прикладных протоколов.

  3. Формирует нужные данные для отправки.

  4. Складывает их в очередь transmit_queue.

Другой поток забирает подготовленные для отправки сообщения из transmit_queue и ‍записывает их в сетевой интерфейс (рис. 1). Если отправка сообщений из очереди не превышает лимит, то формируются и отправляются SYN-пакеты для следующих целей сканирования.

Рис. 1. Схема обработки и отправки пакетов
Рис. 1. Схема обработки и отправки пакетов

Реализация форматов вывода

Эта часть концептуально схожа с модульной реализацией протоколов: здесь тоже есть структура OutputType, в которой прописываются основные функции сериализации. Радует обилие всевозможных форматов вывода: кастомный бинарный, и современный NDJSON, и противный XML, и grep-абельный. Есть даже возможность сохранять данные в Redis. Напишите в комментариях, кто пробовал :)

Некоторые форматы совместимы (или, как пишет автор masscan, вдохновлены ими) с аналогичными утилитами, такими как nmap и unicornscan.

Чтение сырых пакетов

Masscan предоставляет возможность работать с сетевым адаптером через библиотеки PCAP или PFRING, а также читать данные из PCAP-дампа. В файле rawsock.c находится несколько функций, которые абстрагируют основной код от конкретных интерфейсов.

Чтобы выбрать PFRING, нужно воспользоваться опцией ‑‑pfring, а чтобы включить чтение из дампа — указать префикс file: у адаптера.

Полезные опции оригинального masscan

Рассмотрим несколько интересных и полезных возможностей оригинального masscan, про которые редко пишут.

Опция

Описание

Комментарий

--nmap, --help

Справка

Даже вместе эти опции выдают очень мало полезного. Документация тоже содержит неполную информацию и раскидана по ‍разным файлам: README.mdmanFAQ. Еще есть небольшой HOWTO по ‍применению сканера совместно с AFL (American Fuzzy Lop). Если хочется узнать обо всех возможностях, то полный список параметров можно найти только в исходниках (main-conf.c)

--output-format ndjson, -oD, 
--ndjson-status

Поддержка NDJSON

Гигабайтные файлы построчного NDJSON гораздо приятнее обрабатывать, чем JSON. А вывод статуса в формате NDJSON полезен при написании утилит, следящих за работой masscan

--output-format redis

Возможность сохранять результаты 
сразу в Redis

А почему бы и нет? :) Если вы не работали с этим инструментом, почитать о ‍нем можно тут

--range fe80::/67

Поддержка IPv6

Тут все понятно, но будет интересно почитать в ‍комментариях о реальных кейсах использования. В ‍голову приходит сканирование локальной сети или только небольшого диапазона какой-то конкретной страны, полученного через BGP

--http-*

Кастомизация HTTP-запроса

При создании HTTP-запроса можно изменить любые его части под свои задачи: метод, URI, версию, заголовки, тело

--hello-[http, ssl,
smbv1]

Сканирование протоколов на нестандартных портах

Если masscan не получил приветственного пакета от ‍цели, по умолчанию он кидает запрос первым, выбирая протокол с опорой на порт цели. Но иногда хочется посканировать HTTP на каком-нибудь нестандартном порте

--resume

Пауза

Masscan умеет аккуратно останавливаться и продолжать работу с того места, где закончил. По ‍команде Ctrl + C 
(SIGINT) masscan завершается, сохраняя состояние и параметры запуска, а при указании опции ‑‑resume считывает эти данные и продолжает работу

--rotate-size

Ротация файла с результатами

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

--shard

Горизонтальное 
масштабирование

Masscan в псевдослучайном порядке выбирает цели из ‍сканируемого диапазона. Если нужно запускать masscan на нескольких машинах в рамках одного диапазона, то добиться такого же случайного распределения даже между машинами можно, воспользовавшись этой опцией

--top-ports

Сканирование N 
популярных портов (массив top_tcp_ports)

Эта опция пришла из nmap

--script

Скрипты на Lua

Есть сомнение, что опция работает, но сама возможность интересная. Кто использует, отпишитесь, может, у вас есть занятные примеры

--vuln [heartbleed, ticketbleed, poodle, ntp-monlist]

Поиск некоторых известных уязвимостей

Про корректность и ‍работоспособность не можем ничего сказать, так как данный механизм детектирования уязвимостей костылями разбросан по ‍всему коду и конфликтует со многими другими опциями, а в реальных задачах применять его не ‍приходилось

Напомним один важный момент, на которые все натыкаются: 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, но мы пошли в сторону обработки входящих пакетов в несколько потоков в рамках одного сетевого интерфейса. После реализации пайплайн обработки пакетов стал выглядеть так:

Рис. 2. Обновленная схема обработки и отправки пакетов
Рис. 2. Обновленная схема обработки и отправки пакетов

Поток 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-соединения зачастую обрабатывались некорректно, из-за чего происходили разрывы соединений и лишние повторные отправки:

Рис. 3. Пример некорректной обработки сетевых TCP-соединений
Рис. 3. Пример некорректной обработки сетевых TCP-соединений

Также были обнаружены ошибки в работе с памятью, в том числе утечки. Многое удалось исправить, но не все. Например, при сканировании /0:80 наблюдается утечка нескольких диапазонов по 2 байта.

За обнаружение ошибок спасибо коллегам, которые внимательно пользовались нашими наработками, статическим анализаторам (GCC, Clang и VS), UB и memory-санитайзерам. Отдельно хочется упомянуть PVS-Studio: ребята бесподобны — по качеству и удобству им нет равных.

Добавили сборку под разные ОС

Для закрепления результатов мы написали сборку и проверку под Windows, Linux и ‍MacOS с помощью GitHub Actions.

Пайплайн сборки выглядит так (рис. 4):

  • проверка форматирования,

  • статическая проверка Clang-анализатором,

  • сборка в дебаге с санитайзерами и запуск встроенных тестов,

  • сборка и отправка данных в сервисы SonarCloud и CodeQL.

Рис. 4. Пайплайн сборки
Рис. 4. Пайплайн сборки

Скачать скомпилированные бинарники можно из артефактов сборки или релиза:

Рис. 5. Артефакты релиза
Рис. 5. Артефакты релиза

Добавили еще несколько возможностей

Вот оставшиеся менее значимые вещи, которые появились в нашей версии: 

  • ‑‑regex(‑‑regex‑only‑banners) — фильтрация сообщений на уровне данных в ‍TCP. Регулярное выражение применяется на содержимом каждого TCP-пакета. Если сработает регулярное выражение, то информация о соединении будет в ‍результатах.

  • ‑‑dynamic‑set‑host — проставление заголовка host в HTTP-запрос. В качестве значения берется IP-адрес сканируемой цели.

  • Вывод сработок внутренних сигнатур на протоколы masscan в результат.

  • Опция для указания URI в HTTP-запросах. Позже мы убрали ее, так как автор оригинального masccan сам добавил аналогичную функциональность. Семейство опций ‑‑http‑*.

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