ARCHITETTURA DELL' x86
written by AndreaGeddon
Esaminiamo l'architettura generale della famiglia di processori x86.
Prima di tutto alcuni chiarimenti:
1 byte = 8 bit
1 byte = un valore da 00 a FF in esadecimale
1 byte = 1 carattere della tabella ASCII (che va da 0 a 255 decimale = 00 -> FF hex)
Registri generali (General Purpose Registers):
EAX, accumulatore
EBX, base
ECX, contatore
EDX, dati
questi registri nei moderni processori sono tutti a 32 bit, ma prima erano a 16 e prima ancora ad 8 bit. Ecco la loro struttura:
bit 00001111000011110000111100001111
|---AH----|---AL---| AH AL = 8 bit
|---------AX--------| AX = 16 bit
|-------------------EAX-------------------| EAX = 32 bit
però nel cracking il registro verrà considerato non in binario, ma in esadecimale, quindi avremo:
hex 1 2 A 6 B E 9 C
| ah | al | ah al = 1 byte
| ax | ax = 2 byte (1 word)
| eax | eax = 4 byte (1 dword)
anche gli altri registri EBX, ECX e EDX sono strutturati in questo modo. Vengono generalmente usati come variabili. E' importante tenre d'occhio quali registri vengono modificati dopo le call alle API per vedere i valori restituiti: di solito ci si trovano cose interessanti.
Poi ci sono i regsitri puntatori ed indici:
ESP, puntatore allo stack
EBP, puntatore alla base dello stack
EDI, indice destinazione
ESI, indice sorgente
Questi sono registri a 32 bit (che prima erano a 16 bit). ESP punta allo stack correntemente in uso, e quindi ci troverete gli ultimi dati salvati. Per capire meglio lo stack vedi sotto. EBP punta alla base dello stack, e quindi la parte di memoria tra EBP ed ESP identifica tutto lo stack correntemente in uso. EDI ed EDI servono come puntatori da sorgente a destinazione per le operazioni tra stringhe: per esempio se voglio controllare due stringhe di testo e vedere se sono uguali, il codice in assembler sara:
PUSH EDI ----> salva il puntatore alla stringa sorgente
PUSH ESI -----> salva il puntatore alla stringa di destinazione
REPZ CMPSB ----> esegue un compare byte per byte delle stringhe puntate da edi e da esi
JNZ errore -----> se le due stringhe sono diverse salta ad un messaggio di errore
NOTA: i registri fin qui descritti sono stati progettati per svolgere il compito che vi ho descritto, ma ciò non esclude che possano essere usati (e questo nella maggior parte delle volte) per qualsiasi scopo, praticamente sono usati come variabili.
Infine c'è anche EIP, che è il puntatore alla prossima istruzione da eseguire. Questo registro è SEMPRE usato per puntare l'esecuzione della prossima istruzione, e conviene non toccarlo, a meno che non si è sicuri di quello che si sta facendo.
I registri segmenti:
CS, Segmento codice
DS, Segmento dati
SS, Segmento stack
ES, Segmento extra
FS, Da 386 in poi
GS, Da 386 in poi
Questi registri vengono combinati con altri registri per formare gli indirizzi di memoria. Generalmente si può fare a meno di conoscerli, ma è sempre meglio sapere che cosa sono. Per capire perchè vengono usati per formare indirizzi di memoria, leggete il testo relativo al problema dell'indirizzamento dell' x86.
I registri speciali:
Ci sono anche questi registri:
CR0, CR2, CR3
Sono i Control Registers.
TR4, TR5, TR6, TR7
Sono i Test Regsters.
DR0, DR1, DR2, DR3, DR6, DR7
Sono i Debug Registers.
Questi ultimi non sono utili ai fini del cacking, quindi non serve conoscerli.
Il registro FLAG:
Il FLAG register è un registro a 16 bit nel quale ogni bit è considerato separatamente. Ogni bit rappresenta un Flag, ovvero una condizione specificata dalle istruzioni del programma. Ad esempio, se eseguo un compare di due registri, e il cmpare vede che sono uguali, mi setta il flag ZERO su 1. Se la linea successiva contiene l'istruzione JZ (salta se zero flag è attivo), il codice salta alla parte di codice da eseguire nel caso che i registri fossero uguali.
Ecco il suo significato bit-per-bit:
| 11 | 10 | F | E | D | C | B | A | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
flag register
dove:
0 ----> CF Carry Flag (flg di riporto)
viene posto ad uno quando c'è stato un riporto o un prestito dal bit di ordine alto del risultato a 8 0 16 bit.
1 ----> 1
2 ----> PF Parity Flag (flag di parità)
Se è ad uno vuol dire che il risultato dell'operazione eseguita ha un numero pari di 1. Viene spesso usato per il controllo degli errori nella trasmissione dei dati.
3 ----> 0
4 ----> AF Auxiliary Flag (flag ausiliaria)
Se è ad uno allora c'è stato un riporto del nibble inferiore a quello superiore o un prestito dal nibble superiore a quello inferiore.
5 ----> 0
6 ----> ZF Zero Flag
Viene messo a zero se il risultato di un'operazione è zero.
7 ----> SF Sign Flag (flag di segno)
Viene messo ad uno quando il bit superiore del risultato di una operazione è un bit di segno. Se SF è ad 1, il primo bit del risultato sarà: 0 - positivo e 1 - negativo.
8 ----> TF Trap Flag (flag trappola)
9 ----> IF Interrupt Flag
A ----> DF Direction Flag (flag di direzione)
B ----> OF Overflow Flag (flag di overflow)
C ----> IOPL I/O Privilege Level (livello di privilegio I/O)
D ----> IOPL I/O Privilege Level (livello di privilegio I/O)
E ----> NT Nested Task Flag (flag di task annidato)
F ----> 0
10 ----> RF Resume Flag (flag di ripresa)
11 ----> VM Virtual Mode Flag (flag del modo virtuale)
Alcune parole sul funzionamento dello STACK:
Lo stack (dall'inglese "catasta") è una porzione di memoria usata per memorizzare i dati dal programma in esecuzione. Tecnicamente fa parte della categoria delle "pile", ed ha una struttura LIFO (last in first out) che vuol dire che l'ultimo dato immesso sarà poi il primo ad esser preso. E' importante capire il meccanismo dello stack, altrimenti si potrebbero commettere errori gravi. Il solito esempio che si fa è quello di considerare lo stack come una pila di piatti: l'ultimo piatto che poggeremo sulla cima sarà il primo che poi prenderemo. Solo che nel caso dello stack, la pila di piatti è capovolta. Facciamo un esempio:
condizione dello stack all'inizio:
contenuto | WORD 5 | WORD 4 | WORD 3 | WORD 2 | WORD 1 | ||||
indirizzo | 0004 | 0003 | 0004 | 0005 | 0006 | 0007 | 0008 | 0009 | 0010 |
Consideriamo il contenuto e gli indirizzi di memoria. Nel contenuto supponiamo che siano state salvate 5 WORD (cioè 5 valori da 16 bit). Se noi salviamo una nuova word (WORD 6), la nuova condizione dello stack sarà:
contenuto | WORD 6 | WORD 5 | WORD 4 | WORD 3 | WORD 2 | WORD 1 | |||
indirizzo | 0004 | 0003 | 0004 | 0005 | 0006 | 0007 | 0008 | 0009 | 0010 |
Ora se noi eseguiamo l'operazione POP per richiamare un dato dalla memoria, richiameremo WORD 6. Se vogliamo richiamare direttamente la WORD 2 non possiamo (dovremmo definire un puntatore a questa locazione). Dopo aver inserito la WORD 6 di fatto noi abbiamo decrementato lo stack, che prima aveva la sommità all'indirizzo 0006, adesso ce l'ha all'indirizzo 0005.
Se adesso richiamiamo due valori, con ad esempio POP EAX e POP EBX, avremo:
contenuto | WORD 4 | WORD 3 | WORD 2 | WORD 1 | |||||
indirizzo | 0004 | 0003 | 0004 | 0005 | 0006 | 0007 | 0008 | 0009 | 0010 |
lo stack si è incrementato, e in EAX viene messo il valore WORD 6, in EBX il valore WORD 5.
Non c'è nulla di difficile, basta sempre ricordarsi di LIFO (last in first out).
Un errore che capita spesso è quello di scrivere:
PUSH EAX
PUSH EBX
e poi richiamare nei registri il loro rispettivo valore così:
POP EAX
POP EBX
in questo caso verrà messo in EAX il valore prima contenuto in EBX, poi verrà messo in EBX il valore prima contenuto in EAX. Il tutto è stato invertito.
La giusta sequenza sarebbe dovuta essere questa:
PUSH EAX
PUSH EBX
...
POP EBX
POP EAX
in questo modo nei registri viene ripristinato il loro valore di partenza.
Gli INTERRUPT
Gli interrupt sono delle funzioni che il sistema mette a disposizione del programma. Ogni interrupt ha una sua funzione specifica, con uno specifico risultato. Sono un pò delle API in miniatura. Ad esempio, per scrivere una stringa sul testo possiamo utilizzare il seguente codice:
Stringa DB 'Stringa da stampare su schermo$'
...
mov dx, offset Stringa
mov ah, 9
int 21h
Qui usiamo l'interrupt 21 con funzione 9 (in ah), che prende il testo puntato da dx e lo stampa su schermo. Ci sono tanti interrupt per tantissime funzioni che permettono di controllare il pc davvero a bassissimo livello. Per conoscere tutti gli interrupt o vi scaricate la famosa lista di Ralph Brown, o vi prendete un manuale. Insomma, conoscerli è essenziale.
E con questo si conclude la mia piccola guida all'architettura interna degli x86.
AndreaGeddon