Данный текст является переводом статьи Тима Карстенса Programming with pcap 2002 года. В русскоязычном интернете не так много информации по PCAP. Перевод сделан в первую очередь для людей, которым интересна тема захвата трафика, но при этом они плохо владеют английским языком. Под катом, собственно, сам перевод.


Вступление


Давайте начнем с того, что определим, для кого написана эта статья. Очевидно, что некоторое базовое знание C необходимо (если, конечно, вы не хотите просто понять теорию), для понимания кода приведенного в статье, но вам не нужно быть ниндзя программирования: в тех моментах, которые могут быть понятны только более опытными программистам я постараюсь подробно объяснить все концепции. Так же, пониманию может помочь некоторое базовое знание работы сетей, учитывая что PCAP — это библиотека для реализации сниффинга (Прим. переводчика: Сниффинг — процесс захвата сетевого трафика, своего, или чужого). Все представленные здесь примеры кода были протестированы на FreeBSD 4.3 с ядром по умолчанию.


Начало работы: Общая форма приложения PCAP


Первая вещь которую необходимо понять — общая структура PCAP сниффера. Она может выглядеть следующим образом:


  1. Начнем с определения идентификатора интерфейса, трафик с которого мы хотим получить. В Linux это может быть что нибудь вроде eth0, в BSD это может быть xl1, и тому подобное. Мы можем либо указать этот идентификатор в строке, либо попросить PCAP предоставить его нам.
  2. Далее необходимо инициализировать PCAP. На данном этапе нам нужно передать PCAP имя устройства, с которым мы будем работать. При необходимости мы можем захватить трафик с нескольких устройств. Для их различия мы будем использовать дескрипторы сеансов. Так же, как и во время работы с файлами, нам нужно назвать наш сеанс захвата трафика, что бы мы могли отличить его от других подобных сеансов.
  3. В случае, если мы хотим получить какой то определенный трафик (например, только TCP/IP пакеты, или пакеты только с порта 23 и так далее) мы должны создать набор правил, "скомпилировать" их, и применить их к конкретному сеансу. Это трехфазный, тесно связанный процесс. Набор правил изначально находится в строке, а после компилируется в понятный PCAP формат. Компиляция производится вызовом функции внутри нашей программы, она не связана с использованием какого либо внешнего приложения. Далее мы говорим PCAP применить этот фильтр к необходимой нам сессии.
  4. Наконец, мы говорим PCAP начать захват трафика. В случае использования pcap_loop, PCAP будет работать до тех пор, пока не получит столько пакетов, сколько мы ему указали. Каждый раз, когда он получает новый пакет, он вызывает определенную нами функцию. Эта функция может делать все что мы хотим. Она может прочитать пакет, и передать информацию пользователю, она может сохранить его в файл, или вовсе не делать ничего.
  5. После того, как мы закончим работу по захвату, сессию можно закрыть.
    На самом деле это очень простой процесс. Всего пять шагов, один из которых не обязательный (шаг 3). Давайте рассмотрим каждый шаг, и их реализации.

Определение устройства


Это ужасно просто. Есть два способа определить устройство, которое мы хотим прослушивать.


Первый — просто позволить пользователю сказать программе имя того устройства с которого он хочет захватывать трафик. Рассмотрим этот код:


#include <stdio.h>
#include <pcap.h>

int main(int argc, char *argv[])
{
    char *dev = argv[1];
    printf("Device: %s\n", dev);
    return(0);
}

Пользователь определяет устройство указывая его имя в качестве первого аргумента программы. Теперь, строка dev содержит имя интерфейса который мы будем прослушивать в формате понятном PCAP (конечно, при условии, что пользователь дал нам реальное имя интерфейса)


Второй способ также очень прост. Давайте взглянем на программу:


#include <stdio.h>
#include <pcap.h>

int main(int argc, char *argv[])
{
    char *dev, errbuf[PCAP_ERRBUF_SIZE];

    dev = pcap_lookupdev(errbuf);
    if (dev == NULL) 
    {
        fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
        return(2);
    }
    printf("Device: %s\n", dev);
    return(0);
}

В этом случае, PCAP просто установит имя устройства самостоятельно. "Но подожди, Тим", вы скажете. "Что делать со строкой errbuf?". Большинство PCAP команд позволяют нам передать им строку в качестве одного из аргументов. С какой целью? В том случае, если выполнение команды не удастся, PCAP запишет описание ошибки в переданную строку. В этом случае, если выполнение pcap_lookupdev() провалится, сообщение об ошибке будет помещено в errbuf. Круто, не правда ли? Вот так вот и устанавливается имя устройства для захвата трафика.


Настройка устройства для сниффинга


Задача создания сессии захвата трафика так же очень проста. Для этого мы будем использовать функцию pcap_open_live(). Прототип этой функции:


pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)

Первый аргумент — это имя устройства которое мы определили в предыдущем разделе. snaplen это целое число, которое определяет максимальное число байтов, которое может захватить PCAP. promisc, когда установлен в true, устанавливает устройство в неразборчивый режим (так или иначе, даже если он установлен в false, в определенных случаях интерфейс может находится в неразборчивом режиме). to_ms это время чтения в миллисекундах (значение 0 означает отсутствие таймаута; по крайней мере на некоторых платформах, это означает что вы можете дождаться появления достаточного количества пакетов для прекращения сниффинга до того, как закончите анализ этих пакетов. Поэтому вы должны использовать ненулевое время). Наконец, ebuf это строка в которой мы можем хранить сообщения об ошибках (так же, как мы делали до этого с errbuf). Функция возвращает дескриптор сеанса.


Для демонстрации, рассмотрим этот фрагмент кода:


#include <pcap.h>
...
pcap_t *handle;

handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) 
{
    fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
    return(2);
}

Этот код открывает устройство помещенное в переменную dev, говорит читать столько байтов, сколько указано в BUFSIZ (константа, которая определена в pcap.h). Мы говорим переключить устройство в неразборчивый режим, что бы захватывать трафик до момента возникновения какой либо ошибки, и в случает ошибки, поместить ее описание в строку errbuf; и после, в случае ошибки, используем эту строку что бы вывести сообщение о том, что пошло не так.


Замечания по поводу разборчивого/неразборчивого режимов сниффинга: два способа очень различны по стилю. Обычно, интерфейс находится в разборчивом режиме, захватывая только тот трафик, который отправлен именно ему. Только трафик направленный от него, к нему, или маршрутизированный через него будет захвачен сниффером. Неразборчивый режим, наоборот, захватывает весь трафик который проходит через кабель. В среде без коммутации это может быть весь сетевой трафик. Очевидным преимуществом этого способа является то, возможно захватить большее количество пакетов, что может быть полезным, или нет, в зависимости от цели захвата трафика. Однако существуют и недостатки. Неразборчивый режим легко детектируется, один узел может четко определить, находится ли другой в неразборчивом режиме или нет. Так же, он работает только в не коммутируемой среде (например хаб, или маршрутизатор использующий APR). Еще одним недостатком является то, что в сетях с большим количеством трафика может не хватить системных ресурсов для захвата и анализа всех пакетов.


Не все устройства предоставляют одни и те же заголовки канального уровня в прочитанных вами пакетах. Ethernet устройства, и некоторые не-Ethernet устройства, могут предоставить Ethernet заголовки, но другие типы устройств, например такие как замыкающие устройства в BSD и OS X, PPP-интерфейсы, и Wi-Fi-интерфейсы в режиме мониторинга — нет.


Вам нужно определить тип заголовков канального уровня, которые предоставляет устройство, и использовать для анализа содержимого пакетов. pcap_datalink() возвращает тип заголовков канального уровня. (Cм. список значений заголовков канального уровня. Возвращаемые значения — значения DHT_ в этом списке)


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


if (pcap_datalink(handle) != DLT_EN10MB) 
{
    fprintf(stderr, "Device %s doesn't provide Ethernet headers -not  supported\n", dev);
    return(2);
}

который сработает если устройство не поддерживает Ethernet — заголовки. Это может сработать для кода приведенного ниже, который использует заголовки Ethernet.


Фильтрация трафика


Часто мы заинтересованы в захвате только определенного типа трафика. Для примера — бывает такое, что единственное что мы хотим — это захватить трафик с порта 23(telnet) для поиска паролей. Или возможно мы хотим перехватить файл который был отправлен через порт 21(FTP). Может быть мы хотим захватить только DNS трафик (порт 53 UDP). Однако, бывают редкие случаи, когда мы просто хотим слепо захватывать весь интернет трафик. Давайте рассмотрим функции pcap_compile() и pcap_setfilter().


Процесс очень простой. После того, как мы вызвали pcap_open_live() и имеем работающую сессию сниффинга, мы можем применить наш фильтр. Вы спросите, почему просто не использовать обычные if/else if выражения? Две причины: первая — фильтр PCAP эффективнее, потому что он фильтрует непосредственно через BPF; соответственно нам нужно куда меньшее количество ресурсов, ведь драйвер BPF делает это напрямую. Вторая — это то, что фильтры PCAP просто проще.


Перед тем, как применить фильтр, мы должны скомпилировать его. Условие фильтра содержится в обычной строке (или массиве char). Синтаксис достаточно хорошо документирован на главной странице tcpdump.org; Я оставлю это вам на самостоятельное рассмотрение. Однако, мы будем использовать простые тестовые выражения, и, возможно, вы достаточно догадливы что бы самостоятельно вывести правила синтаксиса этих условий из приведенных примеров.


Что бы скомпилировать фильтр мы вызываем функцию pcap_compile(). Прототип определяет эту функцию как:


int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)

Первый аргумент — это наш дескриптор сессии (pcap_t* handle в нашем предыдущем примере). Следующий — это указатель на место, где мы будем хранить скомпилированную версию фильтра. Далее идет само выражение, в обычном строковом формате. После идет целое число, которое определяет, нужно ли оптимизировать выражения фильтра, или нет (0 — нет, 1 — да). Наконец, мы должны определить сетевую маску той сети, к которой мы применяем фильтр. Функция возвращает -1 при ошибке; все остальные значения означают успех.


После компиляции фильтра, время применить его. Вызовем pcap_setfilter(). Следуя нашему формату объяснения PCAP, мы должны рассмотреть прототип этой функции:


int pcap_setfilter(pcap_t *p, struct bpf_program *fp)

Это очень прямолинейно и просто. Первый аргумент — наш дескриптор сессии, второй — указатель на скомпилированную версию нашего фильтра (это должна быть та же переменная, что и в предыдущей функции pcap_compile()).


Возможно этот пример поможет вам понять лучше:


Пример задания, компиляции и применения PCAP фильтра
#include <pcap.h>
...
pcap_t *handle;  /* Дескриптор сесси */
char dev[] = "rl0";  /* Устройство для сниффинга */
char errbuf[PCAP_ERRBUF_SIZE]; /* Строка для хранения ошибок */
struct bpf_program fp;  /* Скомпилированный фильтр */
char filter_exp[] = "port 23"; /* Выражение фильтра */
bpf_u_int32 mask;  /* Сетевая маска устройства */
bpf_u_int32 net;  /* IP устройства */

if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
    fprintf(stderr, "Can't get netmask for device %s\n", dev);
    net = 0;
    mask = 0;
}

handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
    fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
    return(2);
}

if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
    fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
    return(2);
}

if (pcap_setfilter(handle, &fp) == -1) {
    fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
    return(2);
}

Эта программа настроена на сниффинг трафика который проходит через порт 23, в неразборчивом режиме, на устройстве rl0.


Мы можете заметить, что предыдущий пример содержит функцию, о которой мы еще не говорили. pcap_lookupnet() — это функция которая, получая имя устройства возвращает IPv4 сетевой номер и соответствующую сетевую маску (сетевой номер — это адрес IPv4 ANDed с сетевой маской, поэтому он содержит только сетевую часть адреса). Это существенно, потому что нам нужно знать сетевую маску для применения фильтра.


По моему опыту, этот фильтр не работает в некоторых ОС. В моей тестовой среде я обнаружил, что OpenBSD 2.9 c ядром по умолчанию поддерживает этот тип фильтра, но FreeBSD 4.3 с ядром по умолчанию — нет. Ваш опыт может отличаться.


Реальный сниффинг


На текущем этапе мы узнали как определить устройство, приготовить его для захвата трафика, и применить фильтры. Теперь время захватить несколько пакетов. Есть два основных способа захватывать пакеты. Мы можем просто захватить один пакет, или мы можем войти в цикл, который выполняется пока не будет захвачено n пакетов. Мы начнем с того, что покажем, как можно захватить один пакет, и после рассмотрим методы использования циклов. Взглянем на прототип pcap_next():


u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)

Первый аргумент — дескриптор сессии. Второй — указатель на структуру которая содержит общую информацию о пакете, конкретно — время в которое он был захвачен, длина пакета, и длина его определенной части (например, если он фрагментированный). pcap_next() возвращает u_char указатель на пакет, который описан в структуре. Мы поговорим о чтении пакетов позже.


Это демонстрация использования pcap_next() для захвата пакетов:


Захват одного пакета
#include <pcap.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    pcap_t *handle;   /* Дескриптор сессии */
    char *dev;   /* Устройсто для сниффинга */
    char errbuf[PCAP_ERRBUF_SIZE]; /* Строка для хранения ошибки */
    struct bpf_program fp;  /* Скомпилированный фильтр */
    char filter_exp[] = "port 23"; /* Выражение фильтра */
    bpf_u_int32 mask;  /* Сетевая маска */
    bpf_u_int32 net;  /* IP */
    struct pcap_pkthdr header; /* Заголовок который нам дает PCAP */
    const u_char *packet;  /* Пакет */

    /* Определение устройства */
    dev = pcap_lookupdev(errbuf);
    if (dev == NULL) 
    {
        fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
        return(2);
    }

    /* Определение свойств устройства */
    if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) 
    {
        fprintf(stderr, "Couldn't get netmask for device %s: %s\n", dev,   errbuf);
        net = 0;
        mask = 0;
    }

    /* Создание сессии в неразборчивом режиме */
    handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
    if (handle == NULL) 
    {
        fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
        return(2);
    }

 /* Компиляция и применения фильтра */
    if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) 
    {
        fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
        return(2);
    }

    if (pcap_setfilter(handle, &fp) == -1) 
    {
        fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
        return(2);
    }

    /* Захват пакета */
    packet = pcap_next(handle, &header);
    /* Вывод его длины */
    printf("Jacked a packet with length of [%d]\n", header.len);
    /* Закрытие сессии */
    pcap_close(handle);
    return(0);
}

Приложение захватывает трафик любого устройства, полученное через pcap_loockupdev(), помещая его в неразборчивый режим. Оно обнаруживает что пакет попадает в порт 23 (telnet) и сообщает пользователю размер пакета (в байтах). Опять же, программа включает в себя вызов pcap_close(), который мы обсудим позже (хотя он вполне понятен).


Второй способ захвата трафика — использование pcap_loop() или pcap_dispatch() (который в свою очередь сам использует pcap_loop()). Что бы понять использование этих двух функций, нам нужно понять идею функции обратного вызова.


Функция обратного вызова (callback function) не является чем то новым, это обычная вещь в большом количестве API. Концепция, которая стоит за функцией обратного вызова очень проста. Предположим, что у есть программа которая ждет события определенного рода. Просто для примера, предположим что программа ждет нажатие клавиши. Каждый раз, когда пользователь нажимает клавишу, моя программа вызовет функцию, что бы обработать это нажатие клавиши. Это и есть функция обратного вызова. Эти функции используются в PCAP, но вместо вызова их в момент нажатия клавиши, они вызываются тогда, когда PCAP захватывает пакет. Использовать функции обратного вызова можно только в pcap_loop() и pcap_dispatch() которые очень похожи в этом плане. Каждая из них вызывает функцию обратного вызова каждый раз, когда попадется пакет который проходит сквозь фильтр (если конечно фильтр есть. Если нет, то все пакеты, которые были захвачены вызовут функцию обратного вызова).


Прототип pcap_loop() приведен ниже:


int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)

Первый аргумент — дескриптор сессии. Дальше идет целое число, которое сообщает pcap_loop() количество пакетов, которые нужно захватить (отрицательное значение говорит о том, что цикл должен выполняться до возникновения ошибки). Третий аргумент — имя функции обратного вызова (только идентификатор, без параметров). Последний аргумент полезен в некоторых приложениях, но в большинстве случаев он просто устанавливается NULL. Предположим, что у нас есть аргументы, которые мы хотим передать функции обратного вызова, в дополнение к тем, которые передает ей pcap_loop(). Последний аргумент как раз то место, где мы это сделаем. Очевидно, вы должны привести их к u_char * типу, что бы убедится что вы получите верные результаты. Как мы увидим позже, PCAP использует некоторые интересные способы передачи информации в виде u_char *. После того, как мы покажем пример того, как PCAP делает это, будет очевидно как сделать это и в этом моменте. Если нет — обратитесь к справочному тексту по С, так как объяснения указателей находятся за рамками темы этого документа. pcap_dispatch() почти идентична в использовании. Единственное различие между pcap_dispatch() и pcap_loop() это то, что pcap_dispatch() будет обрабатывать только первую серию пакетов полученных из системы, тогда как pcap_loop() будет продолжать обработку пакетов или партий пакетов до тех пор пока счетчик не закончится. Для более глубокого обсуждения различий, смотрите официальную документацию PCAP.


Прежде чем мы приведем пример использования pcap_loop(), мы должны проверить формат нашей функции обратного вызова. Мы не можем самостоятельно определить прототип функции обратного вызова, иначе pcap_loop() не будет знать, как использовать ее. Так что мы должны использовать этот формат в качестве прототипа нашей функции обратного вызова:


void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);

Давайте разберем его более детально. Первое — функция должна иметь void тип. Это логично, потому что pcap_loop() в любом случае не знал бы, что делать с возвращаемым значением. Первый аргумент соответствует последнему аргументу pcap_loop(). Независимо от того, какое значение передается последним аргументом pcap_loop(), оно передается первому аргументу нашей функции обратного вызова. Второй аргумент — это PCAP заголовок, который содержит информацию о том, когда пакет был захвачен, насколько он большой, и так далее. Структура pcap_pkthdr определена в файле pcap.h как:


struct pcap_pkthdr {
    struct timeval ts; /* Время захвата */
    bpf_u_int32 caplen; /* Длина заголовка */
    bpf_u_int32 len; /* Длина пакета */
};

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


Но как можно использовать эту переменную (названную packet) в прототипе? Пакет содержит много атрибутов, так что, как можно предположить, это не строка, а набор структур (для примера, пакет TCP/IP содержит в себе Ethernet заголовок, IP заголовок, TCP заголовок, и наконец, данные). Этот u_char указатель указывает на сериализованную версию этих структур. Что бы начать использовать какую нибудь из них необходимо произвести некоторые интересные преобразования типов.


Первое, мы должны определить сами структуры, прежде чем мы сможем привести данные к ним. Следующая структура используется мной для чтения TCP/IP пакета из Ethernet.


Объявления структур Ethernet, IP, TCP
/* Ethernet адреса состоят из 6 байт */
#define ETHER_ADDR_LEN 6

 /* Заголовок Ethernet */
 struct sniff_ethernet {
    u_char ether_dhost[ETHER_ADDR_LEN]; /* Адрес назначения */
    u_char ether_shost[ETHER_ADDR_LEN]; /* Адрес источника */
    u_short ether_type; /* IP? ARP? RARP? и т.д. */
 };

 /* IP header */
 struct sniff_ip {
    u_char ip_vhl;  /* версия << 4 | длина заголовка >> 2 */
    u_char ip_tos;  /* тип службы */
    u_short ip_len;  /* общая длина */
    u_short ip_id;  /* идентефикатор */
    u_short ip_off;  /* поле фрагмента смещения */
    #define IP_RF 0x8000  /* reserved флаг фрагмента */
    #define IP_DF 0x4000  /* dont флаг фрагмента */
    #define IP_MF 0x2000  /* more флаг фрагмента */
    #define IP_OFFMASK 0x1fff /* маска для битов фрагмента */
    u_char ip_ttl;  /* время жизни */
    u_char ip_p;  /* протокол */
    u_short ip_sum;  /* контрольная сумма */
    struct in_addr ip_src,ip_dst; /* адрес источника и адрес назначения */
 };
 #define IP_HL(ip)  (((ip)->ip_vhl) & 0x0f)
 #define IP_V(ip)  (((ip)->ip_vhl) >> 4)

 /* TCP header */
 typedef u_int tcp_seq;

 struct sniff_tcp {
    u_short th_sport; /* порт источника */
    u_short th_dport; /* порт назначения */
    tcp_seq th_seq;  /* номер последовательности */
    tcp_seq th_ack;  /* номер подтверждения */
    u_char th_offx2; /* смещение данных, rsvd */
    #define TH_OFF(th) (((th)->th_offx2 & 0xf0) >> 4)
    u_char th_flags;
    #define TH_FIN 0x01
    #define TH_SYN 0x02
    #define TH_RST 0x04
    #define TH_PUSH 0x08
    #define TH_ACK 0x10
    #define TH_URG 0x20
    #define TH_ECE 0x40
    #define TH_CWR 0x80
    #define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)
    u_short th_win;  /* окно */
    u_short th_sum;  /* контрольная сумма */
    u_short th_urp;  /* экстренный указатель */
};

Так как в итоге это все относится к PCAP и нашему загадочному u_char указателю? Эти структуры определяют заголовки, которые предшествуют данным пакета. И как мы в итоге можем разбить пакет? Приготовьтесь увидеть одно из самых практичных использований указателей (для всех новичков в С которые думают что указатели бесполезны говорю: это не так).


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


/* Заголовки Ethernet всегда состоят из 14 байтов */
#define SIZE_ETHERNET 14

const struct sniff_ethernet *ethernet; /* Заголовок Ethernet */
const struct sniff_ip *ip; /* Заголовок IP */
const struct sniff_tcp *tcp; /* Заголовок TCP */
const char *payload; /* Данные пакета */

u_int size_ip;
u_int size_tcp;

И теперь мы делаем наше магическое преобразование типов:


ethernet = (struct sniff_ethernet*)(packet);
ip = (struct sniff_ip*)(packet + SIZE_ETHERNET);
size_ip = IP_HL(ip)*4;
if (size_ip < 20) {
    printf("   * Invalid IP header length: %u bytes\n", size_ip);
    return;
}
tcp = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip);
size_tcp = TH_OFF(tcp)*4;
if (size_tcp < 20) {
    printf("   * Invalid TCP header length: %u bytes\n", size_tcp);
    return;
}
payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);

Как это работает? Рассмотрим структуру пакета в памяти. u_char указатель — просто переменная содержащая адрес в памяти.


Ради простоты, давайте скажем, что адрес на который указывает этот указатель это Х. Тогда, если наши структуры просто находятся в линии, то первая из них — sniff_ethernet, будет расположена в памяти по адресу Х, так же мы можем легко найти адрес структуры после нее. Этот адрес — это Х плюс длина Ethernet заголовка, которая равна 14, или SIZE_ETHERNET.


Аналогично, если у нас есть адрес этого заголовка, то адрес структуры после него — это сам адрес плюс длина этого заголовка. Заголовок IP, в отличие от заголовка Ethernet, не имеет фиксированной длины. Его длина указывается как количество 4-байтовых слов по полю заголовка IP. Поскольку это количество 4-байтных слов, оно должно быть умножено на 4, что бы указать размер в байтах. Минимальная длина этого заголовка составляет 20 байтов.


TCP заголовок так же имеет вариативную длину, эта длина указывается как число 4-байтных слов, в поле "смещения данных" заголовка TCP, и его минимальная длина так же равна 20 байтам.


Итак, давайте сделаем диаграмму:


VARIABLE LOCATION(in bytes)
sniff_ethernet X
sniff_ip X + SIZE_ETHERNET
sniff_tcp X + SIZE_ETHERNET + {IP header length}
payload X + SIZE_ETHERNET + {IP header length} + {TCP header length}

sniff_ethernet структура, находясь в первой линии, просто находится по адресу Х. sniff_ip, которая следует прямо за sniff_ethernet, это адрес Х плюс такое количество байтов, которое занимает структура sniff_ethernet (14 байтов или SIZE_ETHERNET). sniff_tcp находится прямо после двух предыдущих структур, так что его локация это — X плюс размер Ethernet, и IP заголовок. (14 байтов, и 4 раза длина заголовка IP). Наконец, данные (для которых не существует определенной структуры) расположены после них всех.


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


Завершение


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


Тим Карстенс 2002. Все права защищены. Распространение и использование, с модификацией и без нее разрешены при соблюдении следующих условий:
Копия должна содержать вышеупомянутое уведомление об авторских правах и этот список условий:
Имя Тима Карстенса не может использоваться для одобрения или продвижения продуктов, полученных из этого документа, без специального предварительного письменного разрешения.

This document is Copyright 2002 Tim Carstens. All rights reserved. Redistribution and use, with or without modification, are permitted provided that the following conditions are met:
Redistribution must retain the above copyright notice and this list of conditions.
The name of Tim Carstens may not be used to endorse or promote products derived from this document without specific prior written permission.
/ Insert 'wh00t' for the BSD license here /

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


  1. DrZlodberg
    13.09.2017 15:43

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


    1. Lupus_Anay Автор
      13.09.2017 16:35

      Я читал, что детектирование возможно произвести, попробовав отправить пинг-запрос, но при этом установить в нем не правильный MAC адрес устройства, оставив IP верным. Тогда интерфейс который находится в неразборчивом режиме ответит на пинг, так как перехватывает все пакеты, минуя фильтрацию по MAC адресу. А если устройство в разборчивом — то не ответит.

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


  1. ilejn
    13.09.2017 16:55

    Хорошо бы отметить, что pcap обладает низкой производительностью. Индустриальные DPI решения ru.wikipedia.org/wiki/Deep_packet_inspection используют dpdk или что-то аналогичное, полностью забирая интерфейс у ядра операционной системы. Думаю, что их присутствие нельзя обнаружить в принципе.
    Про DPDK есть статьи на Хабре, например habrahabr.ru/company/selectel/blog/313150