Packet capturing s libpcap (2)

Packet capturing s libpcap (2)
05.08.2009 12:10 | Články | Oliver Kindernay
V druhom a poslednom dieli seriálu sa pozrieme na filtrovanie trafficu, funkciu pcap_loop a analýzu paketov. Očakáva sa základná znalosť sieťových protokolov.

pcap filtre

Poznámka: Nasledujúcich pár riadkov bude len akýsi preklad manuálovej stránky 'pcap-filter. Preto ak vám angličtina nerobí problémy, odporúčam skôr prečítať si manuálovú stránku ;)'

V minulom dieli sme sa dozvedeli, že linuxový kernel nám poskytuje funkcionalitu, pomocou ktorej môžeme nielen zachytávať a analyzovať pakety, ale ich aj filtrovať, teda stanoviť nejaké pravidlo, ktoré zdelí kernelu, že nám má doručiť len pakety pravidlu zodpovedajúce. Rozhranie, ktoré to umožnuje sa nazýva BPF (Berkeley Packet Filter). My sa však so zložitým BPF trápiť nemusíme, pcap nám poskytuje vlastné, jenoduché rozhranie. Ak viete pracovať s filtrami tcpdump-u, môžete nasledujúcich pár odstavcov preskočiť.

Filtre sú v pcap špecifikované ako string, teda pole znakov. String sa skladá z jedného, alebo viacerích identifikátorov a tzv. ID. Identifikátory pcap rozdeľuje do troch skupín:
  • type - určuje, aký druh informácie je uložený v ID. Možné typy sú host, net, port a portrange. Teda napr. host 192.168.0.1, net 192.168.0.0/24, port 80. Ak neuvedieme žiadny z týchto identifikátorov, nastaví sa host.
  • dir - vyjadrujú smer dopravy dát. Možné smery sú smersrc, dst, src or dst, src and dst, addr1, addr2, addr3, and addr4. Kombinujú sa s vyššie uvedenými typmi. Teda napr. src host 192.168.0.1, dst host 192.168.0.1, dst net 192.168.0.1/24. addr* sa používajú výhradne na linkách IEEE 802.11, teda Wi-fi.
  • proto - obmedzujú filter na určitý protokol. Možné protokoly sú ether, fddi, tr, wlan, ip, ip6, arp, rarp, decnet, tcp and udp. Napr. tcp portrange 20-80, ether src host 0:18:F3:76:7E:55 .
Existujú ešte špeciálne identifikátory, ktoré sa nezaraďujú do vyššie uvedených kategórií: gateway, broadcast, less, greater.
Všetky možné kombinácie popisuje vyššie spomenutá manuálová stránka. My si uvedieme pár, z ktorých pochopíte princíp a vlastné filtre si už poľahky zostavíte sami.
  • host host - Chceme len tie pakety, ktoré prišli od alebo sú určené host. Kedže sme neuviedli protokol, prepdokladá sa IPv4/v6 (to platí aj pri nasledujúcich).
  • src host host - Chceme len tie pakety, ktoré prišli od host.
  • dst host host - Cheme len tie pakety, ktoré sú určené pre host.
  • net net - Chceme len pakety, ktoré prišli alebo sú určené pre sieť net. (Analógia z host)
  • tcp port port - Chceme len TCP pakety, ktoré majú zdrojový, alebo cieľový port port
Všetko sa dá kombinovať logickými operátormi and/&&, or/|, not/! . Platí analógia s jazykom C.

Aplikácia filtra na sniffing session

Aby sme mohli filter aplikovať pomocou 'pcap_setfilter', musíme ho najprv "skompilovať" funkciou 'pcap_compile'. Tá má prototyp:
		int pcap_compile(pcap_t *p, struct bpf_program *fp, const char *str, int optimize, bpf_u_int32 netmask);
	
Funkcii predáme ukazateľ na session handler, ukazateľ na štruktúru 'bpf_program', string obsahujúci filter, indikátor optimalizácie kódu a masku siete. Po zavolaní bude predaná štruktúra obsahovať pripravený kód pre funkciu 'pcap_setfilter'

Odskok ku pcap_lookupnet

Funkcii 'pcap_compile' predávame masku siete, ktorú, žiaľ, nevieme a vymyslieť si ju nemôžeme. Zistiť si ju však môžeme funkciou 'pcap_lookupnet' s prototypom
		 int pcap_lookupnet(const char *device, bpf_u_int32 *netp, bpf_u_int32 *maskp, char *errbuf);
	
Prvý a posledný argument je jasný. Adresy v dvoch ukazateľoch typu 'bpf_u_int32' budú po zavolaní obsahovať masku a adresu siete (big-endian).

'pcap_setfilter' má nasledujúci prototyp:

		 int pcap_setfilter(pcap_t *p, struct bpf_program *fp);
	
Celá záležitosť je veľmi jednoduchá. Predáme session handler a štruktúru naplnenú pomocou 'pcap_compile' a funkcia aplikuje filter na session handler.
Príde mi zbytočné uvádzať teraz príklad, ale ak ste nedočkaví, zoscrollujte dolu a preštudujte úryvok kódu, ktorý pracuje s filtrom.

Funkcia pcap_loop

V minulom dieli sme spomenuli funkciu 'pcap_loop' ako pokročilejšiu a oveľa vhodnejšiu funkciu na zachytávanie paketov ako 'pcap_next'. Začneme s prototypom:

		 int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user);
	
Funkcia neskončí, pokiaľ nezachytí 'cnt' paketov, nevracia teda žiadne dáta relevatné k zachytenému paketu. Ako teda budeme manipulovať so zachytenými dátami? Na to slúži funkcia, ktorú predáme ako tretí parameter. Funkcia má pevne stanovený tvar, a to takýto:
		  void got_packet(u_char *user, const struct pcap_pkthdr *header, const u_char *packet);
	
	
Názov funkcie a názvy parametrov môžu byť, samozrejme, ľubovoľné. Prvý parameter je rovnaký ako ten, predaný do 'pcap_loop'. Je to ľubovoľný pointer na ľubovoľné účely. Druhý je nám už známa štruktúra, ktorú používa aj 'pcap_next'. A konečne posledný sú zachytené dáta. Funkcia bude zavolaná s príslušnými hodnotami vždy, keď 'pcap_loop' zachytí paket. Ak je hodnota 'cnt' menšia ako 0, sniffovať sa bude donekonečna (Pokiaľ sa nevyskytne nejaká chyba).

Analýza zachytených dát

Tak, už sniffujeme a zachytávame dáta. Ako ale zistíme, či sa jedná o IP/ARP, TCP/UDP/ICMP ... paket? V minulej časti bolo spomenuté, že dáta z packet filtru z jadra Linuxu ťaháme nespracované, t. j. v takej podobe ako prišli. Kedže všetci vieme, ako pracuje sieť a jednotlivé protokoly, spracovať ich nebude príliš náročné ;). Každý paket sa skladá z jednej alebo viac vrstiev. Samotné vrsty predstavujú jednotlivé protokoly tak, ako boli postupne pribaľované jeden na druhý. Zoberme si ako príklad ľubovoľný TCP paket. Predpokladajme, že sme na ethernet sieti. Na začiatku samotného paketu teda bude hlavička protokolu ethernet s informáciami o adresátovi a príjmemcovi (MAC adresy) atď. V dátovej časti bude ukrytý IP paket s hlavičkou obsahujúcou IP adresy, TTL, protocol .... V dátovej časti IP paketu už bude samotný TCP datagram, znova s hlavičkou obsahujúcou informácie (porty, sekvenčné číslo ...). Ďalej budú nasledovať TCP dáta a tie zas môžu obsahovať iný protokol atď. atď. Túto situáciu znázorňuje obrázok nižšie.



Linux nám poskytuje sadu štruktúr, ktoré reprezentujú hlavičky jednotlivých protokolov. Hlavičkové súbory so štruktúrami sa nachádzajú v priečinkoch /usr/include/netinet/ a /usr/include/net/. Pomocou týchto štruktúr bude manipulovanie s dátami hračka. Ukážeme si to na názornom príklade, v ktorom budeme pracovať s vyššie uvedeným paketom. Predpokladajme, že dáta máme v premennej' packet'. Najprv deklarujeme štruktúry, ktoré budeme potrebovať:
	...
	#include <net/ethernet.h>
	#include <netinet/ip.h>
	#include <netinet/tcp.h>
	...	

	struct ether_header *ethh;	/*ethernet hlavička*/
	struct iphdr *iph;		/*IP hlavička*/
	struct tcphdr *tcph;		/*TCP hlavička*/
	const u_char *payload;		/*zvyšné dáta*/
	...
	
Teraz budeme postupne priraďovať ukazateľom adresy tak, aby ukazovali na príslušné miesta v pakete. Samozrejme musíme pretypovať.
	...
	ethh = (struct ether_header *)packet;
	iph = (struct iphdr *)(packet + sizeof(struct ether_header));
	...
	/*z ip hlavičky získame jej veľkosť (iph->ihl*4 > 20), uložíme do iplen*/
	...
	tcph = (struct tcphdr *)(packet + sizeof(ether_header) + iplen);
	...
	/*z tcp hlavičky získame jej veľkosť (tcph->doff*4 > 20), uložíme do tcplen*/
	payload = packet + sizeof(ether_header) + iplen + tcplen;
	...
	
Teraz môžeme ľubovoľne manipulovať s dátami, napr. vypísať hodnotu TTL z ip hlavičky takto:
	printf("TTL = %d", iph->ttl);
	
Nesmieme však zabúdať, že všetko je v sieťovom poradí (big-endian), takže ak sme na x86, všetko väčšie ako 15 bitov musíme prekonvertovať na litle-endian funkciami 'ntohs' a 'ntohl'. Napr. cieľový TCP port by sme vypísali takto:

	printf("TCP dst port = %d", ntohs(tcph->dest));
	

Prax

V celom článku sme nevideli poriadny kód, tak si to teraz vynahradíme. Nasledujúci program reprezentuje všetko, čo sme za tie dva diely prebrali. Vysvetlivky ku kódu dúfam nie sú potrebné. Je tam síce pár neznámych vecí, kód však hovorí sám za seba :). Ak budú nejaké nejasnosti, tak prosím do komentárov. Kód je asi príliš dlhý na to, aby ste to študovali tu, takže ak sa vám to bude lepšie čítať tak http://openpaste.org/en/15794/
#include <stdio.h>
#include <pcap.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <netinet/ip.h>	/*struct iphdr*/
#include <net/ethernet.h>	/*struct ether_header*/
#include <netinet/ip_icmp.h>	/*struct icmphdr*/
#include <netinet/in.h>

#define SNAPLEN 1024

void got_packet(u_char *args, const struct pcap_pkthdr *header,  const u_char *packet) 
{
	static int p_count = 1;
	struct iphdr *iph;
	struct icmphdr *icmph;
	struct in_addr in;
	int iplen = 0;

	/*vypiseme cas a dlzku zachyteneho packetu*/
	printf("\n%d. %s%d bytes  ", p_count, ctime((const time_t *)&(header->ts.tv_sec)), header->len);

	/*nechceme vypisovat informacie s ethernet hlavicky - preskocime ju*/
	iph = (struct iphdr *)(packet + sizeof(struct ether_header));
	
	/*zistime dlzku ip hlavicky*/	
	iplen = iph->ihl*4;
	/*skontrolujeme verziu*/
	if(iph->version != 4)
		return;
	/*vypiseme zdrojovu a cielovu IP adresu*/
	in.s_addr = iph->saddr;
	printf("%s > ", inet_ntoa(in));
	in.s_addr = iph->daddr;
	printf("%s", inet_ntoa(in));
	
	/*vypiseme hodnotu TTL*/
	printf("(TTL %d)  ", iph->ttl);
	
	/*obsahuje packet icmp?*/
	if(iph->protocol != IPPROTO_ICMP) 
		return;

	/*nacitame icmp hlavicku*/
	icmph = (struct icmphdr *)(packet + sizeof(struct ether_header) + iplen);
	
	switch(icmph->type) {
		case ICMP_ECHO : 	printf("ICMP echo request\n");
				 	break;

		case ICMP_ECHOREPLY : 	printf("ICMP echo reply\n");
	}
	
	p_count++;

}

int main(int argc, char **argv)
{

	char errbuff[PCAP_ERRBUF_SIZE];
	char *device, *filter = "host google.com";
	bpf_u_int32 net, mask = 0;
	pcap_t *sd;
	struct bpf_program bpfp;

	/*najdeme defultne rozhranie*/
	device = pcap_lookupdev(errbuff);
	if(device == NULL) {
		fprintf(stderr, "%s\n", errbuff);
		_exit(1);
	}
	
	/*zistime masku a adresu siete*/
	if(pcap_lookupnet(device, &net, &mask, errbuff) == -1) {
		fprintf(stderr, "%s\nCould not set filter\n", errbuff);
	}

	/*otvorim sniffing session*/
	sd = pcap_open_live(device, SNAPLEN, 0, 1000, errbuff);
	if(sd == NULL) {
		fprintf(stderr, "%s\n", errbuff);
		_exit(1);
	}

	/*skontrolujeme ci sme na ethernete*/
	if(pcap_datalink(sd) != DLT_EN10MB) {
		fprintf(stderr, "%s is not ethernet\n", device);
		_exit(1);
	}

	/*skompilujeme a nastavime filter*/
	if(mask) {
		if(pcap_compile(sd, &bpfp, filter, 0, mask) == -1) {
			fprintf(stderr, "Could not set filter");
		}
		
		if(pcap_setfilter(sd, &bpfp) == -1) {
			fprintf(stderr, "Could not set filter");
		}
	}

	/*zacneme sniffovat*/
	pcap_loop(sd, 0, got_packet, NULL);
	
	/*uvolnime zdroje*/
	pcap_close(sd);
	pcap_free(&bpfp);

	return 0;


}
		
	
Teraz už len skompilovať, spustiť, pingnúť google.com a radovať sa :).

Záver

A sme na konci. Kedže v slovenčine o tejto problematike takmer nie sú informácie, takýto článok by sa mi celkom hodil, keď som s týmto začínal. Teraz dúfam, že pomôže aspoň niekomu, kto nemá čas prekladať manuálové stránky a rôzne tutoriály. Snažil som sa všetko vysvetliť najpolopatistickejšie ako som vedel a myslím, že je to celkom zrozumiteľné a pochopiteľné. Ak nie, tak rád privítam kritiku dole v komentároch.

Zdroje

Velký prúvodce TCP/IP a systémem DNS, druhé aktualizované vydanie.
http://www.tcpdump.org/pcap.htm
Článok v Hakin9 2/2008 - Programming with libpcap, Sniffing the network from our own aplication, Luis Martin Garcia.
Manuálové stránky.
    • pekny clanok 15.08.2009 | 14:04
      mikeovec   Návštevník
      Vdaka za clanok.
    • datagram 30.09.2009 | 21:58
      Avatar uid0 Debian  Používateľ
      som to prebehol ocami a nasiel som jednu nezrovnalost - TCP ma segmenty, UDP datagramy
      Debian. apt-get into it…