hacking

Studio della vuln. CVE-2022-23093 [prima parte]

Qualche giorno fa, durante una live, mi e’ stata ricordata la CVE in oggetto che interessa i sistemi FreeBSD e su cui avevo un po’ riflettuto a livello di potenziale offensivo. La CVE mi era tornata alla mente anche a seguito di una recente vulnerabilita’ che ha interessato l’implementazione di ICMP sui sistemi Microsoft (CVE-2023-23415). Le due vulnerabilita’ sono molto diverse tra loro: lato FreeBSD e’ l’utility “ping” a presentare la falla mentre lato Windows il problema sembra essere relativo ad una funzione della componente ICMP. Mi e’ venuta voglia di approfondire per capire meglio di cosa si tratta: analizzare e studiare le CVE e’, a mio parere, un ottimo esercizio in ambito cyber sec. E poi come resistere al fascino di uno stack overflow ūüôā

Al di la della tipologia di vulnerabilita’, di per se interessante, vi e’ un altro elemento piu’ strategico che interessa il bug: FreeBSD, pur avendo un livello di diffusione non elevato, e’ alla base di molte appliance di vario tipo tra cui Firewall e Storage System come il mitico NetApp. Questo rende la vulnerabilita’ ancora piu’ interessante.

Preparazione del lab

Prima cosa da fare e’ documentarsi per valutare come riprodurre un ambiente vulnerabile su cui eseguire i test. L’advisory di FreeBSD stessa espone le versioni a cui a novembre e’ stata applicata la patch:

  • 2022-11-29 stable/13, 13.1-STABLE)
  • 2022-11-29 releng/13.1, 13.1-RELEASE-p5)
  • 2022-11-29 stable/12, 12.4-STABLE)
  • 2022-11-29 releng/12.4, 12.4-RC2-p2)
  • 2022-11-29 releng/12.3, 12.3-RELEASE-p10)

Un primo test lo ho condotto su una FreeBSD 11.4 con ultimo aggiornamento (della ISO) il 12 giugno 2020, teoricamente e’ quindi presente la vulnerabilita’. Purtroppo ho riscontrato diversi fastidi dovuti all’assenza delle repository. Versione troppo datata.

Ho ripiegato sulla release 12.2. In questo periodo il mio home-lab e’ un po’ sottosopra a causa di vari cambi di architettura, ad ogni modo ho preparato una VM amd64 su una installazione di Oracle VirtualBox.

Trovate le ISO qui: http://ftp-archive.freebsd.org/pub/FreeBSD-Archive/old-releases/amd64/.

installazione di freebsd

Una volta installata la macchina mi sono limitato a configurare l’accesso SSH per root ed installare il debugger gdb.

Avere a disposizione la macchina ci consente inoltre di scaricare i sorgenti dell’utility che presenta la vulnerabilita’: possiamo scaricarci integralmente la repository della distibuzione.

root@freebsd-12:~ # svnlite checkout https://svn.freebsd.org/base/releng/12.2 /usr/src

Una volta ottenuto il sorgente possiamo verificare nel path /usr/src/sbin/ping/ la presenza dei sorgenti di notro interesse:

ping.c

Ora che abbiamo la nostra base possiamo dedicarci un po’ allo studio della vulnerabilita’ partendo dalla documentazione a disposizione e da quanto reperibile in rete.

Raccolta delle informazione

L’advisory in oggetto e’ stata pubblicato il 29 novembre 2022, quindi quattro mesi fa rispetto alla pubblicazione di questo post. Nell’analisi partiamo proprio da questo documento disponibile qui: https://www.freebsd.org/security/advisories/FreeBSD-SA-22:15.ping.asc.

Al punto II troviamo la descrizione dell’errore che provo qui a sintetizzare. Il programma ping riceve e legge i pacchetti per elaborare la risposta tramite la funzione pr_pack(). Per eseguire questa elaborazione viene ricostruito il dato utilizzando l’IP header, l’ICMP header e, in caso di ICMP error, il QUOTED PACKET che ha generato l’errore. Visto che abbiamo a disposizione il sorgente del programma ping.c possiamo dare un occhio alla funzione per capire meglio di cosa si tratta.

La funzione la si puo’ trovare alla linea 1118 del sorgente di ping.c (sempre con riferimento alla versione 12.2 di FreeBSD) e da qui possiamo iniziare ad analizzare il comportamento del codice.

Partiamo dalle variabili che arrivano alla funzione:

pr_pack(char *buf, ssize_t cc, struct sockaddr_in *from, struct timespec *tv)

dove troviamo:

  • il buffer buf che contiene il pacchetto IP ricevuto in rispota dal comando ping
  • la lunghessa cc del pacchetto
  • l’indirizzo IP from della macchina che ha inviato il pacchetto
  • il tempo tv di arrivo del pacchetto

Subito dopo la dichiarazione delle variabili della funzione troviamo una porzione di codice che la patch va a modificare in parte:

1140         memcpy(&l, buf, sizeof(l));
1141         hlen = (l & 0x0f) << 2;
1142         memcpy(&ip, buf, hlen);

In parole povere il contenuto di buf, contenente il pacchetto IP ricevuto in risposta dal comando ping, viene copiato nella variabile l, viene poi calcolata la lunghezza hlen dell’IP header e quindi copiati hlen byte di buf nella variabile IP.

Come si vede meglio dalle modifiche della patch rilasciata il problema si annida proprio in questa ultima memcpy: se il contenuto del pacchetto IP in risposta fosse opportunamente modificato e’ teoricamente possibile utilizzare la lunghezza dei dati inseriti in buf per ottenere un overflow. L’attacker dovrebbe prima intercettare le richiesta ICPM (un ping dalla macchina target) e rispondere con un errore opportunamente modificato. Non impossibile ma decisamente non semplice.

Ho trovato molto utile quanto riportato in post di archcloudlabs.com dove viene proposto di modificare, utilizzando un debuger, il contenuto delle variabili per ottenere “artificialmente” l’overflow del buffer. Ho trovato il test molto interessante e utile da replicare.

Predisposizione debug del comportamento della funzione

Per poter eseguire il debug del nostro binary dobbiamo necessariamente ricompilare l’eseguibile con gli opportuni flag e anvendo i sorgenti a disposizione non si tratta di una operazione complessa.

opzione -g aggiunta nel MakeFile di ping.c

Se tutto in ordine con due comandi facili (make, make install) dovremmo ottenere in nostro nuovo binary di ping.

Il passo successivo e’ fare in modo di ottenere una risposta da noi gestibile, nel post citato precedentemente si fa riferimento ad uno script in scapy ed e’ proprio quello che avrei fatto anche io, ne abbiamo parlato priprio nella live del 31 marzo… scapy mi piace sempre di piu’.

Qui la prima versione dello script che si occupa di intercettare il pacchetto ICMP e rispondere con un pacchetto da noi opportunamente generato:

#!/usr/bin/env python
from scapy.all import *

def reply_ping(packet):
    if packet.haslayer(ICMP) and packet[ICMP].type == 8:
        eth = packet[Ether]
        ip = packet[IP]
        icmp = packet[ICMP]

        payload = [
		b'\x86\x28',
		b'\x41'*20
	]
        payload = b''.join(payload)


        reply = Ether(src=eth.dst, dst=eth.src)/IP(src=ip.dst, dst=ip.src, ihl=6, options=IPOption(payload))/ICMP(type=3, code=0)/icmp[ICMP].payload
        sendp(reply, iface='eth0')

        print('Invio risposta ICMP ping')

sniff(filter="icmp and icmp[icmptype] == icmp-echo", prn=reply_ping)

Dovrei in questo modo ottenere una risposta type=3 (Destination Unreachable) con la possibilita’ di manipolare il contenuto di IPOption, ovvero la parte di dati che, a quanto ho compreso dal codice di ping.c a dall’advisory, dovrebbe essere passata al buffer che presenta la vulnerabilita’.

Per garantirci una buona visibilita’ su cio’ che sta accadendo e’ comodo predisporre lo script su una macchina dove possiamo disporre anche di Wireshark. Nel mio lab ho utilizzato come macchina di destinazione una semplice kali che simulera’ la macchina dell’attacker in questo contesto.

lo script in ascolto

Lato Wireshark e’ sufficiente filtrare i pacchetti ICMP in arrivo sulla NIC della macchina, visto che saremo solo noi ad eseguire dei ping verso il sistema non avremo problemi ad identificare i pacchetti di nostro interesse.

Rispetto ad un normale ping quello che dobbiamo ottenere e’ una risposta che contenga il nostro payload. Il primo esperimento, tutto da perfezionare, ha ottenuto quanto segue:

cattura del pacchetto modificato

Il payload e’ presente nel pacchetto ma evidentemente la risposta richiede qualche ritocco. Nei prossimi giorni lavorero’ in modo specifico sulla rispsta dalla richiesta ICMP ed al debug del codice di ping.c ricompilato cosi’ da verificare anche cosa avviene a livello di sistema.

La live sul mio canale Twitch di venerdi’ 7 aprile riprende questo piccola lab e si procede con le analisi della vulnerabilita’. Nel prossimo post riportero’ le evoluzioni.

cyber security, hacking

Buffer Overflow lab [I parte]

Premessa doverosa: tratto il tema perch√© mi piace e appassiona molto ma non sono certo un esperto, mi rivolgo quindi a chi come me √® affascinato dall’argomento ed ha interesse ad approfondire passando per un esercizio pratico.

Durante una sessione live di qualche settimana fa (l’argomento era una simulazione di attacco) si √® un po’ discusso e sono emerse domande circa lo sfruttamento delle vulnerabilit√† e le tecniche di exploiting. Ho quindi pensato di dedicare una live (ma ne serviranno almeno due) all’argomento con lo scopo di semplificare il semplificabile e fare qualcosa di molto pratico. La live di gioved√¨ 5 maggio, ancora disponibile sul mio canale Twitch, introduce quindi l’argomento con un piccolo laboratorio estremamente realistico che chiunque pu√≤ replicare. In questo post provo a mettere “in fila” i vari step visti durante la prima parte in live.

Intro: un grammo di teoria

Resistete, sono due cose ma fondamentali. Dobbiamo necessariamente capire come si manifesta un buffer overflow. Partiamo da un esempio molto semplice ed immaginiamo un programma base che ha una funzione main() dove avvengono “cose” ed una funzione task() che fa altre “cose”:

int main() {
    task();
    other_task();
}

int task() {
    char buff[42];
    ...
}

Cose succede nella memoria del nostro elaboratore quando un programma come questo viene eseguito? L’area di memoria interessata √® chiamata stack (pila) perch√© si comporta proprio come una pila di byte nel momento in cui viene riempita di informazioni.

Sul “fondo” viene posizionata la funziona main() e le relative variabili locali, subito sopra si trova l’area di memoria che contiene l’indirizzo a cui andare per tornare alla funzione main() ed infine l’area dedicata alla funzione task().

Ciò che avviene quando il programma viene eseguito è sintetizzabile con i seguenti step:

  1. la funzione main() viene eseguita
  2. viene chiamata la funzione task()
  3. viene salvato il return address nel registro così da riprendere la funzione main() da dove si è fermata una volta eseguita la funzione task()
  4. viene eseguita la funzione task()
  5. viene letto il valore del return address
  6. il programma prosegue dall’istruzione successiva alla chiamata della funzione task()

Se tutto procede senza errori il programma terminer√† con successo. In questo passaggio √® importante comprendere che durante l’esecuzione della funzione task() l’area di memoria allocata dipende dalle esigenze della funzione in termini di “consumo”. Come si nota dallo pseudo-codice la variabile buff[42] √® un array che pu√≤ contenere 42 elementi. Cosa succederebbe se provassimo a scrivere 200 caratteri e non controllassimo questo evento a livello di codice?

Il contenuto del registro dove era stato salvato il return address viene sovrascritto ed il programma tenterebbe di riprendere da una posizione di memoria (AAAA nell’esempio) inesistente o non valida con relativo crash.

Exploiting

Capita la teoria vediamo come √® potenzialmente sfruttabile questo meccanismo. Se potessimo controllare il valore del return address potremmo indirizzare il programma altrove ed eseguire le istruzioni presenti in altre aree di memoria. Con la stessa logica se potessimo scrivere istruzioni arbitrarie, invece delle “AAAA…”, nelle aree di memoria disponibili potremmo impostare il return address cos√¨ da eseguire un jump esattamente dove sono state posizionate le nostre istruzioni arbitrarie.

Per ottenere questo risultato dobbiamo ovviamente sapere quanti byte scrivere nelle differenti aree di memoria. Ci serve quindi calcolare l’esatta dimensione del buffer, l’esatta posizione del registro EIP che contiene il citato return address ed infine l’inizio e la fine dalla successiva area di memoria.

Per ottenere tutte queste informazioni bisogna lavorare un po’ di debug e qualche calcolo esadecimale. Il modo migliore per comprenderlo √® vederlo dal vivo, quindi il prossimo step √® mettere le mani in pasta in un lab.

Lab Setup

Come accennato ad inizio post il laboratorio in questione ha il vantaggio di essere molto il linea con la realt√†. Abbiamo un sistema che ospita un’applicazione vulnerabile la quale espone un servizio contattabile remotamente. Per mettere in piedi il sistema target dobbiamo quindi semplicemente predisporre una VM con sistema operativo Windows e scaricare l’applicazione vulnerabile.

Una volta preparata la vostra VM target dovete procurarvi l’applicazione che presenta la vulnerabilit√†: https://github.com/stephenbradshaw/vulnserver. Di base √® un eseguibile che pu√≤ essere lanciato direttamente dal prompt dei comandi:

Abbiamo quindi la nostra applicazione avviata e pronta a ricevere delle connessioni. Per fare qualche prova e come base anche per le successive operazioni di analisi possiamo usare la solita Kali che ci darà un mano anche successivamente grazie ad alcuni tools comodi. In questo scenario è necessario che le due VMs siano in comunicazione. Nel mio laboratorio condividono anche la stessa network ma non è un requisito.

Testing dell’applicazione

Dalla nostra Kali possiamo quindi provare a connetterci al server che risponde sulla porta 9999 utilizzando semplicemente netcat:

nc 192.168.1.7 9999

Il risultato dovrebbe essere qualcosa di simile:

A sinistra la sessione netcat, a destra la console del servizio.

Il servizio mette a disposizione diversi comandi in grado di ricevere dei valori. Nel caso specifico noi sappiano gi√† qual √® la funzione vulnerabile mentre in una situazione reale l’analista dovrebbe andare a caccia prima di tutto della vulnerabilit√† o del bug da sfruttare. In questa sessione non tocchiamo il tema bug hunting quindi ci limitiamo a saltare al momento in cui, tramite una tecnica molto usata che √® il fuzzing, scoviamo il bug nella funziona TRUN del programma.

Mettiamoci quindi per un attimo nei panni del bug hunter (tra l’altro attivit√† meravigliosa che richiede molta abilit√†) che arriva al test del comando TRUN del server ed utilizziamo un tool della nostra Kali: generic_send_tcp. Questo piccolo software ci consente di creare ed inviare pacchetti TCP al target per vedere cosa succede, dobbiamo prima definire le istruzioni da passare al programma che possiamo mettere in un file di configurazione che io chiamer√≤ fuzz.spk:

s_readline();
s_string("TRUN ");
s_string_variable("FUZZ");

Il file ha tre semplici direttive abbastanza chiare:

  • leggiamo l’output del server cos√¨ da vedere cosa risponde alle nostre sollecitazioni
  • chiamiamo il comando TRUN
  • “spariamo” del contenuto per vedere come reagisce

Una volta pronto il file con le configurazioni possiamo fare fuoco:

generic_send_tcp 192.168.1.7 9999 fuzz.spk 0 0

Dovremmo ottenere il crash dell’applicazione sul server Windows:

A sinistra lo script di fuzzing in esecuzione, a destra il servizio andato in crash.

Il programma va in crash e abbiamo quindi la conferma del bug. Nel nostro caso sappiamo già essere un buffer overflow e in questo lab ci comporteremo di conseguenza cercando questo specifico comportamento.

Analisi della memoria

Abbiamo un servizio che va in crash se sollecitato in un certo modo… andiamo a vedere cosa succede in memoria quando si verifica il crash. Per questo lab utilizzo sulla macchina Windows il software Immunity Debugger; √® una scelta di comodo che assolve al compito specifico di farci vedere cosa viene caricato in memoria quando il programma √® in esecuzione e quando va in crash.

Immunity consente di avviare il servizio direttamente dalla propria interfaccia cos√¨ da “pilotarne” ed analizzarne i singoli step, anche una istruzione alla volta. Avviamo quindi il servizio da Immunity ed avviamo il servizio:

Sulla destra troviamo lo stato dei registri del processore e in questa fase √® interessante vedere che, oltre a non esserci errori, il registro EIP contiene una posizione di memoria che corrisponde a dove “mandare” il puntatore delle istruzioni una volta eseguita l’istruzione in gestione. E’ quini il return address di cui si parlava prima.

Vediamo cosa succede quando lanciamo il nostro fuzzer e mandiamo in crash il programma:

Su questo screenshot si potrebbe parlare per diverse ore. Cosa sta succedendo?

Subito in cima vediamo chiaramente la nostra richiesta: la chiamata alla funzione TRUN arriva direttamente da nostro fuzzer e subito dopo vediamo i valori passati alla funzione, una serie di caratteri “A”.

Altro elemento da notare √® il contenuto dell’area a cui punta il registro ESP, porzione di memoria destinata alle variabili locali del programma in esecuzione. Anche qui vediamo una serie di caratteri “A”.

Infine, forse l’elemento pi√Ļ interessante, il contenuto del registro EIP: 41414141 ovviamente esadecimale. A cosa corrisponde 41 esadecimale? Al carattere “A”. Interessante! Le “A” che abbiamo sparato con il fuzzer sono andate praticamente dappertutto: hanno riempito la porzione di memoria destinata alle variabili della funzione, hanno sovrascritto il registro EIP e sono arrivate a riempire l’area di memoria destinata alle variabili del programma. Quale delle precedenti immagini vi ricorda? Siamo chiaramente davanti ad un buffer overflow.


Per questa prima parte mi fermo qui. Nella seconda parte vediamo come manipolare in modo preciso il contenuto dei registri ed iniziare a scrivere il nostro exploit ora che abbiamo individuato la vulnerabilità.