Сети    

  BSD сокеты ~ 
  Ввод-вывод в Unix ~ 
  Примеры ~ 
  Перехват пакетов ~ 
  Пример сервера ~ 
  Еще примеры ~ 
/Студентам/Сети/Перехват пакетов

Программирование с использованием библиотеки pcap

Общая схема

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

Рассмотрим перечисленные шаги подробно.

Определение интерфейса

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

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


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

Пользователь указывает интерфейс, передавая его имя через первый аргумент нашей программы. Естественно, указываемый пользователем интерфейс должен существовать.

Второй способ - узнать имя интерфейса у самой библиотеки:


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

В этом случае pcap передает нам имя интерфейса, которым он владеет. В строку errbuf будет передано описание ошибки, если таковая возникнет при исполнении вызова pcap_lookupdev().

Открытие интерфейса для перехвата пакетов

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

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

Первый аргумент - имя устройства, которое мы определили на предыдущем шаге. snaplen - целое число, определяющее максимальное количество байт сетевого кадра, которое будет захватываться библиотекой. Если promisc установлен в true, интерфейс переходит в так называемый promiscuous mode (перехватываются пакеты, адресованные другим станциям сети). to_ms - тайм-аут в миллисекундах (в случае, если значение установлено в ноль, чтение будет происходить до первой ошибки, в минус единицу - бесконечно). Наконец, errbuf - строка, в которую мы получим сообщение об ошибке. Функция возвращает хэндл (дескриптор) сессии.

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

#include <pcap.h>
...
pcap_t *handle;
handle = pcap_open_live(somedev, BUFSIZ, 1, 0, errbuf);

Здесь открывается интерфейс, имя которого указано в строке somedev, указывается сколько байт пакета захватывать (значение BUFSIZ определено в pcap.h). Сетевой интерфейс переключается в promiscuous режим. Данные будут читаться до тех пор, пока не произойдет ошибка. В случае ошибки можно вывести ее текстовое описание на экран, используя указатель errbuf.

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

Однако, перехват трафика в promiscuous режиме можно обнаружить; другой узел может с высокой точностью определить, используем ли мы promiscuous режим. Кроме того, он работает только в некоммутируемом окружении (таком как концентраторы или коммутаторы, которые затоплены arp пакетами). В третьих, в случае, если сеть сильно загружена, наша программа будет использовать большое количество системных ресурсов.

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

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

После того, как мы вызвали pcap_open_live() и получили функционирующую сессию перехвата трафика, мы можем применить наш фильтр. Естественно, можно реализовать фильтр вручную, разбирая ETH/IP/TCP заголовки после получения пакета, но использование внутреннего фильтра pcap более эффективно, и кроме того, это проще.

Перед тем тем, как применить фильтр, нужно его "скомпилировать". Выражение для фильтра хранится в обыкновенной строке (массиве символов). Синтаксис таких выражений подробно описан в странице руководства по tcpdump (man tcpdump).

Для компиляции фильтра используется функция 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(). Ее прототип таков:

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

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

В нижеследующем примере демонстрируется использование фильтра:


#include <pcap.h>
...
pcap_t *handle;
// дескриптор сессии

char dev[] = "eth0";
// интерфейс, на котором мы будем слушать

char errbuf[PCAP_ERRBUF_SIZE];
// Строка с ошибкой

struct bpf_program filter;
// Скомпилированное выражение для фильтра

сhar filter_app[] = "port 23";
// Выражение для фильтра

bpf_u_int32 mask;
// Сетевая маска нашего интерфейса

bpf_u_int32 net;
// IP адрес нашего интерфейса

pcap_lookupnet(dev, &net, &mask, errbuf);
handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);
pcap_compile(handle, &filter, filter_app, 0, net);
pcap_setfilter(handle, &filter);

Эта программа подготавливает перехватчик для пакетов, идущих на или с 23-го порта, в promiscuous режиме, на интерфейсе eth0. Пример содержит функцию pcap_lookupnet(), которая возвращает сетевой адрес и маску сети для устройства, имя которого передано ей как параметр. Ее использование необходимо, так как для того, чтобы наложить фильтр, мы должны знать адрес и маску сети.

Перехват пакетов

Существуют две техники перехвата пакетов. Можно перехватывать и обрабатывать по одному пакету, а можно работать с группой пакетов, задав специальный цикл, который будет работать, пока pcap не перехватит заданное количество пакетов. Для работы в первом режиме используется функция pcap_next(). Прототип 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()
{
	pcap_t *handle;
	char *dev;

	char errbuf[PCAP_ERRBUF_SIZE];
	// строка с описанием ошибки

	struct bpf_program filter;
	// скомпилированный фильтр

	char filter_app[] = "port 23";
	// фильтр

	bpf_u_int32 mask;
	// сетевая маска

	bpf_u_int32 net;
	// наш ip адрес

	struct pcap_pkthdr header;
	// заголовок пакета, который заполнит pcap

	const u_char *packet;
	// сам пакет

	// определим интерфейс
	dev = pcap_lookupdev(errbuf);

	// получим сетевой адрес и маску интерфейса
	pcap_lookupnet(dev, &net, &mask, errbuf);

	// откроем сессию перехвата в promiscuous режиме
	handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);

	// скомпилируем и применим пакетный фильтр
	pcap_compile(handle, &filter, filter_app, 0, net);
	pcap_setfilter(handle, &filter);

	// перехватим пакет
	packet = pcap_next(handle, &header);

	// выведем его длину в консоль
	printf("Jacked a packet with length of [%d]\n", header.len);

	// закроем сессию
	pcap_close(handle);
	return(0);
}

Эта программа перехватывает пакеты на устройстве, которое возвращает pcap_lookupdev(), переводя его в pormiscuous режим. Она обнаруживает пакет, который идет через 23-ий порт (telnet) и выводит его размер в байтах. Вызов pcap_close() закрывает открытую сессию перехвата.

Альтернативный метод, хотя и более труден для понимания, но, скорее всего, более полезен. Существуют очень мало (если вообще существуют) перехватчиков пакетов, которые используют pcap_next(). В подавляющем большинстве случаев они используют pcap_loop() или pcap_dispatch() (который, в свою очередь, использует pcap_loop()). Для того, чтобы понять использование этих двух функций, нужно понимать идею callback-функций (функций обратного вызова).

Callback-функции - часто использующийся прием программирования. Принцип довольно прост. Предположим, у вас есть программа, которая ожидает какого-либо события. С целью, например, обработки нажатия клавиши. Каждый раз, когда нажимается клавиша, я хочу вызывать функцию, которая обработает это событие. Функция, которую я использую - callback-функция. Каждый раз, когда пользователь нажимает клавишу, моя программа вызовет callback-функцию. Callback-функции используются в pcap, но вместо того, чтобы вызывать их, когда пользователь нажмет на клавишу, pcap вызывает их, когда получает очередной пакет. pcap_loop() и pcap_dispatch() - функции, которые практически одинаково используют механизм callback-функций. Обе вызывают callback-функцию, каждый раз, когда pcap перехватывает пакет, который проходит через фильтр (если, конечно, фильтр скомпилирован и применен к сессии, в противном случае callback-функции передаются все перехваченные пакеты)

Прототип функции pcap_loop():

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

Первый аргумент - хэндл нашей сессии. Следующее целое число говорит pcap_loop() сколько всего пакетов нужно перехватить (отрицательное значение обозначает, что перехват пакетов должен происходить, пока не произойдет ошибка). Третий аргумент - имя callback-функции (только имя, без скобок). Последний аргумент используется в некоторых приложениях, но обычно он просто установлен в NULL. pcap_dispatch() практически идентична, единственная разница в том, как функции обрабатывают таймаут, величина которого задается при вызове pcap_open_live(). pcap_loop() просто игнорирует таймауты, в отличие от pcap_dispatch(). Детали - в man pcap.

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

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

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

struct pcap_pkthdr {
	struct timeval ts;
	// временная метка
	bpf_u_int32 caplen;
	// длина захваченной части пакета
	bpf_u_int32 len;
	// полная длина пакета
};

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

Как использовать переменную packet? Пакет содержит множество атрибутов, так что, как вы можете представить, он на самом деле не строка, а некоторый набор структур (например, пакет TCP/IP будет содержать ethernet-, ip-, tcp-заголовок и сами данные). Параметр packet, имеющий тип u_char, на самом деле сериализованная версия этих структур. Для того, чтобы получить полезные данные из этих структур, мы должны провести некоторые преобразования.

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

struct sniff_ethernet {
	u_char ether_dhost[ETHER_ADDR_LEN];
	u_char ether_shost[ETHER_ADDR_LEN];
	u_short ether_type;
	 /* IP? ARP? RARP? etc */
};

// IP заголовок
struct sniff_ip {

#if BYTE_ORDER == LITTLE_ENDIAN
	u_int ip_hl:4,
	ip_v:4;
#endif

#if BYTE_ORDER == BIG_ENDIAN
	u_int ip_v:4, /* version */
	ip_hl:4; /* header length */
#endif /* not _IP_VHL */

	u_char ip_tos;
	u_short ip_len;
	u_short ip_id;
	u_short ip_off;

#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 */
};

struct sniff_tcp {
	u_short th_sport;
	u_short th_dport;
	tcp_seq th_seq;
	/* sequence number */
	tcp_seq th_ack;
	/* acknowledgement number */
	
#if BYTE_ORDER == LITTLE_ENDIAN
	u_int th_x2:4, /* (unused) */
	th_off:4; /* data offset */
#endif
#if BYTE_ORDER == BIG_ENDIAN
	u_int th_off:4, /* data offset */
	th_x2:4; /* (unused) */
#endif

	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 */
};

Если используются описания структур из стандартных заголовочных файлов, то иногда для того, чтобы программа, использующая описание TCP-заголовка, откомпилировалась без ошибок, нужно определить символ _BSD_SOURCE, перед подключением заголовочных файлов. Альтернативный способ - определить структуры, описывающие TCP-заголовок, вручную.

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

Сначала объявим переменные, которые нам нужны для разбора u_char пакета на отдельные заголовки:

const struct sniff_ethernet *ethernet;
const struct sniff_ip *ip;
const struct sniff_tcp *tcp;
const char *payload;

int size_ethernet = sizeof(struct sniff_ethernet);
int size_ip = sizeof(struct sniff_ip);
int size_tcp = sizeof(struct sniff_tcp);

Теперь делаем преобазование типов:

ethernet = (struct sniff_ethernet*)(packet);
ip = (struct sniff_ip*)(packet + size_ethernet);
tcp = (struct sniff_tcp*)(
	packet + size_ethernet + size_ip
	);
payload = (u_char *)(
	packet + size_ethernet + size_ip + size_tcp
	);

После этого мы можем обращаться к полям всех структур обычным образом, например:

if ( tcp->th_flags & TH_URG ) { ... };
...
printf("TTL = %d\n", ip->ip_ttl);

Завершение работы

По завершении работы нужно закрыть сессию. Это делается с помощью функции pcap_close():

void pcap_close(pcap_t *p);

Единственный аргумент функции - дескриптор сессии, которую нужно закрыть.

Author: Tim Carstens
Перевод: Пяхтин Лев

Дата последней модификации: 2009-10-15


/Студентам/Сети/Перехват пакетов

Содержимое данного сайта может быть использовано кем угодно, когда угодно, как угодно и для каких угодно целей. Автор сайта не несёт абсолютно никакой ответственности за землетрясения, наводнения, финансовые кризисы, глобальные потепления/похолодания, разбитые тарелки, зуд/онемение в левой/правой пятке читателя, эпидемии/пандемии свинячьего/птичьего/тараканьего и иных гриппов, а также за прочие негативные, равно как и позитивные, последствия, вызванные прямым или косвенным использованием материалов данного сайта кем бы то ни было, включая самого автора. При копировании/цитировании материалов данного сайта любым технически возможным в настоящее время способом, а также способом, могущим стать возможным в будущем, указание (либо неуказание) ссылки на первоисточник лежит, блин, тяжким грузом на совести копирующего/цитирующего.

Valid HTML 4.0 Strict Valid CSS!