8. Identificare le istruzioni e i dati

Poiché in una sezione eseguibile non può essere presente codice, abbiamo bisogno di un modo per identificare quali sequenze di byte sono effettivamente codice e quali no. Il decompilatore sa dove inizia l’esecuzione del programma dal formato del file oggetto (il punto di ingresso del programma). Un algoritmo semplice e sicuro per identificare le regioni di codice consiste nell’analizzare quali istruzioni possono essere raggiunte dal punto di ingresso del programma. Il seguente pseudo-codice produrrebbe un elenco di punti di ingresso della funzione (oltre ad alcune informazioni utili aggiuntive):

callList = entry_address
while callList not empty
   address = pop from callList
   functionList += new function starting at address
   while true
       if instr at address is jump
           workList += jump destination
       if instr at address is call
           callList += call destination
       if instr at address is ret
           if workList empty
               break
       if instr is not conditional
           address = pop from workList
           continue
       address += instr length
   end while
end while

Lo stesso algoritmo può essere applicato a qualsiasi punto di ingresso della funzione. Se il file binario ha una tabella dei simboli (che sia un elenco di etichette di basso livello o informazioni di debug di alto livello), possiamo chiamare l’algoritmo per tutti i punti di ingresso della funzione noti.

Purtroppo l’algoritmo precedente presenta alcuni difetti:

  • non riconosce le funzioni a cui si accede tramite i puntatori di funzione;
  • non segue salti indiretti, come quelli usati nelle istruzioni switch ;
  • non identifica le funzioni presenti nel file binario ma mai chiamate.

Le funzioni a cui si accede tramite i puntatori di funzione si trovano nei seguenti casi:

  • puntatori a funzione diretta archiviati in memoria. In questo caso il decompilatore dovrebbe segnalare che la chiamata avviene tramite una variabile utente. Non è possibile per il decompilatore sapere quali funzioni possono essere chiamate da quelle istruzioni.

  • funzioni virtuali a cui si accede tramite una tabella virtuale C++.

  • funzioni a cui si accede tramite il codice del trampolino della libreria dinamica. Poiché il codice del trampolino deve essere esportato nel linker dinamico affinché il linker dinamico sia in grado di correggerlo con l’indirizzo del punto di ingresso alla funzione di libreria dinamica, questi possono solitamente essere convertiti in modo sicuro in chiamate dirette al codice del trampolino e contrassegnare il codice del trampolino come punto di ingresso della funzione di libreria.

  • funzioni a cui si accede tramite un registro che è stato inizializzato con l’indirizzo di destinazione di una funzione globale. Questo è un caso difficile, che richiede l’analisi del flusso di dati per calcolare quale valore è contenuto nel registro al punto di chiamata. Ma poiché non sappiamo ancora quali byte sono codice e quali dati, non possiamo eseguire tale analisi se non molto tempo dopo.

Poiché abbiamo stabilito che l’algoritmo precedente non troverà tutte le funzioni, dobbiamo applicare ulteriori analisi per identificare le funzioni mancanti. È possibile applicare la seguente euristica:

  • utilizzare un modulo di firma della libreria per identificare sequenze di byte che potrebbero essere il punto di ingresso di una funzione. L’utilizzo di un database di sequenze di byte di funzioni consente inoltre l’identificazione del nome della funzione, di eventuali variabili globali che possono essere utilizzate dalla funzione e dei tipi sia della funzione che dei suoi parametri di input. Se il comportamento della funzione è noto, possiamo contrassegnarlo in modo da non decompilarlo, riducendo così l’output solo alle funzioni utente e non alle funzioni di libreria.

  • chiedere al modulo del processore di fornire una breve sequenza di byte che viene tipicamente utilizzata da un compilatore per impostare lo stack frame all’inizio di una funzione. Sequenze di istruzioni come PUSH EBP + MOV EBP, ESP + SUB ESP, # si trovano tipicamente all’inizio delle funzioni nei programmi x86.

  • supponiamo che le prime istruzioni non-noop dopo un’istruzione RET che non è la destinazione di un salto siano il punto di ingresso di una funzione. Questo è un presupposto rischioso da fare e può considerare come byte di codice che sono invece dati. Una possibilità è fornire all’utente un elenco di tali punti di ingresso e lasciare che l’utente decida se utilizzare tali indirizzi come punti di ingresso di funzioni.

L’output dell’algoritmo è un elenco di punti di ingresso della funzione e un elenco di istruzioni jump e ret, che verranno utilizzate nella costruzione dei blocchi di base di ciascuna funzione. Per ogni funzione, registriamo anche la funzione che chiama.

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