7. Formati file eseguibili

Prima di poter iniziare a decompilare un file devi essere in grado di leggerlo.

Esistono 3 possibili tipi di formati di file:

  • Formati strutturati (COFF, ELF)
  • Formati di streaming con tag (OMF, IEEE)
  • File raw (DOS, immagini rom, S-record ecc.)

Ogni tipo di formato ha i suoi punti di forza e di debolezza. I primi due formati di file portano con sé alcune informazioni che possono aiutare il decompilatore a identificare informazioni utili. D’altra parte, il terzo tipo non fornisce molte informazioni, quindi le informazioni devono essere fornite dall’utente al decompilatore.

Ogni formato di file richiede il proprio lettore del formato file. Per implementare la lettura di questi formati, è necessario procedere con i seguenti step.

Passo 1: identificare il tipo di file

Il primo passo dopo aver aperto il file è identificarne il tipo. I file di flusso strutturati e con tag iniziano con una sequenza di byte ben definita che aiuta a identificarli (questi byte sono chiamati magic numbers perché consentono una veloce discriminazione tra file). Di seguito sono riportate alcune sequenze di byte per formati oggetto comuni:

Formato fileMagic number
PE0x4F
ELF0x45 0x4c 0x46
COFF0x4C 0x01
MACH-O0xcafebabe 0xfeedface

Bisogna procedere con attenzione al parsing dei binari, dal momento che ogni formato file potrebbe avere alcuni dettagli più nascosti e più difficili da gestire. Il formato file COFF ne è l’esempio principale.

Una caratteristica di COFF è che i primi 2 byte identificano sia il formato come COFF che il processore di destinazione. Purtroppo non esiste uno standard per questi 2 byte, e anche per i processori che supportano sia big endian che little endian, gli stessi 2 byte possono apparire in entrambi gli ordini, rendendo difficile identificare con assoluta certezza il file come file COFF.

Vedremo che anche i file COFF per lo stesso processore di destinazione possono avere strutture di dati diverse, perché diversi compilatori hanno scelto di non seguire lo standard (tipicamente a volte usavano campi a 32 bit al posto della definizione originale del campo a 16 bit).

Se non viene rilevata nessuna delle sequenze di cui sopra, il file potrebbe essere un’immagine non elaborata o un formato di file sconosciuto. In questi casi, è richiesto un intervento manuale da parte dell’utente per specificare le informazioni di cui necessita il decompilatore. Si nota questo tipo di richiesta, ad esempio, in IDA o in Ghidra quando il programma non riesce ad individuare correttamente il tipo di file (o contiene qualche errore).

Passo 2: identificare il tipo di processore

Trattandosi di istruzioni macchina il decompilatore deve identificare la CPU target, cioè la CPU in grado di eseguire le istruzioni nel file di input. La decompilazione non richiede l’effettiva esecuzione delle istruzioni, quindi la CPU di destinazione può essere diversa da quella che sta eseguendo il decompilatore (la CPU host). Cioè, i decompilatori dovrebbero essere strumenti incrociati, in grado di accettare file binari generati per diverse architetture di processori.

La selezione della CPU corretta a volte definisce i tipi di dati che il programma di destinazione utilizzerà. Ciò tuttavia non è sempre vero, poiché il programma originale potrebbe essere stato compilato in una varietà di modelli . I seguenti formati di file strutturati forniscono informazioni sull’architettura:

D’altra parte, solo i formati raw forniscono solo una quantità minima di informazioni (a volte non c’è alcuna informazione). In questi casi, è possibile applicare una serie di euristiche per dedurre il tipo di file. Il decompilatore può utilizzare un database di sequenze di codici comuni per identificare la CPU di destinazione. Questo può essere un tentativo lungo, ma a volte ha successo, se non altro per dare un suggerimento all’utente. Se non viene trovata alcuna corrispondenza, l’utente deve fornire (tramite un file di progetto o tramite l’interfaccia utente) la CPU di destinazione che desidera utilizzare prima di procedere con l’analisi del file oggetto.

Passo 3: identificare le aree di codice, dati e informazioni

I formati di oggetti strutturati contengono sia aree di codice che di dati che verranno eseguite quando il programma viene eseguito e supportano anche aree che vengono utilizzate dal sistema operativo durante il caricamento del file in memoria (ma il cui contenuto non viene effettivamente eseguito dalla CPU), e anche aree utilizzate da altri strumenti come un debugger.

I formati ELF e COFF si basano sul concetto di sezioni. Una sezione è un’area nel file che contiene informazioni omogenee, come tutto il codice, o tutti i dati, o tutti i simboli ecc.

Il decompilatore legge la tabella delle sezioni e la utilizza per convertire gli offset dei file in indirizzi e viceversa.

Viene utilizzato anche per consentire all’utente di ispezionare il file per offset o per indirizzo (ad esempio quando si esegue un dump esadecimale del contenuto del file).

Si noti tuttavia che non necessariamente una sezione contrassegnata come eseguibile conterrà solo istruzioni macchina. Altri tipi di dati di sola lettura possono anche essere inseriti in una sezione eseguibile, come stringhe di sola lettura (“const”) e costanti a virgola mobile. Il compilatore o il linker può anche aggiungere codice extra che non è stato generato direttamente dal codice sorgente compilato. Un esempio di codice aggiuntivo sono le tabelle delle funzioni virtuali, la gestione delle eccezioni (try/catch/throw) in C++ e la Global Offset Table (GOT) e la Procedure Linkage Table (PLT) per supportare il collegamento dinamico delle DLL.

E’ quindi importante che il decompilatore identifichi i dati che sono stati inseriti nella sezione di codice, in modo da non disassemblare alcune aree di dati. Se ciò dovesse accadere, molte delle analisi successive utilizzeranno dati errati, con la possibilità di invalidare l’intero processo di decompilazione. Tutti i formati di file dovrebbero fornire almeno l’offset della prima istruzione eseguita dopo che il file è stato caricato in memoria.

Le aree informative possono aiutare enormemente il processo di decompilazione, perché ogni pezzo di informazione aggiuntiva oltre al codice e ai dati eseguiti è un pezzo di informazione che il decompilatore non dovrà indovinare usando la propria analisi.

I file eseguibili non eliminati forniscono vari livelli di informazioni simboliche:

  • indirizzi di etichette globali: potrebbero essere punti di ingresso di funzioni e variabili di dati globali. Si noti tuttavia che la maggior parte delle volte la dimensione di tali oggetti non viene fornita. Cioè, potremmo sapere dove inizia una funzione ma potremmo non sapere dove finisce. Poiché i punti di ingresso delle funzioni statiche potrebbero non essere archiviati nei file, non è affidabile presumere semplicemente che una funzione termini all’inizio del punto di ingresso della funzione successiva.

  • nomi delle librerie importate (collegate dinamicamente) e indirizzi dei punti di ingresso delle biblioteche o del codice del trampolino generato per accedere a tali biblioteche. Se il programma di destinazione è esso stesso una DLL, nel file binario viene memorizzata una tabella di esportazione. La tabella di esportazione fornisce il punto di ingresso delle funzioni esportate dalla DLL.

  • se nel file viene trovata una tabella di rilocazione , il decompilatore può utilizzare le informazioni in essa contenute per dedurre quali istruzioni operano sugli indirizzi anziché sulle costanti numeriche. Questo è molto importante quando si cerca di identificare lo scopo di un’istruzione di montaggio. Ciò significa anche che il decompilatore deve virtualmente collegare il file di destinazione a un indirizzo di memoria fittizio, che può essere totalmente diverso dall’indirizzo effettivo che verrà utilizzato dal sistema operativo, specialmente durante la decompilazione di un file rilocabile (.o, .obj o . dll).

  • se il file è stato compilato con informazioni di debug (-g su sistemi Unix), è possibile trovare molte più informazioni, come l’elenco dei file sorgente e i numeri di riga utilizzati per creare il programma di destinazione, i tipi di variabili e sia variabili globali, statiche di modulo che locali di funzione con i loro nomi. Questo è il caso migliore, quindi un decompilatore deve sfruttare questa ricchezza di informazioni.

L’output del caricatore del formato del file oggetto è un insieme di tabelle che consente alle fasi successive del decompilatore di essere indipendenti da un particolare formato del file oggetto.

Biografia

Sono uno specialista in materia di sicurezza informatica, scrittore, contributore per il progetto Monero, una criptovaluta che è focalizzata nel proteggere le informazioni sulle transazioni. Il libro che ho pubblicato Mastering Monero è diventato una delle migliori risorse per padroneggiare Monero. Più informazioni su di me

Seguimi su Twitter o scrivimi una e-mail. Le donazioni sono molto apprezzate, mi permettono di continuare a lavorare e a scrivere.

Mastering Monero Book