[x] RingZ3r0  Proudly Presents [x]

386+ & Win 32

Viaggio all'interno del memory management di Win32

By GEnius

Salve ragazzi,
visto la continua espansioni dei Sistemi Operativi basati su Win 32 ho deciso
di scrivere qualcosa che riguarda l'argomento ed in particolare su una delle
parti più interessanti dei Sistema Operativi il gestore della memoria.
Il documento tratta alcuni concetti che sono alla base per della programmazione
a basso livello ed anche se non è corredato da esempi, penso che sia utile per
chi non ha una visione chiara del modo protetto e del modello di memoria usato
nelle piattaforme Win32.

- Prima parte: Breve descrizione sui meccanismi di protezione dei processori
               80386+.
- Seconda Parte: Modello di memoria usato nelle piattaforme Win32.

Parte Prima.

L'80386 e tutti i processori compatibili con esso, comprende una serie di
meccanismi di protezione. Essi sono utilizzati dal sistema operativo per
ridurre l'effetto di un bug di un programma o per limitare l'accesso ad alcune
risorse da parte delle applicazioni utente. La protezione si basa su cinque
aspetti:
- Verifica del tipo
- Verifica del limite
- Restrizione del dominio indirizzabile
- Restrizione dei punti di entrata delle procedure
- Restrizione del set d'istruzione.

Questi aspetti della protezione sono applicati nella protezione a livello di
segmento e di pagina. Senza entrare nel dettaglio possiamo brevemente
descrivere i cinque apetti della protezione.

La verifica del tipo a livello di segmento è usata dal processore per
riconoscerne i diversi tipi. Infatti i segmenti possono essere di tipo dati o
eseguibili, ma anche i descrittori agli stessi segmenti possono essere di tipi
diversi. Per le pagine la verifica del tipo serve solo per comprendere se la
pagina è a sola lettura o a lettura/scrittura.

La verifica del limite è applicata solo nella protezione a livello di segmento,
infatti ogni segmento definito ha una propria grandezza, quindi la protezione
del limite controlla che non si cerchi di leggere/scrivere oltre la grandezza
massima del segmento.

La restrizione del dominio indirizzabile è legato al concetto di livello di
privilegio. Il meccanismo del privelegio è stato implementato dalla Intel,
dividendo in 4 livelli da 0 a 3 (detti anche ring 0 - 3) lo stato di privilegio
attuale. Il livello 0 è quello con il privilegio più alto, di solito è usato
dal codice di sistema, mentre il livello 3 è quello con priorità minima ed è
destinato alle applicazioni utente. Il processore al momento dell'esecuzione ha
un proprio livello di privilegio (CPL, livello di privilegio corrente), ed in
base a questo livello è abilitato all'accesso o no ad alcuni segmenti ed ha la
possibilità o no di eseguire alcune istruzioni particolari. La restrizione del
dominio indirizzabile a livello di segmento serve per controllare se con il CPL
attuale è possibile accedere ad un determinato segmento. A livello di pagina
invece, la restrizione del dominio indirizzabile è implementato assegnando a
ciascuna pagina uno dei 2 livelli: Supervisore o Utente. Se il CPL del
processore è 3 il livello corrente è Utente; altrimenti è Supervisore. La
differenza tra Utente e Supervisore è nella possibilità di poter leggere o
leggere/scrivere nelle pagine e nelle tabelle di pagine.

La restrizione dei punti di entrata delle procedure è implementata in modo da
poter eseguire solo le istruzioni che si trovano in segmenti con lo stesso
livello di privilegio di quello corrente (CPL). Cioè il controllo viene
effettuato in ogni jump far e call far, il livello di privilegio del segmento
in cui si deve saltare (chaimato DPL) deve essere uguale al CPL. Naturalmente
ci sono delle eccezioni è possibile infatti eseguire anche codice in segmenti
di livello di privilegio più basso rispetto a quello corrente, ma questo
avviene solo in particolari segmenti. Ed infine è anche possibile (com'era
logico attendersi) trasferire il controllo a livelli di privilegio numericamete
inferiori (cioè a segmenti con privilegio più alto di quello corrente); questo
trasferimento è possibile solo tramite particolari descrittori chiamati porte
ed il processo di passaggio fra livelli di privilegio diversi viene chiamata
callgate (porte di chiamata... brutta traduz.. ma copiata dai manuali Intel).

Infine la restrizione del set d'istruzioni: ci sono alcune istruzioni che
possono essere eseguite solo se il CPL è 0, queste comprendono tutte le
operazioni sui registri di controllo, di debug, di test, ed in più qualche
altra istruzione come HLT, LGDT, LIDT etc.

Ok non sarò stato il massimo della chiarezza ma purtroppo è difficile spiegare
il tutto senza entrare nel dettaglio, comunque anche se non avete compreso
tutto, l'importante è che avete capito i concetti fondamentali della
protezione.

Parte Seconda.

Iniziamo ora un'affascinante discussione sulla gestione della memoria in
ambiente Win32.

Win32 a ring 3 usa il modello di memoria flat (o piatta in italiano), mentre a
ring 0 esso usa il normale metodo di indirizzamento selettore/spiazzamento, con
il paging attivato. Prima di continuare voglio ricordare il protected-mode
addressing model dei processori 80386+.

Nel modo protetto ogni segmento è definito dal programmatore, egli, infatti,
può scegliere alcuni attributi come l'indirizzo base del segmento, la
grandezza, livello di privilegio ed altri parametri. La struttura che definisce
questi attributi è chiamata descrittore. I descrittori sono situati in due
tabelle la LDT (tabella dei descrittori locale) e la GDT (tabella dei
descrittori globale). C'è una sola GDT e più LDT definite a tempo di
esecuzione. Il puntatore alla LDT si trova nel registro LDTR mentre quello alla
GDT si trova nel registro GDTR. Si può selezionare un descrittore caricando un
selettore in un registro di segmento. Un selettore è formato da 16 bit, che
indicano: La tabella scelta (LDT o GDT), un indice in questa tabella ed il
livello di privilegio del richiedente (RPL). Quindi si può indirizzare una
locazione tramite la coppia selettore/spiazzamento (selector/offset); l'offset
può essere a 32 bit o a 16, 32 per segmenti definiti a 32 bit e 16 per segmenti
definiti a 16 bits.

Quando viene modificato un registro di segmento con un nuovo selettore, l'80x86
legge le informazioni del descrittore (selezionato) e crea un indirizzo lineare
(Linear Address) prima di accedere alla memoria. L'indirizzo lineare è creato
leggendo il campo "segment base" del descrittore ed aggiungendoci lo
spiazzamento.

LDTR or GDTR---->|TABELLA DEI DESCRITTORI|
                 |                       |
SELETTORE ---->  | entry.Segment_Base_field  + SPIAZZAMENTO = INDIRIZZO LINEARE

Ora se il meccanismo della paginazione è disabilitato, l'indirizzo lineare è
uguale all'indirizzo fisico.
Se invece la paginazione è abilitata l'indirizzo è diverso da quello fisico. In
questo caso, l'indirizzo lineare è visto dall'80x86 in questo modo:

Indirizzo Lineare = DIR:PAGE:OFFSET

Indirizzo Lineare a 32 bit:
  Bits 0..11  = OFFSET
  Bits 12..21 = PAGE
  Bits 22..31 = DIR

Il meccanismo della paginazione è implementato dall'80x86 in questo modo:
- una pagina è generalmente di 4k (sui 486+ può essere maggiore)
- ci sono due livelli di tabelle di pagine, nelle quali ogni elemento specifica
l'indirizzo del page frame, la protezione e così via.
Nel registro CR3 (detto anche PDBR Page Directory Base Register) c'è un
puntatore alla tabella di pagine di 1° livello (la directory table). L'elemento
della directory table punta ad un page table (la tabella di 2° livello).
L'indirizzo fisico sarà allora creato in questo modo:

         Indirizzo Lineare = DIR:PAGE:OFFSET

CR3 ------>| PAGE DIRECTORY | (o directory table)
           |                |
DIR -----> | entry ---------| -> |PAGE TABLE|
           |                |
PAGE------>| entry.frame_address_field + OFFSET = PHYSICAL ADDRESS

Nota: questo schema è valido solo se le pagine sono di 4k e l'extending address
è disabilitato.

Il modo di indirizzamento FLAT è un semplice modello usato per bypassare la
segmentazione. Il modo di indirizzamento FLAT è creato assegnando ai segmenti
dati e codice (CS e DS..ed anche ES di solito) un indirizzo base uguale a 0. In
questo modo lo spiazzamento altro non è che l'indirizzo lineare, infatti il
campo segment base (che è uguale a 0) viene sommato allo spiazzamento ma
SPIAZZAMENTO + 0 = INDIRIZZO LINEARE! Ci sono diverse implementazione del modo
di indirizzamento FLAT con il meccanismo della paginazione abilitata,
disabilitata o con qualche altra lieve differenza rispetto a quanto spiegato.
Nota, non è possibile disabilitare la segmentazione solo la paginazione può
essere abilitata o disabilitata, il modello FLAT non disabilita la paginazione
la "nasconde" solamente!

Per maggiori informazioni su questi argomenti leggete i manuali Intel.

Ed ora il modello di memoria utilizzato dalle piattaforme win32 per i chip
Intel. Nota: per le versioni di Win NT per processori diversi da quelli Intel e
da quelli compatibili 80386, la discussione seguente potrebbe non essere
valida!

A ring 3 win32 usa il modello FLAT, 3 segmenti principali sono creati uno per
il codice e due per i dati (sono CS,DS e ES), entrambi con il campo indirizzo
base impostato a 0 e limite a 4 GB. Win32 usa la paginazione per questo
l'indirizzo lineare è diverso da quello fisico (ogni pagina è di 4kb). Per la
natura stessa del modello FLAT con la paginazione abilitata a ring 3 i processi
possono vedere solo gli indirizzi mappati nel loro address space e nient'altro.
Non ci sono win32 API (o almeno io spero) che permettono di allocare un
descrittore nella GDT o nella LDT, ma se sappiano dove un descrittore in una
GDT o LDT punta, noi possiamo caricarlo in un registro di segmento e possiamo
quindi indirizzare anche questa regione di memoria con i relativi attributi.
Naturalmente sappiamo che la stessa regione è comunque indirizzabile anche
utilizzando il registro DS di default in quando è l'indirizzo lineare quello
che realmente ci indica una regione di memoria e non i segmenti!. Come
risultato del modello FLAT implementato a ring 3 è possibile scrivere in un
sezione di codice (bisogna però usare WritememoryProcess o altri trucchetti).
Questo è possibile solo perché il segmento dati e di codice hanno lo stesso
indirizzo base e conseguentemente lo stesso spazio di indirizzi, ed essendo
possibile scrivere nel segmento dati... ;).
Nota: l'80x86 non permette di scrivere in un segmento eseguibile!.
Facciamo un esempio per rendere tutto più semplice, supponiamo di avere il
seguente frammento di codice:

.data
data_code  db  40 dup(?)
.code
        mov eax,34 ;fake number!
@1:     mov [data_code], eax    ;accessing data through DS
        ....
        ;è possibile apportare ulteriori modifiche sull'array data_code :-)
        .....
        ;ora supponiamo di aver copiato del "codice" nell'array data_code
        ;per eseguirlo basterà:
@2:     jmp offset data_code    ;accessing data through CS..uh executing ;)
        ; karino no?!!

L'esempio è abbastanza semplice, ed in realtà le uniche istruzioni di rilievo
sono le @1 e @2. Si capisce subito che nell'istruzione @1, in realtà è
sottointeso l'uso del registro DS come segmento da utilizzare perciò
l'indirizzo data_code sarà visto dal processore come un indirizzo che fà parte
di un segmento dati (DS:data_code), ed è quindi possibile scriverci sù.
Analogamente nell'istruzione @2 è implicito l'uso del registro CS e quindi
l'indirizzo data_code è visto come parte di un segmento eseguibile
(CS:data_code) ed è perciò possibile eseguirlo. Da questo se ne deduce che in
win32 ogni pagina in memoria può essere eseguita, per questo motivo i flags
PAGE_EXECUTE (usati in VirtualAlloc e VirtualProtect) negli ambienti win32
progettati per processori Intel praticamente non servono ma esistono solo per
compatibilità con gli ambienti progettati per altri processori. Premesso quindi
che possiamo allocare un qualsiasi blocco di memoria ed eseguirci del codice,
un ulteriore aspetto da tener in considerazione riguarda invece il codice che
si automodifica. Ho già detto che possiamo scrivere anche nelle pagine che
contengono codice, ma questo può essere fatto solo ad una condizione, e cioè
che queste pagine non siano protette da scrittura (se ci troviamo a ring 0
nemmeno questo aspetto ci interesserà più e quindi potremo fare ciò che
vogliamo! Viva la libertà!). Per ottenere questa informazione basta chiamare
un'apposita Api, VirtualQuery, che ci darà alcune informazioni sulla pagina tra
cui il tipo protezione (Lettura o Lettura/scrittura). Per modificare invece il
tipo di protezione basta solo usare VirtualProtect ed il gioco è fatto! Si
potrà ora scrivere nella pagina scelta.

Win32, logicamente, non può usare solo il modello FLAT; infatti, a ring 0 si
possono utilizzare i selettori (o meglio è solo a ring 0 che normalmente si
usano!). In win 9x ogni VM (Virtual Machine) ha una propria LDT e la usa per
accedere alla memoria mentre le applicazioni utente usano praticamente la
stessa LDT. In Nt invece ogni processo ha la sua LDT. Proprio su questa
differenza si basa un metodo per riconoscere Nt da win9x, infatti i selettori
delle appz (a ring 3) win9x usano la LDT mentre quelle Nt usano la GDT. Nella
LDT, ci sono segmenti a 32 o a 16 bit, questo è dovuto al fatto che Win9x ed Nt
possono eseguire codice a 16 bit. In realtà i segmenti non servono a molto per
gestire le applicazioni win 32 anche da ring 0, essi sono invece importanti
nelle applicazioni a 16 bits. Un'ultima nota sui selettori: la LDT e la GDT in
win 9x non sono protetti quindi si può benissimo scriverci sù ;), la cosa
(logicamente) non è vera per Nt (e per tutti i sistemi operativi creati con una
certa logica in mente!).

Naturalmente, anche a ring 0 la paginazione è abilitata, perciò creare
l'indirizzo fisico è un pò più complesso. A ring 0 è possibile allocare una
pagina, e generalmente è la pagina l'unità di allocazione base. Infatti alcuni
servizi VMM richiedono un numero di pagina come argomento, questo altro non è
che la combinazione di DIR:TABLE dell'indirizzo lineare. Per esempio se abbiamo
un indirizzo lineare possiamo trovare il numero di pagina corispondente,
semplicemente con l'istruzione: shr linear_address,12 (la DIR:TABLE è shiftata
a destra nella posizione dello spiazzamento).

Il meccanismo della paginazione implementato nel 386+ è uno strumento molto
comodo per la gestione della memoria virtuale e quindi windows lo sfrutta a
pieno. Infatti un indirizzo lineare, come ho già detto, mi indentifica una
pagina e uno spiazzamento nella pagina, ma nessuno ci assicura che la pagina
sia allocata o che sia in memoria. Se si prova ad accedere ad una pagina non
presente si verifica un'eccezione. Il gestore dell'interruzione (che in questo
caso è la parte del kernel di winzoz che implementa la memoria virtuale)
controlla se la pagina sia presente nel file di swap ed in questo caso rialloca
la pagina in memoria e fa in modo di rieseguire l'istruzione che ha causato
l'eccezione. Nel caso, invece, in cui la pagina non sia presente nel file di
swap, windows visualizza la magica finestra "Errore di pagina non valida" (o
qualcosa di simile non mi ricordo bene!). Capirete quindi come sia facile
implementare la gestione della memoria virtuale da parte di winzoz (grazie
Intel almeno per questo!).

Un'altra caratteristica importante della paginazione è che elimina in parte il
problema della deframmentazione della memoria. Infatti una serie di indirizzi
lineari contigui (che indirizzano più di una pagina) non è detto che facciano
riferimento ad aree di memoria fisicamente contigue. Ciò permette alle
applicazioni di allocare blocchi di memoria  molto grandi e trattarli come se
fossero costituiti da uno spazio fisico contiguo (anche se ciò la maggior parte
delle volte non è vero!). Bisogna comunque sapere che se la paginazione risolve
il problema della frammentazione della memoria fisica, il problema più generico
della "frammentazione" in generale rimane. Infatti si deve stare attenti alla
frammentazione degli indirizzi lineari. Cioè può succedere che gli indirizzi
lineari contigui utilizzabili nello spazio di indirizzamento di un processo
finiscano. Per ovviare a questo problema Win32 mette a disposizione una serie
di flags da utilizzare nelle Api per l'allocazione della memoria. In pratica
una volta allocato il blocco ti viene restituito un handle e non un indirizzo
lineare. Prima di accedere al blocco lo devi "bloccare" (cioè chiami l'api
GlobalLock che ti retituisce l'indirizzo lineare) quindi a fine accesso lo devi
"sbloccare" (api GlobalUnlock), facendo quindi uso di questi handle invece che
di indirizzi lineari, lo spazio degli indirizzi lineari verrà automaticamente
deframmentato da windows; per maggiori informazioni vedere l'Api GlobalAlloc
con il flag GHND.

Ritornado a noi, cerchiamo ora di capire cosa si intende per address space
(spazio di indirizzamento) di un processo. Sicuramente avrete sentito parlare
di Address Space! In effetti è una delle caratteristiche di win 32 ed
sostanzialmente consiste nell'assegnare ad ogni processo un proprio spazio di
indirizzamento. Cioè l'indirizzo 400000 di un processo A punterà ad un'area di
memoria fisica diversa dall'indirizzo 400000 di un processo B. Questo permette
ad ogni processo di accedere solo alle regioni di memoria private ed a quelle
che il sistema operativo decide di "concedergli". Bene ora spieghiamo come il
S.O. implementa questa caratteristica :P
Sappiamo che la memoria è divisa in pagine, che queste pagine sono specificate
dalle tabelle di pagine e che la posizione in memoria di queste tabelle è
specificata nel registro CR3. Sicuramente già sapete che quando c'è un context
switch (il passaggio da un processo ad un'altro) il processore legge le
informazioni per avviare il nuovo processo dal TSS (Task State Segment) dove
oltre alle normali informazioni sui registri ci sono i campi che specificano la
nuova LDT (che andrà in LDTR) e la nuova tabella di pagine di 1° livello (che
andrà in CR3), quindi potenzialmente al passaggio tra un processo ed un'altro
si avrà un cambio delle tabelle di pagine e della LDT. In win32 le LDT cambiano
solo tra le diverse Virtual Machine (in Nt anche tra i diversi processi),
invece le tabelle di pagine cambiano sempre. Bisogna ora porci una domanda:in
un context switch si possono cambiare le tabelle di pagine in modo da
indirizzare memoria fisica completamente differente da quella del processo
precedente? Beh, in teoria sì in pratica no, infatti ci sono alcune aree di
memoria che devono essere comunque comuni a tutti i processi, come ad esempio
la GDT che per definizione è comune a tutti i processi. Ed infatti anche in
win32 alcune parti di memoria sono condivise tra processi, in particolare il
Kernel è condiviso tra tutti i processi. Ogni processo avrà quindi una serie di
indirizzi lineari a disposizione per se stesso (indirizzi che punteranno ad
altre aree fisiche in altri processi) ed in più avrà a disposizione anche
un'altra serie di indirizzi lineari, come quelli riferiti al Kernel, che
saranno uguali per qualunque processo. Questo significa che ci sono delle
tabelle di pagine di secondo livello che verranno usate in ogni processo
(ricordate? esse sono indirizzate tramite la tabella di primo livello), mentre
altre invece saranno uniche per ogni processo.
In particolare in win 9x è documentata la diversificazione degli indirizzi
lineari:
 00000000H -  003fffffH usato dalle VM a livello DOS
 00400000H -  7fffffffH l'area privata di un processo, corrisponde all'address
              space privato
 80000000H - 0bfffffffH usata per codice e dati condivisi, appz 16 bits DPMI
             data etc.
0c0000000H - 0ffbfffffH usata per codice e dati per le VM

E' evidente quindi che la massima memoria privata allocabile dal processo è
circa 2 GB. Inoltre l'esistenza dell'area shared (80000000H - 0bfffffffH) viene
utilizzata da windows per la condivisione di aree di memoria tra i vari
processi. In particolare i memory mapped file vengono creati proprio in
quest'area e ciò ha come conseguenza il fatto che l'indirizzo lineare
restituito dall'Api MapVieOfFile è lo stesso per tutti i processi ;).
In NT il discorso è un po' diverso. Prima della service pack 3 in NT 4.0 le
applicazioni utente (user mode) avevano a disposizione gli indirizzi lineari da
0 a 2 GB mentre i programmi di sistema (kernel mode) avevano a disposizione gli
indirizzi da 2 a 4 GB. Dall'introduzione del service pack 3 i programmi di
sistema hanno a disposizione gli indirizzi lineari da 3 a 4 GB, con una
conseguente diminuzione dello spazio di indirizzamento di un GB per i programmi
in kernel mode, ed un'aumento di un GB invece per le applicazioni utente. WinNt
non ha aree shared, quindi per condividere blocchi di memoria tra più processi
lavora in maniera diversa da win9x, in particolare egli tiene traccia delle
pagine che devono essere condivise e le mappa nello spazio di indirizzamento di
tutti processi che le utilizzano. In altre parole i blocchi di memoria pubblici
saranno mappati per ogni processo che ne fà richiesta ed avranno in genere
indirizzi lineare diversi.

Un'ulteriore aspetto sulla gestione della memoria di win 32 è il copy-on-write.
Quando due o più processi condividono un'area di memoria ed uno di questi la
modifica, il copy-on-write entra in gioco creando una copia privata del blocco
al processo che lo ha modificato. Tutte le modifiche quindi influenzeranno da
ora in poi la copia privata e solo la copia non modificata sarà condivisa con
gli altri processi. Questo è in generale come funziona in copy-on-write in
molti sistemi operativi, vediamolo ora in particolare come è stato implementato
in Nt e win 9x.
In Nt, la cosa funziona così:
le pagine dati scrivibili sono inizializzate dal sistema operativo come a sola
lettura. Quando un processo proverà a scriverci ci sarà un page faults (si
verifica quando si prova a scrivere ad pagina a sola lettura da ring 3) ed il
S.O. creerà quindi un copia privata della pagina per il processo, la mapperà
nell'address space del processo stesso (con l'attributo di lettura/scrittura) e
quindi rieseguirà l'istruzione che ha provocato il page fault. In questo modo
se una nuova istanza del processo viene eseguita essa condividerà solo le
pagine che hanno l'attributo a sola lettura (quelle che non sono state
modificate) con l'istanza precedente. Se poi quest'ultima istanza proverà a
scrivere su queste pagine, scatterà di nuovo il copy-on-write. Questo è un
meccanismo molto importante per il S.O. e gli permette infatti di condividere
dati tra più processi senza eccessivi problemi e nello stesso tempo di copiare
solo le pagine effettivamente modificate dai processi stessi ottimizzando così
l'uso della memoria e la velocità di esecuzione.
In win 9x il copy-on-write non è implementato ma è in certo senso emulato
tramite l'Api WriteProcessMemory. Cioè se si prova a scrivere una pagina
tramite WriteProcessMemory scatterà il copy-on-write come già descritto (copia
privata della pagina, rimapping  etc..). Sfortunatamente WriteProcessMemory non
permette di scrivere nell'area shared di win 9x e quindi non è utilizzabile il
copy-on-write di dati in quell'area (memory mapped files, shared dlls etc).
Un'altro metodo di emulare il copy-on-write in win 9x consiste nell'utilizzare
i memory mapped file creandoli con il parametro PAGE_WRITECOPY, con conseguente
meccanismo del copy-on-write nel caso che un processo provi a scrivere
nell'area di memoria del m.m.f. stesso.

Abbiamo ora finito la discussione generale su come è gestita la memoria negli
ambienti win 32 progettati per processori Intel (gran parte del discorso è
comunque applicabile anche alla versione di Nt per processori Alpha), tra breve
presenterò anche alcuni esempi di programmi a ring 0 (per win 9x la maggior
parte... e forse qualcuno per Nt) che usano le funzioni base per la
manipolazione delle pagine (almeno spero!). Spero che la discussione vi possa
essere stata utile :D

!!!! Fine !!!!

Soliti saluti:

Saluto tutti i membri del gruppo RingZerO e tutti gli amici di #crack-it ed in
particolare (per l'occasione di questo tut):

Kill3xx: per avermi dato alcuni suggerimenti per il tut e per averlo riletto
dopo ogni modifica... dovrebbero darti il premio nobel per la pazienza :-)

NeuRal_NoiSE: per averlo letto e per averne dato un giudizio positivo anche se
a domanda specifica se l'è cavata con un "domani lo leggo meglio" :P

D4eMoN: per aver avuto la pazienza di leggerlo..ma non ho avuto un suo giudizio
:D...cmq il tuo cz di crackme lo potevi fà un pò + umano ...no???!!! :P

Insanity: detto anche l'uomo con il saluto + veloce del west..:-D per aver
pubblicato il documento fidandosi di ciò che ho scritto ;-)