Contatta il reparto vendite 800 13.40.41 | Richiedi il Supporto Partner EN flag

KNOWLEDGE BASE

Knowledge Base

Vai al Video

Dall'app al codice: tecniche di ingegneria inversa e come difendersi

Reverse Engineering: perché si usa e come ci si difende

L’espressione Reverse Engineering (ingegnerizzazione inversa) indica il processo di analisi di un prodotto tecnologico allo scopo di capire come è stato strutturato e quali motivazioni hanno portato a certe decisioni. La tecnica di ingegnerizzazione inversa si applica anche all’ambito software e non sempre a fini “benevoli” perché talvolta viene sfruttata per risalire al codice sorgente e ricreare un’applicazione simile ma adattata a fini fraudolenti.

In questo tutorial verrà spiegato come il reverse Engineering si applica al mondo del software e quando si ricorre ad essa; analizzeremo alcuni casi d’uso interessanti, che vanno dal semplice cracking di un applicativo alla completa decompilazione (ricostruzione dei codici sorgenti a partire dal file eseguibile) di un’applicazione.

Reverse Engineering: di cosa si tratta

L’ingegneria inversa in ambito software è il processo di analisi mediante il quale si ottiene una rappresentazione astratta totale o parziale delle parti che compongono un programma eseguibile. In altre parole, è un insieme di tecniche che ha come obiettivo finale quello di ottenere quante più informazioni sul funzionamento interno di un software, al fine di comprometterne o semplicemente alterarne il comportamento.

Ma Perché si ricorre all’ingegneria inversa? Ebbene, le motivazioni possono essere divise in due categorie: scopi benevoli e intenti malevoli. Nel primo caso rientrano:

  • l’analisi di malware e virus (comportamento, killswitch, ecc.) per capire come funzionano e in che modo vengono costruiti dai programmatori, con lo scopo di riconoscerli, neutralizzarli, isolarli; nel caso dei ransomware, l’analisi è mirata a trovare vulnerabilità che consentono di scoprire le chiavi utilizzate da essi per cifrare i file del malcapitato di turno;
  • la ricerca di falle di sicurezza nei software proprietari allo scopo di correggere le vulnerabilità e rendere i prodotti più sicuri;
  • la manutenzione di software obsoleto;
  • il recupero di informazioni perse (file sorgenti, documentazione di software ecc.).

Per quanto riguarda gli scopi di manutenzione, un esempio può chiarire cosa s’intende: Diablo era un videogame per DOS che non funzionava più sulle versioni moderne di Windows (ossia Windows sistema operativo e non ambiente avviato da DOS): quel gioco è stato interamente decompilato e dagli eseguibili si è potuto ricostruire il sorgente per poi poterlo ricompilare portandolo a funzionare sulle piattaforme Windows e continuare ad utilizzarlo.

Tra gli scopi fraudolenti del Reverse Engineering troviamo:

  • la violazione delle restrizioni nell’utilizzo di software, come ad esempio la rimozione delle licenze;
  • la ricerca di falle di sicurezza e vulnerabilità nei software proprietari allo scopo di creare attacchi informatici;
  • la creazione di malware a partire da software esistente;
  • il furto di informazioni, l’accesso non autorizzato a sistemi e via di seguito.

Purtroppo va detto che il reverse Engineering viene molto utilizzato da chi ha intenzioni tutt’altro che buone, ad esempio per risalire al codice sorgente di un software a pagamento e ricrearne una versione sprotetta, ovvero per violare protezioni di accesso e chiavi di sicurezza.

Banner

Riguardo alla creazione di malware da software esistenti, è un trend di tecniche in continua evoluzione basate sull’utilizzare programmi noti, validati, certificati e quant'altro e sfruttarne le vulnerabilità scovate come vettori per portare avanti degli attacchi; un esempio è stato, qualche anno fa, un malware che utilizzava una versione modificata di TeamViewer per consentire a un attaccante di prendere l’accesso e il controllo della macchina remota sfruttando il servizio TeamViewer (il demone di TeamViewer) e quindi non presentandosi come un malware agli occhi del sistema.

Il processo di compilazione

Per spiegare come avviene il Reverse Engineering è opportuno partire da quello che è il processo che esso inverte, ossia il naturale sviluppo del software.

Banner

Lo sviluppo di un progetto un software parte da tutta la documentazione che descrive il progetto, quindi dai requisiti minimi e tutti i vari diagrammi di flusso, schemi di database, protocollo, fino alla stesura del codice. Quest’ultimo di fatto è una descrizione di alto livello di quello che fa il  programma. Il codice è il software in linguaggio più o meno evoluto e prende il nome di sorgente. Questo deve poi essere compilato, ossia convertito in istruzioni eseguibili dal microprocessore cui è destinato e distribuito; si arriva così al prodotto finale, che è l’applicazione distribuita agli utenti finali.

Possiamo fare un esempio con la seguente porzione di codice in linguaggio C:

#include <stdio.h>

int main()

{

    printf("Hello world\n");

    return 0;   

}

Questo è un file sorgente che possiamo chiamare, ad esempio, main.c (l’estensione indica che è un file scritto in C). Nell’esempio scriviamo un listato con una chiamata una funzione che serve per scrivere Hello World a schermo. Quando si invoca il compilatore, questo trasforma il tutto in una serie di istruzioni per l’architettura di riferimento (target) che nel caso preso ad esempio è x86 a 64 bit, quindi il tutto si traduce nella chiamata alla funzione printf. L’immagine seguente mostra come le istruzioni in C diventano codice assembly (mnemonico) per l’architettura target.

Il tutto viene assemblato in un file che ha una struttura ben particolare: nel caso di Windows si tratta di un file eseguibile .exe ossia di un file che contiene varie cose e tra queste cose c’è anche ovviamente tutta la sezione di codice in linguaggio C. Il risultato in codice macchina (esadecimale) è proposto nell’immagine seguente:

Questo file assemblato e trasformato in codice comprensibile dall’architettura target è il nostro punto di partenza per vedere come fare il procedimento contrario, ossia la decompilazione.

Decompilazione: guida e strumenti di analisi statica e dinamica

Andiamo quindi a eseguire la decompilazione, cioè la ricostruzione per quanto possibile dei sorgenti a partire dal programma finito e perciò dall’eseguibile. Quando facciamo Reverse Engineering abbiamo a che fare con una serie di byte come quella nella figura a destra nell’immagine precedente e dobbiamo tradurre tale sequenza di byte in istruzioni assembly; poi, per capirci qualcosa di più andremo a tradurre questa serie di istruzioni mnemoniche assembly di nuovo in linguaggio C.

Questo può essere svolto da strumenti specifici e in questo tutorial vi proporremo tool Open Source e liberamente scaricabili da tutti. Nello specifico vi spiegheremo l’utilizzo di Ghidra (la cui schermata di lavoro è proposta nell’immagine seguente); ciò che lo rende interessante è il fatto di essere stato sviluppato dalla NSA (National Security Agency) americana e di essere stato reso disponibile su Github quando è stata svelata esistenza del progetto.

Ghidra non solo è gratuito ma è anche Open Source e chiunque può vedere com’è scritto ed è interessante perché si tratta di uno strumento di Reverse Engineering che aggredisce una vastità di architetture di piattaforme: ad esempio consente di disassemblare codice compilato per ARM, per x86 e addirittura per microcontrollori tipo l’8052 Intel.

Esistono due categorie di strumenti di Reverse Engineering che compiono:

- Analisi statica

  • Disassembler (da codice macchina ad assembly)
  • Decompilatori (da assembly a linguaggi di alto livello)

Analisi dinamica

  • Debugger
  • Sniffer / analizzatori di pacchetto
  • Emulatori

Ghidra è uno strumento principalmente di analisi statica, quindi qualcosa che consente di ottenere informazioni su un programma senza che questo sia in esecuzione; questo è molto importante perché non sempre è consigliabile far girare il programma che vogliamo analizzare: per esempio chi deve analizzare i malware lo fa in ambienti protetti e non certo su macchine di produzione.

Nei nostri esempi vedremo come si disassembla, quindi in che modo si ritorna dall’eseguibile al codice assembler (a una rappresentazione in mnemonico dei codici delle istruzioni) che in questo caso è per x86 64; poi spiegheremo cosa significa decompilare, quindi ritornare a una rappresentazione di più alto livello.

Va da sé che l’analisi statica non basta, ci sono anche e sono necessari strumenti per l’analisi dinamica e quindi che consentono di verificare il comportamento del programma che stiamo analizzando mentre questo sta girando e con i quali è possibile capire che cosa fa; tra questi c’è Wireshark, oggetto di un tutorial su questo stesso sito, utile ogni qual volta vogliamo analizzare un software che dialoga col mondo esterno tramite la rete, perché permette di vedere che cosa sta scambiando col mondo esterno, quali sono i pacchetti che transitano, che struttura hanno, se sono cifrati o in chiaro e quant’altro. Ciò rappresenta una forma di analisi dinamica perché per catturare il traffico dobbiamo far girare il software.

Poi chiaramente si parla anche di debug quindi gli strumenti che consentono di intervenire sul flusso di esecuzione giusto per interromperlo in un punto specifico, analizzare lo stato del dei registri e della memoria in quel punto e così via.

Simulazione di una decompilazione

Come esercitazione andremo a decompilare un eseguibile di cui il sorgente non è disponibile: si tratta di un semplice programmino che ci chiede un codice (una password) ma potrebbe essere, ad esempio, il codice seriale di licenza che ci viene richiesto quando dobbiamo installare o utilizzare un programma proprietario. In questo caso, se introduciamo qualcosa di errato, il software risponde che il codice è sbagliato.

Tramite tecniche di Reverse Engineering vediamo di analizzare questo eseguibile per capire come distingue un codice giusto da uno sbagliato.

Banner

 

Come prima cosa apriamo Ghidra e importiamo il nostro eseguibile attraverso il comando Open Project del menu File; selezioniamo il file dalla finestra di ricerca che si apre e Ghidra, se lo riconosce, subito ci mostra le informazioni corrispondenti, ossia che tipo di eseguibile è, con che cosa è stato compilato e riconosce la piattaforma (ossia l’architettura target) per la quale è stato compilato.

Nella finestra Import Result Summary che si apre, Ghidra fornisce anche altre informazioni che si possono estrarre anche solo dal vedere l’eseguibile: ci dice quando è stato compilato, quale versione del compilatore è stata utilizzata ecc.

Poi aprendo effettivamente il programma possiamo analizzarlo: se l’analisi per quel file non è mai stata fatta, una finestra di dialogo ci chiederà se procedere con l’analisi. Come vedete dall’immagine seguente, procedendo già possiamo il nostro programma tradotto in una serie di codici memonici, ossia di istruzioni assembly e di istruzioni offset all’interno della finestra del codice.

 

Nella sezione centrale di sinistra della schermata, Symbol Tree, Ghidra ci dà l’elenco delle funzioni implementate in questo eseguibile e sicuramente ce n’è una, perché tutti i programmi scritti in C ce l’hanno e si tratta della funzione main. Se andiamo a disassemblare questo programma vedremo le istruzioni che lo compongono e come appare nell’immagine seguente possiamo già individuare la funzione main. Qui si individuano le stringhe utilizzate per mostrare i messaggi a schermo, che nel caso specifico riguardano l’invito a inserire il codice o, in caso di codice errato, “Inserisci un codice valido”.

Il disassembler di ghidra evidenzia anche le chiamate a funzione di sistema, quindi quella CALL Print che appare nell’immagine precedente, in questo caso serve a stampare il messaggio “Inserisci un codice valido”.  ancora questo non ci dice nulla sul cos’è effettivamente un codice valido e cosa non lo è.

Una funzione molto utile di Ghidra è la possibilità di mostrare questo codice non come una lista di istruzioni assembly ma piuttosto come un grafo (Function Graph): ogni volta che ci sono dei salti e quindi che c'è un’istruzione condizionale (vale a dire un'istruzione che dice se si verifica una condizione vai a questa linea del codice altrimenti vai a un’altra) Ghidra crea un piccolo blocchetto per ognuna di queste. Insomma, una sorta di rappresentazione grafica che ci consente di analizzare il flusso del programma in maniera visiva, quasi si trattasse di un diagramma di flusso.

Resta inteso che bisogna comunque conoscere approfonditamente le istruzioni dell’architettura di riferimento (in questo caso x86 64) ed avere familiarità con questo processo.

In Ghidra esiste anche il decompilatore, ossia uno strumento che riesce a invertire -per quanto possibile- il processo di traduzione che ha fatto il compilatore, il quale ha preso le istruzioni in linguaggio C e le ha tradotte in istruzioni che comprende la macchina, quindi nel sorgente delle corde del CALL e quant’altro.

Il decompilatore di Ghidra può ricostruire, per quanto è possibile, il codice C corrispondente. Ora c’è da fare una precisazione molto importante: questo processo non sempre è possibile o comunque i risultati possono variare tantissimo perché il compilatore quando compila traduce il C in codice macchina effettuando delle ottimizzazioni, tanto che può stravolgere il codice di partenza.

Nell’immagine seguente vedete, nel riquadro a destra della schermata, il risultato della decompilazione, ossia quello che secondo Ghidra è il listato in C (codice sorgente) del programma che a sinistra appare in assembly.

In breve, il codice sorgente che il decompilatore ricava è equivalente a quello originale, vale a dire che è un programma che si comporta allo stesso modo ma non è esattamente il programma di partenza, il che già di per sé rende difficile la possibilità di invertire e di ricostruire il codice eseguibile.

Poi va considerato che in generale compilatori diversi possono produrre codice differente pur partendo dallo stesso sorgente in C, perché non esiste, quantomeno nel linguaggio C, un match 1 a 1 tra sorgente ed eseguibile. Peraltro la differenza nasce dal fatto che a un eseguibile si può arrivare da varie forme di costruzione del sorgente.

Comunque il risultato fornito dal decompilatore ci dà una rappresentazione sul funzionamento del programma che è decisamente più chiara di quella in mnemonico e che dà tantissime informazioni: come vedete nel listato dell’immagine seguente, ci fa vedere che con la riga 11 si stampa a video il messaggio “Inserisci un codice valido” e poi che avviene la lettura di quello che scrive l’utente in risposta a tale messaggio, quindi poi parte un ciclo (if) che verifica se quello che l’utente ha inserito è corretto oppure no. Nel listato ci sono due condizioni che devono essere soddisfatte e non è chiarissimo che cosa faccia la parte corrispondente, però si vede chiaramente che ci sono due numeri che vengono confrontati uno con 1000 e uno con xc. Il numero che viene confrontato con 1000 è un accumulatore che viene incrementato ogni volta con il valore di questa variabile è l’altro è un contatore.

Per chiarire cosa fa esattamente il codice proponiamo, nell’immagine seguente il codice sorgente effettivo che abbiamo compilato per l’esempio e che poi con Ghidra è stato disassemblato e decompilato. Come vedete ci sono delle similarità, in quanto la nostra funzione main ha ancora una printf che manda a video il solito messaggio “Inserisci un codice valido” e poi fa un salto, ossia  una chiamata check e questa check fa una cosa simile a quanto già visto nel sorgente generato da Ghidra; infatti nel codice originale possiamo capire che in pratica il test consiste nel verificare se la stringa inserita dall’utente è meno di 12 caratteri e se la somma di tutti i caratteri è maggiore di 1000. La somma deriva dal fatto che i caratteri convertiti in codice ASCII corrispondono ognuno a un valore numerico, quindi sommando le lettere si possono ottenere dei numeri.

In questo caso avete visto che il compilatore compila e traduce le nostre istruzioni in qualcosa di molto diverso rispetto a quello che era in origine, ma è un caso particolare.

Quindi ci sono delle differenze, ma il decompilatore ci consente di capire meglio come funziona un programma, specie se non ha alcun tipo di protezione e quindi è facile da decompilare. Pur essendo differente il risultato, dal confronto dell’esempio abbiamo ricavato informazioni sul codice da inserire per l’autemtizcazione, quindi sappiamo che abbiamo bisogno di non più di 12 caratteri la cui somma ASCII sia almeno pari a 1000.

Processo di compilazione in .NET

Non tutte le tecnologie però funzionano allo stesso modo. Vediamo ad esempio dotNET, fermo restando che la descrizione che segue è più o meno analoga anche nel caso di Java. Questa è una distinzione importante rispetto a quello che avviene con i programmi in linguaggio C,

Quando scriviamo un programma in C sharp. Visual Basic .NET, nel momento in cui viene compilato non viene tradotto direttamente in istruzioni in linguaggio macchina ma piuttosto in una rappresentazione intermedia che si chiama CIL; un tempo si chiamava MSIL (Microsoft Intermediate Language) mentre oggi è Common Intermediate Language.

Banner

Si tratta comunque di una specie di assembly che contiene delle istruzioni per un’architettura virtuale che è quella della cosiddetta Common Language Runtime (CLR) e poi quando effettivamente eseguiamo il programma, il compilatore Just In Time (JIT) di .NET traduce quelle istruzioni in istruzioni assembly che poi vengono eseguite dalla macchina target.

 

Il processo è più o meno identico in Java. Il motivo per cui Java è stato concepito in questo modo è per consentire di eseguire lo stesso programma binario, quindi lo stesso programma byte code, su piattaforme differenti, quindi lo stesso byte code può essere seguito su uno smartphone con processore ARM o da un computer desktop con processore x86.

Nel caso di dotNET si è utilizzato un linguaggio intermedio allo scopo di permettere al programmatore di sviluppare con linguaggi di alto livello differenti e di consentire interoperabilità tra gli stessi, infatti in .NET possiamo scrivere nei moduli C sharp in moduli Visual Basic .NET e questi produrranno esattamente lo stesso codice CIL.

Sembra una cosa banale, però questo aspetto implica un prezzo non indifferente in termini di sicurezza, perché la rappresentazione in CIL è molto più vicina linguaggio di alto livello rispetto a quanto non lo sia l’assembly, quindi distribuire un eseguibile che in realtà contiene codice CIL, sebbene ci sia un piccolo loader in ogni eseguibile dotNET, il codice che viene eseguito dal JIT si trova nella sezione DATA dell'eseguibile, quindi è codice CIL e questo ci consente più facilmente di ricostruire i sorgenti perché c’è quasi una corrispondenza 1 a 1 tra le istruzioni, per esempio in C Sharp, e le istruzioni in CIL.

Vediamo quindi cosa succede quando proviamo a fare un processo simile a quello che abbiamo fatto con Ghidra, però con un eseguibile dot.NET invece che compilato dal C. In questo caso utilizziamo un altro tool, anch’esso Open Source, liberamente scaricabile che consente di fare disassembling e in pratica di decompilare dal C Sharp Visual Basic e consente anche di eseguire l’analisi dinamica. Il tool si chiama dnSpy e la sua finestra di lavoro, che appare una volta installato e avviato, è mostrata nell’immagine seguente.

 

Simulazione della decompilazione in .NET

Riprendiamo l’esempio del controllo del codice di licenza ma scritto in dotNET e cerchiamo di capirne la logica utilizzando dnSpy; dalla finestra di lavoro, con il comando di menu File>Open apriamo l’eseguibile (possiamo farlo anche aprendo la cartella in cui si trova e direttamente trascinandolo con drag and drop nell’area Assembly Explorer che si trova nella parte sinistra della schermata di lavoro.

Il programma ci fa vedere, attraverso il disassemblatore, tutto quel che si trova all’interno dell’eseguibile, quindi se ci sono delle risorse binarie tipo immagini e icone inserite (e possiamo estrarle) quali sono i moduli che vengono utilizzati o gli assembly dotNET che vengono utilizzati e poi ci consente di vedere il disassembling CIL del programma (immagine seguente).

Dando uno sguardo al risultato vediamo che ci sono dei riferimenti alle stringhe delle funzioni, che in questo caso sono console write; insomma, si capisce che cosa sta accadendo. Però possiamo anche decidere di vedere una rappresentazione in C Sharp di quel codice.

Questo aspetto è importante perché rispetto al codice decompilato a partire da C++, che non ha i nomi delle variabili, non ha i nomi dei simboli e che ha i controlli check scritti in modo particolare, qui di fatto abbiamo il codice che possiamo leggere, comprendere e modificare; addirittura dnSpy ci consente di esportare -mediante il comando di menu File>Export Objects- tutto il codice decompilato in una Solution Visual Studio, quindi in progetto per Visual studio che poi possiamo continuare a sviluppare. Ci consente anche di fare debugging e modificare il codice direttamente dalla sua interfaccia utente. 

Banner

Con un tool del genere è chiaro che anche chi non è sviluppatore o esperto del linguaggio di ptogrammazione può capire che cosa fa il programma, che nello specifico ci mostra il solito messaggio “Inserisci un codice valido” poi legge quello che l’utente digita e lo passa a una funzione check e se la funzione fornisce come esito che il codice è corretto procede, altrimenti il codice è sbagliato e chiede una nuova introduzione.

Questa check anche qui calcola la somma dei caratteri e se la stringa è di almeno 10 caratteri e la somma è minore di 1000 vuol dire che c’è un errore, altrimenti il codice è corretto.

Quindi la decompilazione di applicazioni che utilizzano una tecnologia byte code è in alcuni casi più semplice perché contrariamente a quello che si può pensare, cioè che dando solo l’eseguibile il cliente ha soltanto quello e quindi non può capire come è stato fatto il programma, in realtà gli stiamo dando il codice.

Per Java il funzionamento è molto simile perché a grandi linee il concetto è lo stesso. La stessa cosa si verifica nei confronti, per esempio, di un’applicazione Android, vale a dire che è possibile disassemblarla e decompilarla più facilmente.

Protezione dalla decompilazione

Esistono delle tecniche che consentono di mitigare questo aspetto che sono i cosidetti “offuscatori” i quali sono tool che modificano ulteriormente il codice per renderlo più o meno intelligibile prima di compilarlo.

Un’altra soluzione consiste nella chiave hardware: per esempio si fanno dei controlli del tipo if la chiave è inserita si procede, else si blocca, però è chiaro che se si decompila l’eseguibile e si rimuove questo costrutto if – else o si pone un salto nel codice si aggira la protezione.

La protezione hardware può essere più complessa, implementando una chiave hardware che abbia a bordo sufficiente potenza di calcolo per effettuare delle operazioni complesse: per esempio la protezione non consiste solo nel dire al programma “Sì hai la licenza, No non ce l'hai”, ma di fatto una piccolissima parte del programma è spostata sulla chiave hardware e viene eseguita (per esempio con un’struzione che rimanda fa un salto a tale hardware) dal microcontrollore che sta a bordo della chiave, quindi è necessario avere collegato il dispositivo fisico per poter eseguire il programma.

Il problema in questo caso è che non tutte le applicazioni si possono proteggere in questo modo: per esempio le applicazioni di calcolo scientifico, dove le performance sono tutto, è impensabile  demandare una parte del calcolo al microcontrollore che sta a bordo della chiavetta.

A livello software è possibile, anche nel caso di applicazioni C++, utilizzare tutta una serie di istruzioni nel codice per impedire al disassembler di eseguire l’analisi statica.

E qui torniamo un attimo su Ghidra: il suo disassembler esegue un’analisi statica e quindi si limita a leggere il file in binario e a tradurre i relativi byte che vede in istruzioni assembly; tipicamente in un’architettura le istruzioni sono a lunghezza variabile, per consentire la massima efficienza, e ciò vale anche per l’architettura x86 64 bit. Quindi ci sono istruzioni da due byte, da 17 bit, da 18 bit ed  il processo di analisi statica avviene leggendo il file di seguito un’istruzione alla volta.

Ci sono delle tecniche di offuscamento che consistono nell’inserire delle istruzioni non corrette in parti di programma che non verranno mai eseguite; quindi si mettono delle istruzioni che fanno confondere il disassembler, perché lui legge cose che non hanno senso e quindi da quel punto in poi perde il filo, però sono istruzioni che non compromettono la funzionalità del programma perché poi sono bloccate da dei salti e quindi la CPU non salta mai a quel punto.

Ghidra: il decompilatore è facile da usare

Ghidra è un tool molto utile anche a chi non è esperto di programmazione, perché sicuramente a chi conosce il linguaggio del sorgente di un programma, poter decompilare l’eseguibile e arrivare ad esso fornisce molti spunti, però anche la possibilità di avere dei diagrammi di flusso del software è un valido punto di partenza. Infatti laddove l’esguibile non fosse decompilabile ma Ghidra riuscisse ad estrarre solo il risultato del disassembly, la possibilità di averlo illustrato in un grafo permette di comprendere come funziona il sorgente e in che modo è strutturato.

Tornando al codice di esempio proposto prima, se volessimo analizzare il disassemblato partendo dalla fine vedremmo che il programma termina dicendoci “codice sbagliato” o “Il codice è corretto” quindi chiaramente possiamo ripercorrere in salita i rami del diagramma di flusso e ricostruire il flusso delle operazioni, il che ci permette in qualche modo di ipotizzare le operazioni logiche corrispondenti (immagine seguente).

In pratica analizzando l’assembly possiamo, dalle due condizioni, identificare i blocchi che determinano come si finisce nell’una o nell’altra; quindi avendo un po’ di familiarità con l’assembly possiamo risalire tutte le condizioni che determinano l’andamento del programma.

Non bisogna essere esperti di programmazione per poter bucare un programma come quello dell’esempio, perché dal diagramma già vediamo che c’è un blocchetto con una diramazione in corrispondenza di ogni istruzione di salto: tutte le istruzioni con la J sono delle Jump (dei salti a numeri d’istruzione indicati di seguito). Le cmp sono istruzioni di comparazione e jle (Jump Less Equal) sono salti condizionati, quindi se quello che c'è nel registro indicato nel blocco è minore o uguale al valore indicato si salta; le jg (Jump if Greater) determinano, invece, dei salti se il valore è maggiore di quello previsto dalle cmp contenute nello stesso blocco

Quindi non ci interessa sapere cosa fa il programma, se il nostro obiettivo è risalire quanto più possibile all’input, che nel caso specifico è la password da inserire; ebbene, se ci interessa trovare quella che fa mostrare a video il messaggio “il codice inserito è giusto” (ma potrebbe anche essere una condizione che consente l’accessoa  una cartella o l’utilizzo di un programma) partiamo da quel blocco e ne ripercorriamo il flusso attraverso le frecce, arrivando a vedere quali sono le condizioni che portano a ciò ed escludendo quelle che determinano l’insuccesso (codice inserito errato).

Una cosa che si può fare è invertire le condizioni di salto: con Ghidra può essere fatto cliccando sul blocco con il pulsante destro del mouse e impartendo, dal menu contestuale, il comando Edit>Instruction; così editiamo l'istruzione e possiamo ad esempio invertire una jump (immagine seguente).

Per ogni modifica, sotto l’istruzione che andiamo a scrivere Ghidra ci propone il relativo codice in linguaggio macchina. Naturalmente per modificare un assembly occorre aprire il programma origine in scrittura. Insomma, Ghidra permette di modificare e invertire il flusso del disassemblato, quindi ricompilarlo e verificare cosa accade.

L’importanza di analizzare un programma

Per l'informatica tranne che negli ambiti specifici dove il programma deve essere scritto seguendo certe convenzioni, non vi sono regole in grado di definire quale programma è fatto bene e quale meno bene; quindi quando si utilizza un software di terze parti ci si affida alle competenze dei programmatori. Poter analizzare i software e capire se sono abbastanza sicuri è un modo per tutelarsi come utenti o committenti.

Come sviluppatori o committenti di programmi, si pone il problema di come ci si difende da chi vorrebbe usare il Reverse Engineering per carpire i segreti dei programmi. Esistono degli strumenti come gli offuscatori già descritti, che in qualche modo permettono di rendere il Reverse Engineering difficile, ma in realtà non è quello il vero strumento con il quale ci si protegge, perché la via maestra da seguire è di scrivere il codice seguendo certe regole. Per esempio le chiavi non si possono gestire come delle costanti.

Chi si occupa di crittografia sa che spesso il 90% dei problemi di sicurezza relativi alla crittografia implementata nelle applicazioni non riguarda gli algoritmi di cifratura ma, la costruzione delle chiavi. Per esempio molti ransomware sono stati bucati e invertiti per questo motivo, per la facilità nel trovare le chiavi.

Tornando alle buone regole e riprendendo gli esempi fatti in questo tutorial, non è difficile scrivere un programma che verifica se la somma dei caratteri ASCII scritto è minore di 1000 e il numero dei caratteri di un codice è almeno 10; il difficile è scriverlo in modo tale che non sia ovvio capire cosa fa quando viene disassemblato ed eventualmente decompilato. Il software non potrà mai essere sicuro se a scriverlo sono degli sviluppatori che sono ben formati.

Un altro esempio è la necessità di gestire le licenze: è vero che tra i clienti quasi nessuno si metterà a scaricare e utilizzare tool come Ghidra, ma è anche vero che le soluzioni chiavi in mano non sempre funzionano, nel senso che non basta avere la chiave hardware affinché un software sia protetto e ciò perché spesso cerce soluzioni, per essere generali e semplici da integrare, magari funzionano benissimo però l’integrazione della chiave hardware non funziona molto bene quindi è facile aggirarla.

Ultimo esempio è l’utilizzo di chiavi di un fornitore di chiavi hardware molto noto che viene utilizzato anche da software house molto blasonate: proprio perché applicate a software noti, è facile che questi siano già stati oggetti delle mire degli hacker e che magari siano già stati craccati, quindi ci si ritrova ad avere un prodotto le cui vulnerabilità sono note.

Suggerimenti per la protezione del proprio codice

Vediamo ora tre modi di proteggere il codice; la primissima cosa è prestare sempre attenzione a quello che succede nel mondo che ci circonda, ovvero controllare quali sono gli strumenti a disposizione del pubblico che riguardano il linguaggio con cui si lavora; ad esempio fino a qualche anno fa dnSpy non esisteva, al massimo c’era ilSpy che era un disassemblatore in grado di mostrare semplicemente codice IL ed era molto più complesso da usare. Quindi se scrivete in C cercate in rete se esistono strumenti per il Reverse Engineering di programmi in C e via di seguito. 

Altro suggerimento è che quando bisogna implementare delle funzionalità di sicurezza e quindi accessi subordinati a codici e chiavi, password o codici non devono mai essere scritti direttamente all’interno dell’eseguibile o dei sorgenti come costanti. Ci sono delle tecniche molto banali per password e codici di qualunque tipo.

Un ultimo suggerimento è mai implementare controlli di accesso basati su una semplice if (una semplice condizione) perché è vero che gli esempi che abbiamo fatto subordinano la verifica della chiave di accesso a due condizioni, però una if restituisce solo vero o falso e come sicurezza è un po’ poco, giacché è facile invertirlo e modificarlo, anche in C 

Come valutare eseguibili prodotti da terzi

Per valutare i software di terze parti sotto l’aspetto della sicurezza occorre innanzitutto fare una distinzione fatta tra software sviluppati internamente per uso esclusivo e software venduti da vendor che invece si rivolgono al mercato generalista. I due scenari sono molto diversi, anche perché tra i software generalisti poi ci sono quelli a codice proprietario e quelli a codice aperto; se possibile conviene preferire un’applicazione Open Source perché tali software sono sottoposti al vaglio di tutti e in generale dell’intera community che vi gira attorno, quindi non c’è neanche bisogno di fare Reverse Engineering e decompilare perché il codice è accessibile a tutti. Una soluzione Open Source può, a parità di feature e a parità di costi di gestione (Open Source non significa gratis...), essere più rassicurante dal punto di vista della sicurezza contro l’effrazione.

A questo proposito è opportuno ricordare che il fatto che si senta parlare di problemi di sicurezza relativamente a una determinata applicazione non significa che sia scritta peggio, ma significa solo che c’è più gente che si occupa di fare analisi di sicurezza e va alla ricerca di vulnerabilità per quell’applicazione. Questo per dire che purtroppo, spesso delle falle rimangono in un prodotto per tantissimo tempo semplicemente perché nessuno le cerca né le trova.

Per le applicazioni proprietarie a codice chiuso, improvvisarsi nell’analisi non è semplice perché quasi sicuramente gli sviluppatori avranno adottato delle strategie per rendere il codice meno leggibile, non fosse altro che per proteggere i loro interessi; quindi le falle di questi o le trovano gli sviluppatori, o non le trova nessuno. Nel software scritto specificatamente per un cliente è difficile riuscire a trovare documentazione o testimonianze di qualcuno che l’ha decompilato. Una cosa che si può fare è quella di aprire gli eseguibili con un editor esadecimale e vedere se ci sono dei messaggi che in qualche modo vi possono far capire qual è il flusso del programma.

Tenete comunque da conto che tutto quello che si può fare per rendere il codice meno invertibile, meno decompilabile, meno chiaro o più sicuro, sono soltanto azioni che servono per rallentare il Reverse Engineering ma non possono impedirlo, perché alla fine se la CPU è in grado di eseguire un programma, in qualche modo il flusso si può comprendere.

Per esempio nello sviluppo dei videogame si soende molto nella protezione del cidice: per esempio si usa una soluzione commerciale che crea una sorta di macchina virtuale con un set di istruzioni random e traduce l’eseguibile in questo set distruzioni che viene effettivamente eseguito da un interprete, quindi disassemblare quello è teoricamente impossibile; eppure si riescono ad aggirare le protezioni.

Una cosa che si può fare sempre, se si desidera conoscere meglio il software commissionato a un terzo e questo non rilascia il relativo codice, è richiedere al fornitore di darvi adeguata documentazione, quindi per esempio che ci dica esattamente quali criteri di crittografia ha utilizzato per la propria applicazione, un po’ come si fa per le policy di privacy quando si rilasciano i propri dati a qualcuno.

Per esempio se si tratta di un’applicazione client-server, domandate almeno se la comunicazione sarà cifrata, perché se non lo è siete esposti a un sacco di rischi. E sicuramente andare a decompilare gli applicativi per scoprirlo può non darvi facilmente la risposta desiderata.

Ripe Ncc Member
vmware
CloudLinux
Plesk
HP
Microsoft