Книга: Сетевое программирование. От основ до приложений
Назад: Глава 21. Проксирование, инкапсуляция
Дальше: Решения для захвата трафика

Глава 22. Перехват и захват трафика

Цель — справедливость, метод — прозрачность. Важно не путать цель и метод.

Джулиан Ассанж, 2011

Введение

Чтобы увидеть, что происходит на интерфейсе адаптера, подключенного к выбранному каналу, используют анализаторы трафика, или снифферы, от английского sniff — «нюхать, чуять». При разработке сетевых приложений они используются достаточно часто.

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

Обсудим структуры данных и константы для IPv4. Для закрепления напишем собственную реализацию простого сниффера на С++ и Python. Затем изучим широко распространенный формат PCAP, используемый для хранения сетевых дампов, и реализуем сниффер, использующий библиотеку libpcap, которая поддерживает данный формат. Посмотрим, какие продукты существуют для Unix-подобных ОС и для ОС Windows. И в конце разберем библиотеки и пакеты, которые облегчают работу с перехватом и захваченными данными.

Перехват трафика

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

В целом способы перехвата трафика можно разделить на следующие группы:

Прослушивание сетевого интерфейса.

• Подключение в разрыв канала. Кроме перехвата, дает возможность изменения данных.

• Ответвление трафика. Например, полностью аппаратно: через зеркальный порт на маршрутизаторе или оптический сплиттер на оптоволокне. Либо программными средствами через брандмауэр.

• Перехват системных вызовов. Реализуется на уровне приложения или всей системы. В последнем случае используется драйвер или модуль ядра. Перехватывать вызовы, не выходя за прикладной уровень, также можно на уровне системы, хотя для этого и требуются административные права. Этот вариант перехвата для Linux мы рассмотрим далее.

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

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

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

В качестве кросс-платформенного решения часто используют Wireshark. В Windows распространены pktmon и Network Monitor, в Unix-подобных ОС — утилита tcpdump.

Неразборчивый режим

Адаптеры, в том числе Ethernet и Wi-Fi, принимают только кадры, в которых прописаны их MAC-адреса или широковещательные адреса. Остальные кадры отбрасываются на аппаратном уровне. Иными словами, адаптер фильтрует данные на канальном уровне. В случае Ethernet, например, он выполняет фильтрацию по MAC-адресу. Исключение — зеркальный порт маршрутизатора, дублирующий все пакеты.

Обычно сниффер может получать лишь пакеты, предназначенные данному адаптеру. Работа в таком режиме может быть полезна для отладки и поиска различных проблем, в основном с конкретным узлом.

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

• из текущего коллизионного домена, например из общей шины или радио­эфира;

• из маркерных или кольцевых сетей;

• с порта зеркалирования маршрутизатора

и других сред. Чтобы сниффер захватывал все пакеты, адаптер нужно перевести в «неразборчивый режим», или promiscuous mode. При этом отключается аппаратная фильтрация и становится возможен захват всех кадров.

Для Wi-Fi-адаптера интерфейс переводится в , для чего создается отдельный интерфейс мониторинга.

Мониторинговый режим Wi-Fi

На самом деле у Wi-Fi-адаптеров тоже есть неразборчивый режим, и вы можете в этом убедиться, попробовав его включить.

Режим мониторинга отличается от неразборчивого режима следующим:

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

Отключает контроль значений CRC адаптером для захваченных пакетов. Это позволяет видеть даже поврежденные данные.

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

В режиме мониторинга адаптер принимает все пакеты. Но неразборчивый режим под­держивают большинство драйверов беспроводных адаптеров, а режим мониторинга — далеко не все.

Для работы с Wi-Fi-адаптерами в Unix-подобных системах стандартом де-факто является пакет Aircrack-ng, состоящий из нескольких консольных утилит. Также весьма известен сетевой анализатор Kismet, который в MacOS называется Kismac. Они позволяют включать данные режимы.

Рассмотрим, как перевести в неразборчивый режим адаптер Ethernet. В Linux это делается с помощью следующего кода:

extern "C"

{

#include <sys/ioctl.h>

#include <sys/socket.h>

// Для IF_PROMISC.

#include <linux/if.h>

// Для ETH_P_ALL/ETH_P_IP.

#include <netinet/if_ether.h>

}

 

...

 

bool enable_promisc = true;

#if defined(WIN32)

// Переносимый вариант создания.

int sock = socket(AF_INET, SOCK_RAW, IPPROTO_IP);

#else

// Создать "сырой" сокет, не привязанный к протоколу.

// Он будет принимать все протоколы, так как установлен тип ETH_P_ALL.

// Вариант для Linux.

int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

#endif

 

// Поля данной структуры:

//   — Имя сетевого интерфейса.

//   — Флаги.

struct ifreq ifr = {0};

std::copy(if_name.begin(), if_name.end(), ifr.ifr_name);

 

...

 

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

if (-1 == ioctl(sock, SIOCGIFINDEX, &ifr))

{

    // Не удалось найти интерфейс.

}

 

// Получить флаги сокета.

if (-1 == ioctl(sock, SIOCGIFFLAGS, &ifr))

{

    // Не удалось получить флаги.

}

 

// Обновить флаги.

if (enable_promisc) ifr.ifr_flags |= IFF_PROMISC;

else ifr.ifr_flags &= ~IFF_PROMISC;

// Установить новое значение флага IFF_PROMISC.

if (-1 == ioctl(sock, SIOCSIFFLAGS, &ifr))

{

    std::cerr << "Unable to set promisc mode!" << std::endl;

}

Сначала был создан raw-сокет, который будет получать все кадры, на что указывает параметр ETH_P_ALL.

ETH_P_ константы — это EtherType, то есть значения кода протокола, которые могут быть установлены в поле кадра Ethernet, за исключением ETH_P_ALL, под который попадает любой протокол.

Другие «популярные» коды — ETH_P_IP для IPv4 и ETH_P_IPV6 для IPv6.

После этого код получил флаги, установленные на интерфейс. Флаг IFF_PROMISC был сброшен или установлен, затем применено слово «флагов». Все требуемые для этого API рассматривались в предыдущих главах.

Внимание! Здесь для Linux был использован непереносимый тип AF_PACKET, который нужен для отправки приложению необработанных данных канального уровня. В обоих случаях можно создавать обычные raw-сокеты, но обработка потока данных будет отличаться. Кроме того, можно использовать обычный дейтаграммный сокет SOCK_DGRAM.

Интерфейс переводится в неразборчивый режим через вызов ioctl(), устанавливающий флаг IFF_PROMISC.

Видим, что интерфейс действительно был переведен в неразборчивый режим:

ip link show eno2

2: eno2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000

   link/ether e4:54:e8:38:26:9c brd ff:ff:ff:ff:ff:ff

   altname enp0s31f6

 

sudo build/bin/b01-ch22-promisc-switcher eno2 e

ip link show eno2

2: eno2: <NO-CARRIER,BROADCAST,MULTICAST,PROMISC,UP> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000

   link/ether e4:54:e8:38:26:9c brd ff:ff:ff:ff:ff:ff

   altname enp0s31f6

В ОС Windows вместо установки данного флага используется вызов SIO_RCVALL:

#include <mstcpip.h>

#include <iphlpapi.h>

 

// Создать сырой сокет.

int sock = socket(AF_INET, SOCK_RAW, IPPROTO_IP);

 

// Необходимо связать IP-адрес, по которому Windows определит сетевой

// интерфейс.

if (-1 == bind(sock_, reinterpret_cast<const struct sockaddr*>(&iface_addr),

    sizeof(iface_addr)))

{

    ...

}

 

...

 

DWORD dwValue = RCVALL_ON;

DWORD dwBytesReturned = 0;

// Выполнить системный вызов.

if (SOCKET_ERROR == WSAIoctl(sock, SIO_RCVALL, &dwValue, sizeof(dwValue),

                             nullptr, 0, &dwBytesReturned, nullptr, nullptr))

{

    // Ошибка.

}

То же самое в Python:

if sys.platform == 'win32':

    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:

        # В Windows интерфейс будет идентифицирован по связанному IP-адресу.

        sock.bind(('192.158.5.3', 0))

        sock.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

Увы, для Linux и других Unix-подобных систем в версии Python по крайней мере 3.10 и ранее в модуле socket нужные константы отсутствуют.

Но можно добавить их самостоятельно, подсмотрев значения в /usr/include/linux/if.h или /usr/include/net/if.h.

Реализуем небольшой класс, выполняющий установку и сброс данного флага внутри блока with.

Сначала определим нужные константы и структуру:

import ctypes

from fcntl import ioctl

import socket

import sys

 

# С точки зрения C-функций и оберток над этими функциями данный класс

# повторяет C-структуру ifreq.

class InterfacePromiscSwitcher(ctypes.Structure):

    # Этих констант нет в модуле socket, поэтому добавим их явно.

    IFF_PROMISC = 0x100

    SIOCGIFFLAGS = 0x8913

    SIOCSIFFLAGS = 0x8914

 

    # А это поля "структуры" ifreq.

    _fields_ = [('ifr_ifrn', ctypes.c_char * 16),

                ('ifr_flags', ctypes.c_short)]

Добавим конструктор так, чтобы класс мог работать как с уже существующим сокетом, так и автономно:

    def __init__(self, if_name: str, *args, **kw):

        super().__init__(*args, **kw)

        # Можно переиспользовать уже созданный ранее сокет.

        # Или создать новый.

        self._sock = (

            kw['socket'] if 'socket' in kw

            else socket.socket(socket.AF_INET, socket.SOCK_DGRAM

        )

 

        self.ifr_ifrn = if_name.encode()

 

    @property

    def socket(self) -> socket.socket | None:

        return self._sock

Универсальные методы для установки неразборчивого режима и для его снятия:

    def set_promisc(self):

        # В API флаги интерфейса — параметр сокета.

        # Поэтому в классическом варианте нужно устанавливать

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

        sock = self._sock

        if 'win32' == sys.platform:  # Windows-блок.

            sock.bind((self.ifr_ifrn, 0))

            sock.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

        else:

            # Блок Unix-like.

            # Получить флаги.

            ioctl(sock.fileno(), self.SIOCGIFFLAGS, self)

            # Включить флаг PROMISC.

            self.ifr_flags |= self.IFF_PROMISC

            # Установить флаги.

            ioctl(sock.fileno(), self.SIOCSIFFLAGS, self)

 

    def unset_promisc(self, close_socket: bool = True):

        if 'win32' == sys.platform:

            self._sock.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

        else:

            # Сбросить флаг PROMISC.

            self.ifr_flags &= ~self.IFF_PROMISC

            # Установить флаги.

            ioctl(self._sock.fileno(), self.SIOCSIFFLAGS, self)

        if close_socket:

            self._sock.close()

Добавим специальные методы для работы с блоком with:

    def __enter__(self):

        self.set_promisc()

        return self

 

    def __exit__(self, *args):

        self.unset_promisc()

И наконец, оформим модуль как утилиту, если он вызван из консоли:

if '__main__' == __name__:

    # Проверим работу класса.

    from sys import argv

    import subprocess

 

    if len(argv) != 2:

        print(f'{argv[0]} <interface name>')

        sys.exit(1)

 

    interface_name = argv[1]

 

    with InterfacePromiscSwitcher(interface_name) as ifreq:

        print(f'Promiscuous mode for the "{interface_name}" enabled:')

        print(subprocess.check_output(['ip', 'a', 'show', interface_name],

                                      encoding='utf8'))

 

    print(f'Promiscuous mode for the "{interface_name}" disabled:')

    print(subprocess.check_output(['ip', 'a', 'show', interface_name],

                                  encoding='utf8'))

Понятно, что вызов команды ip show будет работать только на Linux. Посмотрим на результат:

sudo src/book01/ch22/python/promisc_switcher/promisc_switcher.py wlo1

Promiscuous mode for the "wlo1" enabled:

3: wlo1: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc noqueue

state UP group default qlen 1000

...

 

Promiscuous mode for the "wlo1" disabled:

3: wlo1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000

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

Но с другой стороны, работа адаптера в таком режиме и работа нескольких снифферов на этом адаптере одновременно — ситуация нетипичная. А некорректное завершение программы, в результате которого не был вызван специальный метод __exit__(), — ситуация возможная.

Хотя блок with, по сути, эквивалентен блоку try/finally, процесс может быть принудительно завершен и метод не вызовется. Поэтому флаг может остаться установленным и не сброшенным последующими вызовами программы. Данный флаг на всякий случай стоит сбрасывать всегда.

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

Альтернатива неразборчивому режиму

Можно увидеть, что при запуске в Linux Wireshark или tcpdump утилиты не показывают факт нахождения интерфейса в promisc-режиме.

Wireshark и tcpdump используют libpcap, который задействует . Этот режим применим только для сокетов AF_PACKET.

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

В libpcap использование данного режима :

if (!is_any_device && handle->opt.promisc)

{

    memset(&mr, 0, sizeof(mr));

    mr.mr_ifindex = handlep->ifindex;

    mr.mr_type    = PACKET_MR_PROMISC;

    if (setsockopt(sock_fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,

        &mr, sizeof(mr)) == -1)

    {

        pcap_fmt_errmsg_for_errno(handle->errbuf,

            PCAP_ERRBUF_SIZE, errno, "setsockopt (PACKET_ADD_MEMBERSHIP)");

        close(sock_fd);

        return PCAP_ERROR;

    }

}

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

Подробнее см. .

Реализация простого сниффера

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

Стандартом де-факто на файлы, содержащие данные из сети, является PCAP — Packet Capture, содержащий общий заголовок и массив захваченных пакетов, каждый из которых предварен своим заголовком. Этот стандарт описывает распространенный формат, поддерживаемый различными библиотеками. Он представлен на рис. 22.1.

Рис. 22.1. Формат PCAP

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

Заголовок PCAP-файла:

constexpr auto PCAP_VERSION_MAJOR = 2;

constexpr auto PCAP_VERSION_MINOR = 4;

constexpr auto DLT_EN10MB = 1;

// Полный размер буфера пакета.

constexpr auto BUFFER_SIZE_PKT = 256 * 2561;

 

struct pcap_timeval

{

    // UNIX time_t.

    int32_t tv_sec;

    // Смещение в микросекундах от tv_sec.

    int32_t tv_usec;

};

 

// Заголовок PCAP-файла.

struct pcap_file_header

{

    // "Магическое число".

    const uint32_t magic = 0xa1b2c3d4;

    // Главная версия.

    const uint16_t version_major = PCAP_VERSION_MAJOR;

    // Минорная версия.

    const uint16_t version_minor = PCAP_VERSION_MINOR;

    // Локальная временная зона для коррекции времени относительно GMT.

    int32_t thiszone = 0;

    // Точность временной метки.

    uint32_t sigfigs = 0;

    // Максимальная длина захватываемых пакетов в октетах.

    uint32_t snaplen = BUFFER_SIZE_PKT;

    // Тип соединения.

    uint32_t linktype = DLT_EN10MB;

};

Заголовок каждого пакета, содержащегося в PCAP-файле, а также некоторые константы, относящиеся к нему:

struct pcap_sf_pkthdr

{

    pcap_timeval ts;

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

    uint32_t caplen;

    // Реальная длина пакета.

    uint32_t len;

};

 

// Размер PCAP-заголовка пакета.

constexpr auto BUFFER_SIZE_HDR = sizeof(pcap_sf_pkthdr);

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

constexpr auto BUFFER_SIZE_ETH = 14;

// Размер IP-пакета.

constexpr auto BUFFER_SIZE_IP = BUFFER_SIZE_PKT — BUFFER_SIZE_ETH;

// Смещение Ethernet-заголовка в буфере: сразу за PCAP-заголовком.

constexpr auto BUFFER_OFFSET_ETH = sizeof(pcap_sf_pkthdr);

// Смещение IP-заголовка в буфере.

constexpr auto BUFFER_OFFSET_IP = BUFFER_OFFSET_ETH + BUFFER_SIZE_ETH;

 

#if defined(WIN32)

// Для ОС Windows Ethernet-заголовок отсутствует.

constexpr auto BUFFER_WRITE_OFFSET = BUFFER_OFFSET_IP;

constexpr auto BUFFER_ADD_HEADER_SIZE = BUFFER_SIZE_ETH;

#else

constexpr auto BUFFER_WRITE_OFFSET = BUFFER_OFFSET_ETH;

constexpr auto BUFFER_ADD_HEADER_SIZE = BUFFER_SIZE_ETH;

#endif

Позже в главе мы рассмотрим библиотеки, в которых определены все эти константы и структуры. Например, в библиотеке libpcap они содержатся в заголовочном файле pcap.h. А сейчас воспользуемся ими для записи в PCAP-файл данных, полученных из сети.

В ОС Linux будем использовать сокет типа AF_PACKET и набор функций для получения данных интерфейса:

ifreq get_ifr(const std::string& if_name, int sock)

{

    ifreq ifr = {0};

    std::copy(if_name.begin(), if_name.end(), ifr.ifr_name);

 

    // Получить данные сетевого интерфейса по его имени.

    if (-1 == ioctl(sock, SIOCGIFINDEX, &ifr))

    {

        throw std::system_error(errno, std::system_category(),

                                std::string("Unable to find interface ") +

                                if_name);

    }

 

    return ifr;

}

 

// Вернуть индекс сетевого интерфейса.

int get_if_index(const std::string& if_name, int sock)

{

    const auto if_index = get_ifr(if_name, sock).ifr_ifindex;

    std::cout << "Device index = " << if_index << std::endl;

    return if_index;

}

 

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

auto get_if_address(const std::string& if_name, int sock)

{

    // Структура адреса интерфейса.

    sockaddr_ll iface_addr =

    {

        .sll_family = AF_PACKET, .sll_protocol = htons(ETH_P_IP),

        .sll_ifindex = get_if_index(if_name, sock),

    };

    return iface_addr;

}

В ОС Windows для выполнения этой же задачи реализована одна функция:

auto get_if_address(const std::string& if_name, int sock)

{

    sockaddr_in sa = {.sin_family = PF_INET, .sin_port = 0};

    inet_pton(AF_INET, if_name.c_str(), &sa.sin_addr);

 

    return sa;

}

Основную функциональность сниффера мы реализуем в классе Sniffer. В его конструкторе создадим новый сокет:

#if defined(WIN32)

    sock_(AF_INET, SOCK_RAW, IPPROTO_IP),

#else

    sock_(AF_PACKET, SOCK_RAW, htons(ETH_P_IP)),

#endif

Одинаковый сокет использовать нельзя — нам требуется, чтобы в обеих системах формат кадра был схож: Ethernet с заголовком IP.

Иногда полезно воспользоваться такой возможностью raw-сокетов, как получение данных только определенного протокола. В этом случае вместо ETH_P_ALL необходимо использовать конкретный протокол, а вместо AF_INET — требуемое семейство адресов.

Например, для захвата TCP-пакетов требуется использовать следующие параметры:

int sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);

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

Все это делается в методе init():

bool Sniffer::init()

{

    // Для Windows адрес должен быть привязан до переключения в promisc-режим.

    if (!bind_socket() || !switch_promisc(true) || !write_pcap_header())

        return false;

 

    initialized_ = true;

    return true;

}

Метод привязки сокета к интерфейсу выполняет привязку, используя функцию bind(), так как это универсальный вариант:

bool Sniffer::bind_socket()

{

    if (const size_t len = if_name_.size(); IFNAMSIZ <= len)

    {

        std::cerr << "Too long interface name!" << std::endl;

        return false;

    }

 

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

    const auto iface_addr = get_if_address(if_name_, sock_);

    // Привязать сокет к адресу.

    if (-1 == bind(sock_,

        reinterpret_cast<const sockaddr*>(&iface_addr), sizeof(iface_addr)))

    {

        std::cerr

            << "bind() failed: "

            << sock_wrap_.get_last_error_string() << "." << std::endl;

        return false;

    }

 

    return true;

}

В данном случае установка опции SO_BINDTODEVICE не обязательна.

По крайней мере в Linux вместо передачи адреса вполне работает передача обычной строки, которая содержит название интерфейса, что продемонстрировано ниже в коде сниффера на Python.

После связывания адреса можно запускать цикл захвата пакетов в методе Sniffer::capture():

bool Sniffer::capture()

{

    // Первые 14 байт — фейковый Ethernet-заголовок под протокол IPv4.

    std::array<char, BUFFER_SIZE_HDR + BUFFER_SIZE_PKT> buffer;

    // 0x08 — тип IP в кадре Ethernet. По смещению = 12.

    buffer[BUFFER_OFFSET_ETH + ethernet_proto_type_offset] = 0x08;

    pcap_sf_pkthdr* pkt = reinterpret_cast<pcap_sf_pkthdr*>(buffer.data());

 

    // Прочитать очередной пакет.

    const int rc = recv(sock_, buffer.data() + BUFFER_WRITE_OFFSET,

                        BUFFER_SIZE_IP, 0);

 

    if (-1 == rc)

    {

        // Ошибка приема данных — перестать читать пакеты.

        std::cerr

            << "recv() failed: " << sock_wrap_.get_last_error_string()

            << std::endl;

        return false;

    }

 

    // Соединение разорвано — перестать читать пакеты.

    if (!rc) return false;

    std::cout << rc << " bytes received..." << std::endl;

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

    using namespace std::chrono;

    // Рассчитать временную метку пакета.

    const auto cur_time = duration_cast<microseconds>(

        time_point_cast<microseconds>(

            system_clock::now()).time_since_epoch()

    );

    const auto t_s = seconds(duration_cast<seconds>(cur_time));

    const auto u_s = cur_time - duration_cast<microseconds>(t_s);

 

    // Установить поля заголовка PCAP.

    pkt->ts.tv_sec = t_s.count();

    pkt->ts.tv_usec = u_s.count();

    pkt->caplen = rc + BUFFER_ADD_HEADER_SIZE;

    pkt->len = rc + BUFFER_ADD_HEADER_SIZE;

 

    // Запись пакета в файл.

    of_.write(buffer.data(), rc + BUFFER_SIZE_HDR + BUFFER_ADD_HEADER_SIZE);

    of_.flush();

 

    return true;

}

Остается только переключить адаптер в неразборчивый режим и вызвать метод захвата пакетов в цикле:

bool Sniffer::start_capture()

{

    if (started_ || !initialized_) return false;

    started_ = true;

    // Переключить адаптер в неразборчивый режим.

    if (!switch_promisc(true)) return false;

    std::cout << "Starting capture on interface " << if_name_ << std::endl;

    // Начать цикл захвата.

    while (started_)

    {

        if (!capture()) return false;

    }

    return true;

}

Попробуем запустить сниффер на адаптере, через который идет трафик:

sudo build/bin/b01-ch21-raw-sniffer wlo1 122.pcap

Device index = 3

Starting capture on interface wlo1

300 bytes received...

300 bytes received...

300 bytes received…

...

Видно, что были приняты данные. Теперь откроем этот файл в Wireshark:

wireshark 122.pcap

На рис. 22.2 в окне дампа Wireshark видно, что в файл были записаны кадры с HTTP-запросами и ответами.

Это нормально, потому что в момент запуска сниффера работал браузер и обменивался данными по HTTP с веб-сервером.

Рис. 22.2. Окно данных пакета в Wireshark

Сниффер на Python

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

В сниффере, реализованном на C++, мы не занимались обработкой пакетов, а сохраняли их в известный формат, который мог быть открыт, например Wireshark. Здесь же добавим простейшую обработку, которая производится классами-обработчиками, реализованными условным «пользователем». Сниффер будет выполнять печать различных полей заголовков в консоль.

Обработчики будут иметь константные и вычисляемые поля, часть из которых должен определить пользователь:

Код протокола — это некий код, по которому идентифицируется протокол в PDU уровня ниже. Этот код присутствует только для протоколов выше канального уровня, то есть, например, у Ethernet такого кода нет.

• Строка формата — строка парсера стандартного модуля struct, который используется для разбора заголовка пакета.

• Код вышележащего протокола — вычисляемое свойство, говорящее о том, какой PDU несет в полезной нагрузке данный протокол. Например, 6 для TCP и 17 для UDP.

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

Для произвольного пользовательского протокола код может быть, например, таким:

class ExampleProtocolHeader(NetworkProtocolHeader):

    # Уникальный идентификатор протокола.

    # Другого протокола с таким кодом быть не может.

    protocol_code = 0xdead

    # Распаковка осуществляется из сетевого формата, о чем говорит знак !.

    format_string = '!BH16s'

 

    def __init__(self, packet):

        # Вызов конструктора предка.

        super().__init__(packet)

 

        # Все поля задаются здесь.

        self.field_1, self.field_2, self.field_n = self.fields

 

    @property

    def inner_protocol_code(self) -> int:

        """

        Код для вышележащего протокола, если таковой существует.

        """

        return self.field_1

 

    @property

    def payload_offset(self) -> int:

        """

        Смещение, по которому находятся полезные данные.

        Возможно, PDU другого протокола.

        """

        return self.field_3

Для протоколов транспортного уровня и выше код берется из списка . Это зарегистрированные IANA-номера, которые в Unix-подобных системах можно обнаружить внутри ранее упомянутого файла /etc/protocols.

На канальном уровне все зависит от конкретного протокола. Например, для Ethernet . У IANA также имеется список кодов для этого поля, но коды в нем отличаются от кодов из предыдущего списка. Например, IPv4 в первом списке имеет код 4, то есть это IP-пакет, инкапсулированный в один из других вышележащих протоколов. А в списке EtherType код для IPv4 будет равен 0x0800; это означает, что данный Ethernet-кадр переносит IP-пакет.

В общем случае один класс-обработчик может быть привязан к нескольким номерам и требуется учитывать источник, откуда пришли данные. А с другой стороны, кадры Ethernet, например, тоже могут быть инкапсулированы в IP-датаграмму. Но в сниффере мы несколько упростим задачу и не будем учитывать данный нюанс.

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

Получение строкового представления данных реализовано прямо в классах-­обработчиках.

Внимание! В реальном коде следует выделять представление данных в отдельные классы или другие отдельные сущности.

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

import struct

import binascii

import socket

from typing import Any, final, Final

 

import sys

from abc import abstractmethod

 

# Уже знакомый нам класс.

from promisc_switcher.promisc_switcher import InterfacePromiscSwitcher

 

# Константы из /usr/include/linux/if_ether.h:

ETH_P_IP = 0x0800

ETH_P_ALL = 0x0003

 

# Из /usr/include/asm-generic/socket.h:

SO_BINDTODEVICE = 25

 

# Зададим большой размер буфера для приема данных.

RECV_BUF_SIZE = 65535

Теперь разберемся с обработчиками. Выше было видно, что обработчик протокола наследуется от базового класса, общего для всех обработчиков, — класса NetworkProtocolHeader, в котором реализованы основные функции:

Разбор заголовка в свойстве fields, где просто вызывается функция unpack() из модуля struct.

• Метод unpack_packet() для распаковки пакета, который вызывает обработчики из цикла, выбирая их по коду протокола.

Абстрактные «свойства», которые пользователь должен переопределить в наследниках.

Использование такого класса упрощает работу программистам, реализующим обработчики. Они просто наследуют данный класс и переопределяют свойства. Этого достаточно: обработчики автоматически регистрируются в базовом классе.

Ранее функциональность регистрации была реализована через метаклассы. Но как показал Никита Соболев, использование современного Python значительно упрощает реализацию механизма регистрации. Сейчас он реализован через специальный метод __init_subclass__(), который вызывается, когда от класса наследуется другой класс.

При наследовании класс обработчика регистрируется в методе __init_subclass__():

class NetworkProtocolHeader:

    channel_protocols: Final[set[type]] = set()

    protocols: Final[dict[int, type]] = {}

    format_string = ''

 

    @final

    def __init_subclass__(cls):

        # Вычисление размера заголовка из поля строки формата, задающего

        # порядок его разбора.

        cls.header_size = struct.calcsize(cls.format_string)

        try:

            proto_code = cls.protocol_code

        except AttributeError:

            # Это протокол канального уровня, так как у него

            # отсутствует код.

            NetworkProtocolHeader.channel_protocols.add(cls)

        else:

            proto_class = NetworkProtocolHeader.protocols.get(proto_code)

 

            if proto_class is not None:

                # В production-ready-коде необходимо реализовать

                # свои классы исключений.

                raise KeyError(

                    f'Class for the protocol code {proto_code} already '

                    f'exists [{proto_class.__name__}]!'

                )

                # Добавить класс обработчика протокола уровня выше канального

                # в словарь.

                NetworkProtocolHeader.protocols[proto_code] = cls

 

    def __init__(self, packet):

        self._packet = packet

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

Остальные протоколы, у которых есть поле protocol_code, добавляются в словарь, но только при условии, что в нем еще нет протокола с таким же кодом.

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

В свойстве fields бинарные данные заголовка преобразуются в структуру по строке формата:

    @final

    @property

    def fields(self):

        # Получить поля заголовка.

        return struct.unpack(self.format_string,

                             self._packet[:self.header_size])

 

    # "Абстрактное свойство".

    @property

    @abstractmethod

    def inner_protocol_code(self) -> int:

        """Переопределяемый код вышележащего протокола."""

        pass

 

    @property

    @abstractmethod

    def payload_offset(self) -> int:

        """Переопределяемое смещение заголовка вышележащего протокола."""

        pass

Метод класса unpack_packet() здесь является наиболее сложной частью. Сначала он выполняет некоторые проверки:

    @final

    @classmethod

    def unpack_packet(cls, base_protocol, packet, *,

                      skip_channel_proto_check: bool = False) -> list[Any]:

        """Метод "распаковки" пакета."""

        if not issubclass(base_protocol, NetworkProtocolHeader):

            raise TypeError(

                f'{base_protocol!r} is not a descendant class of the '

                f'{cls.__class__.__name__}!',

             )

        # Отключение проверки того факта, что начальный протокол является

        # протоколом канального уровня. Для чего это нужно, рассказано ниже.

        if (

            not skip_channel_proto_check

            and base_protocol not in cls.channel_protocols

        ):

            # Необходимо генерировать исключение с классом,

            # определенным пользователем.

            raise KeyError(

                f'{base_protocol.__name__} is not a channel protocol class!')

 

        # Результат — список, в который будут записаны все верно

        # инициализированные обработчики протоколов в порядке иерархии.

        headers = []

 

        cur_header = base_protocol(packet)

        headers.append(cur_header)

Затем сводится к циклу, где по коду протокола выбирается класс обработчика и создается его экземпляр, который и разбирает пакет, переданный в конструкторе:

        while True:

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

            # предыдущим вызванным обработчиком.

            packet = packet[cur_header.payload_offset:]

            try:

                # В цикле обработки получаем требуемый класс обработчика

                # по коду протокола.

                inner_code = cur_header.inner_protocol_code

            except NotImplementedError:

                # Протокола вышележащего уровня в данном PDU

                # не предполагается.

                break

            else:

                proto_header_class = cls.protocols.get(inner_code)

 

            if proto_header_class is None:

                break

            # Если такой класс есть, создать его объект — непосредственно

            # обработчик, которому передаются обрабатываемые данные.

            cur_header = proto_header_class(packet)

 

            # Этот объект добавляется к результату.

            headers.append(cur_header)

 

        return headers

Осталось реализовать классы, разбирающие заголовки протоколов. Начнем с Ethernet. На рис. 22.3 показан формат кадра.

Рис. 22.3. Формат Ethernet-кадра

Соответственно, реализация «обработки» кадра не сильно отличается от примера обработчика:

class EthernetHeader(NetworkProtocolHeader):

    # "!" — распаковка из сетевого порядка байтов.

    format_string = '!6s6sH'

 

    def __init__(self, packet):

        super().__init__(packet)

        # Поля.

        self.destination_mac_address, self.source_mac_address, \

            self.protocol = self.fields

 

    @property

    def inner_protocol_code(self) -> int:

        return int(self.protocol)

 

    @property

    def payload_offset(self) -> int:

        return self.header_size

 

    @staticmethod

    def print_mac(addr):

        pa = binascii.hexlify(addr).decode()

        # Склеиваем MAC: по две шестнадцатеричные цифры с двоеточием

        # между ними.

        return ':'.join([pa[i:i + 2] for i in range(0, len(pa), 2)])

 

    def __str__(self):

        # В продуктовом коде метод для представления в строковой форме

        # лучше упростить, а для печати заголовка реализовать отдельный

        # вариант или варианты представления.

        return f'[{self.__class__.__name__}]:\n' \

               f'  Src MAC: {self.print_mac(self.destination_mac_address)}\n'\

               f'  Dst MAC: {self.print_mac(self.source_mac_address)}\n' \

               f'  EtherType: 0x{self.protocol:04x}'

Формат заголовков IP несколько сложнее. Заголовок IPv4 показан на рис. 22.4.

Рис. 22.4. Формат заголовка IPv4

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

class Ipv4Header(NetworkProtocolHeader):

    # Это значение взято из списка значений EtherType.

    # Для корректной обработки необходим список кодов.

    # Но в учебном примере мы эту поддержку не реализуем.

    protocol_code = 0x0008

    format_string = '!BBHHHBBH4s4s'

    # Длина слова в байтах.

    word_length = 4

 

    def __init__(self, packet):

        super().__init__(packet)

 

        (

            self.ip_version,

            self.tos,

            self.total_length,

            self.identification,

            self.fragment_offset,

            self.ttl,

            self.protocol,

            self.header_checksum,

            self.source_address,

            self.destination_address,

        ) = self.fields

 

        # С учетом переменного числа опций IP-заголовок может иметь разную

        # длину.

        # 4-битное поле Internet Header Length содержит размер заголовка

        # в 32-битных словах.

        self.ihl = self.ip_version & 0x0f

        self.ip_version &= 0xf0

        self.ip_version >>= 4

 

        assert self.ip_version == 4

 

        # Минимальная длина — 20 байт. Максимальная — 60, но здесь

        # эта проверка не реализована.

        assert self.ihl * self.word_length >= self.header_size

Класс также содержит два свойства, позволяющих получить код протокола и смещение данных в PDU:

    @property

    def inner_protocol_code(self) -> int:

        return self.protocol

 

    @property

    def payload_offset(self) -> int:

        # Данные следуют сразу после заголовка.

        return self.ihl * self.word_length

 

    def __str__(self):

        spacing = ' ' * 6

        return (

            f'    [{self.__class__.__name__}]:\n'

            f'{spacing}Payload Off: {self.payload_offset}\n'

            f'{spacing}TOS: {self.tos}\n'

            f'{spacing}TTL: {self.ttl}\n'

            f'{spacing}Proto: {self.protocol}\n'

            f'{spacing}Length: {self.total_length}\n'

            f'{spacing}Src IP: '

            f'{socket.inet_ntop(socket.AF_INET, self.source_address)}\n'

            f'{spacing}Dst IP: '

            f'{socket.inet_ntop(socket.AF_INET, self.destination_address)}'

        )

Метод для генерации строкового представления выводит поля заголовка IP.

Возможна упомянутая выше ситуация, когда в IP-дейтаграмме содержится код протокола 4. Это нормально и означает, что используется протокол , то есть один IP-пакет инкапсулируется в другой, как показано на рис. 22.5.

Рис. 22.5. Инкапсуляция в IP

Но обработка таких протоколов в примере сниффера не поддерживается.

Разберем заголовок IPv6, который показан на рис. 22.6.

Рис. 22.6. Формат IP-заголовка для IPv6

В IPv6 необходим разбор опций. Последняя опция будет содержать идентификатор вышележащего протокола в поле Next Header, но мы несколько упростим обработчик:

class Ipv6Header(NetworkProtocolHeader):

    # Это значение взято из списка значений EtherType.

    protocol_code = 0x86DD

    format_string = '!BBHHBB16s16s'

 

    word_length = 4

 

    def __init__(self, packet):

        super().__init__(packet)

 

        (

            first_byte,

            second_byte,

            flow_label_lsb,

            self.payload_length,

            self.next_header,

            self.hop_limit,

            self.source_address,

            self.destination_address,

        ) = self.fields

 

        self.ip_version = (first_byte & 0xF0) >> 4

        self.traffic_class = (

            (first_byte & 0x0F) << 4) |

            ((second_byte & 0xF0) >> 4)

        )

        self.flow_label = (second_byte & 0x0F << 16) | flow_label_lsb

 

        assert self.ip_version == 6

Остальные свойства и метод __str__() аналогичны таковым для IPv4:

    @property

    def inner_protocol_code(self) -> int:

        # На самом деле необходимо разбирать опции заголовка и возвращать

        # поле Next Header последней опции.

        return self.next_header

 

    @property

    def payload_offset(self) -> int:

        return self.header_size

 

    def __str__(self):

        spacing = ' ' * 6

        printable_addr = socket.inet_ntop(

            socket.AF_INET6,

            self.destination_address

        )

 

        return (

            f'    [{self.__class__.__name__}]:\n'

            f'{spacing}Traffic Class: '

            f'{self.traffic_class}\n'

            f'{spacing}Flow Label: {self.flow_label}\n'

            f'{spacing}Next Header: {self.next_header}\n'

            f'{spacing}Payload Length: '

            f'{elf.payload_length}\n'

            f'{spacing}Src IP: '

            f'{socket.inet_ntop(socket.AF_INET6, self.source_address)}\n'

            f'{spacing}Dst IP: '

            f'{printable_addr}'

        )

По аналогии реализуем обработчик ICMP. Так как ICMP не содержит вложенных протоколов, его свойство inner_protocol_code генерирует исключение:

class IcmpHeader(NetworkProtocolHeader):

    # Данный код и коды у TCP и UDP взяты из списка IANA.

    protocol_code = 1

    format_string = '!BBH'

 

    def __init__(self, packet):

        super().__init__(packet)

        self.message_type, self.code, self.checksum = self.fields

 

    @property

    def inner_protocol_code(self) -> int:

        raise NotImplementedError('ICMP has not inner protocol!')

 

    @property

    def payload_offset(self) -> int:

        # Для ICMP размер заголовка всегда постоянный.

        return self.header_size

 

    def __str__(self):

        spacing = ' ' * 10

        return (

            f'        [{self.__class__.__name__}]:\n'

            f'{spacing}Message Type: {self.message_type}\n'

            f'{spacing}Code: {self.code}\n'

            f'{spacing}Checksum: 0x{self.checksum:04x}'

        )

 

class Icmp6Header(IcmpHeader):

    protocol_code = 58

 

    @property

    def inner_protocol_code(self) -> int:

        raise NotImplementedError('ICMP6 has not inner protocol!')

Осталось создать обработчики транспортных протоколов. Заголовок TCP, показанный на рис. 22.7, вероятно, наиболее сложный:

Рис. 22.7. Формат TCP-заголовка

Но его обработчик несложен:

class TcpHeader(NetworkProtocolHeader):

    # Номер из списка IANA.

    protocol_code = 6

    format_string = '!HHLLBBHHH'

    word_length = 4

 

    def __init__(self, packet):

        super().__init__(packet)

        (

            self.source_port, self.destination_port,

            self.sequence_number, self.acknowledge_number, offset_reserved,

            self.tcp_flag,

            self.window,

            self.checksum,

            self.urgent_pointer

        ) = self.fields

 

        self.data_offset = ((offset_reserved & 0xf0) >> 4) * self.word_length

 

    @property

    def inner_protocol_code(self) -> int:

        # Поверх TCP работают произвольные пользовательские протоколы.

        raise NotImplementedError(

            'TCP has arbitrary user-defined inner protocol!')

 

    @property

    def payload_offset(self) -> int:

        return self.data_offset

 

    def __str__(self):

        spacing = ' ' * 10

        return (

            f'        [{self.__class__.__name__}]:\n'

            f'{spacing}Src Port: {self.source_port}\n'

            f'{spacing}Dst Port: {self.destination_port}\n'

            f'{spacing}Seq Number: {self.sequence_number}\n'

            f'{spacing}Ack Number: {self.acknowledge_number}\n'

            f'{spacing}Data Off: {self.data_offset}\n'

            f'{spacing}Flags: 0x{self.tcp_flag:04x}\n'

            f'{spacing}Window: {self.window}\n'

            f'{spacing}Urgent Ptr: {self.urgent_pointer}\n'

            f'{spacing}Checksum: 0x{self.checksum:04x}'

        )

Свойство inner_protocol_code также генерирует исключение, но по той причине, что TCP инкапсулирует произвольные данные и не знает, что за блок в конкретном сегменте. А приложения должны сами знать вышележащий протокол, который они используют.

Вероятно, один из самых простых обработчиков для UDP:

class UdpHeader(NetworkProtocolHeader):

    # Номер из списка IANA.

    protocol_code = 17

    format_string = '!HHHH'

 

    def __init__(self, packet):

        super().__init__(packet)

        (

            self.source_port, self.destination_port,

             self.length, self.checksum

        ) = self.fields

 

    @property

    def inner_protocol_code(self) -> int:

        # Пользователь далее реализует собственные протоколы.

        raise NotImplementedError(

            'UDP has arbitrary user-defined inner protocol!')

 

    @property

    def payload_offset(self) -> int:

        return self.header_size

 

    def __str__(self):

        spacing = ' ' * 10

        return (

            f'        [{self.__class__.__name__}]:\n'

            f'{spacing}Src Port: {self.source_port}\n'

            f'{spacing}Dst Port: {self.destination_port}\n'

            f'{spacing}Length: {self.length}\n'

            f'{spacing}Checksum: 0x{self.checksum:04x}'

        )

Теперь реализуем запуск сниффера. Он должен перевести интерфейс в неразборчивый режим и начать захват пакетов в цикле:

if len(sys.argv) != 2:

    print(f'{sys.argv[0]} <interface name>')

    sys.exit(1)

 

interface_name = sys.argv[1]

# Используем уже реализованный переключатель в promisc-режиме.

with InterfacePromiscSwitcher(interface_name) as ips:

    if 'win32' == sys.platform:

        # В ОС Windows можно использовать обычный raw-сокет.

        sock = socket.socket(socket.AF_INET, socket.SOCK_RAW,

                             socket.IPPROTO_IP)

        sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

        # Вспоминаем, что привязка к интерфейсу через данную опцию

        # работает для сокетов домена AF_INET.

        sock.setsockopt(socket.SOL_SOCKET, SO_BINDTODEVICE,

                        interface_name.encode())

 

        sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

    else:

        # В Linux используются сокеты домена AF_PACKET, чтобы захватить

        # также заголовки Ethernet-кадров.

        sock = socket.socket(socket.PF_PACKET, socket.SOCK_RAW,

                             socket.ntohs(ETH_P_ALL))

        # Привязка через SO_BINDTODEVICE для сокетов AF_PACKET не работает.

        # Можно использовать такой вариант:

        # socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)

        # Тогда не будет возможности получать заголовок кадра Ethernet.

 

    # Привязка к интерфейсу через bind() работает везде.

    # Сюда передается обычная строка — имя сетевого интерфейса.

    # Получение индекса и передача структуры sockaddr_ll не требуется.

    sock.bind((interface_name, 0))

Интерфейс корректно настроен, запустим цикл обработки пакетов:

    # Цикл захвата пакетов.

    while True:

        data = sock.recvfrom(RECV_BUF_SIZE)

        # В случае использования raw-сокетов декодирование начинается с IP.

        # Проверка на то, что первый декодируемый протокол канального уровня

        # выключается:

        # print(NetworkProtocolHeader.unpack_packet(Ipv4Header, data[0],

        #                                           True))

        for p in NetworkProtocolHeader.unpack_packet(EthernetHeader, data[0]):

            # У всех обработчиков вызывается метод __str__().

            print(p)

        print()

Все, сниффер готов. Запустим его с правами суперпользователя и посмотрим на результат:

sudo src/book01/ch22/python/python-sniffer.py wlo1

[EthernetHeader]:

Src MAC: 08:71:90:0f:45:10

Dst MAC: 4c:5e:0c:a7:2e:f6

EtherType: 0x0800

   [Ipv4Header]:

     Payload Off: 20

     TOS: 0

     TTL: 57

     Proto: 6

     Length: 286

     Src IP: 91.105.192.100

     Dst IP: 192.168.2.13

       [TcpHeader]:

         Src Port: 80

         Dst Port: 58200

         Seq Number: 1418259835

         Ack Number: 638575873

         Data Off: 32

         Flags: 0x0018

         Window: 32768

         Urgent Ptr: 0

         Checksum: 0x933b

 

[EthernetHeader]:

Src MAC: 4c:5e:0c:a7:2e:f6

Dst MAC: 08:71:90:0f:45:10

EtherType: 0x0800

   [Ipv4Header]:

     Payload Off: 20

     TOS: 0

     TTL: 64

     Proto: 6

     Length: 585

     Src IP: 192.168.2.13

     Dst IP: 91.105.192.100

       [TcpHeader]:

         Src Port: 58200

         Dst Port: 80

         Seq Number: 638575873

         Ack Number: 1418260069

         Data Off: 32

         Flags: 0x0018

         Window: 9698

         Urgent Ptr: 0

         Checksum: 0xe0be

...

Видим, что Ethernet-кадры разбираются, вполне корректно выводится IP-заголовок и производится определение вышележащего протокола: TCP, UDP, а если запустить ping, то и ICMP.

Внимание! Код сниффера не содержит преобразований байтового порядка, таких как socket.ntohs(). Это работает потому, что формат распаковки задан со знаком «!» в начале, что говорит о необходимости читать все числа так, будто их байты записаны в сетевом порядке.

Поэкспериментировать с заменой сокетов AF_PACKET на raw-сокеты вы можете самостоятельно, так же как и расширить сниффер для обработки других протоколов и, например, сохранения данных.

Такой сниффер, как Wireshark, может работать с правами обычного пользователя. Ему не требуются root-права.

Достигается это несколькими способами:

в случае Windows — работающей в фоне службой;

для старых Unix-подобных систем — вынесением кода перехвата пакетов в отдельный бинарный файл и установкой ему suid-бита, что небезопасно;

для Linux и других новых систем — через привилегии.

В Linux бинарный файл /usr/bin/wireshark предоставляет интерфейс пользователя, не осуществляя захват. В действительности захватывает пакеты утилита /usr/bin/dumpcap.

Файл этой утилиты имеет установленные привилегии CAP_NET_ADMIN и CAP_NET_RAW+eip, которые позволяют настраивать интерфейсы для перевода их в неразборчивый режим и создавать сырые сокеты и сокеты AF_PACKET.

Посмотреть эти привилегии можно через утилиту getcap.

Владеет утилитой /usr/bin/dumpcap группа wireshark, и кроме пользователя root, ее запуск разрешен только пользователям из этой группы.

Таким образом:

• реализуется захват от любого пользователя без использования suid;

• чтобы любой пользователь мог выполнять захват через Wireshark, ему достаточно добавиться в группу wireshark. Например, так: usermod -a -G wireshark <username>.

Назад: Глава 21. Проксирование, инкапсуляция
Дальше: Решения для захвата трафика