|
Una interessante caratteristica introdotta oramai già dalle prime releadse di Linux è quella di poter filtrare i pacchetti di rete che raggiungono un sistema Linux secondo determinati parametri. E' possibile cioè, non solo fare in modo che un processo possa ricevere tutti i pacchetti che raggiungono una scheda di rete, ma è anche possibile filtrare tale flusso dati secondo regole ben determinate.
Una prima domanda, lecita, che ci potremmo fare su quanto appena detto potrebbe essere: «ok, ma questo lo si potrebbe fare anche all'interno del processo elaborando tutti i pacchetti ricevuti, non occorre una specifica funzionalità di nucleo!». In realtà questo ragionamento è fallace per semplici ragioni di performance, infatti il numero di pacchetti di rete che il sistema può ricevere nell'unità di tempo è, spesso, molto alto e un filtro a livello utente è poco performante a causa dei molti cambi di contesto che si renderebbero necessari.
Ecco perché all'interno di Linux sono stati aggiunti i Socket Filter!
Cosa sono
In poche parole i Socket Filter non sono altro che dei filtri che permettono di scegliere, da parte di un processo, quali sono i pacchetti di rete che vuole riceve. In questo modo il processo si concentra solo sui pacchetti a cui è interessato demandando al kernal la scelta di cosa far passare o meno.
I Socket Filter lavorano a livello di Protocol Family, cioè lo strato software appena sopra i device driver delle schede di rete e sono implementati come una macchina a stati che esegue un dato programma (il filtro). La macchina a stati è programmata con un codice pseudo-assembler chiamato BPF (Berkeley packet filter).
Cenni al Berkeley packet filter
Il linguaggio BPF è molto potente e leggero, permette cioè di eseguire in maniera molto veloce il filtraggio dei pacchetti. Il programma tcpdump è sostanzialmente basato su questo linguaggio e il suo utilizzo ne dà un esempio pratico di come sia efficiente e largamente utilizzato.
Ma la cosa interessante del linguaggio BPF, e che lo rende ancora più attraente per chi deve analizzare particolari classi di pacchetti di rete, è che non occorre conoscerlo per utilizzarlo! Infatti all'interno del comando tcpdump è presente una specie di traduttore tra la sintassi delle espresisoni tcpdump e il suo equivalente in BPF! Se cioè chiediamo a tcpdump di mostrarci il codice BPF che implementa un particolare filtro lui lo fa subito!
# tcpdump -d host 192.168.32.37 and port 80 (000) ldh [12] (001) jeq #0x800 jt 2 jf 18 (002) ld [26] (003) jeq #0xc0a82025 jt 6 jf 4 (004) ld [30] (005) jeq #0xc0a82025 jt 6 jf 18 (006) ldb [23] (007) jeq #0x84 jt 10 jf 8 (008) jeq #0x6 jt 10 jf 9 (009) jeq #0x11 jt 10 jf 18 (010) ldh [20] (011) jset #0x1fff jt 18 jf 12 (012) ldxb 4*([14]&0xf) (013) ldh [x + 14] (014) jeq #0x50 jt 17 jf 15 (015) ldh [x + 16] (016) jeq #0x50 jt 17 jf 18 (017) ret #96 (018) ret #0
Nell'esempio ho chiesto a tcpdump di filtrare tutti i pacchetti da/per l'host 192.168.32.37 sulla porta 80. L'interpretazione del meta linguagigo è abbastanza facile (sebbene non banale per chi non mastichi un po' di assembler) e ci mostra come sia facile generare il codice da dare in pasto al sistema per filtrare i pacchetti di rete che ci interessano.
Come si implementano
A questo punto però come si dà questo codice in pasto al sistema? La risposta è ancora più semplice, si usa l'opzione -dd.
Con tale opzione si dice infatti a tcpdump che quello che ci interessa non è il codice in assembler, ma la sua verisone già compilata e pronta da essere inserita in un programma C. Si ha cioè:
# tcpdump -dd host 192.168.32.37 and port 80 { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 16, 0x00000800 }, { 0x20, 0, 0, 0x0000001a }, { 0x15, 2, 0, 0xc0a82025 }, { 0x20, 0, 0, 0x0000001e }, { 0x15, 0, 12, 0xc0a82025 }, { 0x30, 0, 0, 0x00000017 }, { 0x15, 2, 0, 0x00000084 }, { 0x15, 1, 0, 0x00000006 }, { 0x15, 0, 8, 0x00000011 }, { 0x28, 0, 0, 0x00000014 }, { 0x45, 6, 0, 0x00001fff }, { 0xb1, 0, 0, 0x0000000e }, { 0x48, 0, 0, 0x0000000e }, { 0x15, 2, 0, 0x00000050 }, { 0x48, 0, 0, 0x00000010 }, { 0x15, 0, 1, 0x00000050 }, { 0x6, 0, 0, 0x00000060 }, { 0x6, 0, 0, 0x00000000 },
Questa volta l'output di tcpdump può essere preso paro paro e copiato in un file C all'interno di una vettore di strutture struct sock_filter come segue:
struct sock_filter BPF_code[] = { { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 16, 0x00000800 }, { 0x20, 0, 0, 0x0000001a }, { 0x15, 2, 0, 0xc0a82025 }, { 0x20, 0, 0, 0x0000001e }, { 0x15, 0, 12, 0xc0a82025 }, { 0x30, 0, 0, 0x00000017 }, { 0x15, 2, 0, 0x00000084 }, { 0x15, 1, 0, 0x00000006 }, { 0x15, 0, 8, 0x00000011 }, { 0x28, 0, 0, 0x00000014 }, { 0x45, 6, 0, 0x00001fff }, { 0xb1, 0, 0, 0x0000000e }, { 0x48, 0, 0, 0x0000000e }, { 0x15, 2, 0, 0x00000050 }, { 0x48, 0, 0, 0x00000010 }, { 0x15, 0, 1, 0x00000050 }, { 0x6, 0, 0, 0x00000060 }, { 0x6, 0, 0, 0x00000000 }, };
Nel vettore è allora presente il programma che implementa il filto definito sopra; va quindi usata la chiamata di sistema setsockopt() per darlo in pasto al sistema:
filter.len = ARRAY_SIZE(BPF_code); filter.filter = BPF_code; setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter));
Ma facciamo un esempi pratico: supponiamo di voler catturare tutti i pacchetti di ping (icmp-echo e icmp-echoreply) per/da la nostra macchina.
Prima di tutto utilizziamo un programmino che, oltre ad implementare il nostro filtro, si preoccupa anche di dire al sistema che vogliamo ricevere tutti i pacchetti ethernet. Per fare questo dobbiamo utilizzare il protocollo (o, meglio, il domain) PF_PACKET.
Dalle pagine di man della chiamata di sistema socket() si legge:
DESCRIPTION socket() creates an endpoint for communication and returns a descrip- tor.
The domain argument specifies a communication domain; this selects the protocol family which will be used for communication. These families are defined in <sys/socket.h>. The currently understood formats include:
Name Purpose Man page PF_UNIX, PF_LOCAL Local communication unix(7) PF_INET IPv4 Internet protocols ip(7) PF_INET6 IPv6 Internet protocols ipv6(7) PF_IPX IPX - Novell protocols PF_NETLINK Kernel user interface device netlink(7) PF_X25 ITU-T X.25 / ISO-8208 protocol x25(7) PF_AX25 Amateur radio AX.25 protocol PF_ATMPVC Access to raw ATM PVCs PF_APPLETALK Appletalk ddp(7) PF_PACKET Low level packet interface packet(7)
Quindi con il comando man 7 packet si ottiene:
DESCRIPTION Packet sockets are used to receive or send raw packets at the device driver (OSI Layer 2) level. They allow the user to implement protocol modules in user space on top of the physical layer.
The socket_type is either SOCK_RAW for raw packets including the link level header or SOCK_DGRAM for cooked packets with the link level header removed. The link level header information is available in a common format in a sockaddr_ll. protocol is the IEEE 802.3 protocol number in network order. See the <linux/if_ether.h> include file for a list of allowed protocols. When protocol is set to htons(ETH_P_ALL) then all protocols are received. All incoming packets of that protocol type will be passed to the packet socket before they are passed to the protocols implemented in the kernel.
Only processes with effective UID 0 or the CAP_NET_RAW capability may open packet sockets.
SOCK_RAW packets are passed to and from the device driver without any changes in the packet data. When receiving a packet, the address is still parsed and passed in a standard sockaddr_ll address structure. When transmitting a packet, the user supplied buffer should contain the physical layer header. That packet is then queued unmodified to the network driver of the interface defined by the destination address. Some device drivers always add other headers. SOCK_RAW is similar to but not compatible with the obsolete PF_INET/SOCK_PACKET of Linux 2.0.
SOCK_DGRAM operates on a slightly higher level. The physical header is removed before the packet is passed to the user. Packets sent through a SOCK_DGRAM packet socket get a suitable physical layer header based on the information in the sockaddr_ll destination address before they are queued.
Quindi apprendiamo che con il tipo SOCK_RAW si possono spedire/ricevere dei pacchetti di tipo raw dalle schede di rete. Per specificare i pacchetti di tipo Internet Protocol packet su ethernet basta specificare allora come protocol il valore htons(ETH_P_IP) (si veda il file linux/if_ether.h). Si ha cioè:
sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP));
Un possibile corpo del nostro programma risulta allora essere:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <sys/ioctl.h> #include <net/if.h> #include <linux/if_ether.h> #include <linux/filter.h>
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
int main(int argc, char *argv[]) { int sock, n, ret; char buffer[2048]; unsigned char *iph, *ethh; struct ifreq req;
struct sock_filter BPF_code[] = { #include "filter.h" }; struct sock_fprog prog;
/* Create a RAW socket into PF_PACKET domain */ sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)); if (sock <0) { perror("socket"); exit(EXIT_FAILURE); }
/* Set the network card in promiscuos mode */ strncpy(req.ifr_name, "eth0", IFNAMSIZ); ret = ioctl(sock, SIOCGIFFLAGS, &req); if (ret < 0) { perror("ioctl"); exit(EXIT_FAILURE); } req.ifr_flags |= IFF_PROMISC; ret = ioctl(sock, SIOCSIFFLAGS, &req); if (ret < 0) { perror("ioctl"); exit(EXIT_FAILURE); }
/* Attach the filter to the socket */ prog.len = ARRAY_SIZE(BPF_code); prog.filter = BPF_code; ret = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog)); if (ret < 0) { perror("setsockopt"); exit(EXIT_FAILURE); }
while (1) { printf("---\n"); n = recvfrom(sock, buffer, 2048, 0, NULL, NULL); if (n < 0) { perror("recvfrom"); exit(EXIT_FAILURE); }
printf("got %d bytes\n",n);
/* Check to see if the packet contains at least * complete Ethernet (14), IP (20) and TCP/UDP * (8) headers. */ if (n < (14 + 20 + 8)) { fprintf(stderr, "incomplete packet\n"); exit(EXIT_FAILURE); }
ethh = (unsigned char *) buffer; printf("Source MAC address: " "%02x:%02x:%02x:%02x:%02x:%02x\n", ethh[0],ethh[1],ethh[2], ethh[3],ethh[4],ethh[5]); printf("Destination MAC address: " "%02x:%02x:%02x:%02x:%02x:%02x\n", ethh[6],ethh[7],ethh[8], ethh[9],ethh[10],ethh[11]);
iph = (unsigned char *) buffer + 14; /* skip eth header */ if (*iph == 0x45) { /* double check for IPv4 * and no options present */ printf("source host %d.%d.%d.%d\n", iph[12],iph[13], iph[14],iph[15]); printf("destination host %d.%d.%d.%d\n", iph[16],iph[17], iph[18],iph[19]); printf("source,Dest ports %d,%d\n", (iph[20] << 8) + iph[21], (iph[22] << 8) + iph[23]); printf("layer-4 protocol %d\n", iph[9]); } }
return 0; }
In questo programma, dopo aver creato un socket si passa all'impostazione del promiscuos mode per l'interfaccia eth0. Il promiscuos mode serve per catturare tutti i pacchetti che raggiungono la nostra interfaccia di rete, quindi non strettamente necessario per il nostro problema specifico, ma l'ho voluto aggiungere egualmente per rendere il nostro programma ancora più versatile.
Impostato quindi il promiscuos mode (occorrono i privilegi di root per eseguirlo) si passa all'impostazione del filtro il cui codice viene automaticamente generato (sempre come utente root) con il comando make e con Makefile:
TARGETS = filter_packets FILTER = host 192.168.32.37 and port 80
CFLAGS = -Wall -O2
all : $(TARGETS)
filter_packets.c : filter.h filter.h : echo '/* tcpdump -dd $(FILTER) */' > $@ tcpdump -dd $(FILTER) >> $@
clean : rm -rf $(TARGETS) rm -rf filter.h
Questo Makefile ci permette di cambiare al volo il filtro che vogliamo usare semplicemente ridefinendolo al momento della compilazione come segue:
# make FILTER='icmp[icmptype] = icmp-echo or icmp[icmptype] = icmp-echoreply' echo '/* tcpdump -dd icmp[icmptype] = icmp-echo or icmp[icmptype] = icmp-echoreply */' > filter.h tcpdump -dd icmp[icmptype] = icmp-echo or icmp[icmptype] = icmp-echoreply >> filter.h cc -Wall -O2 filter_packets.c -o filter_packets
Bene, una volta impostato il filtro ci si mette allora in attesa dei dati utilizzando la chiamata di sistema recvfrom(). Proviamo il programma; lanciamolo su di una macchina:
# ./filter_packets ---
il processo si sospende in attesa di appacchetti; poi facciamo ping verso questa macchina da un altro host, otterremo che il processo si risveglia ed inizia a ricevere pacchetti:
got 96 bytes Source MAC address: 00:23:54:5f:15:78 Destination MAC address: 00:18:f3:06:7d:bd source host 192.168.32.254 destination host 192.168.32.37 source,Dest ports 2048,47055 layer-4 protocol 1 --- got 96 bytes Source MAC address: 00:23:54:5f:15:78 Destination MAC address: 00:18:f3:06:7d:bd source host 192.168.32.254 destination host 192.168.32.37 source,Dest ports 2048,11208 layer-4 protocol 1 --- got 96 bytes Source MAC address: 00:23:54:5f:15:78 Destination MAC address: 00:18:f3:06:7d:bd source host 192.168.32.254 destination host 192.168.32.37 source,Dest ports 2048,34999 layer-4 protocol 1 --- got 96 bytes Source MAC address: 00:23:54:5f:15:78 Destination MAC address: 00:18:f3:06:7d:bd source host 192.168.32.254 destination host 192.168.32.37 source,Dest ports 2048,49843 layer-4 protocol 1 --- got 96 bytes Source MAC address: 00:23:54:5f:15:78 Destination MAC address: 00:18:f3:06:7d:bd source host 192.168.32.254 destination host 192.168.32.37 source,Dest ports 2048,53925 layer-4 protocol 1 ---
Da notare che se proviamo a fare una qualsiasi altra attività di rete verso l'host su cui gira il nostro filtro non vedremo alcunché in output.
Questa dispensa del corso Programmazione Linux è opera di Rodolfo Giometti (Copyright © 2010) ed è rilasciata dall'autore «as is» (così com'è) e distribuita sotto licenza Creative Commons Attribuzione – Condividi allo stesso modo 2.5 Italia.
|