MokaByte 63 - Maggio 2002 
Packet Sniffing in Java
di
Paolo Mascia
Uno sniffer è una utility per cattura delle informazioni che viaggiano attraverso un tratto di rete. Utilizzando JNI è possibile creare, in Java, un sofisticato sistema per il filtraggio e l'analisi di tutti pacchetti di dati che giungono ad una interfaccia di rete indipendentemente dall'indirizzo di destinazione

Le informazioni che viaggiano attraverso una rete Ethernet, sono ricevute contemporaneamente da tutte le schede connesse al bus interessato dal traffico dei pacchetti di dati. Nell'header Ethernet di ciascun pacchetto è specificato l'indirizzo (MAC address) dell'interfaccia del destinatario. Le interfaccie controllano se questo indirizzo di destinazione corrisponde al proprio e in tal caso passano l'informazione agli strati superiori, altrimenti la ignorano.
Le schede di rete, tuttavia, supportano la cosiddetta modalità promiscua, che consente loro di accettare tutto il traffico in viaggio sulla rete, indipendentemente dall'indirizzo di destinazione, e conseguentemente di passarlo ad una applicazione in grado di analizzarlo.
Predisponendo una scheda di rete in modalità promiscua e realizzando opportuni filtri, è possibile monitorare tutto o parte del traffico che passa attraverso il tratto di rete al quale è connessa la propria stazione.
Questo è il principio sul quale si basano gli analizzatori di rete, strumenti utili per scoprire e risolvere eventuali anomalie.
L'astrazione della java virtual machine pone il limite alla realizzazione di programmi Java in grado di interagire a basso livello con l'hardware della macchina. Tuttavia ricorrendo ai metodi nativi (JNI) e utilizzando degli espedienti opportuni, è possibile realizzare una libreria Java in grado di gestire la scheda di rete in modalità promiscua ed utilizzare il sistema molto veloce di filtraggio dei pacchetti del BPF.
Prima di affrontare le problematiche della programmazione Java, vediamo più in dettaglio l'architettura BPF e la libreria c pcap.

 

BSD Packet Filter
Molte versioni di Unix forniscono una interfaccia a livello di collegamento, mediante un driver basato sul kernel, detto BPF (Berkeley Packet Filter). Questo possiede alcune ottime funzioni che agevolano enormemente l'elaborazione e il filtraggio dei pacchetti.
Il driver BPF possiede, infatti, un meccanismo di filtraggio nel kernel, realizzato mediante una macchina virtuale. Questa macchina è dotata di semplicissime istruzioni, che consentono l'esame di ciascun pacchetto attraverso un piccolo programma che l'utente carica nel kernel. Il programma viene eseguito sul pacchetto appena ricevuto e quest'ultimo viene passato agli strati più alti dell'applicazione solo se corrisponde alle specifiche del filtro.
Il driver BPF è costituito da due componenti: il network tap e il packet filter.
Network Tap - Il network tap non fa altro che collezionare copie dei pacchetti provenienti dal device driver di rete e smistarle alle applicazioni che sono in ascolto. Dopo ciò è il filtro a decidere se la copia del pacchetto verrà accettata. Quando un pacchetto arriva all'interfaccia di rete, normalmente il device driver lo passa allo stack di protocollo. Quando il BPF è attivo, su tale interfaccia, il driver chiama per prima cosa il BPF. Quest'ultimo passa il pacchetto ad ogni filtro a livello utente in ascolto, che si occupa poi di decidere se o quale parte del pacchetto passare all'applicazione.
Quindi per ogni filtro che accetta il pacchetto , il BPF crea una copia del quantitativo richiesto di dati nel buffer associato con quel filtro.
Dopo ciò, il device driver riacquisisce il controllo e consegna il pacchetto allo stack di protocollo se era ivi destinato, altrimenti lo lascia transitare.
Packet Filtering - Poiché i programmi di monitoraggio della rete, spesso, richiedono di esaminare soltanto una parte dei pacchetti, il packet filtering svolge un grosso lavoro di filtraggio. Per minimizzare il carico in memoria, che costituisce il principale collo di bottiglia nelle workstation, il pacchetto viene filtrato là dove il DMA dell'interfaccia di rete lo memorizza senza copiarlo in un altro buffer del kernel. In questo modo, se il pacchetto viene scartato, soltanto i pochi byte richiesti dal processo del filtro vengono referenziati. E' per questo che il BPF è preferibile agli altri, quali ad esempio lo STREAMS NIT della Sun che copia sempre il pacchetto in un buffer prima di passarlo al filtro che dovrà esaminarlo ed in seguito accettarlo o rifiutarlo. In questo modo il NIT spreca cicli di CPU anche per i pacchetti che non corrispondono alle specifiche del filtro.

 

Libpcap
Libpcap è una libreria portatile multipiattaforma che implementa il sistema di filtraggio BPF.
Libcap astrae l'interfaccia del livello collegamento su svariati sistemi operativi e crea un' API semplice e standardizzata. Ciò consente la creazione di codice portabile in grado di utilizzare una sola interfaccia per più sistemi operativi.
Attualmente libpcap è disponibile per quasi tutti i sistemi operativi Unix ed esiste un porting anche in ambiente Windows denominato winpcap.
Scopo di questo articolo è descrivere la creazione di una classe Java in grado di eseguire, tramite le JNI, tutte le funzioni di libcap consentendo così la realizzazione in Java di applicazione per il monitoraggio della rete.

 

Dai puntatori C alle classi Java
La principale differenza tra il C e Java è che il primo utilizza in larga misura i puntatori: Molte delle funzioni della libreria libpcap, quando vengono invocate, allocano degli spazi di memoria contenenti delle strutture dati e ne ritornano l'indirizzo in un puntatore. Questi puntatori vengono poi utilizzati come parametri per le altre funzioni della libreria.
Il primo problema da risolvere è quindi quello di creare una classe Java in grado di memorizzare un puntatore e fornire la stessa dei metodi necessari per accedere ai dati in memoria.
Consideriamo, come esempio, la seguente porzione di codice C che alloca ed inizializza una struttura utilizzata, poi, come parametro per la funzione my_C_function.

struct {
  int x ;
  int y ;
} Point ;

struct Point *p ;

p = malloc(Point) ;

p -> x = 100 ;
p -> y = 100 ;
...
my_C_function ( p ) ;
...

void my_C_function(struct Point *p) {
  int x = p -> x ;
  int y = p -> y ;
  ...
  ...
}

La nostra classe Java dovrà per prima cosa memorizzare il puntatore, compatibilmente con il sistema operativo, in maniera da poterlo sia utilizzare come parametro per le chiamate delle funzioni native che lo richiedono, sia per poter accedere ai dati indicizzati.
A tale scopo è ipotizzabile memorizzare il puntatore in un array di byte copiandone in successione i singoli byte che lo costituiscono.
In tal modo un qualsiasi puntatore viene trasformato in Java in un array di byte e la chiamata alla funzione my_C_function sarà effettuata con un metodo nativo Java dichiarato nel modo seguente:

native void my_function(byte[] p) ;

Le prime istruzioni C dell'implementazione del metodo nativo che richiama la funzione my_C_function, provvederanno a trasformare l'array nel puntatore originario semplicemente copiandone i byte direttamente nell'indirizzo di memoria del puntatore. Se chiamiamo CLib la classe contenente il metodo my_function, il prototipo della funzione C generato da javah sarà:

JNIEXPORT void JNICALL Java_CLib_my_function (JNIEnv *, jobject, jbyteArray) ;

Quindi il codice che implementa la funzione dovrà estrarre il puntatore dall'array jbyteArray, e chiamare la funzione C my_C_function. Il codice seguente mostra come effettuare questa operazione:

JNIEXPORT void JNICALL Java_CLib_my_function (JNIEnv *env, jobject jobj, jbyteArray jba_pointer)
{
struct point *p ;
(*env)->GetByteArrayRegion(env,jba_pointer,0,sizeof(void*),&p );
my_C_function(p) ;
}

Per assegnare al puntatore il suo valore corretto è stato utilizzato il metodo GetByteArrayRegion. Questa funzione copia in memoria i byte dell'array, passato come secondo parametro, a partire dall'indirizzo specificato nell'ultimo parametro. Nel nostro caso quest'ultimo parametro, è l'indirizzo di memoria dove è memorizzato il puntatore. Quindi i byte copiati rappresentano i singoli byte che costituiscono il puntatore nell'ordine in cui sono immagazzinati in memoria. Il numero di byte da copiare è determinato dalla funzione C sizeof(void*) che ritorna le dimensioni di un puntatore generico.

Per passare un puntatore dal C a Java è necessario effettuare l'operazione inversa, cioè copiare in un array di byte i singoli byte che costituiscono il puntatore. Il codice C seguente effettua questa operazione

jbyteArray ret ;
……
ret = (*env)->NewByteArray(env , sizeof(void*) ) ;
(*env)->SetByteArrayRegion(env, ret , 0 , sizeof(void*), &p );
……
return ret ;

Abbiamo visto come è possibile convertire un qualsiasi puntatore in un semplice array di byte e viceversa. L'array così ottenuto, tuttavia, e del tutto generico e non fa riferimento al tipo di struttura puntata. Se, quindi, abbiamo diversi metodi che richiedono puntatori a strutture differenti nasce l'esigenza di tipizzare i parametri. Conviene, in questo caso, utilizzare un metodo aggiuntivo che oltre a tipizzare il parametro, rende trasparente l'espediente utilizzato per memorizzare il puntatore. Ricorreremo quindi a una coppia di metodi così definiti:

public class Clib
{
……
public void myFunction(Point p)
{ my_function(p.getPointer()) ;
}
……
private native void my_function(byte[] p) ;
……
}

La classe Point, che corrisponde al nostro puntatore C, è una sottoclasse della classe Pointer così definita:

public class Pointer {

private byte[] pointer ;

public byte[] getPointer()
{ return pointer ;
}

public void setPointer(byte[] pointer)
{ this.pointer = pointer ;
}

}


public class Point extends Pointer
{
}

In tal modo abbiamo ottenuto i seguenti due risultati:

  • la funzione myFunction può essere chiamata solo con un parametro di tipo Point
  • la chiamata viene effettuata concordemente alla chiamata C con la sola differenza che invece di passare un puntatore alla struttura point, passiamo un oggetto di tipo Point.

E' ora possibile aggiungere alla classe Point, utilizzabile così com'è solo per memorizzare un puntatore, dei metodi per accedere alle variabili membro.

public class Point extends Pointer
{
private static native int get_x (byte[] p ) ;
private static native void set_x (byte[] p, int newValue) ;
private static native int get_y (byte[] p) ;
private static native void set_y (byte[] p, int newValue) ;

public int getX ()
{ return get_x (getPointer()) ;
}

public void setX (int newValue)
{ set_x (getPointer() , newValue) ;
}

public int getY ()
{ return get_y (getPointer()) ;
}

public void setY (int newValue)
{ set_y (getPointer() , newValue) ;
}
}


I metodi C provvederanno a convertire l'array in un puntatore e imposteranno o ritorneranno i valori richiesti:

JNIEXPORT jint JNICALL Java_Point_get_1x (JNIEnv *env, jclass jcl , jbyteArray jba_pointer )
{
struct point *p ;
(*env)->GetByteArrayRegion(env,jba_pointer,0,sizeof(void*),&p);
return p->x ;
}

JNIEXPORT void JNICALL Java_PPoint_set_1x(JNIEnv *env, jclass jcl, jbyteArray jba_pointer , jint new_value)
{
struct point *p ;
(*env)->GetByteArrayRegion(env,jba_pointer,0,sizeof(void*),&p);
p->x = new_value ;
}


Per completare la classe Point mancano ora soltanto i costruttori. Un primo costruttore prevederà come parametro un puntatore ad una struttura già allocata, cosa che si verifica nel caso in cui il puntatore è ritornato da una funzione C.

public Point(byte[] pointer)
{ this.pointer = pointer ;
}

Il secondo costruttore, dovrà invece chiamare un metodo nativo che allochi realmente la memoria necessaria.

private native byte[] newPoint() ;
……
public Point()
{ pointer = newPoint() ;
}


In conclusione le istruzioni che abbiamo visto nel listato (1) si trasformano in Java nelle seguenti:

Point p = new Point( ) ;
p.setX(100) ;
p.setY(100) ;
clib.myFunction(p) ;


Con questa metodologia è quindi possibile utilizzare le funzioni di una libreria C come dei normali metodi Java.

Il package pcksniff
Il package pcksniff, i cui sorgenti sono disponibili in allegato, è una implementazione pratica di quanto esposto nel paragrafo precedente.
Questo package esporta come metodi Java i metodi della libreria C libpcap descritta in precedenza. Ogni funzione C di libcap ha un metodo Java equivalente nella classe Sniffer. I parametri hanno lo stesso significato e la stessa modalità di utilizzo ad eccezione della sostituzione di alcuni parametri con le classi Java equivalenti.

I metodi della classe Sniffer possono essere raggruppati in cinque categorie funzionali

  • cattura diretta dei pacchetti
  • cattura e salvataggio su file dei pacchetti
  • impostazione del filtro
  • statistiche
  • informazioni sul dispositivo di cattura

Analizziamo ora in dettaglio l'implementazione pratica dei metodi principali della classe Sniffer, tralasciando gli altri di cui daremo solo una descrizione delle funzionalità e delle modalità di utilizzo.
Il primo metodo della libreria C libcap che esporteremo in Java è pcap_open_live. Questa funzione ha il compito di impostare la scheda di rete nella modalità voluta.
Il prototipo C è :

pcap_t * pcap_open_live(char* device, int snaplen, int promisc, int to_ms, char *ebuf)

device è una stringa contenete il nome del dispositivo da aprire, snaplen è il numero massimo di byte da catturare, promisc è un flag che specifica se adoperare o meno la modalità promiscua, to_ms imposta il time out di lettura in millisecondi, ebuf è un oggetto di tipo StringBuffer. I parametri di tipo int sono facilmente convertibili nel corrispondente tipo Java.
Per quanto riguarda il nome del dispositivo le JNI sono dotate dei metodi necessari per convertire un array di char a 8 bit in una stringa Java di caratteri unicode e viceversa. Quindi è possibile passare il nome del dispositivo come stringa e all'interno dell'implementazione del metodo nativo, che effettuerà la chiamata alla funzione pcap_open_live, convertirla in un array di caratteri ASCII.
Non è, invece, possibile fare la stessa cosa per il messaggio di errore, poiché questo è un parametro di output. Tuttavia le JNI consentono ai metodi nativi di chiamare i metodi delle classi Java. Quindi come ultimo parametro passeremo un oggetto di tipo StringBuffer. Nel caso in cui l'esecuzione di pcap_open_live fallisca, la funzione C chiamerà il metodo append dell'oggetto StringBuffer passandogli come argomento il messaggio di errore.
Infine il valore di ritorno di pcap_open_live è un puntatore che convertiremo in un array di byte.
La funzione nativa Java avrà quindi il seguente tipo di ritorno e la seguente firma:

private native byte[] pcapOpenLive(String device, int snaplen, int promisc, int to_ms, StringBuffer ebuf) ;

La funzione nativa Java è dichiarata private poiché ritorna un array di byte che dobbiamo convertire nella classe appropriata. Quindi il metodo visibile pubblicamente, che dovrà essere invocato per inizializzare una scheda di rete è :

public Adapter openLive (String device, int snaplen, int promisc, int to_ms, StringBuffer ebuf)
{ return new Adapter(pcapOpenLive(device,snaplen,promisc,to_ms, ebuf )) ;
}

Vediamo ora l'implementazione C del metodo nativo :

JNIEXPORT jbyteArray JNICALL Java_pcksniff_Sniffer_pcapOpenLive(JNIEnv *env, jobject jobj, jstring js_device, jint snaplen, jint promisc, jint to_ms, jobject jo_ebuf)
{

// Conversione della stringa unicode in un array
// di caratteri ASCII
//
1 char *device = (*env)->GetStringUTFChars(env, js_device, 0) ;

2 char *ebuf ;
3 jbyteArray ret ;
4 pcap_t *pcap

// Creazione di un un buffer per contenere
// l'eventuale messaggio di errore
//

5 ebuf = malloc(ERR_BUFF_SIZE) ;

// Chiamata della funzione della libreria libpcap
//
6 pcap = pcap_open_live(device,snaplen,promisc,to_ms,ebuf) ;

7 if (!pcap)
8 {
// Se si è verificato un errore
//ritorniamo un array di byte vuoto
//
9 ret = (*env)->NewByteArray(env,0) ;

// Chiamiamo il metodo append dell'oggetto
// ebuf passandogli il messaggio di errore
//
10 jclass cls = (*env)->GetObjectClass(env, jo_ebuf);
11 jmethodID mid = (*env)->GetMethodID(env, cls, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;");

12 (*env)->CallObjectMethod(env, jo_ebuf, mid, (*env)-> NewStringUTF(env,ebuf));

13}
14else
15{

// Se non si sono verificati errori
// copiamo il puntatore nell'array di byte
//

16 ret = (*env)->NewByteArray(env,sizeof(void*)) ;
17 (*env)->SetByteArrayRegion(env,ret,0,sizeof(void*),&pcap);
18}

19 free(ebuf) ;
20 (*env)->ReleaseStringChars(env,js_device,device) ;

21 return ret ;
}

Il codice alla riga 1 effettua la conversione del nome del dispositivo. Gli oggetti String in Java, rappresentati come jstring nelle JNI, sono delle stringhe unicode a 16 bit, mentre in C una stringa è costituita da caratteri di 8 bit. Per accedere ad una stringa Java passata ad una funzione C o viceversa ritornare una stringa C ad un metodo Java occorre utilizzare le funzioni di conversione delle JNI nell'implementazione del metodo nativo.
La funzione GetStringUTFChar estrae una stringa di caratteri a 8 bit da una jstring a 16 bit. Il terzo parametro ritorna JNI_TRUE se la funzione effettua una copia locale della stringa, altrimenti ritorna JNI_FALSE.
Per liberare le risorse allocate dalla stringa occorre infine invocare la funzione alla riga 20.
Le righe da 10 a 12 invocano il metodo append della classe StringBuffer passata come parametro, aggiungendo il messaggio di errore puntato da *ebuf.
Il primo passo da compiere per invocare un metodo da una funzione nativa è di ottenere un riferimento alla classe che lo contiene. Questo può essere effettuato mediante la funzione delle JNI GetObjectClass (riga 10).
Una volta ottenuta la classe, il secondo passo da compiere è chiamare la funzione GetMethodID (riga 11)per ricavare un identificatore del metodo. Poiché Java supporta l'overloading dei metodi, occore specificare oltre al nome del metodo la sua firma. Le JNI utilizzano la firma per denotare il tipo di ritorno di un metodo Java nella seguente forma generale:

(argument-types)return-type

La tabella seguente riassume la codifica dei tipi utilizzati nella firma di un metodo.

V void
Z boolean
B byte
C char
S short
I int
J long
F float
D double
Lfully-qualified-class class
[type type[]

Per esempio la firma "(I)V" denota un metodo che riceve come argomento un intero e ritorna un void. Un espediente per ricavare rapidamente la firma di un metodo, è utilizzare il tool javap per disassemblare il codice di una classe. Infatti aggiungendo lo switch -s l'utility visualizza l'intera firma dei metodi invece dei normali tipi Java.
Nel nostro caso, quindi, la firma del metodo append della classe StringBuffer è "(Ljava/lang/String) Ljava/lang/StringBuffer".
Ottenuto l'identificativo del metodo è possibile invocarlo mediante la funzione Call<type>Method, così come avviene alla riga 12.
Dall'analisi del codice notiamo che se pcap_open_live fallisce l'array di byte ritornato è vuoto. Quindi dopo aver eseguito openLive occorre invocare il metodo isNull della classe Adapter per verificare se l'operazione è riuscita.

Una volta ottenuto un oggetto Adapter è possibile iniziare la cattura dei pacchetti con il metodo dispatch. Quest'ultimo, analogamente a tutti gli altri invocherà un metodo nativo privato. Le firme dei due metodi sono :

private static native int pcapDispatch(byte[] pcap,PacketConsumer consumer,int cnt) ;

public static int dispatch(Adapter adapter,PacketConsumer consumer,int cnt) ;

I parametri corrispondono sempre in numero e ordine, ad eccezione delle classi che ereditano da Pointer. Queste, nel metodo privato, sono sostituite con il valore di tipo long che ne rappresenta il valore del puntatore memorizzato.
Il secondo parametro è una classe che implementa l'interfaccia PacketConsumer così definita:

public interface PacketConsumer
{ public abstract void consume(long sec,long millis, byte[] packetData);
}

Ogni qualvolta viene catturato un pacchetto, viene invocato il metodo consume di questa classe. I primi due parametri rappresentano l'orario di ricezione nel formato timestamp, mente il terzo è l'array di byte che costituisce il pacchetto in formato raw.
Con queste sole due istruzioni è già possibile realizzare uno sniffer rudimentale.

public class PcktViewer {

public static void main(String[] args)
{
StringBuffer sb = new StringBuffer() ;

if (args.length!=1)
{ System.out.println("\nUsage: PcktViewer <device>\n") ;
System.exit(-1) ;
}

Adapter adapter = Sniffer.openLive(args[0],1600,1,1,sb) ;

if (adapter.isNull())
{ System.out.println(sb) ;
System.exit(-1) ;
}

PacketConsumer consumer = new PacketConsumer()
{
// Chars array used to convert a byte into a hex string
//
char[] hex = {'0','1','2','3','4','5','6','7','8',
'9','A','B','C','D','E','F'} ;

// SimpleDateFormatter used to format the packet time
//
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss .SSS") ;

public void consume(long sec,long millis,byte[] packetData)
{

// Gets the packet length
//
int pcktLength = packetData.length ;

// Gets and formats the packet time
//
Date t = new Date(sec * 1000 + millis) ;
String time = sdf.format(t) ;

// Prints out the packet
//
System.out.println("\n" + time + " Size : " + pcktLength) ;

for (int i=0;i<pcktLength;i++)
{

// New line ever 16 byte
//
if((i%16)==0) System.out.println() ;

// Converts a byte to two chars hex format
//
char hi_nib = hex[(packetData[i] & 0xF0)>>4] ;
char lo_nib = hex[(packetData[i] & 0x0F)] ;

System.out.print("" + hi_nib + lo_nib + " " ) ;
}

System.out.println() ;
}
};

System.out.println("\nType enter to exit.\n") ;

try
{ while(System.in.available()==0)
{ Sniffer.dispatch(adapter,consumer,-1) ;
}
}
catch(Exception ex)
{ System.out.println(ex.getMessage()) ;
}

Sniffer.close(adapter) ;

}
}

L'ultima istruzione del programma close, provvede a ripristinare lo stato della scheda di rete e a liberare le risorse.

Questo tipo di cattura è efficiente e veloce, tuttavia, se intendiamo effettuare delle elaborazioni complesse sui dati in ricezione, può essere utile salvare direttamente i pacchetti con metodi nativi, per poi analizzarlo offline. A questo scopo è possibile utilizzare i seguenti metodi di Sniffer

public static Dumper dumpOpen(Adapter adapter,String fname )

public static int dispatchDump(Adapter adapter,Dumper dumper,int cnt)

public static void dumpClose(Dumper dumper)

Il parametro fname è il nome del file in cui salvare i pacchetti ricevuti. Per analizzarli è possibile utilizzare lo stesso codice dello sniffer precedente, semplicemente sostituendo openLive con openOffline

public static Adapter openOffline(String fname,StringBuffer ebuf)

I pacchetti sono trattati come se fossero ricevuti in tempo reale, con il vantaggio di non incorrere in problemi di overflow o di perdita di dati qualora il tempo di elaborazione degli stessi dovesse risultare notevole.
L'esempio precedente visto, cattura tutti i pacchetti ricevuti dalla scheda di rete. Nelle applicazioni pratiche è spesso necessario filtrare i pacchetti in base all' host di destinazione o sorgente, in base al protocollo, o alla porta. Questo può essere effettuato molto semplicemente creando una stringa con una espressione contenente una o più primitive. Le primitive consistono in un id (nome o numero) preceduto da uno o più qualificatori. Vediamo in dettaglio le principali primitive disponibili:

  • host <host> - abilita la cattura se l'host di destinazione o sorgente coincide con il nome o con l'indirizzo IP specificato.
  • net <network> - abilita la cattura solo se l'indirizzo IP di destinazione o sorgente appartiene alla rete specificata
  • port <port number> - abilita la cattura se la porta di diestinazione o sorgente coincide con quella specificata
  • host, net e port possono essere preceduti dal qualificatore dst o src che aumenta la selettività del filtro specificando se l'indirizzo o la porta sono relativi rispettivamente alla destinazione o alla sorgente. Ad esempio, volendo filtrare tutti i pacchetti che hanno come porta di destinazione la 513 l'espressione è : dst port 513.
  • less <length> - abilita la cattura se la lunghezza del pacchetto è inferiore o uguale al valore specificato in length.
    greater <length> - è l'opposto della primitiva less, e la cattura avviene solo se la lunghezza del pacchetto è maggiore o uguale al valore length.
  • ip proto <protocol> - se i pacchetti sono pacchetti IP, limita il tipo di protocollo dei pacchetti da catturare. Può assumere il valore di : icmp, igrp, udp, nd, o tcp.
    ether proto <protocol> - limita la cattura dei pacchetti al caso in cui siano del tipo ip, arp, rarp o decnet.
  • ip broadcast - abilita la cattura dei pacchetti broadcast
  • ip multicast - è analoga alla precedente limitatamente ai pacchetti multicast

Le varie primitive possono essere concatenate con gli operatori and, or e not o equivalentemente con (&&, || e !) e raggruppati con le parentesi tonde. Se ad esempio vogliamo analizzare tutto il traffico dell'host_A ad eccezione di quello diretto all'host_B l'espressione sarà: host host_A and not host_B.
La sintassi del filtro è analoga a quella utilizzata dal programma tcpdump ed è disponibile in maniera completa sul sito dello stesso.
Una volta determinata la stringa del programma di filtraggio, per abilitare la cattura selettiva del traffico occorre utilizzare le due funzioni della classe Sniffer compile e setFilter.
Il primo metodo si occupa di compilare la stringa contenente l'espessione del filtro in un programma eseguibile dalla macchina virtuale (descritta nel paragrafo relativo al BPF) . Il programma viene memorizzato nell'oggetto di tipo BpfProgram passato come parametro alla funzione compile. La chiamata alla funzione setFilter imposta il programma appena compilato come filtro.
Vediamo quindi il codice di un nuovo sniffer che seleziona i pacchetti in arrivo in base al filtro passato dalla riga di comando.

public class PcktFilter {

public static void main(String[] args)
{
StringBuffer sb = new StringBuffer() ;

if (args.length!=3)
{ System.out.println("\nUsage: PcktFilter <device> <filter> <netmask>\n") ;
System.exit(-1) ;
}

Adapter adapter = Sniffer.openLive(args[0],1600,1,1,sb) ;

if (adapter.isNull())
{ System.out.println(sb) ;
System.exit(-1) ;
}

// Parse the netmask
//



// Compiles the filter
//
BpfProgram bpf_prog = Sniffer.allocateBpfProgram() ;

int retVal = Sniffer.compile(adapter,bpf_prog,args[1],0,0xFFFFFF00) ;

if (retVal!=0)
{ System.out.println("\nSyntax error in filter expression.") ;
System.exit(-1) ;
}

// Sets the filter
//
retVal = Sniffer.setFilter(adapter,bpf_prog) ;

if (retVal!=0)
{ System.out.println("\nError setting the filter.") ;
System.exit(-1) ;
}

PacketConsumer consumer = new PacketConsumer()
{
public void consume(long sec,long millis,byte[] packetData)
{
// Gets the packet length
int pcktLength = packetData.length ;

// Prints out the packet
//
System.out.print("\nTime : " + sec ) ;
for (int i=0;i<pcktLength;i++)
{
// New line ever 16 bytes
//
if((i%40)==0) System.out.println() ;

char c = (char)packetData[i] ;
if ((c<32)||(c>128)) c = '.' ;

System.out.print(c) ;

}
System.out.println() ;

}
};

System.out.println("\nType enter to exit.\n") ;

try
{ while(System.in.available()==0)
{ Sniffer.dispatch(adapter,consumer,-1) ;
}
}
catch(Exception ex)
{ System.out.println(ex.getMessage()) ;
}

Sniffer.close(adapter) ;

}
}


 

Conclusioni
Nel file allegato sono disponibili i sorgenti completi del package pcksniff e di alcuni programmi di esempio. Per compilare la libreria dinamica occorre eseguire i seguenti comandi modificando opportunamente i percorsi delle directory di include e della libreria.

Solaris:
cc -G -so libSniffer.so -I/export/home/jdk1.2/include -I/export/home/jdk1.2/include/solaris pcksniff.c /usr/lib/libpcap.a
Linux:
gcc -shared -o libSniffer.so -I/export/home/jdk1.2/include -I/export/home/jdk1.2/include/linux pcksniff.c /usr/lib/libpcap.a

Una volta generata la libreria occorre indicare al sistema dove trovarla. A tale scopo è necessario impostare sui sistemi Unix la variabile LD_LIBRARY_PATH, mentre in ambiente Windows la variabile PATH.
Sui sistemi Unix, inoltre, per poter utilizzare la scheda di rete in modalità promiscua, occorre avere i diritti dell'utente root.
Una volta compilata la libreria e generate le classi java è possibile testare il corretto funzionamento mediante il programma TestLib disponibile tra i files di esempio.

 

Bibliografia
[1] WinPcap - http://netgroup-serv.polito.it/winpcap/default.htm
[2] Libpcap - http://www-nrg.ee.lbl.gov
[3] The Java Tutorial - http://java.sun.com/docs/books/tutorial/native1.1/index.html
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it