Книга: Сетевое программирование. От основ до приложений
Назад: Глава 22. Перехват и захват трафика
Дальше: Глава 23. TUN/TAP-интерфейсы. Техника Kernel bypass

Решения для захвата трафика

Большинство снифферов не работают с интерфейсами напрямую, сами не записывают пакеты и не реализуют поддержку формата PCAP. На разных платформах эти действия требуют разных подходов и решений, часть из которых рассмотрена ниже.

Сами же снифферы предоставляют интерфейс, средства анализа и прочие дополнительные функции.

Примеры снифферов

Существует множество разнообразных снифферов для выполнения разных задач:

— самый известный сниффер и один из наиболее функциональных. Кросс-платформенный, с открытым исходным кодом, он поддерживает множество протоколов и имеет встроенный скриптовый движок.

— платный и проприетарный инструмент. Сложный конвейер, состоящий из множества компонентов. Сборщики трафика работают везде: от железа до контейнеров и виртуальных машин. Брокер распределяет данные, которые затем проходят фильтрацию, дедупликацию и прочую обработку. Затем по протоколу Netflow данные отправляются в Application Metadata Intelligence, где обогащаются различными метаданными. Например, там выделяются протоколы. Обработанные данные поступают на менеджерскую консоль и предоставляются человеку в удобочитаемом виде.

— платный проприетарный сниффер с широкими возможностями. Выпускается давно и поддерживает более 100 протоколов. Может расшифровывать SSL/TLS «на лету», а также захватывать трафик с удаленных машин, если на них установлен агент.

— графический монитор сети: позволяет строить ее карту, прослушивая трафик. Содержит анализатор пакетов.

— Network Grep, который работает так же, как и grep, но с пакетами из сети. Понимает TCP, UDP, ICMP, IGMP, PPP и др. Поддерживает фильтрацию по разным признакам трафика. Например, следующая команда выполнит отбор TCP-сегментов на порту 80, данные в которых начинаются с GET или POST, иными словами — HTTP-запросов: ngrep -l -q -d eth0 -i "^GET |^POST " tcp and port 80.

— сниффер, широко распространенный в Unix-подобных системах. Реализован поверх библиотеки Pcap, поэтому является кросс-платформенным и работает как на Unix-подобных системах, так и в . Может использовать для записи формат PCap, понятный многим приложениям для анализа сети.

— свободная реализация перехватчика трафика для Windows.

— анализатор трафика от Microsoft для новых версий Windows.

— утилита, которая в Solaris заменяет ngrep и tcpdump.

— простой сниффер. Захватывает пакеты и выводит их данные в настраиваемый текстовый протокол.

— набор инструментов для анализа и пакетов в Linux. Построен на основе RX_RING/TX_RING, которые являются спе­цифичными для Linux способами kernel bypass, похожими на PF_RING. Об этом мы еще упомянем в следующей главе.

Подробнее особенности разных средств анализа трафика расписаны в на Wikipedia.

Wireshark

Wireshark — бесплатный кросс-платформенный анализатор пакетов с открытым исходным кодом, широко используемый для анализа трафика и диагностики. Он имеет графический интерфейс, консольный интерфейс, а также интерфейс на основе NCurses. Распространяется по лицензии GPL.

Используя Wireshark, пользователь может перевести интерфейсы Ethernet в неразборчивый режим, а Wi-Fi — в режим мониторинга. Это дает возможность захватывать пакеты на удаленных машинах, передавая их на узел с установленным Wireshark для анализа в режиме реального времени.

Wireshark способен расшифровывать SSL/TLS «на лету» и выполнять сложный анализ и разбор данных. Для разбора могут быть использованы встроенные сценарии на языке Lua.

Wireshark на платформах Linux, BSD и MacOS X работают поверх libpcap. В дальнейшем мы будем часто использовать Wireshark.

Network Monitor

В Windows, помимо сторонних решений на основе Pcap, существует инструмент . Его устройство показано на рис. 22.8. Он уже не поддерживается, но до сих пор широко используется и его .

Кроме того, существует , который позволяет использовать возможности Network Monitor программно и писать для него модули.

Рис. 22.8. Схема Network Monitor

Network Monitor включает драйвер NMNT.sys, получающий данные с NDIS-драйвера и распределяющий их пакетным провайдерам, которые работают на уровне пользователя. Монитор может на уровне драйвера устанавливать по аналогии с libpcap.

Пакетные провайдеры реализованы как DLL, представляющие набор COM-интерфейсов.

Некоторые основные провайдеры:

— для захвата данных в реальном времени.

— для захвата трафика в файл.

— для захвата статистик-трафика и записи в файл формата ESP.

Интерфейсы содержат следующие методы:

Connect() и Disconnect() для управления подключением к сети.

• Start(), Stop(), Pause(), Resume() для начала и окончания захвата.

• Configure() для настройки захвата — через него передаются .

• Несколько методов для получения состояния и статистики.

На рис. 22.9 показано, как пакетные провайдеры работают с адаптерами.

Можно реализовать свои пакетные провайдеры, которые работают в обход NDIS, обращаясь к драйверу адаптера напрямую. Это делает пакетный монитор очень гибким решением, способным принимать данные в обход сетевой подсистемы ОС Windows.

Рис. 22.9. Взаимодействие пакетных провайдеров и адаптеров

Провайдеры отдают данные , управляемым службой Monitor Control Service. Служба загружает DLL-библиотеки, в которых реализованы мониторы, конфигурирует, создает и уничтожает экземпляры мониторов.

Мониторы отвечают за обработку трафика и генерацию событий по условиям. Например, монитор будет в реальном времени получать трафик от провайдера IRTC и после выполнения заданного условия, например поступления IP-пакета с некорректным адресом, сгенерирует некоторое событие. Эти события можно увидеть в интерфейсе утилиты управления мониторами — Monitor Control Tool.

Также мониторами можно управлять, используя COM-интерфейс . Схема взаимодействия компонентов изображена на рис. 22.10. Особенность мониторов в том, что они быстро оценивают трафик.

Вторая часть работы инструмента Network Monitor — отложенный анализ данных, сохраненных в файлы через провайдер IDelaydC.

Рис. 22.10. Взаимодействие компонентов сетевого монитора

Чтобы пользователь мог работать с этим трафиком, система предоставляет Network Monitor UI. Он использует nmapi.dll и задействует ы и для анализа. Эти компоненты тоже реализованы как DLL.

Парсер обнаруживает и разбирает протоколы. Он работает, когда его вызывает сетевой монитор или эксперт. Каждый парсер разбирает один протокол. Как правило, в одной библиотеке реализован один парсер, однако DLL может содержать несколько парсеров.

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

Эксперт анализирует сетевой трафик, собранный NPP и помещенный в файл захвата. Для этого он работает с анализатором протокола.

Из кадра в файле захвата с помощью парсеров, к примеру, можно распознать такие протоколы, как SMB или TCP/IP. Когда протокол кадра идентифицирован, эксперт извлекает данные кадра. Эксперты могут использоваться для получения данных о корреляции, статистик по конкретным протоколам и других подобных задач.

Во время установки сетевого монитора в подкаталог Experts устанавливаются следующие библиотеки экспертов:

Coalesce.dll — инструмент объединения протоколов.

• Propdist.dll — эксперт по распределению свойств.

• Protdist.dll — эксперт по распределению протоколов.

• Resptime.dll — эксперт по среднему времени ответа сервера.

• Tcpipe.dll — эксперт по повторной передаче TCP.

Topuser.dll — эксперт по наиболее частым пользователям.

Пользователь также может писать библиотеки экспертов самостоятельно.

Интерфейс монитора схож с интерфейсом Wireshark и показан на рис. 22.11.

Рис. 22.11. Интерфейс монитора Netmon

Монитор — интересное решение, но начиная с Windows 10, он заменяется на более ограниченный по возможностям сниффер , который похож на tcpdump, но может читать файлы ETL, генерируемые монитором, и PCAPNG — расширенный формат PCAP от Wireshark.

Формат PCAP и поддерживающие его библиотеки

Мы уже рассмотрели, что собой представляет формат PCAP, когда реализовывали сниффер на C++. Но это прежде всего API, который служит для захвата трафика.

Его реализацию предоставляют несколько библиотек:

— C-библиотека под Unix-подобные системы. Это разработка . Сейчас ее развивает The Tcpdump Group. Код открыт по лицензии BSD. Эту библиотеку можно считать эталонной реализацией.

— порт libpcap на Windows. Реализован на основе и, похоже, больше не поддерживается.

— библиотека, основанная на Winpcap, но поддерживающая модель драйверов . Бинарно совместима с winpcap. Исходный код открыт, распространяется по лицензии GPLv2. Совместима со всеми новыми версиями ОС Windows.

— библиотека для захвата и отправки пакетов от проекта Nmap для Microsoft Windows. Поддерживает все архитектуры и релизы ОС Windows, захват с беспроводных адаптеров, некоторые особенности Windows. Последние версии основаны на NDIS 6.x. Лицензия коммерческая, но код открытый.

Кроме API, существует формат PCAP, который был использован в сниффере выше.

Файл в этом формате состоит из:

заголовка, содержащего версии формата, временную зону и прочие служебные поля;

массива пакетов, каждый из которых предваряется заголовком пакета.

Структуры заголовков были приведены ранее, подробно формат описан . Библиотеки поддерживают этот формат, позволяя читать PCAP-файлы и записывать в них данные.

На основе библиотек PCAP работают tcpdump, ngrep, Wireshark, EtherApe и многие другие снифферы, IDS, сетевые анализаторы. Биндинги одного из вариантов libpcap существуют для языков Python, Java, C# и многих других.

Новые версии библиотек частично поддерживают формат — основной для Wireshark.

Общий принцип работы с этими библиотеками:

1. Получить интерфейс для захвата трафика.

2. Инициализировать PCAP, указав ему устройства, с которых будет производиться захват.

3. Скомпилировать и применить фильтры на трафик, если требуется.

4. Захватить трафик. Если необходимо, обрабатываем и анализируем пакеты.

5. Закрыть сессию захвата, когда работа окончена.

:

pcap_init() — инициализировать библиотеку. Функция выполняет платформозависимые действия, например вызов WSAStartup() на ОС Windows.

• pcap_find_all_devs() — получить устройства.

• pcap_create() — создать сессию захвата.

• pcap_activate() — запустить сессию захвата.

• pcap_set_promisc() — установить неразборчивый режим.

• pcap_set_rfmon() — установить режим мониторинга Wi-Fi.

• pcap_dump_open() — открыть файл дампа .pcap.

• pcap_dump() — записать дамп в файл.

• pcap_dump_close() — закрыть файл дампа.

• Для чтения сессии из файла дампа используется функция pcap_open_offline(), возвращающая дескриптор, или pcap_fopen_offline(), возвращающая FILE *.

• Снапшотами управляет pcap_set_snaplen(), позволяя захватывать, например, только часть пакета с TCP-заголовками.

• Фильтры компилируются через pcap_compile(), а устанавливаются через pcap_setfilter().

• Непосредственно захват производится через функции pcap_loop() в цикле либо pcap_dispatch() и pcap_next().

Библиотека хорошо документирована.

Чтобы прочитать документацию, необходимо установить пакет с библиотекой. В Linux он называется libpcap соответствующей версии, pcap и libpcap-dev, в зависимости от дистрибутива.

В разделе 3 PCAP содержится как подробное руководство, так и отдельные страницы по функциям:

man pcap

Формат строк фильтров описан в man 7 pcap-filter и вполне понятен тем, кто пользуется tcpdump.

Пример фильтра, выбирающего HTTP-пакеты IPv4, на порту 80:

tcp port 80 and (((ip[2:2] — ((ip[0]&0xf)<<2)) — ((tcp[12]&0xf0)>>2)) != 0)

Если сделать вывод на консоль, мы увидим только пакеты, содержащие данные, а не пакеты SYN, FIN, ACK:

sudo ./run tcpdump 'tcp port 80 and (((ip[2:2] — ((ip[0]&0xf)<<2)) — ((tcp[12]&0xf0)>>2)) != 0)'

tcpdump: verbose output suppressed, use -v[v]... for full protocol decode

listening on wlo1, link-type EN10MB (Ethernet), snapshot length 262144 bytes

 

21:02:22.246799 IP 91.105.192.100.http > working-machine.37632: Flags [P.], seq 836468353:836468587, ack 2980469537, win 12258, options [nop,nop,TS val 116702986 ecr 2631864078], length 234: HTTP: HTTP/1.1 200 OK

21:02:22.248368 IP working-machine.37632 > 91.105.192.100.http: Flags [P.], seq 1:518, ack 234, win 2010, options [nop,nop,TS val 2631889105 ecr 116702986], length 517: HTTP: POST /api HTTP/1.1

21:02:30.893396 IP working-machine.53112 > 5.79.79.209.http: Flags [P.], seq 1978165765:1978166130, ack 718071210, win 64240, length 365: HTTP: GET / HTTP/1.1

 

3 packets captured

13 packets received by filter

2 packets dropped by kernel

Пример реализации сниффера на основе libpcap

С использованием библиотеки PCAP писать сниффер гораздо проще, — весь код управления адаптерами и работу с PCAP-файлами библиотека уже предоставляет. Но структуры протоколов все же придется описать самостоятельно.

Начнем с описания интерфейса класса, который распечатывает содержимое пакетов:

#pragma once

 

#include <pcap.h>

 

class PacketPrinter

{

public:

    // Разобрать пакет и вывести содержимое на экран.

    void got_packet(u_char *args, const pcap_pkthdr *header,

                    const u_char *packet);

 

private:

    // Печать данных по 16 байт: смещение   hex   ascii.

    // 00000   47 45 54 20 2f 20 48 54  54 50 2f 31 2e 31 0d 0a

    //   GET / HTTP/1.1..

    void print_hex_ascii_line(const u_char *payload, int len, int offset);

 

    // Вывести содержимое данных пакета: не печатать бинарные данные.

    void print_payload(const u_char *payload, int len);

 

private:

    // Счетчик количества обработанных пакетов.

    int packet_count_ = 1;

};

Код его методов реализуем позже, вначале опишем константы и заголовок Ethernet:

#include "packet_printer.h"

 

...

 

// Размер заголовка Ethernet — 14.

constexpr auto SIZE_ETHERNET = 14;

 

// MAC-адрес 6 байт.

constexpr auto ETHER_ADDR_LEN = 6;

 

// Размер IP-заголовка в байтах.

constexpr auto ip_header_size = 20;

 

// Заголовок Ethernet.

struct sniff_ethernet

{

    // MAC-адрес назначения.

    u_char ether_dhost[ETHER_ADDR_LEN];

    // MAC-адрес источника.

    u_char ether_shost[ETHER_ADDR_LEN];

    // Код протокола сетевого уровня.

    u_short ether_type;

};

Теперь опишем константы для IPv4 и его заголовок:

// Флаг Reserved, который всегда должен быть равен 0.

constexpr u_short ip_rf = 0x8000;

// Флаг Don't Fragment.

constexpr u_short ip_df = 0x4000;

// Флаг More Fragments.

constexpr u_short ip_mf = 0x2000;

// Маска для битов фрагментации IP.

constexpr u_short ip_offmask = 0x1fff;

 

// Заголовок IP.

struct sniff_ip

{

    // Version << 4 | длина заголовка >> 2.

    u_char ip_vhl;

    // Тип сервиса, ToS.

    u_char ip_tos;

    // Полная длина.

    u_short ip_len;

    // Идентификатор.

    u_short ip_id;

    // Поле смещения фрагмента.

    u_short ip_off;

    // TTL.

    u_char ip_ttl;

    // Протокол.

    u_char ip_p;

    // Контрольная сумма.

    u_short ip_sum;

    // Адрес источника.

    in_addr ip_src;

    // Адрес назначения.

    in_addr ip_dst;

};

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

// Длина заголовка IPv4.

constexpr u_char ip_hl(const sniff_ip *ip)

{

    return (ip->ip_vhl) & 0x0f;

}

 

// Версия IPv4 — старшие 4 бита.

constexpr u_char ip_v(const sniff_ip *ip)

{

    return (ip->ip_vhl) >> 4;

}

Заголовок для IPv6:

struct sniff_ipv6

{

    // Поля Version и Class.

    u_int version_class_flow;

    // Длина полезной нагрузки.

    u_short payload_len;

    // Следующий заголовок.

    u_char next_header;

    // Ограничение на количество ретрансляций.

    u_char hop_limit;

    // Адрес источника.

    struct in6_addr src;

    // Адрес назначения.

    struct in6_addr dest;

};

Опишем флаги TCP:

// Флаги TCP.

enum class tcp_flags : u_char

{

      th_fin = 0x01,

      th_syn = 0x02,

      th_rst = 0x04,

      th_push = 0x08,

      th_ack_f = 0x10,

      th_urg = 0x20,

      th_ece = 0x40,

      th_cwr = 0x80

};

Их мы используем при описании TCP-заголовка:

// Заголовок TCP.

struct sniff_tcp

{

    typedef u_int tcp_seq;

    // Порт источника.

    u_short th_sport;

    // Порт назначения.

    u_short th_dport;

    // Номер TCP-последовательности.

    tcp_seq th_seq;

    // Номер подтверждения.

    tcp_seq th_ack;

    // Смещение данных и поле reserved.

    u_char th_offx2;

    // Поле флагов.

    tcp_flags th_flags;

    // Размер окна.

    u_short th_win;

    // Контрольная сумма.

    u_short th_sum;

    // Указатель на срочные данные.

    u_short th_urp;

};

 

// Получить смещение данных, убрав reserved.

constexpr u_char th_off(const sniff_tcp *th)

{

    return (th->th_offx2 & 0xf0) >> 4;

}

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

В различных ОС структуры описаны в разных файлах:

В ОС Windows — в заголовочном файле netiodef.h. Структуры ETHERNET_HEADER, IPV4_HEADER или ip4_hdr, IPV6_HEADER или ip6_hdr, TCP_HDR и т.д.

В Linux — в файлах linux/if_ether.h, linux/ip.h, linux/ipv6.h, linux/tcp.h и т.п. Константы в netinet/in.h.

В BSD-системах структуры могут быть описаны в разных файлах, например в netinet/ip.h.

Теперь рассмотрим методы, работающие с данными.

Метод PacketPrinter::print_hex_ascii_line() не содержит специфичного для сети кода.

Данные полезной нагрузки распечатывает метод print_payload():

// Распечатать полезную нагрузку пакета.

void PacketPrinter::print_payload(const u_char *payload, int len)

{

    int len_rem = len;

    // Количество байтов в строке.

    constexpr int line_width = 16;

    // Счетчик для смещения.

    int offset = 0;

    const u_char *ch = payload;

 

    if (len <= 0) return;

 

    // Данные, которые умещаются в одной строке.

    if (len <= line_width)

    {

        print_hex_ascii_line(ch, len, offset);

        return;

    }

Он разбивает их на строки фиксированной длины и выводит как дамп:

    // Вывод данных, занимающих несколько строк.

    for (;;)

    {

        // Рассчитать длину.

        int line_len = line_width % len_rem;

        // Вывести строку.

        print_hex_ascii_line(ch, line_len, offset);

        // Рассчитать остаток.

        len_rem = len_rem — line_len;

        // Сдвинуть указатель на оставшиеся для печати данные.

        ch = ch + line_len;

        offset = offset + line_width;

        // Количество символов равно или меньше длины строки?

        if (len_rem <= line_width)

        {

            // Вывести последнюю строку и выйти.

            print_hex_ascii_line(ch, len_rem, offset);

            break;

        }

    }

}

Метод got_packet() вызывается при поступлении нового пакета:

void PacketPrinter::got_packet(u_char *args, const pcap_pkthdr *header,

                               const u_char *packet)

{

    (void)args;

    (void)header;

 

    std::cout << "\nPacket number: " << packet_count_++ << std::endl;

    // Ethernet-заголовок.

    const sniff_ethernet *ethernet =

        reinterpret_cast<const sniff_ethernet *>(packet);

    // Сюда будет записан печатный адрес протокола.

    std::string proto_addr;

    // Семейство адресов: AF_INET либо AF_INET6.

    int network_proto_type;

    // Транспортный протокол.

    int transport_proto_type;

    // Адреса протоколов.

    const void *src, *dst;

    // Вспомогательные переменные для получения размеров IP-заголовка.

    size_t size_ip;

    size_t ip_len;

Видно, что метод получил структуру Ethernet-заголовка. После этого он по содержащемуся в нем коду протокола в ether_type выбирает IPv4 или IPv6.

В IPv4 для этого используется поле ip_p:

    switch (ntohs(ethernet->ether_type))

    {

        // IPv4.

        case 0x0800:

        {

            std::cout << "  IPv4 protocol" << std::endl;

            proto_addr.resize(INET_ADDRSTRLEN);

            network_proto_type = AF_INET;

            // Заголовок.

            const sniff_ip *ip =

                reinterpret_cast<const sniff_ip *>(packet + SIZE_ETHERNET);

            // Размер заголовка.

            size_ip = ip_hl(ip) * 4;

            // Будет использовано для получения размера нагрузки.

            ip_len = ntohs(ip->ip_len);

 

            if (size_ip < ip_header_size)

            {

                std::cout

                    << "   * Invalid IP header length: " << size_ip

                    << " bytes" << std::endl;

                return;

            }

 

            transport_proto_type = ip->ip_p;

            src = &ip->ip_src;

            dst = &ip->ip_dst;

            break;

        }

В IPv6 идентификатор транспортного протокола содержится в поле next_header:

        // IPv6.

        case 0x86DD:

        {

            std::cout << "  IPv6 protocol" << std::endl;

            proto_addr.resize(INET6_ADDRSTRLEN);

            network_proto_type = AF_INET6;

            // Заголовок IPv6.

            const sniff_ipv6 *ip =

                reinterpret_cast<const sniff_ipv6 *>(packet + SIZE_ETHERNET);

            size_ip = sizeof(sniff_ipv6);

            ip_len = ntohs(ip->payload_len);

            // Поле "следующий заголовок" — смещение заголовка TCP.

            transport_proto_type = ip->next_header;

            // Адреса.

            src = &ip->src;

            dst = &ip->dest;

            break;

        }

        default:

        {

            std::cout

                << "  Unknown EtherType = 0x" << std::hex

                << ntohs(ethernet->ether_type) << std::endl;

            return;

        }

    }

Адреса выводятся на экран:

    // Печать адресов.

    std::cout

        << std::dec << "    From: "

        << inet_ntop(network_proto_type, src, &proto_addr[0],

                     proto_addr.size()) << "\n"

        << "    To: " << inet_ntop(network_proto_type, dst, &proto_addr[0],

                                   proto_addr.size())

        << std::endl;

Затем выводится тип вышележащего протокола:

    // Определение протокола и печать.

    switch (transport_proto_type)

    {

        case IPPROTO_TCP: std::cout << "    Protocol: TCP" << std::endl;

        // Будем выводить только TCP-сегменты.

        break;

        case IPPROTO_UDP: std::cout << "    Protocol: UDP" << std::endl;

        return;

        case IPPROTO_ICMP: std::cout << "    Protocol: ICMP" << std::endl;

        return;

        case IPPROTO_ICMPV6: std::cout << "    Protocol: ICMPv6" << std::endl;

        return;

        case IPPROTO_IP: std::cout << "    Protocol: IP" << std::endl;

        return;

        case IPPROTO_IPV6: std::cout << "    Protocol: IPv6" << std::endl;

        return;

        default:

            std::cout

                << "    Protocol: unknown ["

                << transport_proto_type << "]" << std::endl;

        return;

    }

Как мы уже говорили, в поле транспортного протокола идентификаторы IPPROTO_IP и IPPROTO_IPV6 могут попасть, если выполняется инкапсуляция.

Но в операторе switch выше осуществляется выход из функции в любом случае, когда транспортный протокол не TCP. То есть для упрощения мы не выполняем рекурсивную обработку инкапсулированных пакетов.

 

Выполним печать содержимого TCP-сегмента:

    // Это TCP-сегмент.

    // Вычислить смещение TCP-заголовка.

    const sniff_tcp *tcp = reinterpret_cast<const sniff_tcp *>(packet +

                               SIZE_ETHERNET + size_ip);

    const int size_tcp = th_off(tcp) * 4;

 

    if (size_tcp < ip_header_size)

    {

        std::cerr

            << "    * Invalid TCP header length: "

            << size_tcp << " bytes" << std::endl;

        return;

    }

    // Вывести порты.

    std::cout

        << "    Src port: " << ntohs(tcp->th_sport) << "\n"

        << "    Dst port: " << ntohs(tcp->th_dport) << std::endl;

    // payload — данные пакета.

    // Определить смещение данных в TCP-сегменте.

    const u_char *payload = static_cast<const u_char *>(packet +

                                SIZE_ETHERNET + size_ip + size_tcp);

 

    // Вычислить размер данных TCP-сегмента.

    const int size_payload = ip_len — (size_ip + size_tcp);

 

    // Вывести дамп полезных данных.

    if (size_payload > 0)

    {

        std::cout << "    Payload (" << size_payload << " bytes):\n";

        print_payload(reinterpret_cast<const unsigned char *>(payload),

                      size_payload);

        std::cout << std::endl;

    }

}

Теперь определим функцию-обработчик. В ней мы создадим экземпляр класса и вызовем его обработчик:

extern "C"

{

#include <pcap.h>

}

 

...

 

#include "packet_printer.h"

 

// Длина захвата по умолчанию — максимальное количество байтов пакета.

constexpr auto SNAP_LEN = 1518;

// Количество пакетов для захвата.

constexpr auto MAX_PACKET_TO_CAPTURE = 10;

 

// Обработчик.

static void pcap_callback(u_char *args, const pcap_pkthdr *header,

                          const u_char *packet)

{

    // Статическая переменная здесь просто для того, чтобы не выделять

    // память.

    static PacketPrinter p_printer;

    // Обработать пакет.

    p_printer.got_packet(args, header, packet);

};

Работа с libpcap начинается в функции main(), в которой сначала нужно выбрать устройство для захвата данных, если оно не было указано явно:

int main(int argc, const char *const argv[])

{

    std::string dev;

    // Буфер ошибок Pcap.

    std::array<char, PCAP_ERRBUF_SIZE> errbuf;

 

    // Далее идет обработка параметров командной строки, которая не показана.

...

    {

        pcap_if_t *all_devs_sp = nullptr;

        // Если устройство не было задано, найти его.

        if (pcap_findalldevs(&all_devs_sp, errbuf.data()) != 0 ||

            nullptr == all_devs_sp)

        {

            std::cerr << "Couldn't find default device: \"" << errbuf.data()

                      << "\"" << std::endl;

            return EXIT_FAILURE;

        }

        assert(all_devs_sp->name);

        // Получить устройство.

        dev = all_devs_sp->name;

        // Освободить список.

        pcap_freealldevs(all_devs_sp);

    }

Выбрав устройство из списка, необходимо освободить список. Затем получим некоторые параметры устройства для печати — библиотека pcap это позволяет — и откроем устройство захвата:

    bpf_u_int32 mask;

    bpf_u_int32 net;

 

    // Получить номер сети и маску, связанные с устройством захвата.

    if (-1 == pcap_lookupnet(dev.c_str(), &net, &mask, errbuf.data()))

    {

        std::cerr

            << "Couldn't get netmask for device \"" << dev

            << "\": " << errbuf.data() << std::endl;

        net = 0 = mask = 0;

    }

 

    // Количество принятых пакетов.

    constexpr int num_packets = MAX_PACKET_TO_CAPTURE;

    // Выражение фильтра: принимать только IP- или IPv6-пакеты.

    constexpr char filter_exp[] = "ip or ip6";

 

    // Вывести информацию о "сессии захвата".

    std::cout << "Device: " << dev << "\n"

              << "Network mask: " << mask << "\n"

              << "Network: " << net << "\n"

              << "Number of packets: " << num_packets << "\n"

              << "Filter expression: " << filter_exp << std::endl;

 

    // Открыть устройство захвата и получить дескриптор Pcap.

    pcap_t *const handle = pcap_open_live(dev.c_str(), SNAP_LEN, 1, 1000,

                                          errbuf.data());

    if (nullptr == handle)

    {

        std::cerr << "Couldn't open device \"" << dev

                  << "\": " << errbuf << "!" << std::endl;

        return EXIT_FAILURE;

    }

С полученным дескриптором устройства мы будем работать дальше. Перед тем как начать захват, убедимся, что устройство ведет себя как Ethernet-адаптер. В обработчике, показанном ранее, мы работаем только с Ethernet-кадрами. Затем скомпилируем фильтр и применим его к хэндлу открытого устройства. Только после этого запустим цикл захвата, передав ему требуемое количество пакетов и обработчик:

    int exit_code = EXIT_SUCCESS;

    // Скомпилированная программа фильтра.

    bpf_program fp{};

 

    try

    {

        std::stringstream ss;

        // Захват производится с Ethernet-совместимого адаптера.

        if (pcap_datalink(handle) != DLT_EN10MB)

        {

            ss << "\"" << dev << "\" is not an Ethernet!" << std::endl;

            throw std::logic_error(ss.str());

        }

        // Скомпилировать выражение фильтра.

        if (-1 == pcap_compile(handle, &fp, filter_exp, 0, net))

        {

            ss << "Couldn't parse filter \"" << filter_exp << "\": "

               << pcap_geterr(handle) << "!" << std::endl;

            throw std::logic_error(ss.str());

        }

        // Применить фильтр.

        if (-1 == pcap_setfilter(handle, &fp))

        {

            ss << "Couldn't install filter \""

               << filter_exp << "\": " << pcap_geterr(handle) << "!"

               << std::endl;

            throw std::logic_error(ss.str());

        }

        // Запустить цикл захвата, передав ему обработчик.

        if (PCAP_ERROR == pcap_loop(handle, num_packets, pcap_callback,

                                    nullptr))

        {

            ss << "Pcap loop failed: " << pcap_geterr(handle) << "!"

               << std::endl;

            throw std::logic_error(ss.str());

        }

    }

Когда захват окончен, необходимо удалить скомпилированный фильтр и закрыть дескриптор устройства захвата:

    catch (const std::exception &e)

    {

        std::cerr << e.what() << std::endl;

        exit_code = EXIT_FAILURE;

    }

    // Очистка.

    pcap_freecode(&fp);

    pcap_close(handle);

    if (!exit_code) std::cout << "\nCapture complete." << std::endl;

 

    return exit_code;

Запустим пример:

sudo ./run build/bin/b01-ch22-pcap-sniffer

Device: wlo1

Network mask: 16777215

Network: 174272

Number of packets: 10

Filter expression: ip or ip6

 

Packet number: 1

IPv4 protocol

   From: 0.0.0.0

   To: 255.255.255.255

   Protocol: UDP

 

Packet number: 2

IPv4 protocol

   From: 0.0.0.0

   To: 255.255.255.255

   Protocol: UDP

 

Packet number: 3

IPv4 protocol

   From: 91.105.192.100

   To: 192.168.2.13

   Protocol: TCP

   Src port: 80

   Dst port: 51672

   Payload (234 bytes):

   0

48 54 54 50 2f 31 2e 31  20 32 30 30 20 4f 4b 0d    HTTP/1.1 200 OK.

00016

0a 43 6f 6e 6e 65 63 74  69 6f 6e 3a 20 6b 65 65    ................

...

Видим, что автоматически был выбран Wi-Fi-адаптер и третий захваченный с него пакет, который пришел на интерфейс с IP-адреса 91.105.192.100, содержит фрагмент HTTP-ответа.

В примере сборка кода производится CMake, и .

PcapPlusPlus

Для C++ существует несколько оберток и надстроек над libpcap и аналогами:

— простая и быстрая обертка. Поддерживает C++11, захват Wi-Fi и расшифровку WPA2.

— кросс-платформенная библиотека C++ для захвата, анализа и создания сетевых пакетов. Работает на ОС Linux, FreeBSD, Android, Windows, MacOS.

В примере использования PcapPlusPlus из репозитория библиотеки сначала открывается «устройство захвата», которое в данном случае является файлом дампа PCAP. Затем начинается чтение файла:

#include <iostream>

#include <pcapplusplus/IPv4Layer.h>

#include <pcapplusplus/Packet.h>

#include <pcapplusplus/PcapFileDevice.h>

 

int main(int argc, char* argv[])

{

    // Открыть PCAP-файл для чтения.

    pcpp::PcapFileReaderDevice reader("1_packet.pcap");

 

    if (!reader.open())

    {

        std::cerr << "Error opening the pcap file" << std::endl;

        return EXIT_FAILURE;

    }

 

    // Прочитать первый пакет из файла.

    pcpp::RawPacket rawPacket;

    if (!reader.getNextPacket(rawPacket))

    {

        std::cerr

            << "Couldn't read the first packet in the file"

            << std::endl;

        return EXIT_FAILURE;

    }

Из каждого пакета, который был прочитан из файла, может быть создан экземп­ляр класса pcpp::Packet, который отображает свойства пакета.

В примере видно, как проверяется тип и, если это пакет IPv4, печатаются его адреса:

    // Разобрать "сырой" пакет и преобразовать в структуры.

    pcpp::Packet parsedPacket(&rawPacket);

 

    // Проверить, что это IPv4.

    if (parsedPacket.isPacketOfType(pcpp::IPv4))

    {

        // Получить адрес источника.

        pcpp::IPv4Address srcIP = parsedPacket.

            getLayerOfType<pcpp::IPv4Layer>()->getSrcIPv4Address();

        // Получить адрес назначения.

        pcpp::IPv4Address destIP = parsedPacket.

            getLayerOfType<pcpp::IPv4Layer>()->getDstIPv4Address();

 

        // Распечатать адреса.

        std::cout

            << "Source IP is '" << srcIP

            << "'; Dest IP is '" << destIP << "'"

            << std::endl;

    }

 

    // Закрыть файл.

    reader.close();

 

    return EXIT_SUCCESS;

}

Библиотека работает не только поверх libpcap, Npcap, WinPcap, но и поверх описанных далее фреймворков DPDK и PF_RING. Кроме того, в ней есть еще несколько интересных дополнительных возможностей:

• Возможность сборки пакетов IPv4 и IPv6 из фрагментов.

• Сборка блоков данных из потока TCP.

• TLS Fingerprinting — идентификация приложений по TLS-рукопожатию.

• Фильтрация пакетов.

• Возможность записи и чтения сжатых PCAP-файлов.

Таким образом, это очень мощная, удобная и при этом простая библиотека.

Pcapy

— сторонний пакет Python, который позволяет напрямую работать с libpcap и WinPcap. Он предоставляет объектный API, который в ос­новном повторяет интерфейс libpcap, позволяя работать в Python-стиле. Однако начиная с версии Python 3.10, оригинальный Pcapy не поддерживается и не работает. Поэтому существует его более современный вариант — , который предоставляет те же возможности.

Пример обработки дампа с использованием Pcapy-NG:

import pcapy

 

# Открыть файл дампа.

dump = pcapy.open_offline('dump.pcap')

 

# Создать дампер, который будет записывать в new_dump.pcap.

dumper = dump.dump_open('new_dump.pcap')

 

# Заголовок и тело первой записи.

hdr, body = dump.next()

 

while hdr is not None:

    # Обход записей.

    dumper.dump(hdr, body)

    print(len(body))

    hdr, body = dump.next()

Резюме

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

Снифферы могут быть программными и аппаратными. Аппаратные снифферы часто используются для диагностики сети или сбора данных, например, в целях безопасности.

Снифферы могут перехватывать данные на канальном и физическом уровне. Обычно имеет смысл выполнять перехват всего трафика, проходящего через адаптер. Для этого его переводят в неразборчивый режим или режим мониторинга в случае Wi-Fi-адаптеров.

Обычно данные не анализируются сразу, а в «сыром» виде записываются для последующего анализа в хранилище, например на диск. Стандартом де-факто является формат записи сетевого дампа — PCAP. Этот формат прост и вместе с тем достаточен для хранения сетевых данных. Он реализован в библиотеках, которые предоставляют API для захвата трафика, например Libpcap, Winpcap, Win10Pcap и Npcap.

Инструменты для анализа трафика, такие как tcpdump, ngrep, Wireshark, EtherApe и другие, используют перечисленные библиотеки.

Существуют оболочки для различных языков, позволяющие использовать функциональность libpcap. Некоторые версии библиотек также поддерживают формат PcapNg, который является основным для Wireshark.

Вопросы и задания

1. Что такое снифферы и как они могут быть полезны в разработке сетевых приложений?

2. Что такое «неразборчивый режим» адаптера и для чего требуется переводить адаптер в этот режим?

3. Как перевести адаптер в неразборчивый режим?

4. Каковы отличия неразборчивого режима от мониторингового в Wi-Fi?

5. Какие семейства и типы сокетов можно использовать для перехвата трафика?

6. О чем говорит тип ETH_P_ALL для raw-сокета?

7. Почему, когда в Linux запущен Wireshark или tcpdump, интерфейс находится не в promisc-режиме?

8. Какой формат является стандартом де-факто для хранения захваченного трафика и как он устроен?

9. Какие форматы дампов трафика поддерживает Wireshark? В чем их отличия от формата PCAP?

10. Почему в коде Python сниффера не используются функции для преобразования атрибутов пакета из сетевого порядка байтов?

11. Какие изменения произошли в инструментах мониторинга трафика, начиная с Windows 10?

12. Какие библиотеки могут использоваться для работы с форматом PCAP в C и C++?

13. Как захватить данные из канала, используя libpcap, и какие шаги для этого требуются?

14. Чем удобна библиотека PCap++?

15. Какой пакет можно использовать для обработки PCAP-дампов в Python?

16. Перепишите C++-сниффер, используя библиотеку PСap++.

17. В примере для сниффера на Python:

a) В классе Ipv4Header для простоты мы работаем только с пакетами, имеющими код заголовка 0x0800, или 0x0008 в сетевом порядке байтов. Какой сетевой протокол имеет такой код? Нужен ли тут список кодов и почему? Если нужен, допишите обработку списка кодов.

б) В классе Ipv6Header для простоты мы не стали разбирать опции протокола, доработайте этот класс разбором опций.

в) Для работы с ICMPv6 мы сделали класс Icmp6Header, который унаследован от IcmpHeader. В идеале следует добавить отдельный класс для ICMPv6. Почему наш вариант работает? Создайте отдельный класс для работы с ICMPv6.

г) Замените тип сокетов AF_PACKET на raw-сокеты и добейтесь работоспособности сниффера.

д) Доработайте пример таким образом, чтобы поддерживались протоколы туннелирования, например IP in IP.

18. Измените код из примера по установке Promisc-режима таким образом, чтобы флаг неразборчивого режима устанавливался перед работой приложения и сбрасывался всегда при завершении работы приложения.

19. В примере реализации сниффера на основе libpcap мы ограничились TCP-трафиком, добавьте в него работу с UDP-трафиком.


Назад: Глава 22. Перехват и захват трафика
Дальше: Глава 23. TUN/TAP-интерфейсы. Техника Kernel bypass