Realizzare un semplice packet sniffer in C su Windows XP

Cos'è un packet sniffer e perché utilizzarlo

Un packet sniffer (letteralmente: "annusatore di pacchetti") è un software, generalmente ad uso diagnostico, che consente di visualizzare e analizzare tutto il traffico di rete fisicamente ricevibile da una interfaccia di rete. Di solito un'interfaccia di rete scarta tutti i frame il cui indirizzo MAC non coincide con il proprio (i frame sono l'equivalente dei pacchetti al livello 2 nello stack ISO/OSI). Quello che fa un packet sniffer è impostare la scheda di rete in promiscuous mode, una modalità di funzionamento che permette al sistema operativo di accedere anche ai frame normalmente scartati dal NIC.

Chiedersi a cosa possa servire un packet sniffer è più o meno come chiedersi a cosa serva un coltellino svizzero: dipende dall'occasione. Giusto per elencarene qualche possibile utilizzo:

  • Effettuare una diagnostica delle connessioni di rete
  • Sniffare password o altri dati sensibili
  • Studiare il funzionamento dei protocolli di rete

In questo testo vedremo come programmare un packet sniffer non degno di essere nemmeno lontanamente paragonato con Ethereal (a.k.a. Wireshark), e probabilmente neanche con tcpdump, ma spero che sarà un esercizio interessante per chi vuole avvicinarsi alla programmazione delle socket a basso livello.

Prerequisiti

Assolutamente indispensabile una conoscenza non eccessivamente superficiale del C (ma non serve che siate Kerningham & Ritchie). Anche se spiegherò quasi tutte le istruzioni utilizzate, un'infarinatura sulla programmazione delle socket sotto Windows è molto consigliata, un buon riferimeto può essere la non aggiornatissima ma sempre valida Winsock programmer's FAQ. È anche bene che conosciate abbastanza bene lo stack TCP/IP, quindi una lettura alle RFC 791 (protocollo IP), RFC 792 (protocollo ICMP), RFC 768 (protocollo UDP) e RFC 793 (protocollo TCP) non può che far bene (anche se come diceva qualcuno leggere le RFC è uno sport fuori moda).

Insomma: qui spiego come scrivere un packet sniffer, non riprendo tutto ab ovo.

Questo packet sniffer si basa sull'utilizzo delle raw socket. La disponibilità delle raw socket sono state al centro di un dibattito piuttosto acceso (il troll di turno era Steve Gibson, che sarà pure un esperto di sicurezza, ma a volte è illuminato da lampi di imbecillità), quindi su alcune piattaforme sono disponibili, su altre no, su altre boh. AFAIK le raw socket sono disponibili su Windows 2000, Windows XP Home, Windows XP Home SP1 e Windows XP Professional agli utenti con privilegi di amministratore, ma non ho indagato più di tanto, quindi siete invitati a sperimentare.

L'idea di base

In realtà la tecnica di base per creare il packet sniffer non è molto complessa: quello che si fa è creare una socket e effettuarne il binding su un certo indirizzo di rete (non è possibile effettuare il binding con INADDR_ANY) e tramite una chiamata alla funzione WSAIoctl comunicare a Windows che si desidera che l'interfaccia di rete su cui è in ascolto la socket sia impostata in modalità promiscua per poter ricevere tutti i dati fisicamente ricevibili. Fatto questo basta un semplice loop su recv per ricevere i dati dalla socket, che restituirà i dati compresi gli header dei protocolli: non resta che stampare a video o su file i dati interessanti. Tutto il resto sono fronzoli per determinare gli indirizzi IP su cui è possibile mettersi in ascolto, elaborare i dati del pacchetto e così via.

Implementazione

Strutture necessarie

Per analizzare comodamente i pacchetti ricevuti è bene creare le strutture adatte per accedere facilmente ai dati contenuti. Dopo una rapida lettura alle RFC prima citate, non è difficile scrivere le seguenti strutture:

// header pacchetto IP
typedef struct ipheader
{
   // versione IP e lunghezza dell'header in
   // word da 32 bit
   unsigned char ver_ihl;
   // Type Of Service
   unsigned char tos;
   // lunghezza totale del pacchetto header compreso
   unsigned short tot_len;
   //identificativo univoco del pacchetto
   unsigned short id;
   // flag del pacchetto e offset del frammento
   unsigned short flag_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 */
   // Time To Live
   unsigned char ttl;
   // identificativo del protocollo
   unsigned char protocol;
   // checksum del pacchetto
   unsigned short checksum;
   // indirizzo del mittente
   unsigned long saddr;
   // indirizzo del destinatario
   unsigned long daddr;
} iphdr;


// header pacchetto ICMP
typedef struct icmpheader
{
   // tipo di messaggio
   unsigned char type;
   // codice del messaggio
   unsigned char code;
   // checksum del pacchetto
   unsigned short checksum;
   // union contenente gli altri dati
   // a seconda del tipo di messaggio
   union
   {
      // struttura per messaggi tipo
      // echo e echo reply
      struct
      {
         unsigned short id;
         unsigned short sequence;
      } echo;
      // indirizzo del gateway per
      // messaggi tipo redirect
      unsigned long gateway;
   } un;
} icmphdr;


// header pacchetto TCP
typedef struct tcpheader
{
   // porta sorgente
   unsigned short sport;
   // porta di destinazione
   unsigned short dport;
   // numero di sequenza del pacchetto
   unsigned long seq;
   // numero di ack nello stream
   unsigned long ack;
   unsigned short off_reserved_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
   // finestra di ottetti disponibile per
   // la ricezione
   unsigned short window;
   // checksum
   unsigned short checksum;
   // puntatore ai dati urgenti
   unsigned short urp;
} tcphdr;


// header pacchetto UDP
typedef struct udpheader
{
   // porta sorgente
   unsigned short sport;
   // porta di destinazione
   unsigned short dport;
   // lunghezza dell'header udp
   // e dei dati
   unsigned short len;
   // checksum del pacchetto
   unsigned short checksum;
} udphdr;

(Per chi se lo stesse chiedendo: sì ho copiato parte di questo codice dal sorgente del kernel Linux, evviva l'open source).

Inizializzazione

Il primo passo è inizializzare i winsock, con la solita riga di codice:

int main(int argc, char *argv[])
{
   WSADATA wsaData;

   /* ... */

   WSAStartup(MAKEWORD(2, 1), &wsaData);

Nel codice che è possibile scaricare potrete anche vedere come indicare i possibili codici di errore in modo significativo.

Per impostare l'interfaccia di rete in modalità promiscua è necessario utilizzare la funzione WSAIoctl, a cui viene passato come parametro il flag SIO_RCVALL. A proposito di questo flag la documentazione di Windows riporta:

SIO_RCVALL: enables a socket to receive all IP packets on the network. The socket handle passed to the WSAIoctl function must be of AF_INET address family, SOCK_RAW socket type, and IPPROTO_IP protocol. The socket also must be bound to an explicit local interface, which means that you cannot bind to INADDR_ANY.

Once the socket is bound and the ioctl set, calls to the WSARecv or recv functions return IP datagrams passing through the given interface. Note that you must supply a sufficiently large buffer. Setting this ioctl requires Administrator privilege on the local computer. SIO_RCVALL is available in Windows 2000 and later versions of Windows.

Seguiamo le indicazioni dell'SDK e creiamo un socket con le impostazioni corrette:

   sock = WSASocket(AF_INET, SOCK_RAW, IPPROTO_IP, NULL, 0, 0);

Anche qui è necessario altro codice per la gestione dei codici di errore.

Fatto questo mostriamo gli indirizzi IP disponibili per effettuare il binding della socket. Per ottenere questo risultato ci si affida ancora una volta a WSAIoctl, a cui si passa il flag SIO_GET_INTERFACE_LIST, che restituisce un array di strutture INTERFACE_INFO, che permette di recuperare l'indirizzo di rete di ogni interfaccia presente sul sistema. Il codice necessario è il seguente:

void listAddresses() {
   //variabile per contenere il l'hostname del sistema
   char hostname[256];
   int i;
   DWORD nBytesReturned;
   SOCKET sd;
   // array di INTERFACE_INFO per ricevere i dati da WSAIoctl
   INTERFACE_INFO interfaceList[20];

   // recuperiamo il nome host del sistema
   if (gethostname(hostname, sizeof(hostname))==SOCKET_ERROR) {
      // in caso di errore si potrbbe rendere l'output più verboso
      fprintf(stderr, "Errore in gethostname(): %d\n", WSAGetLastError());
      WSACleanup();
      exit(-1);
   }

   // WSAIoctl richiede come parametro un socket, ne creiamo uno
   sd = WSASocket(AF_INET, SOCK_DGRAM, 0, 0, 0, 0);
   if (sd == SOCKET_ERROR) {
      fprintf(stderr, "Errore in WSASocket(): %d\n", WSAGetLastError());
      WSACleanup();
      exit(-1);
   }

   // chiediamo la lista delle interfacce disponibili a WSAIoctl
   if (WSAIoctl(sd, SIO_GET_INTERFACE_LIST, 0, 0, &interfaceList, sizeof(interfaceList), &nBytesReturned, 0, 0)==SOCKET_ERROR) {
      fprintf(stderr, "Errore in WSAIoctl(): %d\n", WSAGetLastError());
      closesocket(sd);
      WSACleanup();
      exit(-1);
   }

   // stampiamo i risultati su schermo
   for (i=0; i<(strlen("Indirizzi IP disponibili per ")+strlen(hostname)+3); i++)
      printf("=");
   printf("\n");
   printf("Indirizzi IP disponibili per %s:\n", hostname);
   for (i=0; i<(nBytesReturned/sizeof(INTERFACE_INFO)); i++) {
      printf("* %s\n", inet_ntoa(interfaceList[i].iiAddress.AddressIn.sin_addr));
   }
   closesocket(sd);
}

Si chiede dunque all'utente su quale indirizzo IP voglia effettuare il binding, a tale scopo si utilizza la solita struttura sockadrr_in (usatissima nella programmazione dei winsock) e la funzione bind:

   struct sockaddr_in sockaddr;
   char ipaddr[16];
   int retcode;

   /* ... */

   // azzeriamo il contenuto di sockaddr
   memset(&sockaddr, 0, sizeof(sockaddr));
   printf("Indirizzo ip (es.: 192.168.0.2)? ");
   fflush(stdout);
   fgets(ipaddr, sizeof(ipaddr), stdin);
   printf("Tento di effettuare il binding su %s\n", ipaddr);
   
   // inseriamo l'indirizzo ip in sockaddr, opportunamente convertito
   // in come intero di 32 bit con i byte in "network order"
   sockaddr.sin_addr.s_addr = inet_addr(ipaddr);
   sockaddr.sin_family = AF_INET;
   sockaddr.sin_port = htons(0);


   // cerchiamo di effettuare il bind
   if (bind(sock, (struct sockaddr *)&sockaddr, sizeof(sockaddr))==SOCKET_ERROR) {
      retcode=WSAGetLastError();
      WSACleanup();
      fprintf(stderr, "Errore in bind(): - %d\n", retcode);
      exit(-1);
   }

Veniamo ora al vero fulcro che permette al sistema di funzionare: WSAIoctl. Tale funzione permette di controllare in modo molto accurato il funzionamento delle socket. La signature del metodo è:

int WSAIoctl(SOCKET s, DWORD dwIoControlCode, LPVOID lpvInBuffer, DWORD cbInBuffer, LPVOID lpvOutBuffer, DWORD cbOutBuffer, LPDWORD lpcbBytesReturned, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine)

Quello che ci interessa è richiamare WSAIoctl sulla socket sock passando come argomento dwIoControlCode la costante SIO_RCVALL. Per motivi che mi sfuggono tale costante non è definita negli header, affinchè tutto funzioni è necessario aggiungere la riga:

#define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)

Possiamo quindi richiamare la funzione. Poiché molti parametri non ci interessano, utilizziamo NULL o 0 ove possibile:

   if (WSAIoctl(sock, SIO_RCVALL, &retcode, sizeof(retcode), NULL, 0, &bytesReturned, NULL, NULL)==SOCKET_ERROR) {
      retcode=WSAGetLastError();
      closesocket(sock);
      WSACleanup();
      fprintf(stderr, "Errore in WSAIoctl(): - %d\n", retcode);
      exit(-1);
   }

Possiamo ora eseguire il loop principale, che non fa altro che ciclare su recvfrom e analizzare i pacchetti ricevuti (si noti come la socket si comporti come se fosse di tipo SOCK_DGRAM).

Analisi dei pacchetti

Considerando le strutture dati presentate, se si ha accesso diretto alle struct correttamente formate è relativamente semplice presentarne le informazioni in modo strutturato. Per mostrare le informazioni dell'header IP è sufficiente il seguente codice:

void printIPheaderinfo(iphdr iph) {
   struct in_addr temp;
   printf("Header IP:\n");
   printf("Ver: %d\nLunghezza header: %d byte", iph.ver_ihl >> 4, iph.ver_ihl & 0xF);
   printf("ToS: %d\nLunghezza totale: %d byte", iph.tos, ntohs(iph.tot_len));
   // la funzione ntohs() converte un intero "short" (16 bit) dal "network order" (big endian) all'"host order"
   // (little endian su microprocessori x86)
   printf("ID: %d\n", ntohs(iph.id));
   printf("Flags: RF = %d, DF = %d, MF = %d", iph.flag_off & IP_RF != 0 ? 1:0, iph.flag_off & IP_DF != 0 ? 1:0, iph.flag_off & IP_MF != 0 ? 1:0);
   printf("Fragment offset: %d\n", ntohs(iph.flag_off & IP_OFFMASK));
   printf("TTL: %d\n", iph.ttl);
   printf("Protocol: %d\n", iph.protocol);
   printf("Checksum: 0x%.08x\n", ntohs(iph.checksum));
   temp.s_addr = iph.saddr;
   printf("Source address: %s\n", inet_ntoa(temp));
   temp.s_addr = iph.daddr;
   printf("Destination address: %s\n", inet_ntoa(temp));
}

In modo simile per il protocollo UDP e TCP:

void printTCPheaderinfo(tcphdr tcph) {
   printf("Header TCP:\n");
   printf("Porta sorgente: %d\nPorta destinazione: %d\n", ntohs(tcph.sport), ntohs(tcph.dport));
   printf("Sequence number: %d\nAck number: %d\n", ntohl(tcph.seq), ntohl(tcph.ack));
   printf("TCP header size: %d byte\n", (tcph.off_reserved_flags & 0xF0)>> 7); // shift a destra di 12 e poi a sinistra di 5 (*=32)
   printf("Flags: FIN = %d, SYN = %d, RST = %d, PUSH = %d, ACK = %d, URG = %d\n", tcph.off_reserved_flags & TH_FIN != 0 ? 1:0, tcph.off_reserved_flags & TH_SYN != 0 ? 1:0, tcph.off_reserved_flags & TH_RST != 0 ? 1:0, tcph.off_reserved_flags & TH_PUSH != 0 ? 1:0, tcph.off_reserved_flags & TH_ACK != 0 ? 1:0, tcph.off_reserved_flags & TH_URG != 0 ? 1:0);
   printf("Window: %d\n", ntohs(tcph.window));
   printf("Checksum: 0x.08x\nUrgent pointer: %d\n", ntohs(tcph.checksum), ntohs(tcph.urp));
}

void printUDPheaderinfo(udphdr udph) {
   printf("Header UDP:\n");
   printf("Porta sorgente: %d\nPorta destinazione: %d\n", ntohs(udph.sport), ntohs(udph.dport));
   printf("Lunghezza totale %d\n", ntohs(udph.len));
   printf("Checksum: 0x%.08x\n", ntohs(udph.checksum));
}

Il codice per l'header ICMP è leggermente più complesso a causa delle specifiche del protocollo:

void printICMPheaderinfo(icmphdr icmph) {
   printf("Header ICMP:\n");
   printf("Type: %d\nCode:%d\n", icmph.type, icmph.code);
   switch (icmph.type) {
      case ICMP_ECHOREPLY:
         printf("ICMP_ECHOREPLY\n");
         printf("id: %d\nsequence number: %d\n",  ntohs(icmph.un.echo.id), ntohs(icmph.un.echo.sequence));
         break;
      case ICMP_DEST_UNREACH:
         printf("ICMP_DEST_UNREACH\n");
         break;
      case ICMP_SOURCE_QUENCH:
         printf("ICMP_SOURCE_QUENCH\n");
         break;
      case ICMP_REDIRECT:
         printf("ICMP_REDIRECT\n");
         break;
      case ICMP_ECHO:
         printf("ICMP_ECHO\n");
         printf("id: %d\nsequence number: %d\n",  ntohs(icmph.un.echo.id), ntohs(icmph.un.echo.sequence));
         break;
      case ICMP_TIME_EXCEEDED:
         printf("ICMP_TIME_EXCEEDED - ");
         if (icmph.code == 0)
            printf("ICMP_EXC_TTL - TTL count exceeded\n");
         else if (icmph.code == 1)
            printf("ICMP_EXC_FRAGTIME - Fragment Reassembly time exceeded\n");
         else
            printf("codice sconosciuto\n");
         break;
      case ICMP_PARAMETERPROB:
         printf("ICMP_PARAMETERPROB\n");
         break;
      case ICMP_TIMESTAMP:
         printf("ICMP_TIMESTAMP\n");
         break;
      case ICMP_TIMESTAMPREPLY:
         printf("ICMP_TIMESTAMPREPLY\n");
         break;
      case ICMP_INFO_REQUEST:
         printf("ICMP_INFO_REQUEST\n");
         break;
      case ICMP_INFO_REPLY:
         printf("ICMP_INFO_REPLY\n");
         break;
      case ICMP_ADDRESS:
         printf("ICMP_ADDRESS\n");
         break;
      case ICMP_ADDRESSREPLY:
         printf("ICMP_ADDRESSREPLY\n");
         break;
      default:
         printf("Tipo di messaggio sconosciuto\n");
   }
}

Il ciclo principale che si occupa di ricevere i pacchetti è molto simile a quello che ci si potrebbe aspettare da un'applicazione server che utilizzi l'UDP: è sostanzialmente un ciclo infinito che riceve i datagrammi e li elabora:

void sniff(SOCKET s) {
   char *buff = (char *)malloc(MAX_PACKET_SIZE), *testo;
   long recvdbytes;
   struct sockaddr_in from;
   int sockaddr_size = sizeof(struct sockaddr_in);
   
   if (buff==NULL) {
      fprintf(stderr, "Errore in malloc()\n");
      closesocket(s);
      WSACleanup();
      exit(-1);
   }

   do {
      memset(buff, 0, MAX_PACKET_SIZE);
      memset(&from, 0, sizeof(struct sockaddr_in));
      recvdbytes = recvfrom(s, buff, MAX_PACKET_SIZE, 0, (struct sockaddr *)&from, &sockaddr_size);
      if (recvdbytes==SOCKET_ERROR) {
         fprintf(stderr, "Errore in recvfrom(): %d\n", WSAGetLastError());
         free(buff);
         closesocket(s);
         WSACleanup();
         exit(-1);
      }
      analyzePacket(buff, recvdbytes);
   } while ((recvdbytes > 0) && (getch()==-1));
   
   free(buff);
}

La funzione analyzePacket() stampa i dati dell'header IP e, se possibile, anche quelli del protocollo incapsulato al suo interno tramite le funzioni precedentemente mostrate:

int analyzePacket(char* buff, int size) {
   iphdr *ipheader;
   tcphdr *tcpheader;
   udphdr *udpheader;
   icmphdr *icmpheader;
   int i;
   
   if (size<sizeof(iphdr))
      return -1;
      
   printf("======= Pacchetto catturato =======\n");
   ipheader = (iphdr *)buff;
   printIPheaderinfo(*ipheader);
   switch (ipheader->protocol) {
      case IPPROTO_TCP:
         tcpheader = (tcphdr*)(buff+20);
         printTCPheaderinfo(*tcpheader);
         break;
      case IPPROTO_UDP:
         udpheader = (udphdr*)(buff+20);
         printUDPheaderinfo(*udpheader);
         break;
      case IPPROTO_ICMP:
         icmpheader = (icmphdr*)(buff+20);
         printICMPheaderinfo(*icmpheader);
         break;
      default:
         printf("Protocollo di livello 4 non supportato\n");
         break;
   }

Infine viene stampato il dump in esadecimale del contenuto del pacchetto e l'equivalente ASCII, mostrando solo i caratteri stampabili:

   for (i=0; i<size; i++)
      printf("%.02X ", buff[i]);
   printf("\n");
   for (i=0; i<size; i++)
      if (buff[i]>31 && buff[i]<127)
         printf("%c");
      else
         printf(".");

Conclusioni

Il software presentato è di una semplicità disarmante: l'unica difficoltà è sapere quali sono i parametri corretti con cui richiamare le API di Windows in modo da aver accesso a una socket che legga tutto il traffico in entrata e in uscita, per il resto si tratta di operazioni quasi ovvie.

Le possibili espansioni sono innumerevoli:

  • Supporto per le opzioni previste dal protocollo IP e TCP
  • Supporto per i codici contenuti nei pacchetti ICMP
  • Supporto per altri protocolli
  • Possibilità di filtrare i pacchetti
  • Aggiungere un meccanismo di buffering per rendere più efficiente il programma

e molto altro ancora. Si tenga presente che Windows fornisce nativamente il supporto per i socket di questo tipo solo dallo strato di rete 3, vale a dire che possono essere visualizzati solo i pacchetti del protocollo IP; i pacchetti ARP, ad esempio, sono totalmente ignorati, e non è possibile neanche accedere alle informazioni dello strato 2 (vale a dire gli indirizzi MAC); per fare questo è necessario utilizzare librerie esterne, come ad esempio le librerie winpcap, sviluppate dal Politecnico di Torino.

IppatsuMan v0.4