9. Blocchi elementari da riconoscere

Ora che stiamo iniziando a dare un senso alla sequenza di byte non strutturata nel file binario, il prossimo problema che dobbiamo risolvere prima di poter iniziare a decompilare il codice è essere sicuri che una sequenza di istruzioni sia effettivamente eseguita nella sequenza che vediamo nell’archivio.

Considera la seguente sequenza:

L1:      MOV  EAX, $2
L2:      MUL  EAX, ECX
L3:      MOV  DWORD [0x402000], EAX

Poiché sappiamo dal manuale utente del processore che l’istruzione MUL richiede che uno degli operandi e il risultato siano in EAX, potremmo essere tentati di tradurre la sequenza precedente nella seguente istruzione:

var_402000 = ecx * 2; 

Sarebbe sbagliato per due ragioni:

  1. potrebbe esserci un salto da qualche altra parte del programma a L2. Se ciò accade, non possiamo essere sicuri che EAX conterrà il valore 2!
  2. Il registro EAX può essere riutilizzato dopo aver salvato in memoria il valore dell’istruzione MUL.

Ad esempio, una sequenza più grande potrebbe essere la seguente:

L0:     MOV   EAX, $3
        CMP   EBX, $0
        JNE   L2
L1:     MOV   EAX, $2
L2:     MUL   EAX, ECX
L3:     MOV   DWORD [0x402000], EAX
        MOV   DWORD [0x402010], EAX
        ...

Il modo corretto per tradurre la sequenza di cui sopra sarebbe:

eax = (ebx == 0) ? 2 : 3;
var_402010 = var_402000 = eax * ecx;

Ma come sappiamo che l’esecuzione potrebbe saltare L1 e passare direttamente a L2? E, cosa più importante, come facciamo a sapere che non ci sono altri modi in cui l’esecuzione può andare a L2, magari da qualche altra parte del codice?

Questi problemi possono essere risolti utilizzando una ben nota struttura dati chiamata basic block . Questa struttura dati è una delle più importanti dell’intero decompilatore, poiché gran parte del codice si baserà sulle informazioni memorizzate in essa per molte analisi.

Esistono molte informazioni in letteratura su cosa siano i blocchi di base. Abbiamo già dato una definizione informale. Eccone una più precisa: “un blocco di base è una sequenza di istruzioni; la prima istruzione è l’unico modo per l’esecuzione di entrare in un blocco di base; l’ultima istruzione è l’unico modo per l’esecuzione di uscire dal blocco di base.”

Una conseguenza di questa definizione è che ogni destinazione di salto avvia un nuovo blocco base e ogni istruzione di salto (comprese le istruzioni di ritorno) termina un blocco base.

Per un decompilatore non è chiaro se un’istruzione di chiamata debba terminare un blocco di base. A seconda della sofisticatezza degli algoritmi, si può considerare un’istruzione di chiamata per terminare un blocco di base, oppure si possono semplicemente ignorare le istruzioni di chiamata per il momento.

È comunque chiaro che la destinazione, ovvero un’istruzione di richiamo avvia sempre un blocco base.

Dato che abbiamo già calcolato l’elenco di etichette e salti durante la scansione del codice e dei dati e che abbiamo il punto di ingresso della maggior parte delle funzioni nella nostra tabella delle procedure, possiamo costruire i blocchi di base per una procedura utilizzando il seguente algoritmo:

 current_address = current_procedure->entry_address
    workList.push = current_address
    while workList not empty
        current_address = workList.pop
next:
        block = new Block at current_address
        blockList += block
        label = find label at address higher than current_address
        branch = find branch at address higher than current_address
        if label->address < branch->address_after_branch
            block->length = label->address - block->address
            current_address = label->address
	    goto next
	end if
	block->length = branch->address_after_branch - block->address
        if branch is return	// returns have no known destination
	    continue
        workList.push = branch->destination
        if branch is conditional
            current_address = branch->address_after_branch
            goto next
        end if
        // else will resume from branch destination
    end while

Questo algoritmo registrerà nel set blockList quali sequenze di istruzioni vengono eseguite in sequenza. Ma visto che ci siamo, perché non registriamo anche dove l’esecuzione va da un blocco all’altro? Dopo tutto, può essere utile sapere che nel nostro esempio, dopo aver impostato EAX a 3, lo useremo nell’istruzione MUL, invece che in qualche altra istruzione. Per fare ciò, registriamo 2 informazioni aggiuntive per ogni blocco:

L’elenco dei blocchi che saltano all’istruzione di ingresso del blocco corrente: ovvero l’elenco dei blocchi predecessori (pred in breve); e l’elenco dei blocchi a cui saltiamo quando abbiamo finito di eseguire questo blocco: questo è l’elenco dei blocchi successori (succ in breve). Ecco una versione modificata dell’algoritmo di costruzione del blocco di base che registra queste informazioni aggiuntive:

Block *get_block_at(Address start_address)
{
    Block *block = find in BlockList a block for start_address
    if block == not found
	block = new Block at start_address
        blockList += block
    return block
}

CreateBasicBlocks(current_procedure)
{
    current_address = current_procedure->entry_address
    block = get_block_at(current_address)
    current_procedure->entry_block = block
    workList.push = current_address
    while workList not empty
        current_address = workList.pop
        block = get_block_at(current_address)
next:
        label = find label at address higher than current_address
        branch = find branch at address higher than current_address

        if label->address < branch->address_after_branch
            block->length = label->address - block->address
            current_address = label->address
	    next_block = get_block_at(current_address)
            next_block->preds += block
            block->succs += next_block
            block = next_block
	    goto next
	end if

	block->length = branch->address_after_branch - block->address
        if branch is return	// returns have no known destination
	    continue
        end if

	next_block = get_block_at(branch->destination)
        next_block->preds += block
        block->succs += next_block
        workList.push = branch->destination
        if branch is not conditional
            continue           // will resume from branch destination
        end if
	next_block = get_block_at(branch->address_after_branch)
        next_block->preds += block
        block->succs += next_block
        block = next_block
        current_address = block->address
        goto next
    end while

    sort blockList by block's start address
}

Note :

  • questo algoritmo è ancora incompleto: non tiene conto dei salti indiretti, in quanto presuppone solo che un salto possa avere 2 destinazioni: il caso vero e il caso falso. I salti indiretti, invece, possono avere molte destinazioni. Estenderemo l’algoritmo nella sezione avanzata per aggiungere tutte le destinazioni note all’elenco dei successori per un blocco che termina con un salto indiretto.

  • l’algoritmo utilizza solo salti e ritorni. Dovrebbe ignorare le chiamate di funzione (ovvero, se le istruzioni di chiamata sono state registrate nell’elenco dei rami, devono essere saltate quando si cerca il ramo successivo).

Sebbene questo algoritmo sia più complicato, vedremo nella prossima sezione che le informazioni sui successori e sui predecessori diventeranno molto utili per saperne di più sulla nostra procedura.

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