1. Introduzione e Funzionalità di Base

Questa è la prima di una serie di lezioni in cui vedremo come creare un port scanner in Python. Il codice sarà lo stesso che ho scritto nell'omonimo speed coding per il quale molti mi avete chiesto dettagli aggiuntivi.

Partiremo da una versione embrionale ma funzionante del codice, vedendo poi come operare determinate scelte di design al momento opportuno, vedremo come e perché sia utile in questo contesto utilizzare la programmazione a oggetti e inoltre introdurremo tanti nuovi moduli particolarmente potenti che sono certo vi saranno utili nei contesti più disparati.


Che cos'è un port scanner?

Con port scanning facciamo riferimento ad una tecnica progettata per testare un server al fine di capire quali porte siano in ascolto su questa specifica macchina. Si tratta di una tecnica utilizzata spessissimo dagli amministratori di sistema per verificare la solidità delle proprie reti, e dagli hacker per cercare e sfruttare eventuali vulnerabilità.

Per comprendere al meglio questa definizione dobbiamo chiaramente dare una definizione anche di porta!


Cosa sono le porte?

Le porte permettono ad un calcolatore di effettuare più connessioni contemporaneamente verso altri calcolatori facendo in modo che i dati contenuti nei pacchetti in arrivo, quindi i dati che vengono scambiati nelle richieste, vengano indirizzati ad un processo che li sta aspettando.

Per fare un esempio, immaginate di aver appena creato un sito internet: avrete messo questo sito su un server, in modo che altri utenti possano connettersi e ottenere, ad esempio, l'home page del vostro sito. Però allo stesso tempo dovrete anche connettervi come amministratori al vostro server, magari per caricare dei file javascript e css che possano essere poi renderizzati e quindi mostrati ai vostri utenti.

Per compiere queste diverse operazioni il vostro server utilizzerà dei processi diversi e questi processi saranno in ascolto su porte differenti. Ad esempio, per quanto riguarda una richiesta per la home page del vostro sito, si utilizza la porta 80 o 443: la 80 per connessioni http non criptate e la 443 per connessioni criptate.

Qualora dobbiate invece connettervi al vostro server per effettuare operazioni di manutenzione utilizzerete quasi sicuramente un protocollo come SSH, che in questo caso fa ascolto di default sulla porta 22.

ATTENZIONE: Ci tengo a precisare che questo tutorial è stato realizzato solamente a scopo didattico. A seconda delle leggi di uno specifico stato effettuare port scanning su sistemi sui quali non avete il permesso può essere un reato. Quindi mi raccomando, niente fesserie o azioni illegali.


Come faremo a creare il nostro Port Scanner con Python?

Quello che creeremo sarà un port scanner TCP che farà uso del modulo socket.

Un socket è un'astrazione software che consente a due dispositivi di comunicare tra di loro. Per inizializzare uno di questi socket dobbiamo passare almeno due parametri a socket.socket; anzitutto la famiglia, e quindi la tipologia di socket che vogliamo creare. Con AF_INET scegliamo la famiglia IP v4, e con SOCK_STREAM dichiariamo di volere uno socket TCP.

Impostiamo quindi un timeout per evitare di restare bloccati mentre scansioneremo centinaia o migliaia di porte. Per connetterci utilizziamo connect_ex, che accetta una tupla (indirizzo, porta) e che restituirà 0 qualora la connessione sia andata a buon fine: in tal caso, aggiungeremo la porta scansionata ad una lista OPEN_PORTS definita nell'ambito globale dello script. Ricordiamoci di chiudere la connessione con sock.close.

import socket

OPEN_PORTS = []

def scan_port(ip, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(1)
    conn_status = sock.connect_ex((ip, port))
    if conn_status == 0:
        OPEN_PORTS.append(port)
    sock.close()

La nostra funzione scan_port() è pronta e siamo quasi pronti per utilizzarla! Al posto dell’indirizzo IP, a questa potremmo passare anche un dominio, come ad esempio www.miosito.com, ma questa non è la scelta più robusta: infatti, per far sì che il nostro programma possa funzionare nei contesti più disparati, è preferibile reperire l'indirizzo IP del server da scansionare e passarlo alla nostra funzione scan_port().

Volendo, potremmo scrivere il codice necessario a far ciò direttamente all'interno della funzione, ma questo non è ideale da un punto di vista del design, dove cerchiamo sempre di suddividere il nostro codice in porzioni, ognuna dedicata ad uno specifico compito (principio della Separation Of Concerns).

Per ora possiamo creare invece una funzione dedicata chiamata get_host_ip_addr(), che tramite il modulo socket si occuperà di restituirci l’IP del nostro target. Si tenga a mente che GAI in gaierror sta per Get Address Info, che permette di gestire l'eventualità che ci siano problemi nel reperire l'indirizzo IP di uno specifico target.

import socket

OPEN_PORTS = []

def scan_port(ip, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(1)
    conn_status = sock.connect_ex((ip, port))
    if conn_status == 0:
        OPEN_PORTS.append(port)
    sock.close()

def get_host_ip_addr(target):
    try:
        ip_addr = socket.gethostbyname(target)
    except socket.gaierror as e:
        print(f"C'è stato un errore... {e}")
    else:
        return ip_addr

Siamo ora pronti per testare le nostre funzioni. Sotto ad if __name__ == "__main__", chiediamo all’utente di inserire un target tramite input, ricavandone quindi l’indirizzo IP tramite la nostra funzione get_host_ip_addr().

Creiamo quindi un ciclo infinito con un while True, dove chiedere ad ogni ciclo quale porta si intenda scansionare, passando quindi ip_addr e port alla nostra funzione scan_port(). Questa non è sicuramente la procedura ottimale quando si intende scansionare un migliaio di porte, ma ricordo che questo è il primo episodio di una serie, e il nostro codice verrà aggiornato più e più volte sotto molti aspetti.

Il loop può essere interrotto dall’utente tramite la pressione di CTRL + C, che scatenerà il KeyboardInterrupt, che stiamo gestendo.

import socket

OPEN_PORTS = []

def scan_port(ip, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(1)
    conn_status = sock.connect_ex((ip, port))
    if conn_status == 0:
        OPEN_PORTS.append(port)
    sock.close()

def get_host_ip_addr(target):
    try:
        ip_addr = socket.gethostbyname(target)
    except socket.gaierror as e:
        print(f"C'è stato un errore... {e}")
    else:
        return ip_addr


if __name__ == "__main__":
    print("Programma scritto per solo scopo educativo!!!")
    target = input("Inserire Target: ")
    ip_addr = get_host_ip_addr(target)
    while True:
        try:
            port = int(input("Inserire Porta: "))
            scan_port(ip_addr, port)
            print(OPEN_PORTS)
        except KeyboardInterrupt:
            print("\nExiting...")
            break

Questo era tutto per questo primo episodio, ci vediamo nel prossimo!