
Questo è il repository principale per metasm, un assembler / disassembler / compilatore gratuito scritto in ruby
Autore: Yoann Guillot
Panoramica di base:
Metasm ti consente di interagire con i formati eseguibili (ExeFormat): PE, ELF, Mach-O, Shellcode, ecc. Ci sono tre approcci a un ExeFormat:
Script pronti all'uso si trovano nella sottodirectory samples/; controlla i commenti nelle intestazioni degli script. Puoi anche provare l'argomento --help se ti senti fortunato.
Per maggiori informazioni, controlla la sottodirectory doc/. I file di testo possono essere compilati in HTML usando lo script misc/txt2html.rb.
Ecco una breve panoramica degli interni di Metasm.
Assembly:
Quando si compila, si parte da un testo sorgente (una String di Ruby, composto per lo più da una sequenza di istruzioni/dati/direttive di padding), che viene analizzato.
La stringa viene passata a un'istanza di Preprocessor (che gestisce #if, #ifdef, #include, #define, /* */ ecc., e dovrebbe essere compatibile al 100% con gcc -E), la quale è incapsulata in un AsmPreprocessor per i sorgenti assembly (per gestire definizioni di macro asm, 'equ' e commenti asm ';'). L'interfaccia per fare ciò è ExeFormat#parse(text[, filename, lineno]) oppure ExeFormat.assemble (che chiama .new, #parse e #assemble).
Il (Asm)Preprocessor restituisce token all'ExeFormat, che li analizza come Data, Padding, Labels o direttive del parser. Le direttive del parser iniziano sempre con un punto. Possono essere generiche (.pad, .offset...) o specifiche dell'ExeFormat (.section, .import, .entrypoint...). Vengono gestite da #parse_parser_instruction(). Se l'ExeFormat non riconosce una parola, questa viene passata alla sua istanza CPU, responsabile dell'analisi delle Instructions (o del lancio di un'eccezione). Tutti questi token sono memorizzati in uno o più array nell'attributo @source dell'ExeFormat (il @source di Shellcode è un Array; per PE/ELF è un hash [nome sezione] => [Array di dati analizzati]). Ogni valore immediato può essere un'Expression arbitraria (vedi più avanti).
Puoi quindi assemblare il sorgente in sezioni binarie usando ExeFormat#assemble.
Una volta disponibili i binari delle sezioni, l'intero eseguibile binario può essere scritto su disco usando ExeFormat#encode_file(filename[, format]).
PE ed ELF includono una funzionalità di autoimport che consente la creazione automatica di dati relativi agli import per funzioni note specifiche del sistema operativo (ad es. chiamate non risolte a 'strcpy' genereranno dati così che il binario venga collegato alla libreria libc al runtime).
Lo script samples/{exe,pe,elf}encode.rb può accettare un file sorgente assembly come argomento e compilarlo in un eseguibile funzionante.
Le classi CPU sono responsabili dell'analisi e della codifica delle singole istruzioni. L'attuale parser Ia32 usa la sintassi Intel (ad es. mov eax, 42). Il parser generico riconosce le label come una stringa all'inizio di una riga seguita da due punti (ad es. 'some_label:'). Si possono usare label locali in stile GCC (ad es. '1:', referenziate usando '1b' (all'indietro) o '1f' (in avanti); possono essere ridefinite quante volte serve). I dati sono specificati usando la notazione in stile 'db' (ad es. 'dd 42h', 'db "blabla", 0'). Vedi samples/asmsyntax.rb
EncodedData:
In Metasm tutti i dati binari sono memorizzati come un EncodedData. EncodedData ha 3 attributi principali:
EncodedData ha anche un #virtsize (per es. per le sezioni .bss) e un #ptr (offset interno usato per decodificare le cose).
Puoi fare il fixup di un EncodedData con un hash nome di variabile => valore (il valore dovrebbe essere un'Expression o un valore numerico). Quando lo fai, il target di ogni relocation viene vincolato usando il binding e, se il risultato è calcolabile (nessun nome di variabile esterna usato nell'Expression), il risultato viene codificato usando le informazioni di dimensione/segno/endianness della relocation. Se si verifica un overflow (prova a memorizzare 128 in una relocation signed a 8 bit), viene sollevata un'eccezione EncodeError. Usa il tipo :a32 per consentire il troncamento silenzioso dell'overflow. Se il target della relocation non è numerico, il target rimane invariato se usi EncodedData#fixup, oppure viene sostituito con il target vincolato usando #fixup!.
Disassembly:
Questo codice si trova nel file sorgente metasm/decode.rb, che definisce la classe Disassembler.
Il disassembler necessita di un ExeFormat decodificato (per poter dire quali dati si trovano a quale indirizzo virtuale) e di un entrypoint (un indirizzo virtuale o un nome di export). Può quindi iniziare a disassemblare le istruzioni. Quando incontra un Opcode marcato come :setip, chiede alla CPU la destinazione del salto (un'Expression che può coinvolgere valori di registro, ad es. jmp eax), e fa il backtrace delle istruzioni finché non trova il valore numerico.
Durante la decodifica, il Disassembler mantiene un hash #decoded che associa indirizzi (espressioni/interi #normalize()d) a DecodedInstructions.
Il disassembly genera un grafo di InstructionBlock. Ogni blocco contiene una lista di DecodedInstruction e puntatori al blocco successivo/precedente (per indirizzo).
Il disassembler traccia anche gli accessi ai dati da parte delle istruzioni e memorizza gli Xref corrispondenti. I parametri del backtrace possono essere modificati, e la profondità massima da considerare può essere cambiata specificamente per i backtrace :r/:w (xref di memoria delle istruzioni) usando #backtrace_maxblocks_data. Quando un'Expression viene sottoposta a backtrace, ogni blocco attraversato viene marcato in modo che i loop vengano rilevati e, se viene trovato un nuovo percorso di codice verso un blocco esistente, i backtrace possono essere ripresi usando questo nuovo percorso.
Il disassembler fa pochissime supposizioni e, in particolare, non presume che le funzioni ritornino; lo faranno solo se il backtrace delle istruzioni 'ret' è conclusivo. Questo è piuttosto potente, ma implica anche che qualsiasi errore nel processo di backtrace possa portare a un arresto completo; e significa anche che il disassembler è piuttosto lento.
Il metodo speciale #disassemble_fast può essere usato per ovviare a questo quando il codice è noto per essere ben formato (cioè presume che tutte le chiamate ritornino).
Quando viene trovata una subfunzione, viene creata una DecodedFunction speciale, che contiene un riepilogo degli effetti della funzione (come una DecodedInstruction potenziata). Questo consente al backtracker di 'scavalcare' le subfunzioni, migliorando notevolmente la velocità. Le DecodedFunctions possono essere basate su callback, per consentire un comportamento molto dinamico. Le chiamate a funzioni esterne creano DecodedFunctions dedicate, che contengono alcune informazioni API (ad es. informazioni sul fixup dello stack, accessi base ai parametri...). Queste informazioni possono essere derivate da un header C analizzato in precedenza. Se non è disponibile un prototipo di funzione C, viene usata una voce 'default' speciale, che presume che la funzione abbia un'ABI standard.