4. Esempio di un decompilatore semplice

In questa sezione andremo a costruire un piccolo decompilatore molto semplice che ci consentirà inizialmente di toccare con mano alcuni problemi che derivano dalla traduzione del codice assembly in codice di alto livello. Per ora, struttureremo ogni algoritmo in pseudocodice perché bene o male, l’idea di fondo sarà sempre la stessa indipendentemente dal linguaggio che andrete ad utilizzare.

Come accennato parlando di disassemblatori, il modo più semplice per decompilare un programma è presentare quante più informazioni di alto livello possibile per ogni istruzione. Il più semplice decompilatore parte dalle istruzioni del programma, abitualmente contenute all’interno della sezione “.text” (vedere i diversi formati file eseguibili per maggiori informazioni).

Ipotizziamo che il disassemblatore converta una sequenza di byte nella seguente istruzione:

0x004011E5:   80 4D FD 08          or byte [ebp-0x3], 0x8

Possiamo tradurre questa istruzione in C semplicemente considerando l’effetto che l’istruzione ha sul programma. Una possibile traduzione di questa istruzione è:

ebp[-0x3] |= 0x8;

Un semplice filtro sull’output del disassemblatore tramite il piping | può effettuare gran parte del lavoro:

objdump -d file.exe | translate > file.c

Il programma translate farebbe qualcosa del genere:

while(gets(instr, offset)) {
  ptr = &instr[FIRST_OPCODE_COL];  // skip address, bytes
  ptr = isolate_word(p, opcode);   // get operation
  ptr = isolate_argument(p, &dest); // get destination operand
  if(*ptr == ',')
      ptr = isolate_argument(p + 1, &source); // get source operand

  if(strcmp(opcode, "or") == 0)
      printf("%s  |=  %s\n", dest, source);
  else if(strcmp(opcode, "and") == 0)
      printf("%s  &=  %s\n", dest, source);  
   ... // other operators

   offset += instr->size;
}

La maggior parte delle istruzioni aritmetiche può essere tradotta in questo modo. Alcuni sforzi possono essere dedicati alla stampa carina degli argomenti, in modo che [ebp-0x3] possa diventare ebp[-3]. Se una locazione di memoria è coinvolta nell’istruzione, possiamo semplicemente registrarne l’uso in una tabella ed emettere la dichiarazione della variabile in un file .h.

Le istruzioni di salto sono speciali, perché bisogna definire l’etichetta che corrisponde all’obiettivo del salto, ma questo è facile da fare: basta emettere l’indirizzo di ogni istruzione come etichetta mentre le traduci. Un esempio di questo approccio è riportato qui sotto:

0x004011D6:   EB 0D                jmp L4011E5
. . .
0x004011E5:   80 4D FD 08          or byte [ebp-0x3], 0x8

Questo può diventare:

L4011D6:  goto L4011E5
. . .
L4011E5:  ebp[-0x3] |= 0x8;

Non esiste un approccio giusto o sbagliato per stampare in maniera leggibile gli argomenti di una certa istruzione. Tuttavia, ispirarsi a ciò che i programmatori sono abituati a vedere e a scrivere potrebbe essere una buona idea. L’istruzione [ebp-0x3] non è una istruzione valida in C, mentre ebp[-3] è sintatticamente valida. Puoi sperimentare con altri casi; la maggior parte di essi può essere tradotta in questo modo. Ecco qualche esempio:

0x00401383:   80 FB 49           cmp bl, 0x49
0x00401386:   74 2E              jz L4013B6

L401383:    flags = bl - 0x49;
L401386:    if ( flags == 0 )  goto L4013B6;
0x00401357:   E8 B6 06 00 00     call L401A12
0x0040135C:   . . .

L401357:      esp -= 4;          // make space for return location
              *esp = L40135C;    // save return location
              goto L401A12;      // go to ("call") the function
L40135C:      . . .

Ora abbiamo un piccolo problema. C non consente i return calcolati, quindi come tradurremo RET e prevediamo di tornare a L40135C? Un modo per farlo è registrare gli indirizzi di tutte le istruzioni che seguono un’istruzione di chiamata e utilizzare un’istruzione switch() per andare a quell’etichetta:

// Translating RET
_dest = *esp;
esp += 4;
switch(_dest) {
case 0x40135C:    goto L40135C;
. . .
}

Questo approccio è molto semplice e mostra le basi della decompilazione. Quello che dobbiamo avere in mente riguarda il codice: non è necessario ricreare il sorgente del programma originale; devi solo ricreare il comportamento del programma originale.

Tuttavia, questo approccio presenta diversi difetti, come puoi immaginare. Li consideriamo nel prossimo capitolo.

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