Большинство снифферов не работают с интерфейсами напрямую, сами не записывают пакеты и не реализуют поддержку формата 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 — бесплатный кросс-платформенный анализатор пакетов с открытым исходным кодом, широко используемый для анализа трафика и диагностики. Он имеет графический интерфейс, консольный интерфейс, а также интерфейс на основе NCurses. Распространяется по лицензии GPL.
Используя Wireshark, пользователь может перевести интерфейсы Ethernet в неразборчивый режим, а Wi-Fi — в режим мониторинга. Это дает возможность захватывать пакеты на удаленных машинах, передавая их на узел с установленным Wireshark для анализа в режиме реального времени.
Wireshark способен расшифровывать SSL/TLS «на лету» и выполнять сложный анализ и разбор данных. Для разбора могут быть использованы встроенные сценарии на языке Lua.
Wireshark на платформах Linux, BSD и MacOS X работают поверх libpcap. В дальнейшем мы будем часто использовать Wireshark.
В 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, когда реализовывали сниффер на 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
С использованием библиотеки 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, и .
Для 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-файлов.
Таким образом, это очень мощная, удобная и при этом простая библиотека.
— сторонний пакет 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-трафиком.