(Часть 2)
В первой части рассмотрены общие методы построения программ обработки
сетевых пакетов в ОС Windows.
В этой части более детально описаны функции и процедуры библиотеки захвата
пакетов (libpcap), предоставляющей приложению пользователя интерфейс
высокого уровня для взаимодействия с драйвером захвата пакетов. Программе,
использующей архитектуру pcap, становятся доступны все пакеты,
принимаемые сетевой платой.
Для обеспечения целостности восприятия материала сначала приведем
краткое описание используемых функций.
·
PCAP_T
pcap_open_live (char *device, int
snaplen, boolean promisc, int to_ms, char *ebuf)
Функция предназначена для получения дескриптора структуры захвата пакетов, используемой для записи и просмотра пакетов, передаваемых по сети (режим он-лайн). Device – это строка, задающая открываемый сетевой адаптер. Переменная Snaplen задает максимальное число захватываемых байт. Флаг Promisc переводит адаптер в режим работы “прием всех входящих пакетов”. Переменная to_ms содержит время ожидания пакета в миллисекундах. При возникновении ошибки функция возвращает значение NULL и записывает строку, характеризующую ошибку, в буфер ebuf.
·
PCAP_T pcap_open_offline
(char *fname, char *ebuf)
Функция предназначена для получения дескриптора
структуры захвата пакетов, используемой для записи и просмотра пакетов, сохраненных в файле (режим офф-лайн).
Имя файла указывается в переменной fname.
Если в качестве имени файла указан символ ‘-‘, информация будет считываться из
устройства, определяемого системной переменной stdin.
·
PCAP_DUMPER_T pcap_dump_open
(pcap_t *p, char *fname)
Функция открывает файл, имя которого указано в
переменной fname, для записи всех принятых
пакетов (дампа). Имя файла ‘-‘ означает запись в stdout. Переменная p – это указатель на
дескриптор структуры pcap,
полученный при помощи функции pcap_open_live(). Функция возвращает дескриптор
открытого для записи файла. При возникновении ошибки функция возвращает
значение NULL, при этом описание ошибки может быть получено при
помощи pcap_geterr().
·
CHAR pcap_lookupdev
(char *errbuf)
Функция возвращает указатель на сетевое
устройство, которое возможно использовать
совместно с pcap_open_live() и pcap_lookupnet().
·
INT pcap_lookupnet
(char *device, bpf_u_int32 *netp, bpf_u_int32 *maskp,
char *ebuff)
Функция используется для определения сетевого адреса
и сетевой маски устройства, заданного в переменной device. Эти данные записываются соответственно в
переменные netp и maskp. При возникновении ошибки
функция возвращает значение “–1”.
·
INT pcap_dispatch
(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
Функция используется для сбора и обработки принятых
пакетов. Аргумент cnt задает максимальное число
принимаемых и обрабатываемых пакетов за один сеанс работы подпрограммы.
Значение cnt = -1 означает, обрабатываются все пакеты, принятые
одним буфером. Значение cnt = 0 означает обработку всех
пакетов до возникновения ошибки, достижения конца файла (при работе в режиме
офф-лайн) или до окончания времени ожидания пакета (при приеме пакетов из сети
и ненулевом значении to_ms). Переменная callback задает подпрограмму, вызываемую с тремя аргументами:
указатель u_char user, передаваемый функцией pcap_dispatch(), указатель на структуру pcap_pkthdr,
содержащую сетевые заголовки и данные принятых пакетов, и указатель u_char на
данные этих пакетов. Функция возвращает число считанных пакетов. При достижении
конца файла функция возвращает нулевое значение, значение функции “–1” означает
возникновение ошибки. В последнем случае описание ошибки может быть получено
при помощи функций pcap_geterr() или pcap_perr().
·
VOID pcap_dump (u_char
*user, struct pcap_pkthdr *h, u_char *sp)
Функция записывает принятые пакеты в файл, открытый
функцией pcap_dump_open().
Аргументы функции позволяют использовать ее совместно с pcap_dispatch(). Формат записываемых в файл
данных аналогичен формату программы TCPDUMP и ее аналога WinDump,
включенной в состав дистрибутива WinPCap (см. руководство
пользователя по данной программе).
·
INT pcap_compile
(pcap_t *p, struct bpf_program *fp, char *str, int optimize,
bpf_u_int32 *netmask)
Функция компилирует текстовое описание фильтра str в псевдокод (см. далее). Переменная fp содержит указатель на структуру bpf_program и заполняется функцией pcap_compile(). Переменная optimize определяет наличие оптимизации псевдокода
компилируемой программы. Переменная netmask задает сетевую маску локальной
сети.
·
INT pcap_setfilter
(pcap_t *p, struct bpf_program *fp)
Функция устанавливает программу фильтра для
вызвавшего ее приложения. Переменная fp содержит указатель на
программу фильтра, полученный при вызове функции pcap_compile().
·
INT pcap_loop (pcap_t
*p, int cnt, pcap_handler callback,
u_char *user)
Действия данной функции аналогичны pcap_dispatch(), за тем исключением, что
она считывает пакеты до тех пор, пока не обнулится счетчик cnt или не возникнет ошибка, и не прекращает работы при окончании времени ожидания. Отрицательное
значение cnt заставит функцию работать бесконечно, до возникновения
первой ошибки.
·
U_CHAR pcap_next
(pcap_t *p, struct bpf_program *fp)
Функция возвращает указатель на следующий принятый
пакет.
·
INT pcap_datalink
(pcap_t *p)
Функция возвращает имя устройства сетевого уровня,
например DLT_EN10MB.
·
INT pcap_snapshot
(pcap_t *p)
Функция используется для получения «снимка», т.е.
для захвата определенного количества пакетов. Длина «снимка» указывается при
вызове функции pcap_open_live.
·
INT pcap_is_swapped
(pcap_t *p)
Функция возвращает значение TRUE, если открытый для записи
файл имеет порядок байт, не совпадающий с используемым в операционной системе.
·
INT
pcap_major_version (pcap_t *p)
Функция возвращает старшее число номера версии PCAP,
записываемого в файл.
·
INT
pcap_minor_version (pcap_t *p)
Функция возвращает младшее число номера версии PCAP,
записываемого в файл.
·
FILE *pcap_file
(pcap_t *p)
Функция возвращает имя открытого для записи файла.
·
INT pcap_stats (pcap_t
*p, struct pcap_stat *ps)
Функция возвращает 0 и заполняет структуру pcap_stat значениями, которые несут
различную статистическую информацию о входящих пакетах с момента запуска
процесса до момента вызова этой функции. При возникновении какой-либо ошибки, а
также в случае, когда используемый драйвер не поддерживает статистический
режим, функция возвращает значение “–1”. При этом код описание ошибки можно
получить с помощью функций pcap_perror() или pcap_geterr().
·
INT pcap_fileno
(pcap_t *p)
Функция возвращает дескриптор открытого для записи
файла.
·
VOID
pcap_perror (pcap_t *p, char *prefix)
Функция выводит текст последней возникшей ошибки
библиотеки PCAP на устройстве stderr с префиксом, определяемым переменной prefix.
·
CHAR
*pcap_geterr (pcap_t *p)
Функция возвращает строку с описанием последней
ошибки библиотеки PCAP.
·
CHAR
*pcap_strerror (int error)
Функция используется в том случае, когда strerror по каким-либо причинам недоступно.
·
VOID pcap_close
(pcap_t *p)
Функция закрывает файл, связанный с адаптером p, и высвобождает занимаемые
библиотекой ресурсы.
·
VOID
pcap_dump_close (pcap_dumper_t *p)
Функция закрывает открытый для записи файл.
·
INT pcap_setbuff (pcap_t *p, int dim)
Функция устанавливает размер буфера задержки и
буфера сохранения, связанных с адаптером p, равным значению, указанному в переменной dim. При этом оба старых буфера уничтожаются, и
информация в них теряется. В случае успешного выполнения операции функция
возвращает значение “0”, иначе “–1”. Первоначально оба буфера создаются при
вызове функции pcap_open_live(), по умолчанию размер буфера
задержки и буфера сохранения равен 1 Мб.
·
INT pcap_setmode (pcap_t *p, int mode)
Функция устанавливает режим работы адаптера p в соответствии с заданным в
переменной mode. Допустимые значения
переменной mode – это “0” (режим захвата
пакетов) и “1” (статистический режим). Если адаптер находится в статистическом
режиме, сетевая ловушка, заданная при помощи функций pcap_dispatch() или pcap_loop(),
вызывается драйвером каждые to_ms миллисекунд (параметр
задается при вызове функции pcap_open_live()) и передает библиотеке
два 64-битных числа, содержащих число пакетов и полное количество байт,
удовлетворяющих условиям фильтра.
Для создания фильтра, необходимо составить его текстовое описание и затем использовать текстовую строку с параметрами фильтра в качестве одного из аргументов функции pcap_compile(). Текстовое описание фильтра обусловливается правилами, принятыми для описания фильтра в программе TCPDUMP и ее Win32-версии WinDUMP, включенную в состав дистрибутива WinPCap. Рассмотрим способы задания программы-фильтра.
В текстовом виде фильтр выглядит как выражение, состоящее из одного или нескольких примитивов. Примитивы в выражении определяют возможность приема фильтром входящего пакета. Каждый примитив определяет конкретный элемент пакета протокола стандартной структуры и его значение, сравниваемое фильтром со значением соответствующего элемента входящего пакета. Если значение примитива совпадает со значением элемента пакета, фильтр отмечает его как «логическую истину» (True) и переходит к сравнению следующего примитива. При совпадении всех значений выражения со значениями проверенных элементов пакета фильтр принимает решение о приеме данного пакета, в противном случае входящий пакет игнорируется.
Каждый примитив обычно состоит из одного или нескольких квалификаторов и следующего за ними идентификатора (имя или число). Всего имеется три типа квалификаторов:
type определяет тип имени или номера идентификатора. Возможные значения: host (хост), net (сеть), port (порт) или proto (протокол). Например: ‘host foo’, ‘net 128.3’, ‘port 20’. Если квалификатор отсутствует, по умолчанию принимается host.
dir определяет возможное направление приема и передачи данных объектом, указанным в качестве идентификатора: к нему и/или от него. Допускается указание следующих значений: src (источник), dst (приемник), src and dst (источник и приемник), src or dst (источник или приемник). Например: ‘src host foo’, ‘dst net 128.3’, ‘src or dst port ftp-data’. Если квалификатор не указан, по умолчанию принимается src or dst.
proto определяет тип протокола, используемого объектом, указанным в качестве идентификатора. Возможные значения: ether, fddi, ip, arp, rarp, decnet, lat, sca, moprc, mopdl, tcp и udp. Например: ‘ether src foo’, ‘arp net 128.3’, ‘tcp port 21’. При отсутствии квалификатора значение по умолчанию выбирается по максимальному соответствию указанному идентификатору. Например, ‘src foo’ означает ‘(ip, arp или rarp) src foo’, ‘net bar’ означает ‘(ip, arp или rarp) net bar’, ‘port 53’ означает ‘(tcp или udp) port 53’.
Общие выражения строятся путем
объединения примитивов при помощи логических операторов and (&&),
or (||) и not (!). Например: ‘host foo and not port ftp and not
port ftp-data’. В целях экономии занимаемого строкой объема памяти
одинаковые квалификаторы могут не указываться. Например, ‘tcp dst port ftp or ftp-data or domain’ означает
то же самое, что и ‘tcp dst
port ftp or tcp dst port ftp-data or tcp dst port domain’.
Родственные группы примитивов могут объединяться с помощью тех же логических операторов. Например, tcp and (port ftp or ftp-data). Однако при объединении следует учесть, что операция отрицания имеет высший приоритет, а операции «И» и «ИЛИ» имеют одинаковый приоритет.
Ниже перечислены примеры примитивов, которые можно использовать при построении выражений фильтра:
dst host хост True, если поле IP-пакета «адрес приемника» совпадает со значением идентификатора host (может указываться имя или адрес хоста).
ether host хост True, если поле «источник» или «приемник» ethernet-кадра совпадает со значением идентификатора host (при этом имя или адрес хоста указывается в формате, определяемом квалификатором, в данном случае – ethernet).
ip proto протокол True, если IP-пакет соответствует указанному типу протокола (может указываться номер или имя соответствующего протокола: icmp, igrp, udp, nd или tcp).
При отборе пакетов определенного протокола имя протокола можно указывать без соответствующих квалификаторов ether или ip. Например, вместо ether proto arp можно указать arp, или вместо ip proto tcp можно указать tcp.
В дополнение к перечисленному
выше, имеется еще несколько специальных примитивов, состоящих из одного
ключевого слова:
gateway (шлюз) Используется следующим образом: gateway хост. Эквивалентное выражение для данного примитива выглядит так: ether host хост and not host хост. Фактически это означает, что хост является шлюзом, поскольку ethernet-адрес в пакете совпадает с адресом хост, однако в IP-заголовке адрес хост отсутствует.
mask (маска) Используется совместно с квалификатором net для задания сетевой маски. Например, net сеть mask маска означает отбор пакетов, IP-адрес которых удовлетворяет заданной маске.
broadcast (широковещательный адрес), multicast (групповой адрес) Используется совместно с квалификаторами ip и ehter для определения широковещательных и групповых пакетов. Например: ether broadcast, ip multicast.
less (меньше), greater (больше) Используется для отбора пакетов определенной длины. В качестве идентификатора указывается длина пакета. Например, выражение less 520 означает, что будут приняты все пакеты, длина которых не превышает 520 байт.
Кроме этого, имеется возможность отбора пакетов путем проверки отдельных байт и даже бит независимо от того, где они находятся – в заголовке, данных или концевике пакета. Для этого предусмотрен специальный математический примитив:
expr relop expr True, если математическое выражение истинно. relop это один из символов >, <, >=, =<, =, !=, а expr – выражение, составленное из целочисленных констант по правилам синтаксиса языка С, нормальных двоичных операторов [+, -, *, /, &, | ], оператора длины len и специальных выражений для доступа к данным пакета.
Для доступа к данным пакета используется следующее выражение:
proto [expr:size] Proto - это один из следующих протоколов: ip, ether, fddi, arp, rarp, tcp, udp, icmp. Эта переменная определяет уровень протокола для операции индексации. Переменная expr определяет смещение в байтах применительно к указанному протоколу. Переменная size определяет размер поля, применительно к которому будет выполнена дальнейшая операция сравнения. Эта переменная может принимать значения 1, 2 или 4. Если она не указана, по умолчанию выбирается значение 1. Оператор len дает длину всего пакета.
Например, выражение ether[0]&1 != 0 позволяет перехватывать весь групповой трафик. Выражение ip[0]&0xf != 5 означает отбор IP-пакетов с наличием опций, а выражение ip[6:2]&0x1fff = 0 отбирает только не фрагментированные дейтаграммы.
Приведем несколько примеров выражений для описания фильтра:
host sundown
– принимать все пакеты от хоста с именем sundown
host helios
and (hot or ace)
– перехватывать трафик между хостами helios и hot и helios и ace
ip host helios
and not ace – перехватывать трафик между хостом helios и всеми хостами, исключая ace
gateway snup and (port ftp or ftp-data) – перехватывать весь ftp-трафик, проходящий через шлюз snup
tcp[13]&3 != 0 and not src and dst net localnet – принимать только TCP SYN и FIN пакеты, не предназначенные локальному хосту localnet и не отправленные им.
Метод создания приложения рассмотрим на примере программы PCAP Filter. Эта программа наглядно демонстрирует создание фильтра и осуществление
записи на диск принятых пакетов. Программа может быть откомпилирована под Win32 в
среде Microsoft Visual C++ 6.0 (не забудьте предварительно
установить библиотеку libpcap и выполнить настройку
переменных среды для обеспечения возможности компиляции программ, использующих
эту библиотеку). Для работы программы под Win32
необходимо
установить драйвера WinPCap.
PCAP Filter представляет собой фильтр
общего назначения. В качестве исходных данных при запуске программы необходимо
указать три параметра: источник пакетов, строку с
текстовым описанием фильтра и имя выходного файла. После запуска программа
выполняет процесс захвата, фильтрации пакетов и записи отобранных пакетов в
файл до нажатия пользователем клавиш Ctr+C.
Исходный код программы PCAP Filter приведен ниже.
#include <stdlib.h>
#include <stdio.h>
#include <pcap.h>
#define MAX_PRINT 80
#define MAX_LINE 16
void dispatcher_handler(u_char *,const struct pcap_pkthdr *,
const u_char *);
void usage();
void main(int argc, char **argv) {
// задаем
необходимые переменные:
// дескриптор
адаптера
pcap_t *fp;
// буфер
для хранения описания ошибок
char
error[PCAP_ERRBUF_SIZE];
// имя
устройства адаптера
char *device=NULL;
// входной
файл
char
*ifilename=NULL;
// выходной
файл
char
*ofilename=NULL;
// фильтр
char *filter=NULL;
int i=0;
// дескриптор
выходного файла
pcap_dumper_t
*dumpfile;
// описываем
структуру
struct bpf_program
fcode;
bpf_u_int32
SubNet,NetMask;
// не
заданы параметры - выход
if (argc == 1)
{
usage();
return;
}
// обрабатываем
аргументы командной строки
for(i=1;i<argc;i+=2){
switch (argv[i] [1])
{
case 'i':
{
device=argv[i+1];
};
break;
case 'f':
{
ifilename=argv[i+1];
};
break;
case 'o':
{
ofilename=argv[i+1];
};
break;
case 'p':
{
filter=argv[i+1];
};
break;
}
}
//начинаем процесс
захвата пакетов из сети
if (device !=
NULL){
if ( (fp= pcap_open_live(device, 1514, 1,
20, error) ) == NULL)
{
fprintf(stderr,"\nНевозможно открыть
адаптер.\n");
return;
}
}
//пробуем начать
обрабатывать файл
else if (ifilename
!= NULL){
if ( (fp = pcap_open_offline(ifilename,
NULL) ) == NULL)
{
fprintf(stderr,"\nUnable to find input
file.\n");
return;
}
}
else usage();
if(filter!=NULL){
//получаем адрес подсети
if(device!=NULL){
if(pcap_lookupnet(device, &SubNet,
&NetMask, error)<0){
fprintf(stderr,"\nНевозможно определить маску сети.\n");
return;
}
}
else NetMask=0xffffff; //Если обрабатываем
файл, подразумевается, что мы работаем с подсетью класса C
//Компилируем фильтр
if(pcap_compile(fp, &fcode, filter, 1,
NetMask)<0){
fprintf(stderr,"\nОшибка компиляции
фильтра: неверный синтаксис.\n");
return;
}
//Устанавливаем откомпилированный фильтр
if(pcap_setfilter(fp, &fcode)<0){
fprintf(stderr,"\nОшибка при установке
фильтра\n");
return;
}
}
//Открываем файл
для дампа пакетов
if (ofilename !=
NULL){
dumpfile=pcap_dump_open(fp, ofilename);
if(dumpfile==NULL){
fprintf(stderr,"\nНевозможно открыть
выходной файл\n");
return;
}
}
else usage();
//Все готово –
начало работы!!!
pcap_loop(fp, 0,
dispatcher_handler, (unsigned char *)dumpfile);
}
//Сетевую ловушку нужно вызывать для каждого входящего
пакета
void dispatcher_handler(u_char *dumpfile,
const struct pcap_pkthdr *header, const
u_char *pkt_data)
{
u_int i=0;
//Записываем
пакет в файл
pcap_dump(dumpfile,header,pkt_data);
//следующая
инструкция принудительно записывает принятый пакет на диск.
//Заметьте, что
вызов этой функции для принудительной записи каждого пакета гарантирует
когерентность записи и приема пакетов из сети
//но снижает
общую производительность.
fflush((FILE*)dumpfile);
}
void usage()
{
printf("\nВызов:\npf [-i интерфейс] | [-f имя_входного_файла] -o
имя_выходного_файла -p пакетный_фильтр\n\n");
exit(0);
}
В третьей, заключительной части планируется подробно рассмотреть другую библиотеку из состава WinPCap – packet.dll, позволяющую не только принимать и отфильтровывать, но и передавать пакеты произвольного формата. Напоминаем, что дистрибутив библиотеки WinPCap Вы можете скачать с сайта разработчика