Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
iakovlev.org

Programming with pcap

Tim Carstens
timcarst at yahoo dot com
Further editing and development by Guy Harris
guy at alum dot mit dot edu

Итак , для понимания существа дела нужны базовые знания по С. Совсем необязательно быть гуру.Все будет достаточно хорошо разжевано. Вам также помогут базовые знания по сетям, поскольку в данной статье речь пойдет о пакетном сниффере. Представленный код был протестирован на FreeBSD 4.3.

Яковлев С :Для начала заберите библиотеку libpcap-1.0.0 и соберите ее. Вы получите бинарник libpcap.a , который надо будет положить в каталог с примерами , иначе вы их не сможете собрать.

Getting Started: The format of a pcap application

  1. Для начала нужно определиться с интерфейсом. На линуксе это может быть eth0, на BSD это может быть xl1. Устройство мы будем хранить в форме строки,или же можно попросить pcap, чтобы оно само определило это устройство.
  2. Инициализация pcap. Мы конкретно укажем pcap , что хотим сниффить сетевой интерфейс. Если надо . то сразу несколько интерфейсов. Различать мы их будем с помощью file handles. Для работы с таким файлом нужно установить соответственную "сессию".
  3. Наш снифинг будет распространяться только на TCP/IP-пакеты, проходящие через порт 23, для этого нужно составить набор правил. Правило хранится в строке и конвертируется во внутренний формат pcap.
  4. Мы запускаем основной цикл сниффинга.pcap ловит достаточную порцию пакетов. Каждый раз . когда он получает новый пакет , он вызывает соответственную функцию. Она может распечатать пакет , сохранить его в файле.
  5. Мы закрываем сессию и приложение

На самом деле все очень просто.5 шагов , один из которых - 3 - опционален. Давайте глянем на реализацию :

Setting the device

Существует 2 подхода для инициализации девайса . В первом случае пользователь набирает его в командной строке в качестве параметра программы :

	#include <stdio.h>
 	#include <pcap.h>
 
 	int main(int argc, char *argv[])
 	{
 		 char *dev = argv[1];
 
 		 printf("Device: %s\n", dev);
 		 return(0);
 	}
 

Теперь 2-й вариант :

	#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.

Opening the device for sniffing

Создание сессии снифинга - достаточно простая задача. Для этого мы используем pcap_open_live(). Её прототип :

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

Первый аргумент - устройство,второй - челое число, определяет максимальное количество байт, которое может быть захвачено pcap, третий аргумент - если он true , то переводит интерфейс в promiscuous mode. Четвертый аргумент - определяет тайм-аут в миллисекундах, время , необходимое для фиксации определенного числа пакетов. Пятый аргумент - строка ошибки. Функция возвращает session handler.

Рассмотрим фрагмент :

	 #include <pcap.h>
 	 ...
 	 pcap_t *handle;
 
 	 handle = pcap_open_live(somedev, BUFSIZ, 1, 1000, errbuf);
 	 if (handle == NULL) {
 		 fprintf(stderr, "Couldn't open device %s: %s\n", somedev, errbuf);
 		 return(2);
 	 }
 

Устройство здесь хранится в переменной "somedev", количество байт - в BUFSIZ (см. pcap.h). Устанавливаем устройство в promiscuous mode, делаем отлов пакетов до появления ошибки, которая хранится в errbuf.

Несколько слов о promiscuous / non-promiscuous sniffing: Это 2 различные техники. При non-promiscuous sniffing, мы отловим только те пакеты , которые предназначены именно нам. Все остальное будет просеяно. При Promiscuous mode будет отловлен весь сетевой трафик. В этом случае можно определить аналогичные узлы , которые занимаются тем же самым :-) Promiscuous mode работает при условии non-switched (хаб или свитч не прокатят). При интенсивном сетевом трафике Promiscuous mode может нагрузить машину по полной программе.

Filtering traffic

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

Далее мы вызываем pcap_open_live() и работаем с сессией. Мы будем использовать BPF driver напрямую.

Перед использованием фильтра его нужно "скомпилировать". Фильтовочное выражение хранится в строке (char array).

Для компиляции мы вызываем pcap_compile(). Прототип:

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

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

После этого идет функция pcap_setfilter():

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

1-й аргумент - session handler, 2-й - ссылка на скомпилированную версию выражения.

Код:

	 #include <pcap.h>
 	 ...
 	 pcap_t *handle;		/* Session handle */
 	 char dev[] = "rl0";		/* Device to sniff on */
 	 char errbuf[PCAP_ERRBUF_SIZE];	/* Error string */
 	 struct bpf_program fp;		/* The compiled filter expression */
 	 char filter_exp[] = "port 23";	/* The filter expression */
 	 bpf_u_int32 mask;		/* The netmask of our sniffing device */
 	 bpf_u_int32 net;		/* The IP of our sniffing device */
 
 	 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", somedev, 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, в promiscuous mode, на устройстве rl0.

В этом примере функция pcap_lookupnet() в качестве входящего параметра берет имя устройства и возвращает его IP и маску.

The actual sniffing

А теперь поговорим о захвате пакетов.

Есть 2 техники захвата : мы можем либо захватить один пакет за раз , либо группу пакетов в течение какого-то временного цикла. Сначала рассмотрим первый способ , потом второй , для этого будем использовать pcap_next().

Прототип pcap_next():

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

1-й аргумент - session handler. 2-й - указатель на структуру , хранящую информацию о пакете : время , длина пакета , длина порции или фрагмента . pcap_next() возвращает указатель u_char на пакет.

Пример , показывающий использование pcap_next() для захвата пакета:.

	 #include <pcap.h>
 	 #include <stdio.h>
 
 	 int main(int argc, char *argv[])
 	 {
 		pcap_t *handle;			/* Session handle */
 		char *dev;			/* The device to sniff on */
 		char errbuf[PCAP_ERRBUF_SIZE];	/* Error string */
 		struct bpf_program fp;		/* The compiled filter */
 		char filter_exp[] = "port 23";	/* The filter expression */
 		bpf_u_int32 mask;		/* Our netmask */
 		bpf_u_int32 net;		/* Our IP */
 		struct pcap_pkthdr header;	/* The header that pcap gives us */
 		const u_char *packet;		/* The actual packet */
 
 		/* Define the device */
 		dev = pcap_lookupdev(errbuf);
 		if (dev == NULL) {
 			fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
 			return(2);
 		}
 		/* Find the properties for the device */
 		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;
 		}
 		/* Open the session in promiscuous mode */
 		handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
 		if (handle == NULL) {
 			fprintf(stderr, "Couldn't open device %s: %s\n", somedev, errbuf);
 			return(2);
 		}
 		/* Compile and apply the filter */
 		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);
 		}
 		/* Grab a packet */
 		packet = pcap_next(handle, &header);
 		/* Print its length */
 		printf("Jacked a packet with length of [%d]\n", header.len);
 		/* And close the session */
 		pcap_close(handle);
 		return(0);
 	 }
 

Устройство определяется с помощью pcap_lookupdev() в режиме promiscuous mode. Находится 1-й пакет на 23-м порту (telnet) и выводится размер пакета в байтах. Новая функция - pcap_close() - про нее попозже.

Другая техника захвата посложнее и возможно , более полезная. Чаще вместо pcap_next() используется pcap_loop() или pcap_dispatch(). Для их понимания нужно усвоить , что такое callback function.

Принципиально в Callback functions нет ничего такого особенно нового. Концепция такова : пусть у меня есть программа , которая ожидает событие от какого-то порта. Допустим , мы ждем . когда пользователь нажмет на клавишу. Каждый раз при нажатии клавиши вызывается функция - callback function. Аналогичные функции используются в pcap, но вместо ожидания нажатия они ждут , когда произойдет захват пакета. Называются они pcap_loop() и pcap_dispatch().

Прототип pcap_loop() :

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

1-й аргумент - session handle. 2-й - указывает , сколько пакетов для pcap_loop() войдет в одну порцию. 3-й - имя самой callback function , без параметров. 4-й - обычно NULL. Функция pcap_dispatch() почти индентична. Разница лишь в том , что pcap_dispatch() получает лишь первую порцию пакетов , в то время как pcap_loop() будет продолжать до тех пор , пока счетчик не обнулится.

Перед тем как показать пример использования pcap_loop(), нужно проверить формат callback function. Нельзя произвольно определять прототип callback's prototype. Прототип нашей callback function:

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

Тип у функции - void. 1-й аргумент соответствует последнему аргументу функции pcap_loop() и передается оттуда каждый раз. 2-й аргумент - pcap header. Структура 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 this packet (off wire) */
 	};
 

Наиболее интересен в got_packet последний аргумент. Это еще один указатель на u_char, и он указывает на первый байт данных пакета, который захвачен функцией pcap_loop().

Он представляет из себя набор структур - Ethernet header, IP header, TCP header. Этот указатель указывает на сериализованную версию этих структур. Чтобы использовать эти структуры , надо использовать преобразование - typecasting.

Определения этих структур :

/* Ethernet addresses are 6 bytes */
 #define ETHER_ADDR_LEN	6
 
 	/* Ethernet header */
 	struct sniff_ethernet {
 		u_char ether_dhost[ETHER_ADDR_LEN]; /* Destination host address */
 		u_char ether_shost[ETHER_ADDR_LEN]; /* Source host address */
 		u_short ether_type; /* IP? ARP? RARP? etc */
 	};
 
 	/* IP header */
 	struct sniff_ip {
 		u_char ip_vhl;		/* version << 4 | header length >> 2 */
 		u_char ip_tos;		/* type of service */
 		u_short ip_len;		/* total length */
 		u_short ip_id;		/* identification */
 		u_short ip_off;		/* fragment offset field */
 	#define IP_RF 0x8000		/* reserved fragment flag */
 	#define IP_DF 0x4000		/* dont fragment flag */
 	#define IP_MF 0x2000		/* more fragments flag */
 	#define IP_OFFMASK 0x1fff	/* mask for fragmenting bits */
 		u_char ip_ttl;		/* time to live */
 		u_char ip_p;		/* protocol */
 		u_short ip_sum;		/* checksum */
 		struct in_addr ip_src,ip_dst; /* source and dest address */
 	};
 	#define IP_HL(ip)		(((ip)->ip_vhl) & 0x0f)
 	#define IP_V(ip)		(((ip)->ip_vhl) >> 4)
 
 	/* TCP header */
 	struct sniff_tcp {
 		u_short th_sport;	/* source port */
 		u_short th_dport;	/* destination port */
 		tcp_seq th_seq;		/* sequence number */
 		tcp_seq th_ack;		/* acknowledgement number */
u_char th_offx2; /* data offset, 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; /* window */ u_short th_sum; /* checksum */ u_short th_urp; /* urgent pointer */ };

Кстати: Тут автор пишет , что на Slackware Linux 8 box (stock kernel 2.2.19) (какая древность :-)) это вообще не компилится. Проблема оказалась в include/features.h. Для разрешения проблемы нужно было определить
#define _BSD_SOURCE 1
Можно также использовать альтернативную TCP header structure, которая лежит тут : here. У меня на 10-й сузе все собралось на ура.

Эти хидеры фигурируют в данных пакета.

Как выудить данные из TCP пакета ?

/* ethernet headers - длина всегда 14 байт */
 #define SIZE_ETHERNET 14
 
 	const struct sniff_ethernet *ethernet; /* The ethernet header */
 	const struct sniff_ip *ip; /* The IP header */
 	const struct sniff_tcp *tcp; /* The TCP header */
 	const char *payload; /* Packet 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 - это указатель на адрес памяти.

Если значение этого указателя равно X , то адрес структуры sniff_ethernet тоже равно X, отсюда мы можем найти адрес следующей структуры - X плюс длина Ethernet header,равная 14 ,или SIZE_ETHERNET.

Далее аналогично получаем адрес IP header, но он не имеет фиксированной длины; длина равна числу 4-байтных слов. Это число умножаем на 4 и получаем длину следующей структуры . Минимальная длина - 20 байт.

TCP header также имеет переменную длину;его длина - это "data offset" TCP header, и минимум это тоже 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 = X. sniff_ip = X + 14 байт, или SIZE_ETHERNET). sniff_tcp = X +14 + (4 * IP header length).

Теперь мы знаем , как установить callback function, вызвать ее, и вычислить атрибуты захваченного пакета.

Исходники лежат тут

Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье
Galaran
  большое спасибо за перевод)
2009-08-21 07:31:42
doctor
  отличная статья, спасибо
2011-06-14 16:29:16
дима
  спасибо, то что надо!
2012-06-15 17:05:37
Alatar
  Статья хорошая в качестве вводной, но следует отметить, что код магического преобразования очень условный - 

 он подразумевает, что Вы точно уверены, что это фрейм Ethernet DIX, он содержит именно IP, а он, в свою очередь, - именно TCP.

 Надо помнить, что на самом деле SIZE_ETHERNET тоже не константа, а между эзернетом и IP могут прятаться инкапсулирующие протоколы, 

 например VLAN_TAG. 
2013-08-29 19:02:49