SpiDEr10 - Let's DisAsm!

Data

by Spider

 

giugno 2002

UIC's Home Page

Published by Quequero

...

Bene come avevo gia annunciato in precedenza...Lo spazio commenti e' mio, il sito e' mio voi siete in gran parte gay hihihihihihi cosi ho deciso di riprendere possesso del mio spazio occupato ABUSIVAMENTE da voi lameri :)
E' per questo che il tutorial e' slightly edited by me..Infatti spider mi aveva fatto alcuni complimenti, dicendomi che ero una delle migliori persone sulla terra...MA....Per DOVERE DI CAUSA sono stato COSTRETTO a cancellarli e a scrivere liberamente hihihihihi

Qualche tuo eventuale commento sul tutorial :P

Peccato...Ti sei giocato il tuo unico spazio di liberta' :)...Vendo questo quadretto per un banner porno, contattatemi per mettere qui la vostra pubblicita' ihihhih NdQue

 

Home Page: http://bigspider.cjb.net 
E-mail: [email protected]

Canali IRC frequentati: #crack-it e #asm su irc.azzurra.org
Nickname: ^Spider^
 

Difficoltà

( )NewBies (X)Intermedio ( )Avanzato ( )Master

 

In questo tutorial parleremo di un argomento molto interessante ma poco trattato: il disassembling. In rete si trovano diversi disassemblatori fatti in C, ma non sono riuscito a rimediarne quasi neanche uno fatto in assembler. SpiDEr è una disasm engine interamente scritta in assembler, in grado di disassemblare sia a 16 sia a 32 bit, con supporto di tutte le istruzioni dei processori Pentium, incluse le istruzioni FPU ed MMX (non SSE ed SSE2).


SpiDEr10 (SPIder's Disasm Engine Release 1.0)
Let's disasm!
Written by Spider

 
Allegato
 

Introduzione


Il disassembling mi ha sempre affascinato, e più volte in passato l'idea di costruire una disasm engine ha fatto capolino fra il mucchio di cose che avevo in mente di fare, e una volta avevo già iniziato una disasm engine. Il risultato? Un disastro... Il codice era molto inefficiente, ingombrante, disordinato e difficile da capire, al punto che non riuscivo quasi più ad andare avanti... e allora mi sono fermato. Tempo dopo mi è tornato in mente il progettino, e, data l'esperienza passata, sono stato un po' più cauto. Prima di ricominciare la disasm engine, ho lasciato trascorrere un po' di tempo durante il quale progettavo mentalmente la maniera migliore per ottenere un risultato soddisfacente; mi sono documentato, ho studiato e ristudiato i manuali Intel, e poi, finalmente, ho iniziato la fase di coding.

Tools usati


- MASM 6.0 o superiore
- I manuali Intel (architettura IA32). Li trovate su http://developer.intel.com. In particolare, il volume che noi utilizzeremo sarà il secondo ("Instruction Set Reference") nel Chapter 2 ("Instruction Format") e nell'Appendix A ("Opcode Map"). Il Chapter 2 parla del formato generale delle istruzioni, nonché di ModR/M byte e SIB byte. L'Appendix A presenta invece esempi di Look-Up tables, ovvero le tabelle su cui gravita il funzionamento della disasm engine stessa.
- La vostra musica preferita (essenziale: aiuta a concentrarsi :-) )

Essay

Bene, è il momento di incominciare. Prima di poter parlare della disasm engine vera e propria, è essenziale soffermarsi sul formato generale delle istruzioni (Chapter 2).

Formato generale delle istruzioni

Tutte le istruzioni dell'architettura Intel possono essere generalizzate in un formato standard (anche se alcune istruzioni si discostano un po' da esso), e ciò semplifica molto il nostro lavoro :-)
Tale formato può essere schematizzato come segue:

+----------------------+-------------+--------+--------+----------------+----------------+
| Instruction prefixes |   Opcode    | ModR/M |  SIB   | Displacement   |   Immediate    |
+----------------------+-------------+--------+--------+----------------+----------------+
|  fino a 4 prefissi   | 1 o 2 bytes | 1 byte | 1 byte | 1, 2 o 4 bytes | 1, 2 o 4 bytes |
+----------------------+-------------+--------+--------+----------------+----------------+

I prefissi sono facoltativi, e in una istruzione ne possono coesistere fino ad un massimo di 4. Essi si suddividono in 4 gruppi:

Ogni istruzione può contenere solo un prefisso di ogni gruppo. E' ininfluente l'ordine dei prefissi, nel caso ne siano presenti diversi contemporaneamente.
Il prefisso LOCK può essere usato solo con alcune istruzioni (ADD, SUB, AND, OR, CMP, XOR, ecc.); i prefissi di ripetizione possono essere utilizzate solo con le istruzioni di manipolazione delle stringhe (ad esempio MOVSB).

I segment override prefixes servono per selezionare un segmento diverso da quello predefinito per una determinata istruzione, quando si utilizzano operandi di memoria. Un esempio varrà più di mille parole. Se utilizziamo l'istruzione "add eax,dword ptr[edi]", il segmento predefinito per il secondo operando è DS. Se noi vogliamo utilizzare il segmento SS, dobbiamo mutare l'istruzione in "add eax,dword ptr ss:[edi]". Quando facciamo ciò, l'assemblatore non fa altro che anteporre all'istruzione il SS segment override prefix.

I branch hints sono stati introdotti nei processori Intel Pentium 4 e Intel Xeon, e non fanno altro che suggerire al processore se un salto condizionale salterà o meno, in modo che questo possa regolarsi di conseguenza per ottimizzare l'uso della cache. Poiché l'importanza di questi prefissi è del tutto secondaria, possiamo semplicemente ignorare la loro presenza :P

L'operand-size override prefix serve per selezionare una dimensione dell'operando diversa da quella predefinita. Se ad esempio in un ambiente a 32 bit utilizziamo l'istruzione "mov ax,bx", l'assemblatore antepone l'operand-size override prefix all'istruzione "mov eax,ebx".

L'address-size override prefix ha un funzionamento simile all'op-size, e serve a selezionare un tipo di indirizzamento per gli operandi di memoria diverso da quello di default. Ad esempio, se in ambiente a 32-bit vogliamo usare l'istruzione "mov eax,dword ptr[bx+si]", non possiamo far altro che utilizzare questo prefisso.

 

L'opcode ha un'ampiezza di 1 o 2 bytes; talvolta, il ModR/M byte (che vedremo tra poco) contiene un'estensione di 3 bit dell'opcode primario. Le istruzioni con opcode di 2 bytes iniziano con l'escape 0Fh; inoltre le istruzioni FPU hanno opcodes di 2 bytes che iniziano con una delle seguenti escapes: D8h, D9h, DAh, DBh, DCh, DDh, DEh o DFh.

 

Il ModR/M byte e il SIB byte sono utilizzati da moltissime istruzioni, specialmente quelle che possono contenere operandi di memoria. il ModR/M è composto da tre parti:

+-------+------------+------------+
|  mod  | reg/opcode |    r/m     |
+-------+------------+------------+
| 2 bit |   3 bit    |   3 bit    |

- Il mod si combina con il r/m ed a volte con il SIB byte per ottenere 24 modi di indirizzamento e 8 registri (vedremo meglio tra poco in una tabella).
- Il reg/opcode può servire a due scopi, a seconda dell'opcode primario: può indicare un registro, oppure può fornire un'estensione dell'opcode primario. Ad esempio, l'opcode 80h significa "ADD" se il il reg/opcode è uguale a 0, ma significa "OR" se è 1 o "ADC" se è 2, e così via.
- Il r/m (register or memory) può specificare un registro (quando il mod è uguale a 11b) oppure può essere combinato con il mod e l'eventuale SIB per specificare un indirizzo di memoria.

Per alcuni valori del ModR/M byte (per la precisione, quando il r/m è uguale a 100b e il mod NON è uguale a 11b) può essere necessaria la presenza del cosiddetto SIB byte, che completa l'addressing mode. Il SIB è composto da tre parti, dalle cui lettere iniziali prende il nome:

+-------+------------+------------+
| scale |   index    |    base    |
+-------+------------+------------+
| 2 bit |   3 bit    |   3 bit    |

- Lo scale field specifica il fattore per cui deve essere moltiplicato il registro specificato dall'index field.
- L'index field specifica il registro indice.
- Il base field specifica il registro base.

In questo modo, nell'addressing a 32 bit, si ottengono operandi di memoria che hanno la seguente sintassi generale:

base + index*scale + displacement

in cui il displacement può essere di 1 byte o di 4 bytes. Queste informazioni saranno tra poco riassunte in una tabella.

 

Il displacement può avere una dimensione di 1 byte o 4 bytes per l'addressing a 32 bits, e di 1 byte o 2 bytes per l'addressing a 16 bit. La presenza o meno del displacement e la dimensione dello stesso sono stabilite dal ModR/M byte.

 

Gli operandi immediati possono avere una dimensione di 1, 2 o 4 bytes. Ogni istruzione può avere al massimo un operando immediato, con la sola eccezione dell'istruzione ENTER che dispone di 2 operandi immediati.

 

Passiamo adesso all'analisi delle varie addressing forms specificate dal ModR/M byte e dal SIB byte.

A 16 bit si può utilizzare soltanto il ModR/M byte. Nella seguente tabella (copiata direttamente dal secondo dei manuali Intel) si riassumono i valori che il ModR/M byte può assumere a 16 bit:

Nella prima colonna (effective address) sono elencati tutti i 32 possibili modi di indirizzamento assunti dal ModR/M byte in base al mod field e al R/M field. Le prime 24 righe indicano i valori assumibili dagli operandi di memoria; le diciture disp8 e disp16 indicano un displacement rispettivamente di 8 e di 16 bits. La ultime 8 righe (corrispondenti ad un mod uguale ad 11b) selezionano invece dei registri.
La seconda e terza colonna indicano i valori binari di mod e r/m corrispondenti al metodo di indirizzamento specificato dalla prima colonna. Nella parte alta della tabella si hanno invece i possibili valori e i rispettivi significati del reg/opcode field.

Per cercare di entrare meglio nel meccanismo, cerchiamo di fare mentalmente ciò che la nostra disasm engine dovrà essere in grado di fare: disassembliamo le seguenti istruzioni (supponendo di trovarci in un ambiente a 16 bits): 00FE e 01890400.
Nel primo caso abbiamo un primary opcode corrispondente a 00. Se guardiamo le lookup tables nell'Appendix A del manuale oppure consultiamo qualunque altra opcode reference, vedremo che 00 corrisponde all'istruzione "add r/m8,r8". Il primo operando può dunque essere un registro o un operando in memoria, pertanto sarà definito dal mod e dal r/m; il secondo operando può essere solo un registro, ed è quindi specificato dal reg field. Entrambi gli operandi sono della dimensione di un byte. Osservando la tabella, vediamo che ad un ModR/M uguale ad FE corrispondono il registro DH specificato da mod e r/m, e il registro BH specificato dal reg field: pertanto l'istruzione sarà "
ADD DH,BH".
Nel secondo caso abbiamo l'opcode 01, che corrisponde all'istruzione "add r/m16,r16"; anche in questo caso, il primo operando è un registro o un operando di memoria, e il secondo un registro, entrambi della dimensione di una word. Osserviamo che ad un ModR/M uguale a 89 corrisponde
[BX+DI]+disp16. Il disp16 corrisponde alla word successiva al ModR/M byte, che è 0004h (attenzione alla notazione Intel!). Il primo operando sarà perciò "[BX+DI+0004]". Il secondo operando, dato che il reg field è uguale ad 1 e ci troviamo a 16 bit, è "CX". L'istruzione disassemblata sarà dunque "ADD [BX+DI+0004],CX".

 

Nell'addressing a 32 bits le cose sono un po' più complicate, soprattutto perché oltre al ModR/M byte abbiamo da gestire il SIB byte. 
Nella gestione di indirizzi di memoria a 32 bit, dobbiamo attenerci alla seguente sintassi generale:

[base + index*scale + displacement]

Ovviamente in un indirizzo di memoria non dobbiamo indicare TUTTI questi elementi, ma solo quelli necessari. Si ottengono così i seguenti metodi di indirizzamento:

[displacement]
[base]
[base + displacement]
[index*scale + displacement]
[base + index + displacement]
[base + index*scale + displacement]

Per maggiori informazioni riguardo ai metodi di indirizzamento, vedere il primo manuale Intel (Basic Architecture) nella sezione 3.7.3.2.

Adesso vedremo come è possibile codificare tutti questi diversi operandi di memoria. Stavolta le tabelle sono due: una per il ModR/M e una per il SIB. Vediamo la prima:


 

Come potete vedere, la tabella è strutturata in modo molto simile a quella precedentemente esaminata, quindi non mi soffermerò molto. In questa tabella non abbiamo più la nomenclatura "disp16" ma incontriamo "disp32", che chiaramente indica un displacement di 32 bits. Del tutto nuova è invece la presenza di [--][--]. Come leggiamo dalla nota in fondo alla tabella, questa nomenclatura indica la presenza del SIB byte, che ovviamente è il byte immediatamente seguente al ModR/M byte.

Proviamo a disassemblare le stesse istruzioni che abbiamo decodificato poco fa, ma stavolta immaginiamo di trovarci in un ambiente a 32 bit.
Nel caso della prima istruzione (00FE) ci troviamo di fronte esattamente alla stessa istruzione di poco fa, ovvero
"ADD DH,BH".
Nel caso della seconda istruzione
(01890400), abbiamo un ModR/M pari a 89h; il secondo operando, specificato dal reg field, sarà sicuramente uguale ad ECX. Il primo, dato che il Mod è uguale ad 10b e il R/M è uguale a 001b, sarà un "disp32[ecx]", ovvero "[ecx+disp32]"; il displacement è di 4 bytes, ma... noi abbiamo 2 bytes dopo il ModR/M (0400)! L'istruzione è quindi incompleta e non disassemblabile. Se aggiungessimo 2 volte 00 (018904000000) avremmo un'istruzione completa e il displacement sarebbe uguale a 00000004. L'istruzione sarà dunque "ADD [ECX+00000004],ECX".

Come possiamo vedere dalla tabella, con il solo ModR/M possiamo gestire i primi 3 metodi di indirizzamento, ossia:

[displacement]
[base]
[base + displacement]

Per gli altri 3, abbiamo bisogno di ricorrere al SIB. Vediamo la rispettiva tabella:

Anche questa tabella assomiglia molto alle precedenti. In questo caso abbiamo i valori del Base field, che indicano, appunto, quale registro è utilizzato come base. Nella colonna etichettata "Scaled Index" abbiamo invece il significato assunto dai valori di scale factor e index. Molto importante è notare la presenza di [*]: come leggiamo nella nota in fondo alla tabella, tale nomenclatura serve ad indicare un disp32 senza base quando il Mod è 00b; in tutti gli altri casi, indica che si utilizza EBP come base.

Come al solito, disassembliamo qualcosa per capire meglio: 2B9C8E00104000 e 2A044D00104000.
La prima istruzione inizia con l'opcode 2B, che corrisponde all'istruzione "
sub r32, r/m32". Il ModR/M è uguale a 9C, in cui il reg field indica il primo operando, che è EBX. Mod e R/M indicano invece "[--][--]+disp32". Dobbiamo quindi vedere il SIB byte, e alla fine dobbiamo ricordarci di aggiungere il disp32. il SIB, immediatamente dopo il ModR/M byte, è uguale a 8E: ciò implica come base il registro ESI e come index*ss il valore ECX*4. Ricordandoci del disp32, codificato dopo il SIB, potremo dire con certezza che l'istruzione è "SUB EBX,[ESI + ECX*4 + 00401000]".
La seconda istruzione inizia con l'opcode 2A, ovvero l'istruzione incriminata è "
sub r8,r/m8". Il ModR/M è 04, ovvero il reg field indica AL (il primo operando) e Mod e R/M indicano "[--][--]". Dobbiamo quindi vedere il byte successivo, cioè il SIB. Esso è uguale a 4D, che implica come base il simbolo [*]. Dato che il Mod field del ModR/M byte è 0, abbiamo un disp32 senza base, da aggiungere dopo all'indice. Index*SS corrisponde invece ad ECX*2. Il secondo operando è dunque [ECX*2+00401000], e il disassemblato finale sarà "SUB AL,[ECX*2+00401000].

 

La disasm engine - Struttura generale

Adesso che sappiamo come sono fatte in generale le varie opcodes, possiamo entrare più in dettaglio e cominciare a progettare la struttura della nostra disasm engine.
Ho ritenuto utile creare la disasm engine come file dll, in modo da poterla riutilizzare senza nessuna modifica in qualunque programma.
Un metodo fra i più utilizzati e fra i più efficaci per la creazione di disassemblatori è sicuramente quello che sfrutta le cosiddette look-up tables. Queste sono delle tabelle che contengono informazioni su tutte le varie opcodes. L'opcode stessa sarà utilizzata come indice all'interno dell'array: se ad esempio ci trovassimo di fronte ad un'istruzione che inizia con
17, basterebbe leggere l'elemento 17 dell'array contenente le informazioni necessarie al disassembler, poi in un altro array preleveremo un puntatore al nome dell'istruzione, ecc.

Dovremo quindi iniziare creando 3 tabelle: la tabella dei nomi che conterrà il nome di tutte le istruzioni; la lookup delle sintassi contenente informazioni su tutte le istruzioni; la lookup dei nomi contenente i puntatori ai nomi delle istruzioni, contenuti nella tabella dei nomi.

La tabella dei nomi è semplicemente un elenco di tutte le istruzioni:

;Tabella dei nomi:
DE_AAA db "AAA",0
DE_AAD db "AAD ",0
DE_AAM db "AAM ",0
DE_AAS db "AAS",0
DE_ADD db "ADD ",0
DE_ADC db "ADC ",0
DE_AND db "AND ",0
DE_ARPL db "ARPL ",0
DE_BOUND db "BOUND ",0
DE_BSF db "BSF ",0
DE_BSR db "BSR ",0
DE_BSWAP db "BSWAP ",0
DE_BT db "BT ",0
...

Per le istruzioni che hanno uno o più operandi, aggiungiamo anche uno spazio, così non dovremo preoccuparci di farlo in seguito.

La lookup dei nomi è, come abbiamo detto, un array di puntatori ai nomi delle varie istruzioni, e sarà così formata:

onebytenames dd offset DE_ADD ;00
dd offset DE_ADD ;01
dd offset DE_ADD ;02
dd offset DE_ADD ;03
dd offset DE_ADD ;04
dd offset DE_ADD ;05
dd offset DE_PUSHES ;06
dd offset DE_POPES ;07
dd offset DE_OR ;08
...

Per le istruzioni non valide o le opcodes a cui non corrisponde un nome (ad esempio i prefissi, o le escapes per le istruzioni di 2 bytes, ecc.) ci limitiamo a scrivere NULL.

La lookup delle sintassi è la più complessa ma anche la più interessante. Dato che le informazioni riguardanti ogni istruzione sono davvero parecchie, ho pensato di creare una lookup in cui ogni elemento occupasse 8 bytes. Nella prima dword vanno informazioni generali sull'istruzione, che vedremo dopo; l'uso dei restanti 4 bytes varia a seconda dell'istruzione: in genere serve per identificare le caratteristiche dei vari operandi (chiariremo meglio in seguito: prima è meglio definire bene la prima dword).
Il numero di flags che andrebbero nella prima dword è piuttosto alto: il manuale intel nelle sue lookup tables ne riporta ben 19. Vediamoli singolarmente:

A Indirizzo diretto: l'istruzione non ha ModR/M byte e l'operando è un indirizzo codificato direttamente all'interno dell'istruzione. E' utilizzato soltanto da 2 istruzioni: far CALL (opcode 9A) e far JMP (opcode EA).
C Il reg field del ModR/M byte seleziona un Control Register.
D Il reg field del ModR/M byte seleziona un Debug Register.
E L'opcode primario è seguito da un ModR/M byte che specifica l'operando; questo può essere un general-register o un indirizzo di memoria.
F EFLAGS Register. E' utilizzato da alcune istruzioni (PUSHFD e POPFD) che possono essere trattate come istruzioni senza operandi: può dunque essere ignorato.
G Il reg field del ModR/M byte seleziona un General Register.
I Operando immediato. L'operando è un valore immediato codificato nei bytes che seguono l'istruzione.
J L'istruzione contiene un indirizzo relativo che deve essere sommato all'instruction pointer. E' utilizzato dalla grande maggioranza delle istruzioni di salto (es. JMP, CALL, LOOP)
M Il ModR/M byte può riferirsi soltanto alla memoria. E' utilizzato da diverse istruzioni: BOUND, LES, LDS, LSS, LFS, LGS, CMPXCHG8B. Dal punto di vista di un disassemblatore può essere ignorato.
O L'istruzione non ha ModR/M byte e l'operando è un indirizzo codificato direttamente all'interno dell'istruzione. E' utilizzato solo da alcune varianti dell'istruzione MOV.
P Il reg field del ModR/M byte seleziona un MMX Register.
Q L'opcode primario è seguito da un ModR/M byte che specifica l'operando; questo può essere un MMX register o un indirizzo di memoria.
R Il ModR/M byte può riferirsi soltanto ad un general-register. Dal punto di vista di un disassemblatore, anche questo può essere ignorato.
S Il reg field del ModR/M byte seleziona un segment Register.
T Il reg field del ModR/M byte seleziona un Test Register. Questo flag è quasi inutile, dato che le istruzioni di lettura e scrittura sui Test Registers sono quasi del tutto undocumented.
V Il reg field del ModR/M byte seleziona un XMM Register.
W L'opcode primario è seguito da un ModR/M byte che specifica l'operando; questo può essere un XMM register o un indirizzo di memoria.
X La memoria è indirizzata da DS:SI o DS:ESI (MOVS, CMPS, OUTS, LODS). Può essere ignorato senza problemi.
Y La memoria è indirizzata da ES:DI o ES:EDI (MOVS, CMPS, INS, STOS, SCAS). Come il precedente può essere ignorato.

Togliendo tutti i flags  non necessari, sono giunto ad un primo elenco di costanti, che è il seguente:

DE_NS equ 0FFFFFFFFh ;opcode non valida o non supportata

DE_NO equ 000000000h ;istruzione senza operandi

DE_A equ 000000001h  ;Direct Address
DE_M equ 000000002h  ;L'opcode primario è seguito da un ModR/M byte
DE_J equ 000000004h  ;L'istruzione contiene un indirizzo relativo
DE_O equ 000000008h  ;Istruzione senza ModR/M byte; l'indirizzo dell'operando 
                     ;è codificato come una word/dword nell'istruzione
DE_C equ 000000010h  ;ModR/M->reg specifica un control register (ad es. CR0)
DE_D equ 000000020h  ;ModR/M->reg specifica un debug register (ad es. DR7)
DE_G equ 000000040h  ;ModR/M->reg specifica un general register (ad es. EBX)
DE_P equ 000000080h  ;ModR/M->reg specifica un packed quadword MMX-register (ad es. MM3)
DE_S equ 000000100h  ;ModR/M->reg specifica un segment register (ad es. FS)
DE_T equ 000000200h  ;ModR/M->reg specifica un test register (ad es. TR3)
DE_V equ 000000400h  ;ModR/M->reg specifica un 128-bit XMM register (ad es. XMM4)

DE_E equ 000000800h  ;ModR/M byte specifica un general-purpose register o un indirizzo di memoria
DE_Q equ 000001000h  ;ModR/M byte specifica un MMX register o un indirizzo di memoria
DE_W equ 000002000h  ;ModR/M byte specifica un XMM register o un indirizzo di memoria

In fase di coding si è poi manifestata la necessità di aggiungere alcune altre costanti, ovvero le seguenti:

DE_X equ 000004000h  ;0F o excape opcode per le istruzioni FPU (vedere tabelle supplementari)
Costante necessaria per la gestione di istruzioni con opcode di 2 bytes o istruzioni FPU.

DE_R equ 000008000h  ;ModR/M->reg fornisce un estensione dell'opcode (vedere tabelle supplementari)
Necessaria per la gestione di istruzioni in cui il reg field completa l'opcode primario.

DE_B equ 000010000h  ;l'istruzione ha uno o più operandi
Questa costante è stata aggiunta per evitare che le istruzioni aventi solo operandi immediati vengano scambiate per istruzioni senza operandi (dato che i flags sono gli stessi).


DE_PREFIX equ 000020000h ;prefisso (e.g. F0)

Identifica un prefisso (es.: 66h, 67h, F0h, ecc.)

Le ultime 2 costanti sono state aggiunte per la gestione di un ristretto gruppo di istruzioni, ovvero quelle che cambiano nome e/o sintassi a seconda che ci si trovi a 16 o 32 bit, e più precisamente:

DE_F equ 000040000h  ;l'istruzione segue 2 tabelle diverse, a seconda che l'operand-size
                     ;attribute sia settato o meno (esempio: scasw/scasd)

DE_F2 equ 000080000h ;l'istruzione segue 2 tabelle diverse, a seconda che l'address-size
                     ;attribute sia settato o meno (l'unico esempio è jcxz/jecxz) 

Tutti questi flags forniscono informazioni ancora insufficienti per poter disassemblare un'istruzione: dobbiamo perciò definire il significato della seconda dword nella syntax lookup-table.

Per la maggior parte delle istruzioni, la seconda dword fornisce informazioni riguardo agli operandi dell'istruzione. Il primo byte della seconda dword indica il numero di operandi (da un minimo di 0 ad un massimo di 3). Ognuno dei bytes successivi contiene un insieme di flags che servono a identificare le caratteristiche di ogni operando. Molti di questi flags servono a identificare la dimensione dell'operando. Il manuale Intel riporta una marea di flags, che vedremo singolarmente... ma non preoccupatevi, per il disassembling potremo tralasciare molti di essi :-)

a Due operandi di memoria di una word/dword ciascuno, a seconda dell'operandi-size attribute. Questo flag è utilizzato solo dall'istruzione BOUND, e può essere tralasciato.
b Operando di un byte, indipendentemente dall'operand-size attribute.
c Operando di un byte o una word, a seconda dell'operand-size attribute. Inizialmente l'avevo incluso nella disasm engine, ma poi mi sono accorto che nessuna istruzione utilizza questo flag, pertanto possiamo ignorarlo.
d Double-word, indipendentemente dall'operand-size attribute.
dq Double-quadword, indipendentemente dall'operand-size attribute.
p Puntatore nella forma seg:offset, a 32bit o 48bit a seconda dell'operand-size attribute.
pi Quadword MMX technology register. Può essere ignorato, dato che disponiamo già del flag q (vedi sotto).
ps 128-bit packed single-precision floating-point data. Non ho incluso questo flag in quanto è utilizzato da istruzioni SSE/SSE2, che non sono supportate da questa disasm engine. Ad ogni modo, la sua importanza è secondaria.
q Quadword, indipendentemente dall'operand-size attribute.
s 6-byte pseudo-descriptor. Ignorarlo non comporta problemi.
ss Elemento scalare di un 128-bit packed single-precision floating data. Essendo utilizzato solo da istruzioni SSE/SSE2, possiamo ignorarlo.
si Doubleword integer register. Possiamo ignorarlo senza problemi.
v Word o Doubleword, a seconda dell'operand-size attribute.
w Word, indipendentemente dall'operand-size attribute.

Togliendo i flags che non ci servono, otteniamo le seguenti costanti:

DE_OP_b  equ  0  ;byte, indipendentemente dall'operand-size attribute
DE_OP_d  equ  1  ;doubleword, indipendentemente dall'operand-size attribute
DE_OP_dq equ  2  ;double-quadword, indipendentemente dall'operand-size attribute
DE_OP_q  equ  3  ;quadword, indipendentemente dall'operand-size attribute
DE_OP_v  equ  4  ;word o dword, a seconda dell'operand-size attribute
DE_OP_w  equ  5  ;word, indipendentemente dall'operand-size attribute
DE_OP_p  equ  6  ;puntatore a 32-bit o a 48-bit, a seconda 
                 ;dell'operand-size attribute (es.: call (9A) )

Tutti questi flags sono contenuti nei 3 bits meno significativi del byte che descrive ogni operando. Oltre alla dimensione dell'operando, dobbiamo conoscere anche altre caratteristiche: se l'operando è un immediato, se è un operando definito dal reg field o dal r/m field, ecc. Ecco l'elenco di tutti i flags necessari, la cui necessità si è talvolta manifestata solo in fase di coding:

DE_OP_a equ 08h     ;l'operando è il registro accumulatore (e.g. ADD AL, imm8 (04))
DE_OP_modrm equ 10h ;l'operando è specificato da mod e r/m e dal SIB Byte
DE_OP_reg equ 20h   ;l'operando è specificato dal reg field del ModR/M byte
DE_OP_i equ 40h     ;l'operando è un valore immediato
DE_OP_o equ 80h     ;l'operando è un general-register specificato dai 3 bits meno 
                    ;significativi dell'opcode (ad esempio INC r32)

Le ultime costanti aggiunte sono le due seguenti, che identificano operandi speciali:

;Operandi speciali:
DE_OP_one equ 0FFh  ;l'operando è "1"
DE_OP_CL equ 0FEh   ;l'operando è "CL"

Questi flags sono sufficienti per specificare tutto ciò che serve nel disassembling della maggior parte delle istruzioni.
Nelle istruzioni con operando relativo, ovvero quelle con il flag
DE_J, la seconda dword identifica la dimensione dell'operando, e può valere uno di questi flags, che abbiamo già incontrato:

DE_OP_b  equ  0  ;byte, indipendentemente dall'operand-size attribute
DE_OP_v  equ  4  ;word o dword, a seconda dell'operand-size attribute


Per i segment-override prefixes, ho usato la seconda dword per agevolare la gestione del prefisso stesso, assegnando un valore che fa riferimento alla seguente tabella:

DE_segoverride_table db 0,0,0,0 ;0
db "ES:",0                      ;1
db "CS:",0                      ;2
db "SS:",0                      ;3
db "DS:",0                      ;4
db "FS:",0                      ;5
db "GS:",0                      ;6

Per le istruzioni con i flags DE_F o DE_F2, contiene l'offset di un descrittore contenente a sua volta 2 dword: la prima è l'indirizzo della tabella dei nomi, la seconda è l'indirizzo della tabella delle sintassi. Vedremo meglio in seguito.

Per le istruzioni con il flag DE_R contiene l'offset di un descrittore contenente a sua volta l'offset della tabella delle sintassi e l'offset della tabella dei nomi. Anche in questo caso vedremo meglio più avanti.

 

La disasm engine - Dettagli

Vediamo adesso in dettaglio cosa dovrà fare la nostra disasm engine per svolgere i diversi compiti. Partiamo dall'analisi del reg field.

Il reg field

Per una maggiore semplicità del codice, ho preferito fare tutto in maniera abbastanza modulare: il codice di interpretazione del reg field, così come quello del ModR/M byte, è contenuto in una procedura a parte, richiamabile all'occorrenza.
La gestione del reg field si articola in due fasi: innanzitutto la procedura deve riconoscere il tipo di registro codificato nell'istruzione. Rispolverando la lista dei flags, notiamo infatti la presenza delle seguenti righe: 

DE_C equ 000000010h ;ModR/M->reg specifica un control register (ad es. CR0)
DE_D equ 000000020h ;ModR/M->reg specifica un debug register (ad es. DR7)
DE_G equ 000000040h ;ModR/M->reg specifica un general register (ad es. EBX)
DE_P equ 000000080h ;ModR/M->reg specifica un packed quadword MMX-register (ad es. MM3)
DE_S equ 000000100h ;ModR/M->reg specifica un segment register (ad es. FS)
DE_T equ 000000200h ;ModR/M->reg specifica un test register (ad es. TR3)
DE_V equ 000000400h ;ModR/M->reg specifica un 128-bit XMM register (ad es. XMM4)

L'operando specificato dal reg può dunque essere un control register, un debug register, un general register, un MMX register, un segment register, un test register (anche se le istruzioni che utilizzano i test register sono quasi del tutto undocumented, e per questo momentaneamente non le ho incluse) o un XMM register. Dopo aver individuato la categoria a cui appartiene il registro, la procedura deve prelevare il valore del reg field dal ModR/M byte e quindi utilizzarlo per individuare il registro nella corretta tabella. Non riporto il codice, che potrete invece trovare nella procedura ManageRegField nel file SpiDEr10.asm contenuto nell'allegato.


Mod, r/m e SIB (32 bit)

Tralasciando di parlare della gestione del ModR/M byte a 16 bit (che è piuttosto semplice), passiamo all'analisi di Mod, r/m e SIB a 32 bit.

Per quanto concerne la gestione del ModR/M e del SIB a 32 bit, basta tenere presenti le tabelle che abbiamo visto precedentemente. L'algoritmo che ho seguito non è probabilmente il più efficiente, a tutto vantaggio però della semplicità.
Innanzitutto bisogna controllare il Mod field.

  1. Se il Mod è uguale a zero l'operando si trova in memoria, e si controlla il r/m field:
    1. se questo è uguale a 4 si deve passare all'analisi del SIB e non vi è presenza di displacement;
    2. se questo è uguale a 5 si ha solo un displacement di 32 bit;
    3. in tutti gli altri casi, l'operando di memoria è dato dal registro indicato da r/m field.
  2. Se il Mod è uguale a 01b l'operando si trova in memoria, e si controlla il r/m field:
    1. se questo è uguale a 4 si gestisce il SIB e si aggiunge il displacement di 8 bit;
    2. negli altri casi, l'operando di memoria è dato dal registro indicato da r/m field, a cui va sommato il displacement di 8 bit.
  3. Se il Mod è uguale a 10b l'operando si trova in memoria, e si controlla il r/m field:
    1. se questo è uguale a 4 si gestisce il SIB e si aggiunge il displacement di 32 bit;
    2. negli altri casi, l'operando di memoria è dato dal registro indicato da r/m field, a cui va sommato il displacement di 32 bit.
  4. Se il Mod è uguale a 11b, l'operando è un registro specificato dal reg field.

Nel quarto caso (ovvero per un register operand) bisogna tenere conto del fatto che tale operando può essere un general, un MMX o un XMM register, a seconda della presenza rispettivamente dei flag DE_E, DE_Q e DE_W:

DE_E equ 000000800h ;ModR/M byte specifica un general register o un indirizzo di memoria
DE_Q equ 000001000h ;ModR/M byte specifica un MMX register o un indirizzo di memoria
DE_W equ 000002000h ;ModR/M byte specifica un XMM register o un indirizzo di memoria

Potete trovare un esempio di gestione del ModR/M byte nella procedura ManageModRM nel file SpiDEr10.asm contenuto nell'allegato.

 

Come abbiamo già visto, quando il Mod specifica un memory operand (ovvero quando è uguale a 00b, 01b o 10b) e il r/m field è uguale a 4 (100b) si ha la presenza del SIB byte, codificato immediatamente di seguito al ModR/M byte. Anche per il SIB byte basta tenere presente le tabella vista prima per poterne decifrarne correttamente il significato.

La procedura che ho seguito io si articola nelle seguenti fasi:

  1. Prelievo di Scale e Index, ovvero dei 5 bit più significativi del SIB. Essi vengono utilizzati come indice in una tabella così strutturata:

DE_SIB_table db "EAX+",0,0,0,0
             db "ECX+",0,0,0,0
             db "EDX+",0,0,0,0
             db "EBX+",0,0,0,0
             db 0,0,0,0,0,0,0,0
             db "EBP+",0,0,0,0
             db "ESI+",0,0,0,0
             db "EDI+",0,0,0,0

             db "EAX*2+",0,0
             db "ECX*2+",0,0
             db "EDX*2+",0,0
             db "EBX*2+",0,0
             ...

  1. Analisi del Base field: se questo è diverso da 101b (5 decimale), il Base field rappresenta un general register; se invece è uguale a 5, si deve controllare il Mod, in quanto se quest'ultimo è uguale a 00b, l'operando è rappresentato da un displacement di 32 bit immediatamente seguente al SIB byte. Se invece il Mod è diverso da 00b, il Base field specifica regolarmente il registro EBP.

 Trovate la procedura di gestione del SIB byte nel file SpiDEr10.asm con il nome di ManageSIB.


I prefissi

Come ben sappiamo, ogni istruzione può contenere fino ad un massimo di 4 prefissi. Da tali prefissi dipendono alcune caratteristiche sull'istruzione che segue: se ad esempio ci troviamo di fronte ad un'opcode che inizia per 66, abbiamo la presenza dell'operand-size override prefix, ed in fase di disassembling dovremo fare attenzione agli operandi la cui larghezza varia a seconda dell'operand-size attribute, come specificato dalle lookup-tables.

Per i prefissi del primo gruppo (F0, F2 ed F3, rispettivamente LOCK, REPNE e REP), è sufficiente anteporre all'istruzione il nome del prefisso. Poiché i prefissi hanno tutti un'opcode simile, ho preferito fare una tabella come questa:

DE_prefixes_group1 db "LOCK ",0,0,0
                   db 0,0,0,0,0,0,0,0
                   db "REPNE ",0,0
                   db "REP ",0,0,0,0

In questo modo potremo utilizzare i 4 bit meno significativi del prefisso (0, 2 e 3) come indice nella tabella.

 

Per i segment-override prefix, ho utilizzato la seconda dword dei flags nella lookup-table per identificare il registro interessato. Questo valore viene quindi messo in una variabile che verrà utilizzata nella gestione degli operandi di memoria. In particolare, la variabile, che in SpiDEr10.asm si chiama segoverride, può contenere uno di questi valori:

0 - Nessun segment override prefix
1 - ES-segment override prefix
2 - CS-segment override prefix
3 - SS-segment override prefix
4 - DS-segment override prefix
5 - FS-segment override prefix
6 - GS-segment override prefix

Tali valori serviranno da indice nella seguente tabella: 

DE_segoverride_table db 0,0,0,0 ;0
                     db "ES:",0 ;1
                     db "CS:",0 ;2
                     db "SS:",0 ;3
                     db "DS:",0 ;4
                     db "FS:",0 ;5
                     db "GS:",0 ;6

La gestione dell'eventuale segment-override prefix deve essere eseguita contemporaneamente alla gestione di ogni operando di memoria (ad esempio nella gestione del ModR/M byte).

 

L'operand-size override prefix (opcode 66) è piuttosto semplice da gestire: non fa altro che invertire l'operand-size attribute. Se ci troviamo ad eseguire codice a 32 bit, tale prefisso permette l'utilizzo di operandi a 16 bit, e viceversa. Nella nostra disasm engine basterà contenere in una variabile il valore dell'operand-size attribute, ed invertirlo in presenza dell'operand-size override prefix.

 

L'address-size override prefix (opcode 67) funziona in modo analogo all'operand-size, con la differenza che questo inverte l'address-size attribute, ovvero permette di utilizzare l'indirizzamento a 16 bit in codice a 32 bit o quello a 32 bit in codice a 16 bit. Anche per questo basterà invertire il valore della variabile contenente l'address-size attribute.


Istruzioni con indirizzi relativi

Vediamo adesso come dobbiamo comportarci di fronte alle istruzioni il cui operando è un indirizzo relativo, come JMP, Jcc, CALL, LOOP, etc.
In queste istruzioni (identificate nella lookup-table con il flag
DE_J), l'operando è un indirizzo che si riferisce al valore corrente del registro EIP. E' quindi necessario sapere l'indirizzo in memoria dell'istruzione da disassemblare.
In alcune istruzioni, la dimensione dell'operando è di 1 byte, e avremo il flag DE_OP_b nella seconda dword di flags. In tal caso, per trovare il valore dell'operando, all'indirizzo dell'istruzione bisogna aggiungere la lunghezza dell'istruzione stessa e sommare il valore del byte successivo all'opcode, ovviamente sign-extended. Ovvero:

movsx eax,byte ptr[ebx] ;ebx contiene il byte successivo all'opcode
add eax,[realaddress]   ;aggiunge l'indirizzo dell'istruzione
add eax,[len]           ;\__aggiunge la lunghezza
inc eax                 ;/    dell'istruzione 

Quando invece nella seconda dword di flags della lookup-table avremo DE_OP_v, significa che la lunghezza dell'operando è word o dword, a seconda dell'operand-size prefix. Si procede in modo analogo al precedente caso. Trovate il codice di gestione delle istruzioni con indirizzi relativi nel file SpiDEr10.asm, a partire dall'etichetta RelativeAddress.

Istruzioni con indirizzo diretto (Direct Address)

Due istruzioni (CALL far (Opcode 9A) e JMP far (opcode EA)) utilizzano questo particolare formato. Per disassemblare le istruzioni di questo tipo basta tenere presente che dopo l'opcode primario sono codificati prima il l'offset e poi il segmento del puntatore far, ovviamente con i bytes invertiti per la notazione Intel. A 16 bit avremo quindi l'offset nella prima word dopo l'opcode e il segmento del puntatore far nella word immediatamente successiva; a 32 bit avremo l'offset nella dword che segue l'opcode primario e il segmento nella dword successiva. Trovate il codice di gestione delle istruzioni di questo tipo nel file SpiDEr10.asm, a partire dall'etichetta DirectAddress.

 

Istruzioni con offset diretto (Direct Offset)

4 varianti dell'istruzione MOV utilizzano un formato particolare: di seguito all'istruzione è codificato l'indirizzo di memoria come operando. Esse sono:

A0 - MOV AL, [ADDRESS]
A1 - MOV eAX,[ADDRESS]
A2 - MOV [ADDRESS],AL
A3 - MOV [ADDRESS],eAX

Poiché queste istruzioni sono poche e relativamente rare, ho preferito creare per ognuna il codice di gestione, senza dover creare altre tabelle con sintassi del primo e del secondo operando: soluzione rapida e forse non meno efficace (dal punto di vista della velocità) delle lookup-tables. Trovate il codice di gestione di queste 4 istruzioni a partire dall'etichetta DirectOffset.

Istruzioni con estensione dell'opcode nel reg-field

Per le istruzioni in cui il reg field contiene un estensione dell'opcode, il metodo più ovvio è quello di utilizzare i 3 bits del reg field come indice in una tabella di 8 elementi, con la stessa sintassi delle altre lookup tables. Trovate il codice di gestione delle opcodes estese all'etichetta extension.

Istruzioni che cambiano nome/sintassi a seconda che ci si trovi a 16 o 32 bit

Per le istruzioni che cambiano nome e/o sintassi a seconda dell'address-size o dell'operand-size attribute, la soluzione che mi è sembrata più adeguata è quella di avere nella seconda dword di flags un puntatore ad un array di 2 puntatori: il primo punta ad un array contenente un puntatore al nome dell'istruzione a 16 bit e un puntatore al nome a 32 bit; il secondo punta ad un array contenente la sintassi a 16 bit e quella a 32 bit.
Dal punto di vista della disasm engine, sarebbe bastato conoscere il nome a 16 bit e quello a 32 bit e avere una sintassi univoca. In realtà, anche la sintassi cambia: ad esempio tra l'istruzione PUSHF (16 bit) e l'istruzione PUSHFD (32 bit) oltre al nome cambia anche la dimensione dell'operando. Per maggior precisione (e maggior flessibilità) ho quindi preferito fare due tabelle anche per la sintassi.

Escapes per le two-byte opcodes e le istruzioni FPU

Le istruzioni con opcode di due bytes e le istruzioni FPU iniziano con opcodes particolari, che prendono il nome di escapes e non vanno confuse con i prefissi.
L'escape per le opcodes di 2 bytes è
0F. Per gestire le opcodes con questa escape basta utilizzare il secondo byte dell'opcode come indice in una tabella dei nomi e in una delle sintassi strutturata analogamente a quelle per le istruzioni single-byte. Ovviamente l'escape 0F deve essere conteggiata per il calcolo della lunghezza complessiva dell'istruzione.
Le istruzioni FPU iniziano invece con delle escapes che vanno da D8 a DF. Ad ogni escapes sono associate due tabelle diverse: se l'opcode (identificato con il ModR/M byte) che segue l'escape è compreso tra 00 e BF, si seguirà una tabella; se è compreso tra C0 e FF si seguirà un'altra tabella.
Le istruzioni che hanno l'opcode compreso tra 00 e BF sono istruzioni con un operando in memoria. Ad ogni escape sono associate 8 istruzioni di questo tipo, che vengono identificate in base al reg field del ModR/M byte, che in questo caso prende il nome di nnn field.
Quando invece l'opcode è compreso tra C0 e FF, l'intero ModR/M byte è utilizzato come estensione dell'opcode; queste istruzioni non hanno operandi o (più spesso) gli operandi sono identificati dall'opcode stesso. Ad esempio, all'opcode
D8C5 corrisponde l'istruzione "FADD ST(0), ST(5)".
Per disassemblare queste istruzioni ho preferito fare due sole tabelle: una per le istruzioni FPU con ModR/M compreso tra 00 e BF ed una quelle con il ModR/M byte compreso tra C0 e FF. Ovviamente in questo modo si evita di dover fare una tabella per ogni escape, ma nel contempo bisogna stare attenti perché bisogna combinare il nibble meno significativo dell'escape con l'opcode contenuta nel ModR/M byte in modo da utilizzare il valore risultante come indice all'interno della tabella.
Trovate il codice di gestione delle istruzioni two-bytes dopo l'etichetta
twobyte e quello di gestione delle istruzioni FPU dopo l'etichetta FPU_instruction, nel file SpiDEr10.asm; le tabelle sono invece contenute in SpiDErData.asm.

 

Con questo si chiude questa piccola guida al disassembling. Spero di essere stato chiaro; in caso contrario, non esitate a scrivermi e cercherò di rimediare :-)

Note finali

Dunque dunque, vediamo un po'... saluti ad AndreaGeddon che in ML ha fatto pubblicità alla mia disasm engine (grazie Andrè :P), Quequero (il grande, come ho messo anche accanto al titolo... :P LOL NdQue), +Malattia, Yado, [kill3xx] che non vedo da un sacco di tempo :(, TheMR, Bubbo, Case, albe, _Blackdeath_, ph0b0s, Cieli Sereni, DsE, cHr, True-love, Quake2^AM, CrazyKiwi, _d31m0s_, Ritz, Pbdz, x86, NikDH, Dark-Angel, Dades, CyberPK, Guzura, littleluk, BluDestiny, unics, Pincopall (-ino... hihihi), Master Shadow, JEYoNE, ded, Gogetto, tutto il crew di RingZer0, gli studenti dell'UIC, i membri di Itassembly e i frequentatori di #crack-it e #asm.

Disclaimer

Il disassembling di programmi commerciali è illegale. L'autore non incoraggia chi volesse utilizzare le informazioni quivi contenute per scopi illegali. Questo tutorial è stato scritto a solo scopo informativo e di miglioramento del linguaggio assembly.


</body> </html>