Cos'è un disassemblatore?
Decompongo un disassemblatore in due parti, prima un decodificatore che accetta un codice esadecimale e genera un'istruzione di assemblaggio (possibilmente con la lunghezza di l'istruzione decodificata se il linguaggio assembly ha istruzioni di lunghezza variabile). E poi un algoritmo di disassemblaggio che utilizzerà il decodificatore per navigare nel codice eseguibile.
L'obiettivo generale di un disassemblatore, a mio modesto parere, sarebbe quello di recuperare tutto il possibili esecuzioni che possono essere costruite da un dato eseguibile e presentarlo in un formato conciso e leggibile dall'uomo.
Problemi di disassemblaggio
Ci sono molti problemi che un disassemblatore può incontrare durante lo smontaggio un programma binario. Uno dei più difficili sarebbe gestire il codice di auto-modifica . In effetti, fino ad ora non esiste una vera rappresentazione leggibile dall'uomo per il programma auto-modificante. Quindi, tutti i disassemblatori quando affrontano un codice che si auto-modifica falliscono miseramente nell'output di qualcosa di chiaramente comprensibile.
Il secondo problema che può fermare un disassemblatore è che di tanto in tanto il programma binario salta in un altro posto per eseguirne alcuni codice (chiamate di funzione, if-then-else, switch, ...). E, se la maggior parte di questi salti sono statici (l'indirizzo dove saltare è codificato staticamente nel codice), ci sono alcuni salti che dipendono dal contesto dell'esecuzione. Di solito chiamiamo questi salti salti dinamici (al contrario di salti statici ). Questi salti dinamici costringono il disassemblatore a tenere traccia non solo della sintassi delle istruzioni ma anche della loro semantica per non perdersi quando la incontra.
Infine un ultimo problema è che non tutti i programmi binari possono essere assunti per seguire una precisa ABI (Application Binary Interface), definendo una precisa interfaccia per le chiamate di funzione o un modo per gestire le strutture dati. In effetti, alcuni programmi binari sono realizzati a mano o con un compilatore modificato che tenterà di fuorviare i disassemblatori. Pertanto, il disassemblatore dovrà riconoscere una chiamata di funzione dalla sua semantica e non solo dalla sua sintassi.
Correttezza di un disassemblatore
Come abbiamo affermato in precedenza, l'obiettivo finale di un disassemblatore è ricostruire tutte le possibili tracce di esecuzione da un programma binario. Ovviamente, il più delle volte questo è estremamente difficile, quindi possiamo definire tre tipi di disassemblatori:
- Disassemblatore esatto : ipoteticamente, dovrebbe emettere tutte le tracce corrette che può essere eseguito sul programma binario e solo queste tracce.
- Disassemblatore eccessivamente approssimato : l'output di questo dovrebbe includere tutto il possibile tracce, possibilmente con alcune in più.
- Disassemblatore poco approssimato : l'output di questo dovrebbe essere incluso nelle possibili tracce ma non fornire non fattibili.
Tecniche esistenti e dove classificarle
Per ora, le due tecniche più popolari sono sweep lineare e attraversamento ricorsivo (vedi qui per maggiori dettagli).
Entrambi sono abbastanza ampiamente utilizzati in natura da numerosi reverse-engineer. Ma, in realtà, nessuna di queste tecniche è né esatta, né eccessiva, né sotto-approssimativa. Entrambi producono qualcosa che non è niente di ciò che abbiamo appena visto prima (a volte inventeranno un percorso che non sarà mai raggiunto, ea volte se ne dimenticheranno un altro).
Esistono tecniche più avanzate con maggiore preoccupazione per la correttezza (ad esempio Jakstab, McVeto, McVeto sul codice auto-modificante, ... ), ma la ricerca del recupero esatto è sicuramente fuori portata.
Quindi, scegliere tra sotto e iperapprossimazione dipende da quale sarà l'utilizzo dell'output del disassembler.