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.