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
|