Интерфейсы доступа к канальному уровню На рис. 2.1 были изображены основные программные интерфейсы, используемые для создания большинства современных сетевых приложений. Вместе с тем известны и широко применяются другие программные интерфейсы, использующиеся при создании специфических сетевых приложений, к которым можно с полным основанием отнести программы мониторинга сети и сетевого трафика на различных аппаратных и операционных платформах. Главные функциональные возможности этих программ основываются на их способности перехватывать пакеты физической среды и фильтровать их для извлечения необходимой информации. К примеру, многие модули протокола RARP на UNIX-системах представляют собой обычное приложение, которое имеет непосредственный доступ к канальному уровню. Подобного рода программные средства для получения приложением верхнего уровня доступа непосредственно к канальному уровню существуют на многих операционных платформах, здесь же мы уделяем внимание только UNIX-подобным и Windows-системам: 1. Пакетный фильтр BPF – BSD Packet Filter. Впервые он был описан в 1992 году, а в 1993 г. авторы Steven McCanne и Van Jacobson опубликовали статью "The BSD Packet Filter: A New Architecture forUser-level Packet Capture. Proceedings of the 1993 Winter USENIX Technical Conference (San Diego, CA, Jan. 1993), USENIX". BPF считается стандартом для систем 4.4BSD, BSD/386, NetBSD и FreeBSD, он исследует каждый пакет, проходящий через канальный уровень в обоих направлениях и может сопоставлять его с заданным пользователем фильтром. При условии удовлетворения критерию фильтрации копия пакета канального уровня помещается в выделенный ядром буфер, который ассоциируется с данным фильтром. На базе BPF функционирует наиболее известная и популярная программа сбора сетевой статистики в UNIX-системах – tcpdump. Следует также добавить, что из всех предлагаемых в обзоре методов доступа лишь BPF обладает возможностью напрямую слать пакеты непосредственно в канал связи. 2. Интерфейс DLPI – Data Link Provider Interface – обеспечивает доступ к канальному уровню для системы SVR4 и производных от нее. Этот интерфейс был разработан компанией AT&T и у разработчиков приложений под UNIX-платформы не так широко распространен. 3. Linux-системы. Для получения информации с канального уровня используют специфичный тип связи, задаваемый с помощью сокетного интерфейса указанием стиля связи SOCK_PACKET при создании сокета, например: sd=socket(AF_UNIX, SOCK_PACKET, htons(ETH_P_ALL)); или sd=socket(PF_PACKET, SOCK_RAW, htons (ETH_P_ALL)); Указание типов получаемых с канального уровня пакетов для среды Ethernet задается в поле параметра протокола с помощью констант вида ETH_P_XXX, где XXX могут быть – IP, IPV6, ARP или ALL – все. Так как этот метод доступа не реализует фильтрацию пакетов, то объем получаемой информации может быть очень значительным. Отсеять лишний трафик можно, используя средства Linux Socket Filter (LSF). 4. Windows-системы. Для них на базе драйвера NDIS была разработана программа доступа к канальному уровню WinDump, представляющая собой порт известного UNIXприложения tcpdump. WinDump использует архитектуру WinPcap (см. ниже более подробное ее описание) для захвата пакетов и анализа сети в Windows-системах (win32). Пакетный фильтр является драйвером устройства, который добавляет в Windows 9x, Windows МЕ, Windows NT и Windows 2000 возможность захватывать и посылать данные напрямую с сетевой карты, фильтровать и сохранять в буфере захваченные пакеты. С помощью WinDump получен к примеру лог-файл Интернет-сессии в приложении 4. Для обеспечения возможности просмотра содержания всех сетевых фреймов, циркулирующих по каналу связи, сетевой адаптер должен быть переведен в режим приема всех фреймов – promiscuous mode ("неразборчивый" режим). П р им еч а н ие Следует различать "просто" возможность получения пакетов канального уровня (LINUX) и возможность их получения при прямом доступе к канальному уровню (tcpdump, WinDump). Интерфейс DLPI в принципе обеспечивает оба варианта. На рис. 5.1 отображена архитектура некоторых наиболее распространенных методов доступа к канальному уровню. Рис.5.1 Интерфейсы доступа к информации канального уровня Основные особенности BPF можно сформулировать следующим образом: 1. BPF назначает фильтр и два буфера (на рисунке они объединены) на каждый процесс, запросивший его функции. Фильтр создается приложением и передается в BPF путем вызова функций ioctl(). Оба буфера ( их размер обычно 4 кб) постоянно размещены в памяти BPF. Первый буфер (store buffer – буфер сохранения) используется для приема данных от сетевого адаптера, второй (hold buffer – буфер задержки) используется для копирования пакетов, направляемых приложению. Буфера обслуживаются "попеременно", в зависимости от ситуации – например, из адаптера нет никакой информации, можно передавать в приложение. 2. Когда очередной пакет поступает на вход сетевого интерфейса, драйвер устройства канального уровня сразу передает его наверх системному стеку протоколов. Однако если установлен BPF, то драйвер сначала вызывает функцию сетевой ловушки (hook), которая передает входящий пакет каждому из созданных приложениями фильтров в соответствии с параметром, называемым "длина захвата – capture length". Например, для tcpdump значение длины захвата по умолчанию равно 68 байт, что эквивалентно 14 байт Ethernetheader +20 байт IP-header + 20 байт TCP-header + 14 байт данных. Если пакет соответствует параметру фильтрации, ловушка копирует заданное фильтром число байт из памяти драйвера в BPF-буфер сохранения, после чего драйвер сетевого интерфейса продолжает обработку этого пакета для обычной передачи в стек протоколов операционной системы. Надо иметь в виду, что соответствующий условиям фильтрации входящий пакет игнорируется фильтром в том случае, если буфер сохранения заполнен, а буфер задержки недоступен (т.е. приложение считывает из него данные), в связи с чем возможна потеря некоторых пакетов, что часто бывает поводом для вопросов на форумах Usenet. Одной из важных компонентов большинства рассмотренных методов доступа к канальной информации в среде UNIX-подобных ОС является специальная свободно-распространяемой библиотека функций для захвата сетевых пакетов под названием libcap или libpcap (packet capture library.) С помощью библиотеки захвата приложение взаимодействует с драйвером BPF. Для получения принятых пакетов из буфера задержки приложение вызывает системную функцию считывания. Если буфер задержки заполнен (или истекло время ожидания), BPF копирует содержимое буфера в память приложения и передает ему управление. Данная библиотека обеспечивает приложения независимостью от технологии построения сети (Ethernet, FDDI и т.д.), что способствует переносимости данной библиотеки на различные программно-аппаратные платформы. На основе этой библиотеки разработан Windows-клон программы tcpdump, WinDump и Windows-клон UNIX-библиотеки WinPcap, (ознакомиться с ее подробным содержанием можно по адресу http://www.cherepovetscity.ru/insecure/reading/papers/libpcap.htm.) В библиотеке libpcap насчитывается около 25-ти различных функций, обеспечивающих взаимодействие проложения верхнего уровня с BPF. Подробное описание всех функций и их применения в контексте описываемой задачи выходит за рамки этой книги. Вторая архитектура захвата сетевых пакетов, отраженная на рисунке 5.1 в применении к программе WinDump – это WinPCAP. Прежде чем ее рассматривать, надо описать основные особенности ее прототипа – архитектуры PCAP (Packet Capture – захват пакетов), базирующейся на спецификации NDIS (Network Driver Interface Specification – спецификация интерфейса сетевого драйвера) – набора правил организации обмена данными между драйвером сетевого адаптера и драйверами протоколов сетевого уровня - IP, IPX и т.д. Основное назначение NDIS состоит в том, чтобы предоставить возможность драйверу протоколов принимать и передавать пакеты по сети независимо от используемой операционной платформы. Данная архитектура полностью совместима с архитектурой BPF и позволяет фильтровать пакеты аналогично BPF, но на уровне пользователя. Программно архитектура PCAP (и соответственно WinPCAP) была реализована в виде NDISдрайвера, имеющего некоторые отличия от BPF: процесс фильтрации проходит на уровне пользователя, поэтому каждый входящий пакет копируется из ядра в буфер приложения еще до его фильтрации. При этом происходит существенная потеря процессорного времени и оперативной памяти, поскольку в память приложения копируются все пакеты, в том числе те, которые не нужны данному приложению. в ядре ОС для хранения и передачи пакетов отсутствует буферизация, по сравнению с BPF. А так как в ОС с разделением времени приложение вынуждено делить процессорное время с другими программами, в том числе и с высокоприоритетными системными сервисами, при этом возможны ситуации, когда приложение не будет активно в момент прихода очередного пакета или приложение занято выполнением других задач. В отсутствии буфера в ядре ОС возникновение этих ситуаций приведет к потере пакета. Решение этих проблем было найдено в виде использования для фильтрации и буферизации не приложения, а специального драйвера захвата пакетов (packet capture driver), управление которым осуществляется при помощи все той же библиотеки libpcap, что обеспечивает программную совместимость двух архитектур. Отсутствие возможности низкоуровневого управления сетевым интерфейсом в семействе ОС Windows привело к возникновению архитектуры WinPCAP, адаптировавшей существующую архитектуру PCAP для Windows. Версия PCAP для Win32 также основана на драйвере захвата пакетов, структура и принцип действия которого аналогичен его предшественнику для UNIX. В соответствии с обозначениями, принятыми в оригинальной версии libpcap, исходный код для взаимодействия с драйвером находится в наборе файлов pcap-xxx.c (и соответствующий ему заголовочный файл pcap-xxx.h), где xxx – указывает на операционную систему (например, pcaplinux.c), и к уже существующим файлам были добавлены pcap-win32.c и pcap-win32.h. Основному изменению подвергся принцип взаимодействия приложения верхнего уровня с драйвером захвата пакетов. Libpcap для Win32 взаимодействует с аппаратным обеспечением через интерфейс, поставляемый динамической библиотекой packet.dll. В который раз вспоминаем, что в "UNIX- все – файл", и сетевой адаптер или модем трактуются системой как стандартный файл, в результате чего в UNIX можно использовать известный нам системный вызов select(), чтобы узнать, поступил ли пакет на вход сетевого адаптера, а в Windows select() так применить нельзя. Принципиально Windows-программист может использовать функции libpcap для обеспечения работоспособности исходного кода приложения, но при этом возможности приложения будут ограничены (например, libpcap не позволяет отправлять пакеты через сетевой интерфейс). Использование функций packet.dll для Win32 делают возможности приложения практически неограниченными. Для обеспечения максимальной совместимости при компиляции исходного кода libpcap на различных ОС часть кода, предназначенная для Windows, принято отделять от остального кода директивами #ifdef и #ifndef, что позволяет компилировать исходный код libpcap как на Windows, так и на UNIX, например: #ifdef WIN32 /* исходный код для Windows */ #endif Архитектура WinPCAP дополняет стандартные функции Win32 API возможностью принимать и передавать данные по сети, минуя стек протоколов операционной системы и взаимодействуя непосредственно с сетевым адаптером хоста. Таким образом, WinPCAP состоит из трех основных компонентов: драйвера устройства захвата пакетов (paсket.vxd), низкоуровневой динамической библиотеки (packet.dll) и системно-независимой библиотеки wpcap.dll (основанной на libpcap версии 0.5). В качестве примера рассмотрим программирование сетевого анализатора (сниффера) для платформы Linux. Текст программы с комментариями любезно предоставлен автором, Владимиром Мешковым. Анализатор будет функционировать по следующему алгоритму: a. после запуска на выполнение определяются параметры сетевого интерфейса eth0, такие как IP адрес, маска подсети, размер MTU, индекс, и интерфейс переводится в неразборчивый режим (promiscuous mode). В этом режиме интерфейс принимает все пакеты, циркулирующие в сети, даже если они не адресованы данному хосту; b. создается пакетный сокет и выполняется его привязка к выбранному сетевому интерфейсу (eth0). Далее анализатор в бесконечном цикле выполняет прием сетевых пакетов и отображает данные об этом пакете - MAC-адреса и IP-адреса отправителя и получателя, размер пакета, размер IP заголовка, тип транспортного протокола (TCP/UDP), порт отправителя и получателя. Выход из цикла осуществляется по приходу сигнала SIGINT (генерируется комбинацией клавиш Ctrl+C); c. получив сигнал SIGINT, анализатор прерывает цикл приема пакетов, снимает флаг неразборчивого режима с сетевого интерфейса и завершает выполнение. Ниже следует описание функций, применяемых в программном коде. Функция getifconf(). Будет определять параметры сетевого интерфейса и переключать его режимы. Ее прототип: int getifconf(__u8 *, struct ifparam *, int); Функция принимает три параметра: указатель на строку, содержащую символьное имя сетевого интерфейса; указатель на структуру, в которой будут сохранены параметры сетевого интерфейса. флаг, определяющий режим работы интерфейса. Функция getsock_recv(). Будет создавать пакетный сокет. int getsock_recv (int); Единственным параметром функции является индекс сетевого интерфейса, к которому будет привязан сокет. Определение структуры struct ifparam, в которой будут храниться параметры сетевого интерфейса, разместим в файле analizator.h. Листинг 5.4. Заголовочный файл analizator.h #include <linux/types.h> #define PROMISC_MODE_ON 1 // Флаг включения неразборчивый режим #define PROMISC_MODE_OFF 0 // Флаг выключения неразборчивого режима struct ifparam { __u32 ip; // IP адрес __u32 mask; // Маска подсети int mtu; // Размер MTU int index; // Индекс интерфейса } ifp; Рассмотрим подробно каждую функцию. Листинг 5.5. Функция определения параметров сетевого интерфейса и переключения режимов (файл getifconf.c) #include <linux/socket.h> #include <linux/ioctl.h> #include <linux/if.h> #include <linux/in.h> #include "analizator.h" int getifconf(__u8 *intf, struct ifparam *ifp, int mode) { int fd; struct sockaddr_in s; // Структура ifreq описана в <linux/if.h> содержит текущие параметры и настройки // сетевого интерфейса хоста. struct ifreq ifr; memset((void *)&ifr, 0, sizeof(struct ifreq)); if((fd = socket(AF_INET,SOCK_DGRAM,0)) < 0)return (-1); sprintf(ifr.ifr_name,"%s",intf); /* Проверяем флаг режима. Если он установлен в 0, неразборчивый режим * необходимо отключить, поэтому сразу выполняется переход на метку setmode. */ if(!mode) goto setmode; /* Определяем IP адрес сетевого интерфейса */ if(ioctl(fd, SIOCGIFADDR, &ifr) < 0) { perror("ioctl SIOCGIFADDR"); return -1; } memset((void *)&s, 0, sizeof(struct sockaddr_in)); memcpy((void *)&s, (void *)&ifr.ifr_addr, sizeof(struct sockaddr)); memcpy((void *)&ifp->ip, (void *)&s.sin_addr.s_addr, sizeof(__u32)); /*Определяем маску подсети */ if(ioctl(fd, SIOCGIFNETMASK, &ifr) < 0) { perror("ioctl SIOCGIFNETMASK"); return -1; } memset((void *)&s, 0, sizeof(struct sockaddr_in)); memcpy((void *)&s, (void *)&ifr.ifr_netmask, sizeof(struct sockaddr)); memcpy((void *)&ifp->mask, (void *)&s.sin_addr.s_addr, sizeof(u_long)); /* * Определяем размер MTU */ if(ioctl(fd, SIOCGIFMTU, &ifr) < 0) { perror("ioctl SIOCGIFMTU"); return -1; } ifp->mtu = ifr.ifr_mtu; /* * Индекс интерфейса */ if(ioctl(fd, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl SIOCGIFINDEX"); return -1; } ifp->index = ifr.ifr_ifindex; /* Устанавливаем заданный режим работы сетевого интерфейса */ setmode: /* Получаем значение флагов */ if(ioctl(fd, SIOCGIFFLAGS, &ifr) < 0) { perror("ioctl SIOCGIFFLAGS"); close(fd); //В случае неудачи закрываем сокет return -1; } /* В зависимости от значения третьего параметра функции, устанавливаем или * снимаем флаг неразборчивого режима */ if(mode) ifr.ifr_flags |= IFF_PROMISC; else ifr.ifr_flags &= ~(IFF_PROMISC); /*Устанавливаем новое значение флагов интерфейса */ if(ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) { perror("ioctl SIOCSIFFLAGS"); close(fd); //В случае неудачи закрываем сокет return (-1); } return 0; } Значения команд ioctl() SIOCGIFADDR, SIOCGIFNETMASK, SIOCGIFMTU и др. определены в файле <linux/sockios.h>. Этот файл не включен в список заголовочных файлов, т.к. он уже определен в <linux/socket.h>. Листинг 5.6 Функция создания пакетного сокета (файл getsock_recv.c) #include <linux/socket.h> #include <linux/types.h> #include <linux/if_ether.h> #include <linux/if_packet.h> int getsock_recv(int index) { int sd; // Дескриптор сокета /* При работе с пакетными сокетами для хранения адресной информации сетевого * интерфейса вместо структуры sockaddr_in используется структура sockaddr_ll * (см. <linux/if_packet.h>) */ struct sockaddr_ll s_ll; /* Cоздаем пакетный сокет. Т.к. MAC-адреса мы тоже собираемся обрабатывать, * параметр type системного вызова socket принимает значение SOCK_RAW */ sd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if(sd < 0) return -1; memset((void *)&s_ll, 0, sizeof(struct sockaddr_ll)); /* Заполним поля адресной структуры s_ll */ s_ll.sll_family = PF_PACKET; // Тип сокета s_ll.sll_protocol = htons(ETH_P_ALL); // Тип принимаемого протокола s_ll.sll_ifindex = index; // Индекс сетевого интерфейса /* Привязываем сокет к сетевому интерфейсу. Если на хосте активен только один сетевой интерфейс, * делать это не обязательно. При наличии двух и более сетевых плат пакеты будут приниматься * сразу со всех активных интерфейсов, и если нас интересуют пакеты только из одного сегмента * сети, целесообразно выполнить привязку сокета к нужному интерфейсу */ if(bind(sd, (struct sockaddr *)&s_ll, sizeof(struct sockaddr_ll)) < 0) { close(sd); return -1; } return sd; } Цикл приема сетевых пакетов и отображение результатов будет выполняться в главной функции main(). П р им еч а н ие Привязку сокета к сетевому интерфейсу eth0 можно произвести и с помощью функции setsockopt(), например: rc = setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, "eth0\x00", strlen("eth0\x00")+1); Обратите внимание, имя устройства необходимо завершать нулевым символом. Листинг 5.7 Главная функция (файл analizator.c) #include <stdio.h> #include <signal.h> #include <sys/socket.h> #include <linux/if_ether.h> #include <linux/in.h> #include <linux/ip.h> #include <linux/tcp.h> #include "analizator.h" /* * В буфере buff будут сохранятся принятые сетевые пакеты. Значение ETH_FRAME_LEN равно * максимальной длине кадра Ethernet (1514) и определено в <linux/if_ether.h> */ __u8 buff[ETH_FRAME_LEN]; /* * Функция mode_off заменяет стандартный обработчик сигнала SIGINT. Задача этой функции - по * приходу сигнала SIGINT вывести интерфейс из состояния "Promiscuous mode" в обычный режим. */ void mode_off() { if(getifconf("eth0", &ifp, PROMISC_MODE_OFF) < 0) { perror("getifconf"); exit(-1); } return; } /* Главная функция */ int main() { __u32 num = 0; int eth0_if, rec = 0, ihl = 0; struct iphdr ip; // Структура для хранения IP заголовка пакета struct tcphdr tcp; // TCP заголовок struct ethhdr eth; // Заголовок Ethernet-кадра static struct sigaction act; /* Получаем параметры сетевого интерфейса eth0 и переводим его в неразборчивый режим */ if(getifconf("eth0", &ifp, PROMISC_MODE_ON) < 0) { perror("getifconf"); return -1; } /* Отобразим полученные параметры сетевого интерфейса */ printf("IP адрес - %s\n",inet_ntoa(ifp.ip)); printf("Маска подсети - %s\n",inet_ntoa(ifp.mask)); printf("MTU - %d\n", ifp.mtu); printf("Индекс - %d\n", ifp.index); /* Получим дескриптор пакетного сокета */ if((eth0_if = getsock_recv(ifp.index)) < 0) { perror("getsock_recv"); return -1; } /* Определим новый обработчик сигнала SIGINT - функцию mode_off */ act.sa_handler = mode_off; sigfillset(&(act.sa_mask)); sigaction(SIGINT, &act, NULL); /* Запускаем бесконечный цикл приема пакетов */ for(;;) { memset(buff, 0, ETH_FRAME_LEN); rec = recvfrom(eth0_if, (char *)buff, ifp.mtu + 18, 0, NULL, NULL); if(rec < 0 || rec > ETH_FRAME_LEN) { perror("recvfrom"); return -1; } memcpy((void *)&eth, buff, ETH_HLEN); memcpy((void *)&ip, buff + ETH_HLEN, sizeof(struct iphdr)); if((ip.version) != 4) continue; memcpy((void *)&tcp, buff + ETH_HLEN + ip.ihl * 4, sizeof(struct tcphdr)); /* Выводим MAC-адреса отправителя и получателя */ printf("\n%u\n", num++); printf("%.2x:%.2x:%.2x:%.2x:%.2x:%.2x\t->\t", eth.h_source[0],eth.h_source[1],eth.h_source[2], eth.h_source[3],eth.h_source[4],eth.h_source[5]); printf("%.2x:%.2x:%.2x:%.2x:%.2x:%.2x\n", eth.h_dest[0],eth.h_dest[1],eth.h_dest[2], eth.h_dest[3],eth.h_dest[4],eth.h_dest[5]); printf("Длина заголовка - %d, ", (ip.ihl * 4)); printf("Длина пакета - %d\n", ntohs(ip.tot_len)); /*Если транспортный протокол - TCP, отобразим IP адреса и порты получателя и отправителя */ if(ip.protocol == IPPROTO_TCP) { printf("%s (%d)\t->\t",inet_ntoa(ip.saddr), ntohs(tcp.source)); printf("%s (%d)\n",inet_ntoa(ip.daddr), ntohs(tcp.dest)); printf("TCP пакет\n"); } } return 0; } После запуска программа переводит интерфейс в неразборчивый режим и запускает бесконечный цикл прослушивания сетевого трафика и отображения результатов. Факт включения неразборчивого режима интерфейса будет зафиксирован в файле /var/log/messages: eth0: Promiscuous mode enabled. device eth0 entered promiscuous mode Бесконечный цикл работы анализатора будет прерван после нажатия комбинации клавиш Ctrl+C. Программа получит сигнал SIGINT, и обработчик этого сигнала функция mode_off() снимет флаг неразборчивого режима с интерфейса. При этом в файле /var/log/messages появится запись: device eth0 left promiscuous mode