Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Захват пакетов при помощи библиотеки libpcap

Оригинал: Capturing Packets in Your C Program, with libpcap
Автор: Pankaj Tanwar
Дата публикации: 1 Февраля 2011 г.
Перевод: А.Панин
Дата перевода: 11 октября 2012 г.

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

Перемещение всех данных в сетях осуществляется в форме пакетов, являющихся единицей данных для сетей. Для понимания того, какие данные находятся в пакете, необходимо понимать иерархию сетевых протоколов в рамках сетевой модели. Если вы не знакомы с сетевой моделью ISO OSI (Open Systems Interconnection - базовая эталонная модель взаимодействия открытых систем), настоятельно рекомендую изучить соответствующую документацию. Неплохим источником информации является статья в Wikipedia.

Термин "пакет" впервые вводится на сетевом уровне. Основные протоколы этого уровня: IP (Internet Protocol - межсетевой протокол), ICMP (Internet Control Message Protocol - протокол межсетевых управляющих сообщений), IGMP (Internet Group Management Protocol - протокол управления группами Интернета) и IPsec (набор протоколов для обеспечения защиты данных, передаваемых по протоколу IP). Протоколы транспортного уровня включают в себя TCP (Transmission Control Protocol - протокол управления передачей), ориентированный на создание постоянного соединения; UDP (User Datagram Protocol - протокол пользовательских дейтаграмм), не требующий постоянного соединения; SCTP (Stream Control Transmission Protocol - протокол передачи с управлением потоком), сочетающий в себе свойства двух приведенных выше протоколов. Прикладной уровень содержит множество часто используемых протоколов, таких как: HTTP, FTP, IMAP, SMTP и множество других.

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

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

Основы работы с libpcap

Библиотека libpcap является платформонезависимой библиотекой с открытым исходным кодом (версия для Windows носит название winpcap). Известные снифферы tcpdump и Wireshark используют эту библиотеку для работы.

Для разработки нашей программы, нам необходим сетевой интерфейс, на котором будет производиться захват пакетов. Мы можем назначить это устройство самостоятельно или воспользоваться функцией, предоставляемой libpcap: char *pcap_lookupdev(char *errbuf).

Эта функция возвращает указатель на строку, содержащую название первого сетевого интерфейса, пригодного для захвата пакетов; в случае ошибки возвращается NULL (это справедливо и для других функций libpcap). Аргумент errbuf предназначен для пользовательского буфера, в который будет помещено сообщение об ошибке в случае возникновения - это очень удобно при отладке программ. Размер этого буфера должен быть не меньше PCAP_ERRBUF_SIZE (на данный момент 256) байт.

Работа с устройством

Далее мы открываем выбранное сетевое устройство, используя функцию pcap_t *pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *errbuf). Она возвращает идентификатор устройства, представленный в виде переменной типа pcap_t, которая может использоваться в других функциях libpcap. Первый аргумент - сетевой интерфейс, с которым мы хотим работать, второй - максимальный размер буфера для захвата данных в байтах.

Установка минимального значения второго параметра полезна в том случае, когда необходимо производить захват только заголовков пакетов. Размер Ethernet-кадра равен 1518 байтам. Значения 65535 будет достаточно для захвата любого пакета из любой сети. Аргумент promisc устанавливает, будет ли устройство работать в promiscous-режиме или нет. (В promiscous-режиме сетевая карта будет генерировать прерывания для всех данных, которые она получает, а не только для тех, которые подходят по значению MAC-адреса. Подробнее читайте в Wikipedia).

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

Захват данных

Теперь нам необходимо начать захват пакетов. Давайте используем функцию u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h). Здесь *p - это указатель, возвращаемый функцией pcap_open_live(); следующий аргумент - это указатель на переменную типа struct pcap_pkthdr, в которой возвращается первый принятый пакет.

Функция int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user) используется для сборки пакетов и их обработки. Она возвращает количество пакетов, заданное аргументом cnt. Функция обратного вызова используется для обработки принятых пакетов (нам необходимо будет задать эту функцию). Для передачи дополнительной информации в эту функцию мы будем использовать параметр *user, являющийся указателем на переменную типа u_char (нам необходимо будет самостоятельно производить приведение типов в зависимости от необходимого типа данных, передаваемых функции).

Прототип функции обратного вызова вызова выглядит следующим образом: void callback_function(u_char *arg, const struct pcap_pkthdr* pkthdr, const u_char* packet). Первый аргумент является аргументом *user, переданным функции pcap_loop(); следующий аргумент является указателем на структуру, содержащую информацию о принятом пакете. Поля структуры struct pcap_pkthdr представлены ниже (взято из файла pcap.h):
struct pcap_pkthdr {
	struct timeval ts;  /* отметка времени (time stamp) */
	bpf_u_int32 caplen; /* размер принятых данных (length of portion present) */
	bpf_u_int32 len;    /* размер данного пакета (length of this packet (off wire)) */
}

Альтернативой функции pcap_loop() является функция pcap_dispatch(pcap_t *p, int cnt, pcap_handler callback, u_char *user). Единственным отличием является то, что это функция возвращает результат по истечении времени, заданного при вызове pcap_open_live().

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

До этого времени мы получали все пакеты, приходящие на сетевой интерфейс. Теперь давайте применим функцию pcap, позволяющую отфильтровывать трафик приходящий на заданный порт. Мы можем использовать эту функцию также для работы только с пакетами для заданного прокола, например, ARP или FTP. Для начала нам нужно составить фильтр, используя данную функцию:
int pcap_compile(pcap_t *p, struct bpf_program *fp, const char *str, int optimize, bpf_u_int32 mask);

Первый аргумент аналогичен во всех функциям библиотеки и рассмотрен ранее; второй аргумент представляет собой указатель на составленную версию фильтра. Следующий - это выражение для фильтра. Это выражение может быть названием протокола, таким как ARP, IP, TCP, UDP и.т.д. Вы можете найти множество примеров выражений в руководствах pcap-filter и tcpdump, которые должны быть установлены в вашей системе.

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

После составления, давайте применим фильтр при помощи функции int pcap_setfilter(pcap_t *p, struct bpf_program *fp). Второй аргумент функции - составленная версия выражения для фильтрации трафика.

Получение информации IPv4

int pcap_lookupnet(const char *device, bpf_u_int32 *netp, bpf_u_int32 *maskp, char *errbuf)

При помощи этой функции можно получить сетевой адрес IPv4 и маску сети, присвоенные данному сетевому интерфейсу. Сетевой адрес будет записан по адресу *netp, а маска сети - по адресу *mask.

Небольшая программа для захвата пакетов

Теперь давайте напишем программу, которая поможет нам понять принцип работы pcap. Назовем файл с исходным кодом sniff.c. Это программа из раздела руководств сайта tcpdump.org, автором которой является Martin Casado. Сначала подключим необходимые заголовочные файлы.
#include <pcap.h> 
#include <stdio.h> 
#include <stdlib.h>
#include <errno.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <netinet/if_ether.h> 
Далее введем функцию обратного вызова для обработки принятых пакетов. Эта функция будет просто печатать текущее количество принятых пакетов. Позднее мы напишем другую функцию обратного вызова. Эта же функция настолько очевидна, что не требует объяснений.
void my_callback(u_char *args, const struct pcap_pkthdr* pkthdr, const u_char* 
	packet) 
{ 
	static int count = 1; 
	fprintf(stdout, "%3d, ", count);
	fflush(stdout);
	count++; 
}
Теперь рассмотрим функцию main(). Здесь использованы функции, которые мы разбирали ранее:
int main(int argc,char **argv) 
{ 
	int i;
	char *dev; 
	char errbuf[PCAP_ERRBUF_SIZE]; 
	pcap_t* descr; 
	const u_char *packet; 
	struct pcap_pkthdr hdr;
	struct ether_header *eptr; /* net/ethernet.h */ 
	struct bpf_program fp;     /*выражение фильтрации в составленном виде */ 
	bpf_u_int32 maskp;         /*маска подсети */ 
	bpf_u_int32 netp;          /* ip */ 

	if(argc != 2){
		fprintf(stdout, "Usage: %s \"expression\"\n" 
			,argv[0]);
		return 0;
	} 

	/* Получение имени устройства */
	dev = pcap_lookupdev(errbuf); 
	
	if(dev == NULL) {
		fprintf(stderr, "%s\n", errbuf);
		exit(1);
	} 
	/* Получение сетевого адреса и маски сети для устройства */ 
	pcap_lookupnet(dev, &netp, &maskp, errbuf); 

	/* открытие устройства в  promiscuous-режиме */ 
	descr = pcap_open_live(dev, BUFSIZ, 1,-1, errbuf); 
	if(descr == NULL) {
		printf("pcap_open_live(): %s\n", errbuf);
		exit(1);
	} 

	/* теперь составляется выражение фильтрации*/ 
	if(pcap_compile(descr, &fp, argv[1], 0, netp) == -1) {
		fprintf(stderr, "Error calling pcap_compile\n");
		exit(1);
	} 

	/* применение фильтра*/ 
	if(pcap_setfilter(descr, &fp) == -1) {
		fprintf(stderr, "Error setting filter\n");
		exit(1);
	} 

	/* функция обратного вызова используется в цикле */ 
	pcap_loop(descr, -1, my_callback, NULL); 
	return 0; 
}
Скомпилируйте программу при помощи команд, приведенных ниже и запустите с привилегиями пользователя root (необходимо для задействования promiscous-режима):
$ gcc -lpcap sniff.c -o sniffer
# ./sniffer ip

На рисунке 1 представлен примерный вывод программы.
Вывод программы, использующей libpcap
Рисунок 1: Вывод программы

Поскольку в качестве выражения фильтрации задана строка ip, ваш экран скоро будет заполнен отметками о приеме IP-пакетов. Можете заменить ip на другое выражение, например, tcp, arp, и.т.д. - посмотрите примеры на странице руководства tcpdump.

Другая реализация функции обратного вызова, которая выводит содержимое пакетов, принятых на основе заданного выражения фильтрации (она уже в sniff.c):
 void another_callback(u_char *arg, const struct pcap_pkthdr* pkthdr, 
	const u_char* packet) 
{ 
	int i=0; 
	static int count=0; 

	printf("Packet Count: %d\n", ++count);             /* Количество пакетов */
	printf("Recieved Packet Size: %d\n", pkthdr->len); /* Длина заголовка */
	printf("Payload:\n");                              /* А теперь данные */
	for(i=0;i<pkthdr->len;i++) { 
		if(isprint(packet[i]))            /* Проверка, является ли символ печатаемым */
			printf("%c ",packet[i]);       /* Печать символа */
		else 
			printf(" . ",packet[i]);       /* Если символ непечатаемый, вывод . */
		if((i%16==0 && i!=0) || i==pkthdr->len-1) 
			printf("\n"); 
	}
}

Вы можете изменить строку с вызовом функции pcap_loop() в функции main(), где производится вызов функции my_callback() для использования нового варианта функции. Скомпилируйте измененную программу и запустите с тем же выражением в качестве аргумента. Вывод, представленный на рисунке 2, содержит данные из IP-пакетов.
Вывод, показывающий содержимое пакетов
Рисунок 2: Вывод, показывающий содержимое пакетов.

Думаю, на этом мы можем остановиться. Протестируйте программу самостоятельно, поэкспериментируйте с pcap и вы поймете, насколько мощный компонент лежит в основе лучших (и наших любимых) снифферов tcpdump и Wireshark.