next   previous   contents

Inizializzazione delle librerie e ciclo di cattura dei pacchetti

 Una volta analizzato il file di configurazione e' necessario inizializzare le librerie LibMcrypt e LibPcap utilizzate rispettivamente nelle operazioni di cifratura e decifratura, e per la cattura dei pacchetti di rete.
 Per prima cosa viene chiamata la funzione mcrypt_module_open, che permette di creare un descrittore da passare alle funzioni usate successivamente. Tra i possibili parametri, devono essere specificati algorithm e mode, due puntatori a carattere che specificano l'algoritmo ed il metodo di cifratura da utilizzare. In caso di errore la funzione ritorna il valore MCRYPT_FAILED.

/*open mcrypt module*/
hd = mcrypt_module_open(CryptAlgName, NULL, "cfb", NULL);
if (hd==MCRYPT_FAILED)
 {
 printf("Can't open mcrypt module\n");
 exit(EXIT_FAILURE);
 }

 Gli algoritmi utilizzati e testati con servknock sono Blowfish, Twofish, Rijndael, Serpent ed Xtea, mentre il metodo di cifratura e' cfb.
 Prima di procedere e' stato inoltre necessario costruire il vettore di inizializzazione e la chiave simmetrica necessaria per le operazioni di cifratura e decifratura, partendo dai valori di iv e password specificati nel file di configurazione.
 Per prima cosa si e' verificato quale fosse la massima dimensione in bytes della chiave per l'algoritmo utilizzato, passando il descrittore precedentemente creato alla funzione mcrypt_enc_get_key_size, ed allocando in seguito la memoria necessaria per contenerla (puntata da Key) con una calloc.
 A questo punto si genera la chiave ricorrendo alla funzione mhash_keygen_ext della libreria LibMhash. I parametri passati a tale funzione sono l'algoritmo di generazione della password, una struttura di tipo KEYGEN ove sono specificati l'algoritmo di hashing utilizzato ed alcuni altri parametri, un puntatore all'area di memoria dove sara' memorizzata la chiave, la dimensione della chiave stessa, la stringa da cui generarla (password, ovvero la password definita nel file di configurazione) e la sua lunghezza.
  In maniera del tutto simile, si e' verificato se l'algoritmo di cifratura prescelto avesse o meno un vettore di inizializzazione, passando alla funzione mcrypt_enc_mode_has_iv il descrittore hd. In caso affermativo viene calcolata la dimensione in bytes del vettore di inizializzazione tramite la funzione mcrypt_enc_get_iv_size, si alloca la memoria necessaria per contenerlo con una calloc, e lo si genera ricorrendo nuovamente alla funzione mhash_keygen_ext.
 In questo caso i parametri passati alla funzione sono stati l'algoritmo di generazione della password, la struttura di tipo KEYGEN gia' usata precedentemente, un puntatore all'area di memoria dove sara' memorizzato il vettore d'inizializzazione (IV), la lunghezza del vettore stesso, la stringa da cui generarlo (temp_iv, anch'essa definita nel file di configurazione) e la sua lunghezza.
 Sia nel caso della chiave che del vettore di inizializzazione, gli algoritmi di generazione e di hashing scelti sono stati il generatore delle chiavi utilizzato da mcrypt ed md5, rispettivamente identificati dalle costanti KEYGEN_MCRYPT ed HASH_MD5.

/* get algorithm key size */
keysize = mcrypt_enc_get_key_size(hd);
if (hd==MCRYPT_FAILED)
 {
 printf("Can't open mcrypt module\n");
 exit(EXIT_FAILURE);
 }

/* generate the key using the password */
if (mhash_get_keygen_salt_size(KEYGEN_MCRYPT)==0)
 salt_size=10;
else
 salt_size = mhash_get_keygen_salt_size(KEYGEN_MCRYPT);
salt = calloc( 1, salt_size);
Key = calloc( 1, keysize);
data.hash_algorithm[0] = MHASH_MD5;
data.count = 0;
data.salt = salt;
data.salt_size = salt_size;
mhash_keygen_ext(KEYGEN_MCRYPT, data, Key, keysize, password, strlen(password));

/*generate the iv using the temp_iv*/
if(mcrypt_enc_mode_has_iv(hd))
 {
 /*get iv size*/
 ivsize=mcrypt_enc_get_iv_size(hd);
 IV= calloc(1, ivsize);
 mhash_keygen_ext(KEYGEN_MCRYPT, data, IV, ivsize, temp_iv, strlen(temp_iv));
 }

 A questo punto, si puo' passare all'inizializzazione della libreria LibPcap, la quale permette di accedere al datalink layer in maniera portabile, esportando una API unica tra i vari sistemi Unix-like.
 La scelta di questa libreria e' giustificata dalla necessita' di accedere ai diversi pacchetti prima che possano essere filtrati dal firewall. Si deve intervenire su di essi ad un livello inferiore del modello ISO/OSI, e poiche' i packet filter normalmente lavorano al terzo livello di tale modello, e' necessario leggere i pacchetti quando ancora si trovano al datalink layer.
 Prima di iniziare ad analizzare il traffico di rete (in gergo “sniffare”) per il device specificato nel file di configurazione, si verifica se l'interfaccia esiste ed e' attiva, chiamando pcap_lookupnet. Questa funzione determina l'indirizzo IP e la subnet mask associate all'interfaccia specificata in IfaceName ed in caso di errore ritorna il valore -1 :

/* check if the dev is up and read its parameters */
if ((pcap_lookupnet(IfaceName,&netp,&maskp,errbuf))==-1)
 {
 printf ("Error: pcap_lookupnet(): %s\n\n", errbuf);
 exit(EXIT_FAILURE);
 }

 A questo punto e' necessario creare un descrittore per la cattura dei pacchetti (un puntatore del tipo pcap_t), invocando la funzione pcap_open_live.
 Il primo parametro passatole e' il nome del device di rete da utilizzare (“eth0”, “eth1”, “ppp0”, ecc.), mentre il secondo, snaplen, indica la massima dimensione espressa in bytes della porzione di pacchetto catturabile. Essendo interessati solamente agli header del pacchetto, leggerlo per intero potrebbe essere uno spreco di risorse. Infatti il tempo necessario per trasferire i pacchetti dal kernel al processo utente e quello per processarli una volta ricevuti potrebbe risultare troppo alto, e di conseguenza alcuni pacchetti “scappare” alla cattura.
 Per decidere tale valore si e' considerato che la dimesione massima delle intestazioni IP e TCP, in assenza di opzioni, e' di 60 bytes ciascuno. A questi 120 bytes e' infine necessario aggiungere la lunghezza dell'header relativo al tipo di link_layer. Poiche' tra i link_layer riconosciuti, lo scostamento massimo utilizzato ammonta a 24 bytes, il valore di snaplen e' stato posto a 144. Catturando i primi 144 bytes di ciascun frame, e' quindi possibile conoscere le informazioni fondamentali al fine del riconoscimento delle sequenze, ovvero il protocollo del pacchetto incapsulato, ed i dati degli header TCP ed UDP.
 L'argomento successivo e' un flag che per tutti i valori diversi da zero indica alla libreria di porre l'interfaccia in modalita' promiscua, al fine di ottenere qualunque pacchetto che passi per il device. Ai fini del portknocking si vogliono analizzare esclusivamente i pacchetti destinati al nostro indirizzo, quindi tale valore viene settato a 0. In questo modo la visibilita' dei pacchetti e' ristretta a quelli con l'indirizzo del mittente o del destinatario uguali all'indirizzo hardware dell'interfaccia di rete sulla quale avviene la cattura.
 Il quarto parametro, indica invece un timeout, il cui scopo e' quello di evitare che il kernel debba passare al processo utente ogni pacchetto nel momento in cui viene ricevuto. Specificando tale parametro, il kernel avra' infatti licenza di accumulare i pacchetti fino al raggiungimento del timeout specificato e restituirli tramite un'unica chiamata di sistema, con evidenti vantaggi dal punto di vista delle performances. L'ultimo argomento, infine, e' un buffer di caratteri, che viene riempito dal testo degli eventuali messaggi d'errore.

/* open our interface NOT in promisc mode and without timeout */
if ((descr = pcap_open_live (IfaceName, MAX_BYTES, 0, 500, errbuf))==NULL)
 {
 printf("Error: pcap_open_live(): %s\n\n",errbuf);
 exit(EXIT_FAILURE);
 }

 Il passo successivo consiste nel determinare il tipo di link layer, per mezzo di una chiamata a pcap_datalink, che restituisce un numero che lo rappresenta. Tale operazione e' necessaria per conoscere come sono incapsulati i dati, e ricavare gli offset da cui sono effettivamente memorizzate le varie informazioni. Una volta noto questo valore si puo' usare un costrutto switch per specificare l'offset da utilizzare :

/* look for our datalink and get our offset */
if ((datalink = pcap_datalink(descr)) < 0)
 {
 printf("Error: pcap_datalink(): %s\n\n", errbuf);
 exit(EXIT_FAILURE);
 }

switch (datalink)
 {//switch
 case DLT_EN10MB: /*ethernet*/
  offset = 14;
  break;
 case DLT_NULL:
 case DLT_PPP: /*ppp*/
  offset = 4;
  break;
 case DLT_SLIP: /*serial line*/
  offset = 16;
  break;
 case DLT_RAW: /*raw ip packet*/
  offset = 0;
  break;
 case DLT_SLIP_BSDOS:
 case DLT_PPP_BSDOS:
  offset = 24;
  break;
 case DLT_LINUX_SLL: /*Linux "cooked" capture encapsulation*/
  offset = 16;
  break;
 default:
  printf("Error: Unknown Datalink Type: (%d)\n\n", datalink);
  exit(EXIT_FAILURE);
 }//endswitch

 Una feature molto interessante offerta dalla libreria libpcap e' la possibilita' di agganciare dei filtri ad un descrittore, in modo da ricevere solo i pacchetti che soddisfino determinate condizioni. Il filtro specificato nella stringa filter viene compilato nel formato BPF tramite la funzione pcap_compile, i cui argomenti sono: un descrittore per la cattura dei pacchetti, un puntatore ad una strutura di tipo bpf_program (dove verra' memorizzato il programma compilato), una stringa che rappresenta il filtro da compilare, un flag per attivare/disattivare l'ottimizzazione del programma, ed un intero che rappresenta la maschera di rete dell'interfaccia usata.
 Il filtro viene in seguito “attaccato” al descrittore utilizzando pcap_setfilter, i cui unici due parametri sono un puntatore di tipo pcap_t (il descrittore), ed uno alla struct bpf_program inizializzata con pcap_compile. Una volta agganciato il filtro si puo' liberare la memoria da esso utilizzata grazie a pcap_freecode.

 /* compile the filter */
if(pcap_compile(descr,&fp,filter,0,netp) == -1)
 {
 printf( "Error calling pcap_compile()\n\n");
 exit(EXIT_FAILURE);
 }

/* set the filter */
if(pcap_setfilter(descr,&fp) == -1)
 {
 printf("Error calling pcap_setfilter()\n\n");
 exit(EXIT_FAILURE);
 }

/*free memory*/
pcap_freecode(&fp)

 Come si puo' notare, nei sorgenti di servknock sono presenti le righe di codice che permettono l'utilizzo dei filtri, anche se attualmente sono inutilizzate: la stringa filter ha infatti valore NULL.
 Poiche' servknock e' stato sviluppato e testato esclusivamente sotto linux, l'utilizzo dei filtri non avrebbe portato significativi benefici alle prestazioni, dato che le funzionalta' di BPF sarebbero solamente state emulate. Le righe di codice precedenti sono pertanto presenti nell'ottica di un eventuale porting su sistema BSD, dove queste features sarebbero native e non necessiterebbero dell'emulazione.
 Dopo avere scelto ed impostato un'interfaccia, e raccolte le informazioni sul tipo di link layer, puo' avere inizio la sessione di sniffing, leggendo i pacchetti grazie alla funzione pcap_next. I due argomenti che devono esserle passati sono il descrittore restituito dalla precendete chiamata a pcap_open_live ed un puntatore a una struttura pcap_pkthdr che verra' riempita con le informazioni di base sul pacchetto. La funzione restituisce un puntatore ad un buffer che contiene la parte di pacchetto catturata.
 Una volta catturato, ogni pacchetto viene poi passato alla funzione proc_packet, che ha il compito di analizzarlo, per riconoscere o invalidare le eventuali sequenze di knock :

/* read packets */
while(1)
 {
 /* read with pcap_next() */
 if ( (packet = (char *) pcap_next (descr, &hdr)) == NULL)
  continue;
 /*process the packet*/
 proc_packet(packet, offset, &hdr);
 }

 Ulteriori informazioni riguardanti la libreria libpcap ed i concetti fondamentali alla base del suo utilizzo sono presenti nella sezione 3.4.

Cattura e gestione dei pacchetti in Module Knock

Cattura dei pacchetti
 Dato che Module Knock e' stato progettato come modulo aggiuntivo di Hogwash, non ci si deve occupare direttamente della cattura dei pacchetti per mezzo delle Libpcap, che invece vengono catturati e gestiti dall'intrusion detection system.
 Come per la soluzione standalone servknock , la cattura dei pacchetti in Hogwash avviene a livello datalink: e' quindi possibile accedere agli header dei pacchetti prima che essi possano essere scartati dal firewall, che tipicamente lavora al terzo/quarto livello della pila ISO/OSI. In questo modo e' possibile analizzare i pacchetti e capire se facciano parte o meno di una sequenza di knock valida. Il layer datalink e' dunque il piu' basso che puo' essere visto dal software, poiche' il layer inferiore e' rappresentato dall'hardware. Si ricorda infatti che il datalink layer si trova al secondo livello del modello ISO/OSI, ed il suo compito e' quello di assicurare la consegna dei messaggi al giusto device e dividerli in frame che vengono trasmessi utilizzando lo strato fisico sottostante.
 Allo stato attuale Hogwash puo' accedere al datalink layer per mezzo del BPF in sistemi BSD, per mezzo della famiglia di socket PF_PACKET sotto Linux o tramite la lettura di file prodotti da tcpdump. L'accesso per mezzo di DLPI di Solaris e' in fase di implementazione, e quindi non ancora perfettamente funzionante. Nel file di configurazione e' quindi possibile specificare come Hogwash comunichera' con l'interfaccia; le possibili opzioni sono:
- linux_raw
- obsd_bpf
- osx_bpf
- tcpdump
- solaris_dlpi


 Nello sviluppo del prototipo, e' stata utilizzata l'opzione linux_raw, che sta ad indicare l'utilizzo delle socket di tipo SOCK_RAW, della famiglia PF_PACKET per la cattura dei pacchetti. Le raw socket sono un tipo di socket che permettono di scendere al di sotto del quarto livello della stratificazione ISO/OSI.

 Tramite la funzione OpenInterfaceLinuxRaw, contenuta in packet_linux_raw.c all'interno della cartella packets di Hogwash, si puo' accedere ad un interfaccia tramite raw sockets. In particolare con la chiamata socket() viene creata la socket.

fd=socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

 In seguito la funzione bind associa il descrittore del socket all'indirizzo della scheda di rete utilizzata per la cattura dei pacchetti, specificato dalla struttura sockaddr_ll sll.

bzero(&sll,sizeof(struct sockaddr_ll));
  sll.sll_family=AF_PACKET;
  sll.sll_ifindex=get_device_id(fd, Interface->Name);
  sll.sll_protocol=htons(ETH_P_ALL);
  ssize=sizeof(struct sockaddr_ll);

  errnum=bind(fd, (struct sockaddr*)&sll, sizeof(sll));
  if (errnum==-1){
    printf("Error Binding socket\n");
    return FALSE;
  }

 Una volta effettuata questa operazione Hogwash e' in grado di cominciare la lettura dei pacchetti di rete dall'interfaccia, semplicemente per mezzo della chiamata read(), che avviene all'interno della funzione ReadPacketLinuxRaw, passando come parametri il socket descriptor, il buffer in cui verra' memorizzato il pacchetto e la dimensione del buffer stesso.

count = read(Interface->FD, (char*)p->RawPacket, TYPICAL_PACKET_SIZE-1);

 I pacchetti cosi' catturati vengono posti in coda all'interno di un array di 512 posizioni in attesa di essere processati.
 Prima della cattura viene quindi individuato uno slot disponibile (PacketSlot) all'interno dell'array per mezzo della funzione GetEmptyPacket. Nel caso venga trovato con successo lo slot, il pacchetto puo' essere memorizzato nel campo RawPacket, presente all'interno di un elemento PacketRec, definito nell'header file hogwash.h.
L'array di PacketRec e' utilizzato per situazioni multi-thread, in cui cioe' vengano creati piu' thread di cattura dei pacchetti per due o piu' interfacce di rete presenti sulla macchina.
 Tipicamente, nel caso i pacchetti vengano intercettati su una sola interfaccia, non esistono piu' thread paralleli di cattura e quindi viene utilizzata una sola posizione dell'array (PacketSlot assume sempre valore 0).

Decodifica
 Una volta catturato un pacchetto puo' essere processato: per prima cosa e' necessario estrarre tutte le informazioni riguardanti gli header che lo incapsulano.
 Come gia' detto in precedenza il paccheto catturato puo' essere quindi visto come una “collezione” di strutture (ad esempio per un pacchetto TCP/IP saranno compresi gli header Ethernet, IP, TCP, ed il payload del pacchetto stesso). Semplicemente il contenuto delle strutture rappresentanti il pacchetto catturato viene posto in una stringa accessibile da un puntatore u_char * (il campo RawPacket di PacketRec). Tale puntatore puo' essere visto come una rappresentazione “serializzata” di queste strutture, e quindi per accedere ad esse si rende necessario ricorrere a delle operazioni di typecasting. Queste operazioni sono affidate in Hogwash a quelli che vengono chiamati decoder, struct C definite in questo modo:

typedef struct decoder_rec{
  char Name[MAX_NAME_LEN];
  int ID;
  unsigned char DependencyMask[MAX_RULES/8];
  struct test_rec* Tests;
  struct module_rec* Modules;
  struct decoder_rec* Children;
  struct decoder_rec* Parent;
  struct decoder_rec* NextChild;

  void* (*DecodeFunc) (int PacketSlot);

  char Active; /*true if anything actually uses it*/
} DecoderRec;

Quando Hogwash viene avviato, viene allocata una serie di decoder, i quali vengono successivamente inizializzati. Di particolare rilevanza e' il puntatore a funzione Decodefunc: a seconda del tipo di decoder questo verra' fatto puntare ad una funzione adeguata, utilizzata per l'estrazione dell'header desiderato. Ad esempio, nel caso il decoder sia di tipo IP, DecodeFunc verra' fatto puntare alla funzione DecodeIP contenuta nel codice sorgente decode_ip.c, collocato nella cartella decoders di Hogwash. Questa funzione, conoscendo l'indirizzo di memoria del pacchetto e l'offset dell'header Ethernet, permette quindi di estrarre le informazioni contenute nell'header IP:

data->Header=(IPHdr*)(p->RawPacket+p->BeginData);

La funzione di decodifica e' quindi un operazione ricorsiva che consiste nell'applicazione dei decoders al pacchetto tramite una visita in preordine dell'albero dei decoders. Questo albero e' costruito grazie ai tre puntatori a struct decoder_rec, Children, Parent e NextChild, presenti all'interno della struct decoder_rec:

Un altro aspetto importante della struttura dei decoder, e' la presenza di un puntatore ad una struct module_rec. Il campo modules infatti punta alla testa di una lista di moduli associati al decoder: in questo modo, una volta che il decoder e' stato applicato al pacchetto vengono eseguite le funzioni di analisi implementate nei vari moduli.


next   previous   contents