MokaByte Numero  38  - Febbraio  2000
 
Costruzione di un port scanner in Java
di 
Matteo Baccan
Uno dei programmi che qualsiasi hacker ha nel suo cassetto degli attrezzi è quello per la scansione delle porte in una rete TCP/IP. Questo tipo di tool si rivela infatti utilissimo per scovare eventuali problemi di sicurezza o server attivi sulla rete TCP/IP

Come è possibile sapere se su una macchina è montato un WEB server o se esistono dei server telnet attivi, nella classe C di indirizzi del nostro provider? 
Come capire se esiste un sito FTP sulla stessa macchina dove è montato anche un servizio HTTP? I software di port scanner servono appunto per verificare queste cose. Vediamo ora com’è possibile controllare l’esistenza di un dato server, cosa sono le porte del protocollo TCP/IP e quali sono quelle standard. 
Dopo questa prima analisi vedremo come da JAVA sia possibile creare uno scanner "platform indipendent" ed estremamente performante sfruttando la tecnica dei thread.

Cosa sono le porte TCP/IP
Una qualsiasi macchina, per presentarsi sulla rete Internet, deve avere un indirizzo TCP/IP attivo. Che sia esso dinamico, quindi assegnato da un provider o da un server DHCP, o statico, quindi attribuito in modo fisso all’atto della configurazione del protocollo, non importa, l’importante è che esista.
Oltre all’indirizzo, ognuna di queste macchine ha poi una serie di porte disponibili ad essere interrogate. Ma vediamo esattamente cosa sono tali porte.
Immaginiamo che un indirizzo TCP/IP sia una via di una grande città. In tale via vi sono N abitazioni. Ognuna di queste abitazioni ha un proprio numero civico al quale possiamo bussare per verificare l’esistenza di una certa persona. 
Essendo il numero civico l’elemento di distinzione fra le diverse abitazioni, sulla stessa via possiamo identificare, in modo certo, le diverse persone che vi abitano. Allo stesso modo su un computer, identificato da un indirizzo TCP/IP, possiamo distinguere fra i diversi server che vi sono montati, in quanto rispondono a porte TCP/IP diverse.
Esistono anche quelli che sono dei default. Ad esempio un server WEB risponde normalmente sulla porta 80, un servizio telnet sulla 23 e un FTP alla 21. Per sapere quali sono i default più utilizzati, provate a prendere, dalla vostra directory Windows, il file SERVICES. In tale file sono presenti alcuni di quelli che sono i default maggiormente diffusi. Si parla di default infatti perché nessuno ci vieta di montare un server telnet su una porta diversa dalla 23. Il server sicuramente funzionerà, ma occorrerà avvisare i client, che verranno utilizzati per la connessione, che il server è montato su una porta diversa da quella standard. 
La tecnica del cambio della porta di default può risultare molto utile contro eventuali attacchi alla rete da parte di hacker alle prime armi, che non controllano tutte le porte di una certa classe di indirizzi, ma soltanto quelle di default. In questo modo un’eventuale incursione, o comunque un’eventuale scansione di una rete tramite le porte standard non porterebbe a nessun risultato.
 
 
Figura 1 - Single IP port scanner

 

Esistenza di una porta attiva
Una volta compreso il perché esistano porte diverse che controllano l’attività dei vari servizi Internet, vediamo come sia possibile costruire un semplice programma Java in grado di controllare se ad una porta risponde un determinato server, oppure se su una certa porta non è installato nessun programma.

echo, 7
ftp-data, 20
ftp, 21
telnet, 23
smtp, 25
time, 37
name, 42
bootp, 67
tftp, 69
finger, 79
http, 79
pop3, 110
nntp, 119
login, 513
printer, 515
route, 520
Per effettuare questo controllo occorre, prima di tutto, conoscere l’esistenza della classe Socket, del package java.net.
Tale classe è in grado di attivare una connessione remota, con una determinata macchina su una determinata porta. Per tale ragione si rivela molto utile ai nostri scopi. 
Le righe di codice che andremo a scrivere, per effettuare la connessione saranno pertanto le seguenti:
String cIP = "192.168.0.1";
int nPort = 23;
Socket test = new Socket( cIP, nPort );
dove con cIP viene identificato l’indirizzo IP della macchina che vogliamo controllare e nPort la porta alla quale vogliamo accedere.
L’unico problema che possiamo incontrare, durante l’esecuzione di questo codice, è dato dal fatto che il costruttore della classe Socket potrebbe generare un’eccezione. Se infatti guardiamo il sorgente di tale costruttore:
public Socket(String host, int port) throws UnknownHostException, IOException{
  this(InetAddress.getByName(host), port, null, 0, true);
}
possiamo notare che vengono generate due eccezioni. La prima di tipo UnknowHost che identifica il fatto che la macchina che stiamo cercando di controllare non esiste o non è raggiungibile con la nostra configurazione TCP/IP e la seconda di tipo IO, cioè di trasmissione dati. 
Per poter catturare queste due eccezioni, evitando che il nostro programma vada in errore, basterà semplicemente impostare un try ... catch. In questo caso però, dato che non ci servono dei dettagli sul perché è accaduto l’errore, possiamo scrivere del codice completamente generico che in una sola catch riesca a gestire tutte le casistiche d’errore. Tale codice sarà così composto:
try{
  // codice per la connessione
}
catch(Throwable e){}
Al posto di gestire due catch specifiche sulle eccezioni UnknownHostException e IOException,
generate da Socket, possiamo semplicemente gestirne una sulla classe Throwable. Tale classe è la base di tutte le eccezioni e pertanto, qualsiasi sia quella generata, sicuramente verrà catturata da questa catch generica.
Il cuore del programma di port scanner è finalmente pronto. Se infatti la connessione tramite Socket non genererà nessun errore, vorrà dire che la porta esiste ed è attiva, in caso contrario vorrà dire che quella porta non è attiva, o che esiste un firewall, dal quale siamo costretti a passare, che ci preclude l’utilizzo della porta che stiamo controllando.
È infatti possibile che, fra la nostra macchina e il range di macchine sulle quali stiamo attivando una scansione, esista un firewall, che controlli quale porte sono disponibili all’accesso tramite Internet e quali no. È possibile quindi che, pur essendo attiva una determinata porta, il nostro calcolatore non sia in grado di controllarla, in quanto la richiesta di accesso arriva da un indirizzo IP non riconosciuto e pertanto viene bloccata ancora prima di raggiungere la macchina da controllare.
 
 
Figura 2 - Class C port scanner

 
 
 

Scansione di un range di porte
Proviamo ora a potenziare la scansione che abbiamo costruito fino a questo momento aggiungendo, prima di tutto, la gestione di un’intera classe C di indirizzi e successivamente incapsulando il codice all’interno di un thread. In questo modo sarà possibile parallelizzare più processi.
Con classe C si identifica quel gruppo di indirizzi che hanno uguale radice numerica. Facendo conto che una determinata macchina abbia l’indirizzo 192.168.0.100, le macchine, facenti parte della sua Classe C di indirizzi, sono tutte quelle comprese fra 192.168.0.1 e 192.168.0.255.
Per modificare il programma, in modo che sia in grado di gestire l’intera classe, effettueremo solamente una piccola variazione all’indirizzo da controllare, facendo in modo che tale indirizzo diventi dinamico:

String cIP = "192.168.0.";
int nPort = 23;
for( int nClassC=0; nClassC<=255; nClassC++ ){
  try {
    Socket test = new Socket( cIP+nClassC, nPort );
    test.close(); 
   } catch ( Throwable e ) {}
}
In questo caso viene sfruttata una caratteristica di Java che ci permette di sommare ad una stringa un valore intero. La somma fra queste due tipologie di dati subisce infatti una conversione automatica del valore int in una stringa. È per questa ragione che, sommare un oggetto String ad uno di tipo int, non porta a nessun errore, a differenza di molti altri linguaggi di programmazione.
A questo punto siamo in grado di effettuare una scansione di un’intera classe C, sulla porta 23, o su una qualsiasi altra porta che decideremo di andare a controllare. Il problema che ci si pone ora di fronte è quello, inevitabile, delle performance. Un algoritmo del genere è sicuramente funzionante, ma pecca di velocità. Infatti la creazione dell’oggetto Socket blocca completamente il programma, che rimane in attesa di "un segnale di vita" da parte della macchina che si sta tentando di raggiungere. Perché allora dobbiamo perdere tutto questo tempo, nell’attesa che qualcuno ci risponda? Perché non possiamo parallelizzare più processi, in modo da sfruttare a pieno la potenza elaborativa della macchina che stiamo utilizzando?
Per poter andare incontro a quest’esigenza java ci mette a disposizione una classe del package java.lang che fa esattamente al caso nostro. Tale classe è Thread. Gli oggetti Thread hanno infatti una caratteristica molto importante che è quella di non bloccare il programma nel quale sono eseguiti. Viene creato un processo parallelo che entra in esecuzione insieme alla parte di codice nella quale è stato creato. Per chi ha lavorato col linguaggio C su piattaforme UNIX, l’effetto é esattamente uguale a quello provocato da una fork().
Per incapsulare la scansione su un Thread siamo però costretti a creare una piccola classe che conterrà, al suo interno, il singolo controllo di porta. Tale classe ha però una caratteristica molto importante, che è data dal metodo che ne determinerà l’attivazione. Tale metodo si chiama run() e sarà il metodo che dovremo sovrascrivere, inserendo al suo interno il codice di scansione.
Il risultato che otterremo sarà quindi:
public class checkSingleIP extends Thread{
private String cIP;
private int nPort;

  public checkSingleIP(String cIP, int nPort ){
    this.cIP = cIP;
    this.nPort = nPort;
  }

public void run(){
   try {
     Socket test = new Socket( cIP, nPort );
     System.out.println( nPort +" disponibile" );
     test.close();
   }
   catch ( Throwable e ) {
      System.out.println( nPort +" NON disponibile" );
   }
 }
}

Ora, per poter attivare il singolo processo di controllo IP, dovremo semplicemente creare un oggetto della classe checkSingleIP e mandarlo in esecuzione tramite il metodo start che a sua volta chiamerà il metodo run che siamo andati ad implementare nella nostra classe. Nel main della classe principale andremo così a fare un richiamo alla classe checkSingleIP:
checkSingleIP ip = 
    new checkSingleIP( "192.168.0.1", 23 );
ip.start();
A questo punto il programma è sufficientemente veloce e ci assicura una buona affidabilità nella scansione di una rete di indirizzi TCP/IP. In realtà, esiste un problema rappresentato dal numero massimo di connessioni socket che la macchina sulla quale si esegue il programma può supportare. Su alcuni sistemi operativi, se andiamo oltre una certa soglia di socket attivi, il programma non è più in grado di effettuare la connessione e genera un’eccezione, ancora prima di aver tentato una connessione con la macchina remota. 
 
Figura 3 - Port Listener

 

Conclusioni
Costruire un port scanner è un’operazione molto semplice. Il difficile risiede solo nel fatto di rendere efficace l’algoritmo di scansione, tramite l’aggiunta di algoritmi che ne possano aumentare l’efficacia o tramite l’aggiunta di accorgimenti che ne possano parallelizzare i processi. I thread ci permettono fortunatamente la programmazione concorrente ed in questo caso hanno aiutato molto nella costruzione di un programma performante. Esistono però molti altri ambiti applicativi, nei quali è possibile utilizzare tale tecnica, come ad esempio i Socket Server. Attenzione però a non attivare troppi processi all’interno di una macchina, in quanto essi potrebbero rallentare a dismisura le performance di tutte le altre applicazioni.

Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it