Creare un Port Scanner applicazione Android

In questo tutorial, si realizzerà una versione semplificata di port scanner in cui vedremo come usare gli strumenti di Rete e i Task Asincroni ed altro ancora.

android port scanner appJava, nato come tecnologia multipiattaforma, vede nelle reti uno dei suoi “habitat” più naturali. Tale predisposizione è stata ereditata dai framework basati su questo linguaggio e, tra di essi, non poteva mancare Android, sistema leader della “comunicazione a distanza”.

Scopo di questo tutorial è la realizzazione di un semplice port scanner. Si tratta di uno strumento ben noto negli ambienti della sicurezza informatica e vanta ottimi programmi come esemplari: un esempio per tutti è Nmap. Questo tutorial non ha pretese di crearne una versione professionale, ma solo di illustrare come poche classi e qualche tocco di layout siano sufficienti nel framework di Android per predisporne uno basilare. 

Per seguire l'esempio di seguito riportato occorre qualche nozione sui protocolli di rete. Senza eccessive digressioni, premettiamo solo che Internet si basa in gran parte sull'infrastruttura TCP/IP. Ciò significa che ogni macchina che voglia offrire servizi in Rete necessita di un indirizzo IP, che la renda riconoscibile dalle altre macchine, ed ogni suo programma che voglia permettere connessioni remote deve rimanere in ascolto su una porta TCP.

Il portscanner dell'esempio partendo da un indirizzo IP fornito, scandaglierà un lotto di porte TCP, individuato da una numero di porta d'inizio ed uno finale, e cercherà di capire su quali di esse ci sia un servizio in ascolto.

Di cosa abbiamo bisogno?

Gli strumenti che verranno usati per la realizzazione dell'esempio sono: 

  • Un semplice layout: sarà dotato di tre campi di testo (tre TextView) per permettere all'utente di introdurre l'indirizzo IP su cui indagare e due numeri interi rappresentanti i limiti dell'intervallo di porte TCP da testare; 
  • Task asincroni: si ha bisogno di un qualcosa che assomigli ad un thread secondario, ossia un flusso di esecuzione del programma, che svolga per noi il lavoro di indagine sulle porte TCP “dietro le quinte” per poi restituire i risultati all'interfaccia grafica che li mostrerà all'utente; 
  • Strumenti di Rete: in questo tutorial useremo il Socket, la classe del package java.net che rappresenta l'essenza minima di una connessione ad un servizio in Rete denotato da indirizzo IP e porta TCP. 

Il Layout

Il form di cui abbiamo bisogno non è più complicato di quelli presentati in altri tutorial di questo sito. Essendo un form piuttosto statico, il modo più comodo per realizzarlo sarà un layout XML da richiamare poi via codice Java. 

<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" android:id="@+id/tabella">
<TableRow>
    <TextView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:text="IP: " />
    <EditText
        android:id="@+id/ip"
        android:layout_width="400dp"
        android:layout_height="50dp"
        android:text=""/>
	</TableRow>
<TableRow>
   <TextView android:layout_width="50dp"
    android:layout_height="50dp" android:text="Porta iniziale: " />
   <EditText android:id="@+id/startint"
    android:layout_width="20dp"
    android:layout_height="50dp"
    android:text="1"/>
</TableRow>
<TableRow>
   <TextView android:layout_width="50dp"
    android:layout_height="50dp" android:text="Porta finale: " />
   
</TableRow>
<TableRow>
   <Button android:id="@+id/start" android:text="Avvia scansione" android:layout_width="fill_parent"
    android:layout_height="wrap_content"/>
    
</TableRow>
<TextView android:layout_width="50dp"
    android:layout_height="wrap_content" android:text="" android:id="@+id/message"
    android:layout_marginLeft="5dp"
    android:textSize="22sp"
    android:background="#CCCCCC"
    android:visibility="invisible"
	/>
</TableLayout>

Come si vede nello stralcio di codice XML, il form contiene tutti i campi necessari all'inserimento delle informazioni necessarie all'avvio della scansione: indirizzo IP, estremi dell'intervallo di porte TCP da esplorare ed un'ultima TextView per l'esposizione degli eventuali risultati ottenuti. Il click del pulsante "Avvia scansione" innesca un metodo privato della Activity, startScan:

protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		Button start=(Button) findViewById(R.id.start);
		start.setOnClickListener(
				new OnClickListener() {
					@Override
					public void onClick(View v) {
						startScan();
					}
				});
	}

I dati inseriti tramite questo modulo non possono essere in testo libero ma devono rispettare formati ben precisi:

  • indirizzo IP (versione 4) composto da 4 numeri interi compresi tra 0 e 255 separati da un punto;
  • porte TCP indicate da un numero intero, non zero, che non ecceda il valore 65535.

Non è strano che ci sia un'azione di controllo sul formato dei dati inseriti. Ciò avviene in ogni form realizzato da chi tiene non solo alla sicurezza dell'applicazione ma anche al comfort dell'utente che si sentirà guidato nella corretta compilazione dei dati.

L'oggetto predisposto per l'incapsulamento di quanto inserito tramite il form è un'istanza della classe Point, annidata nella Activity. Si tratta di un semplice bean che contiene solo una stringa che rappresenta l'indirizzo IP e due Integer che indicano gli estremi dell'intervallo di porte TCP.

class Point
	{
		private String ip;
		private int startInterval;
		private int endInterval;
		public Point(String ip,int si,int ei)
		{
			this.setIp(ip);
			this.setStartInterval(si);
			this.setEndInterval(ei);
		}
	/*
		Omissis: tutti i vari getter e setter dei membri privati
	*/
}

Il metodo startScan, attivato al click del pulsante presente nel form, è funzionalmente suddiviso in due parti: 

  • la prima si occupa di popolare un oggetto di classe Point con i dati inseriti nel form, 
  • la seconda avvia la scansione vera e propria, previo controllo del formato dei dati iniziali.

In caso di immissione di riferimenti IP o TCP non validi, verrà attivata una procedura con lo scopo di mostrare un adeguato messaggio di errore.

private void startScan()
	{
		EditText ip=(EditText) findViewById(R.id.ip);
		EditText startInt=(EditText) findViewById(R.id.startint);
		EditText endInt=(EditText) findViewById(R.id.endint);
		Point toDiscover=createPoint(ip.getText().toString(),startInt.getText().toString(),endInt.getText().toString());
		if (toDiscover!=null)
			new SocketTest().execute(toDiscover);
		else
			{
				TextView text=new TextView(getApplicationContext());
				text.setText(wrongFormatError);
				text.setBackgroundColor(Color.BLACK);
				text.setTextSize((float) 40.0);
				Toast messaggio=new Toast(getApplicationContext());
				messaggio.setDuration(Toast.LENGTH_LONG);
				messaggio.setGravity(Gravity.TOP | Gravity.LEFT, 120, 120);
				messaggio.setView(text);
				messaggio.show();
			}
	}

Il metodo createPoint, richiamato nel codice precedente, appare un buon punto per inserire i controlli sul formato dei dati. Per farlo si useranno, parsing di stringhe, confronti numerici ed il metodo isIP4ValidAddress per verificare che la stringa contenente l'indirizzo IP sia verosimile. 

private Point createPoint(String ip,String start,String end)
	{
		try
		{
		Integer startInt=Integer.parseInt(start);
		Integer endInt=Integer.parseInt(end);
		if ((startInt<=0 || startInt>65535) || (endInt<=0 || endInt>65535))
			throw new NumberFormatException();
		if (!this.isIP4ValidAddress(ip))
		{
			this.wrongFormatError=ip+" non è un indirizzo IP valido";
		}
		else if (startInt>endInt)
			{
			this.wrongFormatError="La porta TCP di partenza deve essere minore o uguale a quella di fine";
			}
		else
			return new Point(ip,startInt,endInt);
		}
		catch(Exception e)
		{
			this.wrongFormatError="Formato delle porte TCP non valido, usare interi compresi tra 1 e 65535";
		}
		return null;
	}
	
	// verifica di un indirizzo IP
	private boolean isIP4ValidAddress (String ip) {
	    try {
	        if (ip == null || ip.length()==0) {
	            return false;
	        }

	        String[] parts = ip.split( "\\." );
	        if ( parts.length != 4 ) {
	            return false;
	        }

	        for ( String s : parts ) {
	            int i = Integer.parseInt( s );
	            if ( (i < 0) || (i > 255) ) {
	                return false;
	            }
	        }

	        return true;
	    } catch (NumberFormatException nfe) {
	        return false;
	    }
	}

Qualora almeno una delle tre informazioni non risultasse corretta verrà inserito un apposito messaggio d'errore in wrongFormatError, un membro privato dell'Activity di tipo String.
Tale stringa, in caso di errore, nella seconda parte di startScan diverrà il corpo di un messaggio di avvertimento mostrato tramite un Toast.

Lavori asincroni

Quando un'interfaccia grafica serve ad attivare un lavoro piuttosto lungo si usa isolare questa attività in un altro thread, un filone parallelo di esecuzione. In Android, esiste un modo comodo per poter creare questa situazione (anche se non robusta come un vero contesto multithreading).

Consiste nell'estensione della classe AsyncTask. In questo modo, si definisce la classe SocketTest già apparsa nel metodo startScan dove se ne istanziava un oggetto e ne veniva invocato il metodo execute per attivare il lavoro in background. 

class SocketTest extends AsyncTask
	{
		@Override
		protected Void doInBackground(Point... arg0) 
		{
			...
			...
		}
		
		private void updateProgressTextView(int p)
		{
			 TextView txt=(TextView) findViewById(R.id.results);
	         txt.setText("Progresso lavoro: "+Integer.valueOf(p).toString()+"%");
		}
		
		protected void onProgressUpdate(Integer... progress) 
		{
	        updateProgressTextView(progress[0].intValue());
	    }
		
		protected void onPostExecute(Void unused) 
		{
			updateProgressTextView(100);
			TextView txt=(TextView) findViewById(R.id.message);
			txt.setVisibility(View.VISIBLE);
			txt.setText(getResultMessage());
	     }
	}

Il codice Java appena illustrato mette in luce i tre metodi oggetto di ridefinizione mediante override.

Si tratta di:

  • doInBackground: il vero e proprio lavoro asincrono. Contiene le attività di rete, pertanto verrà trattato separatamente più avanti; 
  • onProgressUpdate: eseguito ogni volta che viene invocato publishProgress in doInBackground. Include il codice che serve ad aggiornare gli elementi grafici che comunicano all'utente lo stato di avanzamento del lavoro asincrono. In questo caso si tratta di aggiornare una TextView che riporta la percentuale di lavoro già eseguito; 
  • onPostExecute: invocato alla fine del lavoro asincrono, popola la TextView che contiene l'esito delle operazioni di portscanning svolte. 

L'elenco di eventuali porte individuate verrà custodito in una struttura dati, un Set<Integer>, interna all'activity per poi essere mostrate tramite il codice contenuto nel metodo onPostExecute. Il metodo getResultMessage si occupa nell'occasione di preparare un messaggio sintetico dei risultati, a patto che ve ne siano:

private String getResultMessage()
	{
		if (results!=null && results.size()>0)
		{
			StringBuffer msg=new StringBuffer();
		msg.append("La scansione ha trovato le seguenti porte TCP aperte nell'intervallo:\n");
		
        Iterator it=results.iterator();
        while(it.hasNext())
        {
        	Integer i= it.next();
        	msg.append(i+"\n");
        }
        return msg.toString();
		}
		else
		{
			return "La scansione non ha trovato porte TCP aperte nell'intervallo";
		}
	}

Il lavoro di Rete

L'uso di un AsyncTask è richiesto da Android stesso che non vuole che lavori di Rete, connotati solitamente da tempi di latenza incerti, avvengano nello stesso thread che si occupa di gestire il layout. Inoltre per motivi di sicurezza, affinchè il nostro dispositivo possa connettersi alla rete Internet è necessario specificare nel file manifest dell'applicazione l'apposito permission XML: 

  <uses-permission android:name="android.permission.INTERNET"/>

Ho già avvertito il lettore che questo port scanner non è il massimo della profesionalità “hacker” ma serve da pretesto per illustrare l'interazione con le reti in un app e il coinvolgimento nel lavoro di task asincroni. Comunque funziona e svolge un lavoro di Rete vero. Lo fa senza chiamare in causa particolari librerie ma usando il minimo indispensabile per una connessione di Rete: la classe Socket. Il meccanismo usato è semplice ma efficace ed interamente contenuto nel metodo doInBackground. 

protected Void doInBackground(Point... arg0) {
			Socket s=null;
			results=new TreeSet();
			int i=0;
			int diff=arg0[0].getEndInterval()-arg0[0].getStartInterval()+1;
				for(int y=arg0[0].getStartInterval();y<=arg0[0].getEndInterval();y++)
				{				
					try {
						publishProgress((int) ((i++ / (float) diff) * 100));
						s=new Socket(arg0[0].getIp(),y);
						results.add(Integer.valueOf(y));
						s.close();
					}
				 catch (IOException e) {
					// Eccezione: connessione non permessa
				}
					catch (Exception e) {
						// altre eccezioni
					}
					
				}
			return null;
		}

Per ogni porta TCP da testare viene istanziato un nuovo Socket passando al costruttore l'indirizzo IP e la porta TCP. Se ciò non solleverà un'eccezione vuol dire che la porta è aperta e disponibile a connessioni e pertanto il suo numero sarà registrato nella struttura dati dei risultati.

In caso negativo, verrà intercettata l'eccezione sollevata ed il programma andrà avanti. L'eccezione che potrebbe scattare nel caso di mancata connessione è ConnectException. Comunque si tenga presente che tutte le eccezioni collegabili all'uso dei Socket sono figlie di SocketException e pertanto derivanti da IOException. Questo è il motivo per il quale si è messo un blocco catch che intercetta solo le eccezioni di questa categoria. 

Autore: Giuseppe Maggi
Ingegnere Informatico e docente, da diversi anni, di informatica professionale in centri di formazione torinesi. Nonostante le necessità portino a trattare varie tecnologie, dal C/C++ al .NET, dal web in HTML/CSS/jQuery a Linux, la predilezione resta sempre per il mondo Java ed i suoi vari ambiti di utilizzo, tra cui, ovviamente, Android. 

 

Tipo/Autore: Pubblicato da: CorsoAndroid.it

© 2011-2024 CorsoAndroid.it - Tutti i diritti riservati. Corso Base Android per rompere il ghiaccio Creare app per android
NB: Tutti i marchi citati sono di proprietà dei rispettivi proprietari. Android is a trademark of Google Inc.