Code44free's Blog

Старая статья, снифер на С с использованием libpcap

Posted in c/c++, networking, programming by code44free on Январь 2, 2013

Послушаем?

В этой статье речь пойдет о «сетевых нюхачах», о сниферах. Если точнее, о том, каким образом можно самому написать снифер. Хотя в массовом сознании снифер ассоциируется со взломом, с разрушением, изначально сниферы были созданы для «мирных» целей, отладка сетевых приложений, сетевых частей операционных систем, протоколов передачи данных.

Принцип работы снифера достаточно прост. Переводим сетевую карту в Promiscuous mode, в этом режиме сетевая карта принимает все пакеты проходящие по сети, а не только предназначенные ей, то есть с MAC адресом назначения не совпадающем с MAC адресом сетевой карты. Далее в зависимости от операционной системы через определенный механизм получаем эти пакеты, анализируем их. Я написал, «в зависимости от операционной системы», поскольку различные операционные системы предоставляют различные механизмы для работы с непосредственно сетевой картой компьютера, в Solaris это интерфейс DLPI, FreeBSD — BPF, Linux — SOCK_PACKET. В результате, что бы написать снифер для работы на этих операционных системах, необходимо написать три различных подсистемы для работы с сетевой картой. Но не все так мрачно. Одним из результатов проекта http://tcpdump.org (разработка известного анализатора сетевых пакетов), стала разработка библиотеки libpcap. Она предоставляет удобный интерфейс для перехвата сетевых пакетов, перенесена на множество платформ, перечисленные выше Solaris, FreeBSD, Linux, практически все Unix’like, и Windows (WinPcap). Все эти библиотеки можно свободно скачать с сайта разработчика.

Libpcap.

Библиотека libpcap предоставляет следующие базовые функции:

pcap_lookupdev(char *) — выполняет поиск устройств для «прослушивания», в errbuf возвращается сообщение об ошибке:

#include
int main()
{
char *dev, errbuf[PCAP_ERRBUF_SIZE];
dev = pcap_lookupdev(errbuf);
printf("DEV: %s\n", dev);
return 0;
}

pcap_lookupnet(char *device, bpf_u_int32 *net, bpf_u_int32 *mask, char *) — запрашивает сетевую карту о классе сети и маске. Первый аргумент функции, устройство найденное функцией pcap_lookupdev, net — указатель на переменную для возврата информации о класе сети, mask — маска сети, и последний параметр буфер для сообщения об ошибке:

. . . .
char *net, *mask;

pcap_lookupnet(somedev, &net, &mask, errbuf);
addr.s_addr=net;
printf("NET: %s ", inet_ntoa(addr));
addr.s_addr=mask;
printf("MASK: %s\n", inet_ntoa(addr));

pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *) — функция создает сессию для прослушивания и возвращает ее идентификатор. Первый аргумент, устройство найденное функцией pcap_lookupdev(), snaplen указывает максимальное количество байт для перехвата в пакете, promisc установленный в 1, указывает на то, что сетевую карту необходимо перевести в promiscuous mode, to_ms устанавливает таймаут чтения, при to_ms = 0 слушать до наступления ошибки, to_ms = -1 слушать непрерывно. Последний параметр, буфер в который необходимо поместить сообщение об ошибке:

. . . .
pcap_t *handle;

handle = pcap_open_live(somedev, BUFSIZ, 1, 0, errbuf);

pcap_compile(pcap_t *, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask) — компилирует фильтр отбора пакетов из текстового вида, во внутреннее представление. Первый аргумент функции идентификатор сессии созданной фукцией pcap_open_live() ранее. Далее идет указатель на структуру куда будет помещен собраный фильтр, str — текстовая строка содержащая фильтр, optimize — указывает должен ли фильтр оптимизироваться или нет (0 — нет, 1 — да), netmask — маска сети в которой находится компьютер.

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

    dst host host — пакеты где IP адрес получателя равен host host
    src host host — пакеты где IP адрес отправителя равен host host
    dst net net — пакеты в которых сеть получателя равна net net
    src net net — пакеты в которых сеть отправителя равна net net
    port port — пакеты направленные на порт port
    ip proto protocol — пакеты определенного протокола (TCP, UDP, ICMP)

Полный список возможных параметров можно получить обратившись к документации на сайте http://tcpdump.org.

pcap_setfilter(pcap_t *, struct bpf_program *fp) — после того как фильтр был скомпилирован применяем его. Первый аргумент — указатель сессии, второй — указатель на скомпилированный фильтр.

. . . .
struct bpf_program fp;

pcap_compile(handle, &fp, "ip proto tcp", 0, net);
pcap_setfilter(handle, &fp);

Для перехвата пакетов библиотека libpcap предоставляет два пути. Первый считывать один пакет в момент времени используя функцию pcap_next() или организовать цикл с использованием функции pcap_loop().

pcap_next(pcap_t *, struct pcap_pkthdr *) — первый аргумент указатель сессии, второй — указатель на структуру в которой будет храниться информация о пакете:

struct pcap_pkthdr {
struct timeval ts; // время перехвата пакета
bpf_u_int32 caplen; // длинна фрагмента (если пакет фрагментирован)
bpf_u_int32 len; // длинна пакета
};

. . . .
struct pcap_pkthdr *header;
const u_char *packet;

packet = pcap_next(handle, &header);
printf("Grab a packet with lenth: %d\n", header.len);

pcap_loop(pcap_t *, int cnt, pcap_handler callback, u_char *) — первый аргумент — указатель сессии, второй — количество перехваченных пакетов для выхода из цикла, отрицательное значение — продолжать пока не возникнет ошибка. Callback — имя функции для обработки пакета, последний аргумент это параметры для передачи функции обработки пакета. В большинстве ситуаций это значение устанавливают в NULL, поскольку функция pcap_loop передает все необходимые аргументы самостоятельно.

. . . .
void callback(u_char args, const struct pcap_pkthdr , const u_char* packet)
{
}
. . . .
pcap_loop(handle, -1, callback, NULL);

pcap_close(pcap_t *) — закрывает сессию. Единственный аргумент для этой функции указатель сессии.

В бой.

Вооружившись этой информацией можно приступать к программированию.

Все части, из которых состоит функция main() программы, были описаны выше, поетому нет смысла повторятся(см. 134-168). Приступим к созданию функции обработчика полученных пакетов — callback(u_char *args, const struct pcap_pkthdr* pkthdr, const u_char* packet)(см. 125-132). На что следует обратить внимание, функция имеет тип void, это естественно, поскольку функция pcap_loop(), из которой она вызывается, все равно не знает, что делать с возвращаемым значением. Первый параметр, args, это параметры которые возможно были переданы в последнем параметре функции pcap_loop(). Следующий параметр — это указатель на структуру с информацией об пакете, последний, packet — это указатель на перехваченый пакет.

Получив пакет, вычленим из него информацию об Ethernet уровне. Для этого напишем функцию handle_ethernet()(см. 82-91). Ethernet заголовок пакета имеет следующую структуру, описанную в файле /usr/include/net/ethernet.h (см. 12):

struct ether_header {
u_char ether_dhost[ETHER_ADDR_LEN];
u_char ether_shost[ETHER_ADDR_LEN];
u_short ether_type;
};

Где, ether_dhost — MAC адрес получателя пакета, ether_shost — MAC адрес отправителя пакета, ether_type — тип протокола. Типы протоколов перечислены в том же заголовочном файле, например для IP-протокола — это ETHERTYPE_IP 0x0800, для ARP протокола, ETHERTYPE_ARP 0x0806 и так далее. Наша функция будет возвращать тип протокола поэтому и тип функции будет u_short (см. 82). В строке 84 объявляем указатель на структуру *eth типа struct ether_header, для хранения Ethernet заголовка. В строке 86 вырезаем из пакета непосредственно Ethernet заголовок. Теперь мы можем получить информацию обращаясь к структуре *eth (см. 87,88), в нашем примере выбираем MAC адрес отправителя и MAC адрес получателя. В строке 90 возвращаем тип протокола предварительно преобразовав его из сетевого порядка следования байтов в серверный.

Теперь на основании типа протокола решаем, что делать дальше (см. 128-131). В нашем примере обрабатываем дальше только протокол IP (см. 128), для всех остальных просто выводим на терминал информацию об типе протокола.

Для обработки IP заголовка напишем функцию handle_ip() (см. 93-106). Структура IP заголовка описана в строках 16-31, ip_vhl — версия протокола, ip_tos — содержит информацию о том как следует обрабатывать пакет(минимальная задержка, максимальная пропускная способность, максимальная надежность, минимальная стоимость), ip_len — длинна пакета, ip_id — уникальный идентификатор пакета, ip_off — флаг управляющий фрагментацией пакетов, ip_ttl — время «жизни» пакета (количество маршрутизаторов какое пакет может пройти перед тем как будет удален), ip_p — протокол, ip_sum — контрольная сумма. Типы протоколов (ip_p) описаны в файле /usr/include/netinet/in.h (напр. TCP — IPPROTO_TCP 6, UDP — IPPROTOUDP 17, и т.д.). В описании структуры IP заголовка обьявлены, но у нас нигде не используются два макроса IP_V(см. 18) для определения версии протокола и IP_HL(см. 19) для определения длинны заголовка.

Функция handle_ip() не будет возвращать значений поэтому ее тип будет void. В строке 95 обьявляется указатель на структуру для хранения IP заголовка. Далее (см. 97) вырезаем IP заголовок из пакета, т.е. от начала пакета со смещением в размер структуры ether_header, на длинну структуры my_ip. Далее выводим на терминал интересующую нас информацию из IP заголовка просто обращаясь к полям структуры ip. В конце на основании типа протокола (см. 102-105), для TCP протокола продолжаем разбор пакета вызывая функцию handle_tcp()(см. 102), для UDP и ICMP, просто выводим информацию о типе протокола, для остальных пишем UNKNOWN. Структура TCP заголовка описана в файле /usr/include/netinet/tcp.h:

struct tcphdr {
u_short th_sport;
u_short th_dport;
tcp_seq th_seq;
tcp_seq th_ack;
u_int th_x2:4,
th_off: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_PUSH|TH_ACK|TH_URG|TH_ECE|TH_CWR)
u_short th_win;
u_short th_sum;
u_short th_urp;
};

Где, th_sport — номер порта отправителя, th_dport — номер порта получателя, th_seq — порядковый номер, th_ack -номер подтверждения, th_x2 — не используется, th_off — смещение данных, th_flags — флаги пакета(SYN -запрос на подключение, ACK — подтверждение, RST — разорвать соединение вследствии ошибок, FIN — закрыть соединение, и т.д.), th_urp — указатель необходимый для выравнивания данных, th_win — число байтов получаемых адресатом, th_sum — контрольная сумма.

Функция handle_tcp()(см. 108), также будет иметь тип void. В строке 111, обьявляется указатель на структуру *tcp, для хранения TCP заголовка. Далее идет определение переменной data для хранения данных передаваемых в пакете. В строках 113, 114 выполняем вычисление суммы длинн ethernet и ip заголовков, которую присваеваем переменной iplen, и длинна этих заголовков, плюс длинна tcp заголовка, и присваиваем переменной tcplen. Далее вырезаем TCP заголовок (см. 116). Теперь выводим интересующую нас информацию (см. 117, 118).

Если кроме информации из ethernet, ip, tcp и так далее заголовков нам необходимо увидеть передаваемые данные, то необходимо в вызове функции handle_tcp() из фукции handle_ip()(см. 102) добавить еще один параметр, длинну пакета(ip->ip_len), и соответсвенно изменить прототип фукции handle_tcp() добавив параметр u_int16_t len (см. 108). Эта информация необходима, чтобы вычислить размер передаваемых данных (см. 120). Вырезать данные из пакета можно налогично тому как, это мы делали с ethernet, ip, tcp заголовками ранее. Поскольку в полученных данных будут непечатаемые символы, то просто распечатать содержание *data не получится, кроме этого, хотелось, что бы вывод данных был красиво структурирован. Для этого можно воспользоваться фукцией ascii_print_with_offset()(см. 40-80), эта функция заимствована мной из исходников tcpdump, разработчиками tcpdump из кода NetBSD.

Заключение.

Как видите создать простой снифер с использованием библиотеки libpcap дело нескольких минут. К тому же этот снифер будет работать и на Linux и на Solaris и на FreeBSD, на любой системе на которую перенесена libpcap.

Также, хотелось бы сказать, что данный способ «подслушивания» пригоден только для локальной сети построенной на хабах, в случае сети на свичтах, можно прослушивать только трафик исходящий/приходящий на свой копьютер. Бытует мнение, что в сети построенной на свитчах невозможно «подслушать». Описанным в этой статье способом, действительно невозможно, но существует технология arp poisoing (отравление ARP), с помощью которой можно прослушивать и компьютеры в сети построенной на свитчах.

/*						
	gcc -o sniffer main.c -lpcap		
*/						
#include <pcap.h>				
#include <stdio.h>				
#include <errno.h>				
#include <sys/socket.h>				
#include <sys/types.h>				
#include <netinet/in.h>				
#include <arpa/inet.h>				
#include <netinet/if_ether.h>			
#include <net/ethernet.h>			
#include <netinet/tcp.h>			
#include <ctype.h>				
							
struct my_ip {
	u_int8_t 	ip_vhl;
#define	IP_V(ip)	(((ip)->ip_vhl & 0xf0) >> 4)
#define	IP_HL(ip)	((ip)->ip_vhl & 0x0f)
	u_int8_t	ip_tos;
	u_int16_t	ip_len;
	u_int16_t	ip_id;
	u_int16_t	ip_off;
#define	IP_DF 0x4000
#define	IP_MF 0x2000
#define	IP_OFFMASK 0x01fff
	u_int8_t	ip_ttl;
	u_int8_t	ip_p;
	u_int16_t	ip_sum;
	struct	in_addr	ip_src, ip_dst;
}; 
void handle_tcp(u_char *args, const struct pcap_pkthdr* pkthdr, 
		const u_char* packet, u_int16_t len);

#define HEXDUMP_BYTES_PER_LINE 16
#define HEXDUMP_SHORTS_PER_LINE (HEXDUMP_BYTES_PER_LINE / 2)
#define HEXDUMP_HEXSTUFF_PER_SHORT 5
#define HEXDUMP_HEXSTUFF_PER_LINE 
	(HEXDUMP_HEXSTUFF_PER_SHORT*HEXDUMP_SHORTS_PER_LINE)
void ascii_print_with_offset(register const u_char *cp, 
			register u_int length, register u_int oset)
{
	register u_int i=0;
	register int s1, s2;
	register int nshorts;
	char hexstuff[HEXDUMP_SHORTS_PER_LINE * 
			HEXDUMP_HEXSTUFF_PER_SHORT + 1], *hsp;
	char asciistuff[HEXDUMP_BYTES_PER_LINE+1], *asp;

	nshorts=length/sizeof(u_short);
	hsp=hexstuff; asp=asciistuff;
	while(--nshorts>=0) {
		s1=*cp++;
		s2=*cp++;
		(void)snprintf(hsp, sizeof(hexstuff)-(hsp-hexstuff)," %02x%02x", s1, s2);
		hsp+=HEXDUMP_HEXSTUFF_PER_SHORT;
		*(asp++)=(isgraph(s1) ? s1 : '.');
		*(asp++)=(isgraph(s2) ? s2 : '.');
		if(++i >= HEXDUMP_SHORTS_PER_LINE) {
			*hsp=*asp='\0';
			printf("\n0x%04x\t%-*s\t%s", oset,
				 HEXDUMP_HEXSTUFF_PER_LINE, 
				hexstuff, asciistuff);
			i=0; hsp=hexstuff; asp=asciistuff;
			oset+=HEXDUMP_BYTES_PER_LINE;
		}
	}
	if(length & 1) {
		s1=*cp++;
		(void)snprintf(hsp, sizeof(hexstuff)-(hsp-hexstuff)," %02x", s1);
		hsp+=3;
		*(asp++)=(isgraph(s1) ? s1 : '.');
		++i;
	}
	if(i>0) {
		*hsp=*asp='\0';
		(void)printf("\n0x%04x\t%-*s\t%s", oset, HEXDUMP_HEXSTUFF_PER_LINE, 
				hexstuff, asciistuff);
	}
}

u_short handle_ethernet(u_char *args, const struct pcap_pkthdr* pkthdr, const u_char* packet)
{
	struct ether_header *eth;

	eth=(struct ether_header *) packet;
	printf("ETH\tsource: %s", ether_ntoa(eth->ether_shost));
	printf(" dest: %s\n", ether_ntoa(eth->ether_dhost));
		
	return ntohs(eth->ether_type);
}

void handle_ip(u_char *args, const struct pcap_pkthdr* pkthdr, const u_char* packet) 
{
	const struct my_ip* ip;

	ip=(struct my_ip *)(packet+sizeof(struct ether_header));
	printf("IP\tsource: %s ", inet_ntoa(ip->ip_src));
	printf("dest: %s\n", inet_ntoa(ip->ip_dst));
	printf("\ttos: %d len: %d id: %d ttl: %d\n", ip->ip_tos, ip->ip_len, 
		ip->ip_id, ip->ip_ttl);
	if(ip->ip_p==IPPROTO_TCP){ handle_tcp(args, pkthdr, packet, ip->ip_len); 
	} else if(ip->ip_p==IPPROTO_UDP) { printf("(UDP)\n");
	} else if(ip->ip_p==IPPROTO_ICMP) { printf("(ICMP)\n");
	} else { printf("UNKNOWN\n"); }
}
	
void handle_tcp(u_char *args, const struct pcap_pkthdr* pkthdr, 
		const u_char* packet, u_int16_t len) 
{
	struct tcphdr* tcp;
	u_char *data;
	int iplen=sizeof(struct ether_header)+sizeof(struct my_ip);
	int tcplen=iplen+sizeof(struct tcphdr);
	
	tcp=(struct tcphdr *)(packet+iplen);
	printf("TCP\tsport: %d", ntohs(tcp->th_sport));
	printf(" dport: %d\n", ntohs(tcp->th_dport));
	//data=(u_char *)(packet+tcplen);
	//len=len-(sizeof(struct my_ip)+sizeof(struct tcphdr));
	//ascii_print_with_offset(data, len, 0);
	printf("\n\n");
}

void callback(u_char *args, const struct pcap_pkthdr* pkthdr, const u_char* packet)
{
	u_int16_t etype=handle_ethernet(args, pkthdr, packet);
	if(etype==ETHERTYPE_IP) { handle_ip(args, pkthdr, packet); 
	} else if(etype==ETHERTYPE_ARP) { printf("(ARP)\n"); 
	} else if(etype==ETHERTYPE_REVARP) { printf("(RARP)\n"); 
	} else { printf("(UNKNOWN)\n"); }
}

int main(int argc, char *argv[])
{
	char *dev;
	char errbuf[PCAP_ERRBUF_SIZE];
	pcap_t* descr;
	const u_char *packet;
	struct pcap_pkthdr hdr;
	struct bpf_program fp;
	struct in_addr addr;
	bpf_u_int32 maskp;
	bpf_u_int32 netp;
	
	if(argc==2) {
		if((dev=pcap_lookupdev(errbuf))==NULL) {
			printf("%s\n", errbuf); exit(1); }
		pcap_lookupnet(dev, &netp, &maskp, errbuf);
		if((descr=pcap_open_live(dev, BUFSIZ, 1, 0, errbuf))==NULL) {
			printf("pcap_open_live(): %s\n", errbuf); exit(1); }
		if(pcap_compile(descr, &fp, argv[2], 0, netp)==-1) {
			printf("Error pcap_compile()\n"); exit(1); }
		if(pcap_setfilter(descr, &fp)==-1) {
			printf("Error pcap_setfilter\n"); exit(1); }
		printf("DEV: %s\n", dev);
		addr.s_addr=netp;
		printf("NET: %s\n", inet_ntoa(addr));
		addr.s_addr=maskp;
		printf("MASK: %s\n", inet_ntoa(addr));
		pcap_loop(descr, -1, callback, NULL);
		pcap_close(descr);
	} else {
		printf("Usage: %s \"filter\"\n", argv[0]);
	}

	return 0;
}
Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: