
This is the main repository for metasm, a free assembler / disassembler / compiler written 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.
Ia32 implementa una voce :default specifica, che gestisce la risoluzione automatica del fixup dello stack, supponendo che l'ultima istruzione 'call' ritorni. Questo può portare a risultati inaspettati; per la massima precisione è consigliato un header C contenente informazioni per tutte le funzioni esterne (vedi samples/factorize-headers-peimports per uno script che genera tale header da un'installazione completa di Visual Studio e dal binario target).
Ia32 implementa anche un callback specifico GetProcAddress/dlsym, che restituirà il valore di ritorno corretto se i parametri possono essere sottoposti a backtrace.
Gli script che implementano un disassembler completo sono samples/disassemble{-gui}.rb. Vedi i commenti per i tasti della GUI.
Manipolazione di ExeFormat:
Puoi codificare/decodificare un ExeFormat (cioè decodificare sezioni, import, header, ecc.)
Costruttore: ExeFormat.decode_file(str), ExeFormat.decode_file_header(str) Metodi: ExeFormat#encode_file(filename), ExeFormat#encode_string
I file PE ed ELF hanno una controparte LoadedPE/LoadedELF, in grado di lavorare con versioni memory-mmaped di questi formati (ad es. per fare debug di processi in esecuzione).
VirtualString:
Una VirtualString è un oggetto simile a String: puoi leggerne e riscriverne le slice. Può essere usata come EncodedData#data e consente quindi la virtualizzazione della maggior parte degli algoritmi di Metasm. Non puoi cambiare la lunghezza di una VirtualString. Prendere una slice di una VirtualString restituirà o una String (per dimensioni piccole) o un'altra VirtualString (una 'finestra' sull'altra). Puoi forzare l'ottenimento di una piccola VirtualString usando il metodo #dup(offset, length). Qualsiasi metodo non implementato chiamato su di essa viene inoltrato a una String congelata che è una copia completa della VirtualString (da evitare se possibile: la stringa sottostante potrebbe essere molto grande e lenta da accedere).
Attualmente sono implementate 3 VirtualStrings:
Le versioni Win/Lin sono piuttosto potenti e consentono facilmente cose come disassembly/patching di processi live (usando LoadedPE/LoadedELF come ExeFormat).
Debugging:
Metasm include alcune interfacce per gestire il debug. Le classi WinOS e LinOS offrono accesso ai processi del sistema operativo sottostante (ad es. OS.current.find_process('foobar') recupererà un processo in esecuzione con foobar nel nome del file; poi process.mem può essere usato per accedere alla sua memoria.)
Le API di debug di basso livello di Windows e Linux hanno un'interfaccia Ruby di base (PTrace e WinAPI); che vengono usate dalla classe Debugger unificata di alto livello. Il debug remoto è supportato tramite il protocollo wire del server GDB.
I debugger di alto livello possono essere creati con la seguente riga Ruby: Metasm::OS.current.create_debugger('foo')
Può esistere un solo tipo di classe host debugger alla volta; per fare debug di più processi, collegati ad altri processi usando la classe esistente. Questo è dovuto al modo in cui l'API di debug del sistema operativo funziona su Windows e Linux.
I backend di basso livello sono definiti nella sottodirectory os/, il front-end è definito in debug.rb.
Un'interfaccia di debug da console Linux è disponibile in samples/lindebug.rb; usa un look and feel simile a SoftICE (semplificato). Può parlare con un socket gdb-server; usa un target [udp:]host:port.
L'esempio disassembler-gui consente l'interazione con processi live quando si usa come target 'live:'.
Parser C:
Metasm include un parser C scritto a mano. Gestisce tutti i costrutti che conosco, eccetto i float esadecimali:
L'analisi di file C dovrebbe essere fatta usando un ExeFormat esistente, con il metodo parse_c_file. Questo assicura che le macro/ABI specifiche del formato siano definite correttamente (es: dimensione del tipo 'long', ABI per passare parametri alle funzioni, ecc.)
Quando analizzi una stringa C usando C::Parser.parse(text), ricevi un oggetto Parser. Contiene un campo #toplevel, che è un C::Block, il quale contiene #structs, #symbols e #statements. Le funzioni di primo livello si trovano nell'hash #symbol le cui chiavi sono i nomi dei simboli, associati a un oggetto C::Variable che contiene le funzioni. I parametri/attributi della funzione sono accessibili tramite func.type, e il codice si trova in func.initializer, che è esso stesso un C::Block. Sotto troverai una struttura ad albero di C::Statements (If, While, Asm, CExpressions...)
Un C::Parser può essere #precompiled per trasformarlo in una versione semplificata più facile da compilare: i typedef vengono rimossi, le sequenze di controllo vengono trasformate in 'if (XX) goto YY;' ecc.
Per compilare un programma C, usa PE/ELF.compile_c, che creerà un C::Parser con macro specifiche dell'exe definite (ad es. PE o ELF).
Gli header specifici del vendor potrebbero richiedere l'uso di #pragma prepare_visualstudio (per analizzare gli header di Microsoft Visual Studio) o di prepare_gcc (per gcc); quest'ultimo può essere auto-rilevato (o no). Gli header vendor testati sono VS2003 (incl. DDK) e gcc4; ymmv.
Attualmente la CPU#compilation di un codice C genererà un sorgente assembly (testo), che può poi essere analizzato e assemblato in codice binario.
Vedi ExeFormat#compile_c e samples/exeencode.rb