Assembly Programming Journal, traduzione a cura di Little-John per la comunit� italiana del reverse engeneering (RingZer0)*** http://ringzer0.cjb.net *** ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. Ott/Nov 98 :::\_____\::::::::::. Issue 1 ::::::::::::::::::::::.......................................................... A S S E M B L Y P R O G R A M M I N G J O U R N A L http://asmjournal.freeservers.com asmjournal@mailcity.com T A B L E O F C O N T E N T S ---------------------------------------------------------------------- Introduzione....................................................mammon_ "VGA Programming in Mode 13h".............................Lord Lucifer "SMC Techniques: The Basics"...................................mammon_ "Going Ring0 in Windows 9x".....................................Halvar Column: Win32 Assembly Programming "The Basics"..............................................Iczelion "MessageBox"..............................................Iczelion Column: The C standard library in Assembly "_itoa, _ltoa and _ultoa"...................................Xbios2 Column: The Unix World "x86 ASM Programming for Linux"............................mammon_ Column: Issue Solution "11-byte Solution"..........................................Xbios2 ---------------------------------------------------------------------- +++++++++++++++++++++++ Sfida ++++++++++++++++++++ Scrivi un programma che visualizzi la sua command line in 11 bytes ---------------------------------------------------------------------- ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::..............................................INTRODUZIONE by mammon_ Benvenuti alla prima issue dell'Assembly Programming Journal. Il linguaggio Assembly � stato oggetto di rinnovato interesse per molti programmatori; la ragione di ci� dovrebbe essere la reazione al boom improvviso di programmi RAD-developed di bassa qualit� (vedi Delphi, VB, ecc) rilasciati come free/shareware negli anni scorsi. Il linguaggio Assembly � solido, veloce, e spesso ben fatto � � un pi� difficile trovare programmatori inesperti che sviluppano in assembler che non, diciamo, in Visual Basic. La selezione degli articoli � qualcosa di eclettico e dovrebbe dimostrare il focus di questo giornale: p.e., si rivolge alla comunit� dei programmatori assembler, non un tipo particolare di coding, come Win32, virus, o programmazione di demo. Siccome il magazine � appena nato e molti dei suoi scopi possono sembrare poco chiari, dedicher� il resto di questa introduzione alle domande pi� comuni che ho ricevuto via email per i diversi chiarimenti. Quanto spesso sar� pubblicato il giornale? ------------------------------------ Salvo fatalit�, ogni issue sar� rilasciata a mesi alterni. Che tipo di articoli saranno accettati? ---------------------------------------- Qualsiasi cosa che abbia a che fare con il linguaggio assembly. Ovviamente repliche di materiale gi� pubblicato in precedenza non sono necessarie se non quando migliorano o chiarificano il materiale precedente. Il pi� sar� incentrato sugli instruction sets della famiglia Intel x86; comunque il coding per gli altri processori � accettabile (per� sarebbe davvero una gran cortesia indicare un emulatore x86 per il processore riguardo al quale tu scrivi). Personalmente sono alla ricerca di articoli sull'assembly language che mi interessano: ottimizzazione del codice, demo/graphics programming, virus coding, asm coding per unix e altri OS, e OS-internals. Le demo (con il sorgente) e 'ASCII art' di qualit� (per le copertine della issue, logo per gli articoli, ecc) sono davvero benvenuti. Per quale livello di esperienza nel coding � inteso il mag? -------------------------------------------------------- Il giornale intende coinvolgere gli asm-coders di ogni livello. Ogni issue conterr� per lo pi� tecniche e codice per beginners e intermediate, dato che saranno, per forza di cose, di maggiore richiesta; comunque uno degli obiettivi dell'APJ � di includere abbastanza materiale 'advanced' per poter interessare anche i "pro-coders". Come sar� distribuito il mag? -------------------------------- L'Assembly Programming Journal ha la sua propria web page http://asmjournal.freeservers.com che conterr� la issue rilasciata e un archivio delle issue precedenti. La pagina contiene anche un guestbook e una disucssion board per gli articolisti e i lettori. Un abbonamento via email pu� esser ottenuto inviando una email a asmjournal@mailcity.com con il subject "SUBSCRIBE"; a partire dalla prossima issue, l'Assembly Programming Journal sar� inviato via email all'indirizzo da cui hai inviato la mail. Wrap-up ------- Questo � per lo pi� la "faq". Enjoy the mag! ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::...........................................FEATURE..ARTICLE VGA Programming in Mode 13h by Lord Lucifer Questo articolo descriver� come programmare grafica VGA Mode 13h usando il linguaggio assembly. Mode 13h � la modalit� grafica 320x200x256, ed � veloce e molto conveniente dal punto di vista del programmatore. Il buffer video comincia all'indirizzo A000:0000 e finisce all'indirizzo A000:F9FF. Ci� significa che il buffer � di 64000 bytes e che ogni pixel in mode 13h � rappresentato da un byte. E' facile settare il mode 13h e il buffer video con l'assembly language: mov ax,0013h ; Int 10 - Video BIOS Services int 10h ; ah = 00 � Setta il Video Mode ; al = 13 - Mode 13h (320x200x256) mov ax,0A000h ; punta il segment register es a A000h mov es,ax ; possiamo ora accedere al buffer video come ; offset dal registro es Alla fine del tuo programma, vorrai probabilmente ripristinare il text mode. Ecco come: mov ax,0003h ; Int 10 - Video BIOS Services int 10h ; ah = 00 � Setta il Video Mode ; al = 03 - Mode 03h (80x25x16 text) Accedere ad un pixel specifico nel buffer � anche molto semplice: ; bx = coordinata x ; ax = coordinata y mul 320 ; moltiplica y per 320 per ottenere la riga add ax,bx ; aggiungi questo alla x per otten. l'offset mov cx,es:[ax] ; ora si pu� accedere al pixel x,y da es:[ax] Hmm... questo era facile, ma quella moltiplicazione � un po' lenta e dovremmo sbarazzarcene. Questo � pure facile da fare, semplicemente usando il 'bit shifting' invece della moltiplicazione. Shiftando un numero a sinistra � come moltiplicarlo per 2. Noi vogliamo moltiplicare per 320, che non � una potenza di 2, ma 320 = 256 + 64, e 256 e 64 sono tutti e due potenze di 2. Quindi una maniera pi� veloce di accedere ad un pixel �: ; bx = coordinata x ; ax = coordinata y mov cx,bx ; copia bx in cx, per salvarlo temporaneam. shl cx,8 ; shift a sinistra per 8, che � come ; moltiplicare per 2^8 = 256 shl bx,6 ; ora shift sin. per 6, che � come ; moltiplicare per 2^6 = 64 add bx,cx ; ora somma queste 2 insieme, che � come ; moltiplicare effettivamente per 320 add ax,bx ; infine aggiungi la coord x a questo valore mov cx,es:[ax] ; ora si pu� accedere al pixel x,y da es:[ax] Beh, il codice � un po' pi� lungo e sembra anche pi� complicato, ma posso garantire che � molto pi� veloce. Per disegnare i colori, usiamo una 'color look-up table' (tavola di riferimento colori, NdT). Questa look-up table � un array di 768 campi (3x256). Ogni indice della table � proprio l'offset index*3. I 3 bytes ad ogni indice contengono i valori corrispondenti (0-63) al rosso, al verde, e al blu. Quindi il numero totale dei colori possibili � 262144. Comunque, siccome la table � di soli 256 elementi, solo 256 colori differenti sono possibili in un dato momento. Il cambiamento della palette dei colori � realizzato attraverso le porte I/O della scheda VGA: La Porta 03C7h � la Palette Register Read port (Porta di lettura) LA Porta 03C8h � la Palette Register Write port (Porta di scrittura) La Porta 03C9h � la Palette Data port (Porta dati) Ecco come cambiare la palette dei colori: ; ax = indice della palette ; bl = componente rossa (0-63) ; cl = componente verde (0-63) ; dl = componente blu (0-63) mov dx,03C8h ; 03c8h = Palette Register Write port out dx,ax ; scegli l'index mov dx,03C9h ; 03c8h = Palette Data port out dx,al mov bl,al ; setta il valore rosso out dx,al mov cl,al ; setta il valore verde out dx,al mov dl,al ; setta il valore blu Questo � tutto. Leggere la palette del colore � quasi lo stesso: ; ax = indice della palette ; bl = componente rossa (0-63) ; cl = componente verde (0-63) ; dl = componente blu (0-63) mov dx,03C7h ; 03c7h = Palette Register Read port out dx,ax ; scegli l'index mov dx,03C9h ; 03c8h = Palette Data port in al,dx mov bl,al ; prendi il valore rosso in al,dx mov cl,al ; prendi il valore verde in al,dx mov dl,al ; prendi il valore blu Cosa abbiamo ora bisogno di sapere � la procedura per disegnare un pixel di un certo colore in una certa locazione sul monitor. E' molto facile dato ci� che gi� sappiamo: ; bx = coordinata x ; ax = coordinata y ; dx = colore (0-255) mov cx,bx ; copia bx in cx, per salvarlo temporaneam. shl cx,8 ; shift a sinistra per 8, che � come ; moltiplicare per 2^8 = 256 shl bx,6 ; ora shift a sin. per 6, che � come ; moltiplicare per 2^6 = 64 add bx,cx ; ora somma queste 2, che � come ; moltiplicare effettivamente per 320 add ax,bx ; infine aggiungi la coord x a questo valore mov es:[ax],dx ; copia il colore dx nella memory location ; questo � tutto Ok, ora noi sappiamo come gestire il Mode 13h, gestire il video buffer, disegnare un pixel, e editare la color palette. Il mio prossimo articolo tratter� il disegno di linee, l'utilizzo del vertical retrace per rendering pi� uniformi, e ogni cosa che mi verr� in mente fino ad allora... ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::...........................................FEATURE..ARTICLE SMC Techniques: The Basics by mammon_ Uno dei vantaggi della programmazione in assembler � che hai piene facolt� di controllo sull'applicazione: la 'ginnastica binaria' del codice di un virus dimostra ci� pi� di ogni altra cosa. Uno dei "trucchi" utilizzati dai virus, che ha avuto poi seguito negli schemi di protezione dei programmi, � il codice automodificante (SMC = self-modifyng code). In quest'articolo non tratter� di virus polimorfici o di motori di mutazione (mutation engines); non analizzer� nessuno schema di protezione in particolare, non considerer� trucchi anti-debugger/anti-disassembler, e non affronter� l'argomento della PIQ (Prefetch Instruction Queue, ndt). Questo articolo � solamente una prima trattazione riguardante il codice automodificante, per coloro a cui il concetto risulta nuovo e da implementare. Episodio 1: Cambiamento di opcode (opcode alteration) ----------------------------------------------------- Una delle forme pi� pure del codice automodificante � il cambiare il valore di una istruzione prima che sia eseguita... a volte come il risultato di una comparazione, e a volte per nascondere il codice da occhi curiosi. Questa tecnica segue essenzialmente questo schema: mov reg1, codice-da-sostituire mov [indir-su-cui-scrivere], reg1 in cui 'reg1' pu� essere qualsiasi registro, e '[indir-su-cui-scrivere]' dovrebbe essere un puntatore all'indirizzo da cambiare. Nota che il 'codice-da-sostituire' dovrebbe essere una istruzione in formato esadecimale, ma posizionando il codice da qualche altra parte nel programma -- in una subroutine non chiamata, o in un segmento diverso -- � possibile semplicemente trasferire il codice compilato da una locazione ad un'altra attraverso l'indirizzamento indiretto, come segue: call changer mov dx, offset [string] ;questo sar� eseguito ma ignorato label: mov ah, 09 ;questo non sar� mai eseguito int 21h ;questo chiuder� il programma .... changer: mov di, offset to_write ;carica l'indirizzo del codice da scrivere ;in DI mov byte ptr [label], [di] ;scrivi il codice nella locazione 'label' ret ;ritorna dalla chiamata to_write: mov ah, 4Ch ;codice di fine programma questa piccola routine far� chiudere il programma, anche se in un disassembler essa all'inizio sembra essere una semplice routine di scrittura di stringa. Nota che combinando l'indirizzamento con dei loops, intere subroutine -- anche programmi -- possono essere sovrascritti, e il codice da scrivere -- che pu� essere presente nel programma come dati -- pu� essere criptato con un semplice XOR per farlo sfuggire da un disassembler. Il seguente � un programma in assembler per dimostrare il cambiamento dal "vivo" del codice; esso chiede all'utente una password, poi cambia la stringa da scrivere a seconda che la password sia corretta o meno. ; smc1.asm ================================================================== .286 .model small .stack 200h .DATA ;buffer for Keyboard Input, formatted for easy reference: MaxKbLength db 05h KbLength db 00h KbBuffer dd 00h ;strings: nota che la password non � criptata, ma potrebbe esserlo szGuessIt db 'Care to guess the super-secret password?',0Dh,0Ah,'$' szString1 db 'Congratulations! You solved it!',0Dh,0Ah, '$' szString2 db 'Ah, damn, too bad eh?',0Dh,0Ah,'$' secret_word db "this" .CODE ;=========================================== start: mov ax,@data ; setta il registro di segmento mov ds, ax ; uguale alla direttiva "assume" mov es, ax call Query ; chiede all'utente la password mov ah, 0Ah ; funzione DOS 'Ricevi input ; dall'utente' mov dx, offset MaxKbLength ; inizio del buffer int 21h call Compare ; confronta la password e cambia il ; codice exit: mov ah,4ch ; funzione 'Chiudi al DOS' int 21h ;=========================================== Query proc mov dx, offset szGuessIt ; stringa di richiesta password mov ah, 09h ; funzione 'visualizza stringa' int 21h ret Query endp ;=========================================== Reply proc PatchSpot: mov dx, offset szString2 ; stringa 'hai fallito' mov ah, 09h ; funzione 'visualizza stringa' int 21h ret Reply endp ;=========================================== Compare proc mov cx, 4 ; num. di bytes nella password mov si, offset KbBuffer ; inizio del buffer di input della ; password mov di, offset secret_word ; indirizzo della password corretta rep cmpsb ; confronto password or cx, cx ; sono uguali? jnz bad_guess ; no, non applicare il patch mov word ptr cs:PatchSpot[1], offset szString1 ;cambia in GoodString bad_guess: call Reply ; output della stringa per visualizzare ; il risultato ret Compare endp end start ; EOF ======================================================================= Episodio 2: Encryption (la traduzione di questo termine � proprio brutta, encriptazione, quindi ho preferito lasciare l'originale inglese) ------------------------------------------------- Senza dubbio l'encryption � la forma pi� comune di codice automodificante utilizzato oggigiorno. E' utilizzata dai packers e dagli exe-encriptors o per comprimere o per nascondere codice, dai virus per rendere oscuri i propri contenuti, dagli schemi di protezione per nascondere dati. La forma base dell'encription pu� essere: mov reg1, indir-da-sovrascrivere mov reg2, [reg1] manipola reg2 mov [reg1], reg2 dove 'reg1' sarebbe il registro contenente l'indirizzo (l'offset) della locazione da sovrascrivere, e 'reg2' un registro temporaneo che carica i contenuti del primo e poi li modifica attraverso operazioni matematiche (ROL) oppure logiche (XOR). L'indirizzo da cambiare � caricato in reg1, il suo contenuto � poi modificato all'interno di reg2, ed infine riscritto nella locazione originale ancora contenuta in reg1. Il programma della sezione precedente pu� essere modificato in modo tale che esso decripti la password sovrascrivendola (in tal modo questa rimane decriptata finch� il programma non � terminato) prima cambiando la 'parola segreta' come segue: secret_word db 06Ch, 04Dh, 082h, 0D0h e poi cambiando la routine di confronto, Compare, per cambiare la locazione della 'secret_word' nel segmento dati: ;=========================================== magic_key db 18h, 25h, 0EBh, 0A3h ;non molto sicura! Compare proc ; Passo 1: decripta la password mov al, [magic_key] ; metti il byte1 della maschera XOR in al mov bl, [secret_word] ; metti il byte1 della password in bl xor al, bl mov byte ptr secret_word, al ; cambia il byte1 della password mov al, [magic_key+1] ; metti il byte2 della maschera XOR in al mov bl, [secret_word+1] ; metti il byte2 della password in bl xor al, bl mov byte ptr secret_word[1], al ; cambia il byte2 della password mov al, [magic_key+2] ; metti il byte3 della maschera XOR in al mov bl, [secret_word+2] ; metti il byte3 della password in bl xor al, bl mov byte ptr secret_word[2], al ; cambia il byte3 della password mov al, [magic_key+3] ; metti il byte4 della maschera XOR in al mov bl, [secret_word+3] ; metti il byte4 della password in bl xor al, bl mov byte ptr secret_word[3], al ; cambia il byte4 della password mov cx, 4 ;Passo 2: Comfonta le passwords...nessun ;cambiamento da qui in poi mov si,offset KbBuffer mov di, offset secret_word rep cmpsb or cx, cx jnz bad_guess mov word ptr cs:PatchSpot[1], offset szString1 bad_guess: call Reply ret Compare endp Nota l'aggiunta della locazione della 'magic_key' (chiave magica) che contiene la maschera XOR per la password. Tutto ci� potrebbe essere stato eseguito in maniera pi� sofisticata con un loop, ma con soli 4 byte il codice sopra velocizza il tempo di debugging (e, inoltre, il tempo di scrittura della articolo ( e della traduzione, ndt !)). Nota come la password � caricata, XORata, e riscritta un byte la volta; utilizzando codice a 32-bit, l'intera password (dword) pu� esser scritta, XORata e riscritta in un sol colpo. Episodio 3: Giocherellando con lo stack --------------------------------------- Questo � un trucco che ho imparato mentre decompilavo del codice di SunTzu. Ci� che accade qui � abbastanza interessante: lo stack � spostato nel segmento codice del programma, cosicch� il top dello stack punta al primo indirizzo da essere modificato (che, inoltre, dovrebbe essere uno vicinissimo alla fine del programma per il modo in cui lo stack funziona); il byte a questo indirizzo � POPato in un registro, manipolato, e PUSHato indietro nella sua locazione originale. Lo stack pointer (SP) � poi decrementato in modo che l'indirizzo successivo da essere modificato (1 byte pi� basso in memoria) � ora nel top dello stack. In pi�, i byte sono XORati con una porzione del codice stesso del programma, anche per camuffare il valore della maschera XOR. Nel codice seguente, ho scelto di utilizzare i byte dallo Start: (200h quando � compilato) fino a -- ma non includendolo -- Exit: (214h quando � compilato; Exit-1=213h). Comunque, come nel codice originale di SunTzu ho mantenuto la sequenza "inversa" della maschera di XOR sicch� il byte 213h � il primo byte della maschera di XOR, e il byte 200h ne � l'ultimo. Dopo alcuni esperimenti ho capito che questo era il modo pi� facile per sincronizzare una patch -- o un editor esadecimale -- al codice che manipola lo stack; dal momento che lo stack si muove all'indietro (uno stack che si sposta in avanti � pi� un problema che una soluzione), utilizzando una maschera XOR "inversa" che permetta a tutti e due i puntatori di file in un patcher di essere INCati o DECati in sincronia. Come mai questa � una issue? A differenza dei due esempi precedenti, la seguente non contiene una versione criptata del codice da modificare. Questa contiene giusto il code di origine che, quando compilato, risulta essere nei byte non criptati che sono elaborati attraverso la routine di XOR, criptati, ed infine eseguiti (che, se hai seguito il discorso, si dimostrer� subito di non esser buono... in ogni caso � un ottimo metodo per far crashare la VM di DOS (Virtual Machine, ndt)). Una volta che il programma � compilato bisogna o cambiare i byte da decriptare a mano, o scrivere un patcher che faccia ci� per te. La prima � pi� conveniente, l'ultima � pi� sicura e diventa una necessit� se hai intenzione di conservare il codice. Nell'esempio seguente ho incluso 2 CCh (int 3) nel codice prima e dopo la fine dei byte da decriptare; un patcher deve solo ricercare questi due valori, contare i byte nel mezzo, ed infine eseguire lo XOR con i byte tra 200h e 213h. Ancora una volta, questo esempio � la continuazione di quello precedente. In questo ho scritto una routine per decriptare per intero la routine 'Compare' della sezione precedente XORandola con i byte compresi tra 'Start' e 'Exit'. Ci� � eseguito settando il segmento di stack come segmento di codice, poi settando il puntatore di stack uguale all'ultimo indirizzo di codice (il pi� alto) da modificare. Un byte � POPato dallo stack (es. la sua locazione originale), XORato, e PUSHato indietro nella sua posizione originale. Il byte successivo � caricato decrementando il puntatore di stack. Quando tutto il codice � decriptato, il controllo � restituito alla appena decriptata routine 'Compare' e l'esecuzione continua normalmente. ;=========================================== magic_key db 18h, 25h, 0EBh, 0A3h Compare proc mov cx, offset EndPatch[1] ;inizio dell'indiriz-da-sovrascriv + 1 sub cx, offset patch_pwd ;fine dell'indiriz-da-sovrascriv mov ax, cs mov dx, ss ;salva lo stack segment -- importante! mov ss, ax ;setta lo stack segment come code segment mov bx, sp ;salva lo stack pointer mov sp, offset EndPatch ;inizio dell'indiriz-da-sovrascriv mov si, offset Exit-1 ;inizio dell'indiriz della maschera XOR XorLoop: pop ax ;prendi il byte-da-patchare in AL xor al, [si] ;XORa al con la XorMask push ax ;scrivi il byte-to-patch in memoria dec sp ;carica il successivo byte-da-patchare dec si ;carica il successivo byte della maschera ;XOR cmp si, offset Start ;fine della maschera XOR? jae GoLoop ;Se No, continua mov si, offset Exit-1 ;reinizializza la maschera XOR GoLoop: loop XorLoop ;XORa il byte successivo mov sp, bx ;ripristina lo stack pointer mov ss, dx ;ripristina lo stack segment jmp patch_pwd db 0CCh,0CCh ;Identifcation mark: START patch_pwd: ;Nessun cambiamento da qui mov al, [magic_key] mov bl, [secret_word] xor al, bl mov byte ptr secret_word, al mov al, [magic_key+1] mov bl, [secret_word+1] xor al, bl mov byte ptr secret_word[1], al mov al, [magic_key+2] mov bl, [secret_word+2] xor al, bl mov byte ptr secret_word[2], al mov al, [magic_key+3] mov bl, [secret_word+3] xor al, bl mov byte ptr secret_word[3], al ;compare password mov cx, 4 mov si, offset KbBuffer mov di, offset secret_word rep cmpsb or cx, cx jnz bad_guess mov word ptr cs:PatchSpot[1], offset szString1 bad_guess: call Reply ret Compare endp EndPatch: db 0CCh, 0CCh ;Identification Mark: END Questo genere di programmi � davvero difficile da debuggare. Per testarlo, ho sostituito 'xor al, [si]' prima con 'xor al,00', che non encripta ed � utile per cercercare eventuali errori nel codice, e poi con 'xor al, EBh', che mi ha permesso di verificare che venivano criptati i byte corretti (non fa mai male dare una controllatina, dopotutto). Episodio 4: Conclusione ------------------------ Tutto ci� dovrebbe dare le basi sul codice automodificante. Ogni programma che utilizza tali tecniche sar� pieno di insidie. La cosa pi� importante � avere il programma completamente funzionante prima di iniziare a sovrascrivere parti del suo codice. Inoltre, crea sempre una applicazione che esegua l'inverso di ogni routine di decriptazione/criptazione -- non solo per velocizzare la compilazione e il test automatizzando l'encriptazione di codice che sar� decriptato durante l'esecuzione, ma anche per disporre di un buono strumento per controlli utilizzando un disassemblatore (es. encripta il codice, dissasemblalo, decripta il codice, disassemblalo, confrontalo). Infatti, � una buona idea mantenere la porzione di codice automodificante del tuo programma in un eseguibile diverso e testarlo sulla versione definitiva, finch� tutti i bug sono eliminati dalla routine di decriptazione, e solo allora aggiungi la routine di decriptazione al codice finale. I segni di riconoscimento CCh (codemarks) sono anch'essi estremamente utili. Infine, esegui il debug con il debug.com per applicazioni DOS -- il debugger � veloce, piccolo, e se crasha hai solo perso una finestra di DOS. Esempi pi� complessi di codice automodificante pu� esser trovato nel codice di Dark Angel, il motore Rhince, o in qualsiasi motore di mutazione utilizzato nei virus polimorfici. Si ringrazia Sun-Tzu per la tecnica dello stack usata nella su applicazione ghf-crackme. ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::...........................................FEATURE..ARTICLE Going Ring0 in Windows 9x by Halvar Flake Questo articolo fornisce una breve visione generale su due modi per andare al livello Ring0 in Windows 9x in modo non documentato, sfruttando il fatto che in Win9x nessuna delle tavole di sistema pi� importanti sono sulle pagine che sono protette da un accesso a basso privilegio (low-privilege access). Una conoscenza di base del Protected Mode e degli OS Internals � richiesta, fa' riferimento al tuo Assembly Book per questo:-) Le tecniche presentate qui non sono assolutamente una maniera buona/pulita per raggiungere un livello di privilegio pi� alto, ma siccome richiedono uno sforzo di coding davvero piccolo, a volte sono pi� more piacevoli da implementare che non un rigoroso VxD. 1. Introduzione ---------------- In tutti i Sistemi Operativi moderni, la CPU va in protected mode, sfruttando le caratteristiche particolari di questo mode per implementare la virtual memory, il multitasking ecc. Per gestire l'accesso alle risorse system-critical (e perci� per fornire stabilit�) un OS ha bisogno di livelli di privilegio, in modo che un programma non possa repentinamente uscirsene dal protected mode ecc. Questi livelli di privilegio sono rappresentati sulle CPU x86 (mi riferisco all'x86 intendendo il 386 e i seguenti) con dei 'Rings', in cui il Ring0 � quello con i privilegi pi� alti e il Ring3 � quello con i privilegi pi� bassi. In teoria, l'x86 pu� gestire 4 livelli di privilegio, ma Win32 ne usa solo 2, Ring0 come 'Kernel Mode' e Ring3 come 'User Mode'. Dal momento che il Ring0 non � richiesto dal 99% delle applicazioni, in Win9x l'unico modo documentato per usare le routine del Ring0 � attraverso i VxDs. Ma i VxDs, pur rappresentando l'unica maniera stabile e raccomandata, sono faticosi da scrivere e grandi, quindi in un paio di situazioni particolari, le altre vie per raggiungere il Ring0 possono tornare utili. La CPU stessa amministra le transizioni di livello di privilegio in due modi: attraverso le Exceptions/Interrupts e attraverso i Callgates. I Callgates possono essere inseriti nella LDT o nella GDT, Interrupt-Gates si trovano nella IDT. Noi trarremo profitto dal fatto che queste tables possono essere scritte liberamente dal Ring3 in Win9x (NON IN NT !). 2. Il metodo della IDT ----------------- Se si verifica una exception (or is triggered), la CPU guarda nella IDT al descriptor corrispondente. Questo descriptor d� alla CPU un Address e un Segment a cui trasferire il controllo. Un Interrupt Gate descriptor assomiglia a questo: --------------------------------- --------------------------------- D D 1.Offset (16-31) P P P 0 1 1 1 0 0 0 0 R R R R R +4 L L --------------------------------- --------------------------------- 2.Segment Selector 3.Offset (0-15) 0 --------------------------------- --------------------------------- DPL == I 2 bits che contengono il Descriptor Privilege Level P == Il bit Present R == bits Reserved La prima word (Nr.3) contiene la pi� bassa word dell'address a 32-bit dell'Exception Handler. La word a +6 contiene la word di ordine pi� alto. La word a +2 � il selector del segment in cui risiede l'handler. La word a +4 identifica il descriptor come Interrupt Gate, contiene il suo privilegio e il bit present. Ora, per usare la IDT per andare a Ring0, creeremo un nuovo Interrupt Gate che punta alla nostra procedura Ring0, salveremo quello vecchio e lo sostituiremo con il nostro. Poi causeremo quella exception. Invece di passare il controllo all'handler proprio di Windows, la CPU eseguir� il nostro codice di Ring0. Non appena abbiamo finito, ripristineremo il vecchio Interrupt Gate. In Win9x, il selector 0028h punta sempre ad un Segmento Ring0-Code, che si estende per il completo intervallo dello spazio di indirizzamento di 4 GB. Useremo questo come nostro Segment selector. Il DPL deve essere 3, dal momento che stiamo chiamando dal Ring3, e il bit present deve essere settato. Quindi la word a +4 sar� 1110111000000000b => EE00h. Questi valori possono essere hardcodati nel tuo programma, noi dobbiamo solo aggiungere l'offset della nostra Procedura Ring0 al descriptor. In quanto exception, ne dovresti usare preferibilmente una che si verifica raramente, quindi non usare l'int 14h ;-) Io user� l'int 9h, dal momento che (per quanto ne so) non � usato sul 486+. Il codice di esempio segue (da compilare con il TASM 5): -------------------------------- taglia qui ----------------------------------- .386P LOCALS JUMPS .MODEL FLAT, STDCALL EXTRN ExitProcess : PROC .data IDTR df 0 ; Questo conterr� i contenuti del registro IDTR SavedGate dq 0 ; Salviamo il gate che posizionamo qui OurGate dw 0 ; Offset low-order word dw 028h ; Segment selector dw 0EE00h ; dw 0 ; Offset high-order word .code Start: mov eax, offset Ring0Proc mov [OurGate], ax ; Mette l'offset words shr eax, 16 ; nel nostro descriptor mov [OurGate+6], ax sidt fword ptr IDTR mov ebx, dword ptr [IDTR+2] ; carica il Base Address della IDT add ebx, 8*9 ; Indirizzo del descriptor int9 in ebx mov edi, offset SavedGate mov esi, ebx movsd ; Salva il vecchio descriptor movsd ; nel SavedGate mov edi, ebx mov esi, offset OurGate movsd ; Sostituisce il vecchio handler movsd ; con il nostro, nuovo int 9h ; Genera l'exception, quindi ; passa il controllo alla nostra ; procedura Ring0 mov edi, ebx mov esi, offset SavedGate movsd ; Ripristina il vecchio handler movsd call ExitProcess, LARGE -1 Ring0Proc PROC mov eax, CR0 iretd Ring0Proc ENDP end Start -------------------------------- taglia qui ----------------------------------- 3. The LDT Method ----------------- Un'altra possibilit� per eseguire del Ring0-Code � quella di installare un cosiddetto callgate o nella GDT o nella LDT. Sotto Win9x � un po' pi� facile usare la LDT, dato che i primi 16 descriptors in essa sono sempre vuoti, quindi qui fornir� il codice solo per questo metodo. Un Callgate � simile ad un Interrupt Gate ed � usato per trasferire il controllo da un segmento low-privileged ad un segmento high-privileged usando una istruzione CALL. Il formato di un callgate �: --------------------------------- --------------------------------- D D D D D D 1.Offset (16-31) P P P 0 1 1 0 0 0 0 0 0 W W W W +4 L L C C C C --------------------------------- --------------------------------- 2.Segment Selector 3.Offset (0-15) 0 --------------------------------- --------------------------------- P == Present bit DPL == Descriptor Privilege Level DWC == Dword Count, numero di argomenti copiati nello stack del ring0 Quindi tutto ci� che ci resta da fare � creare un callgate, scriverlo in uno dei primi 16 descriptors, poi effettuare una far call a quel descriptor per eseguire il nostro Ring0 code. Codice di esempio: -------------------------------- taglia qui ----------------------------------- .386P LOCALS JUMPS .MODEL FLAT, STDCALL EXTRN ExitProcess : PROC .data GDTR df 0 ; Questo conterr� i contenuti del registro IDTR CallPtr dd 00h ; Siccome stiamo usando il primo descriptor (8) ed dw 0Fh ; � posizionato nell'LDT e il livello di privilegio ; � 3, il nostro selector sar� 000Fh. ; Ci� perch� i due bits low-order del selector ; sono il livello di privilegio, e il 3� bit ; � settato se il selector � nella LDT. OurGate dw 0 ; Offset low-order word dw 028h ; Segment selector dw 0EC00h ; dw 0 ; Offset high-order word .code Start: mov eax, offset Ring0Proc mov [OurGate], ax ; Mette l'offset words shr eax, 16 ; nel nostro descriptor mov [OurGate+6], ax xor eax, eax sgdt fword ptr GDTR mov ebx, dword ptr [GDTR+2] ; carica il Base Address della GDT sldt ax add ebx, eax ; L'indirizzo del descriptor LDT in ; ebx mov al, [ebx+4] ; Carica il base address mov ah, [ebx+7] ; della stessa LDT in shl eax, 16 ; eax, fa riferimento al tuo manuale mov ax, [ebx+2] ; del pmode per i dettagli add eax, 8 ; Salta il NULL Descriptor mov edi, eax mov esi, offset OurGate movsd ; Sposta il nostro callgate personale movsd ; nella LDT call fword ptr [CallPtr] ; Esegui la nostra procedura Ring0 xor eax, eax ; Pulisci la LDT sub edi, 8 stosd stosd call ExitProcess, LARGE -1 Ring0Proc PROC mov eax, CR0 retf Ring0Proc ENDP end Start -------------------------------- taglia qui ----------------------------------- Bene, popolo questo � tutto per ora. Questo metodo pu� essere facilmente adattato per usarlo con la GDT, che ti permetter� di salvare un po' di bytes nel caso tu abbia da fare una forte ottimizzazione. Ad ogni modo, usa questi metodi con cura, che NON funzioneranno su NT e non sono in generale una maniera pulita o stabile per effettuare queste operazioni. Credits & Thanks ---------------- Il metodo della IDT preso dal virus CIH & dall'esempio di Stone alla url http://www.cracking.net. Il metodo della LDT � fatto da me, ma senza l'aiuto di IceMan & The_Owl sarei ancora confuso, quindi tutti i crediti vanno a loro. ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::................................WIN32.ASSEMBLY.PROGRAMMING Win32 ASM: The Basics by Iczelion Tools necessari: -Microsoft Macro Assembler 6.1x : il supporto MASM per la programmazione Win32 comincia dalla versione 6.1. L'ultima versione � la 6.13 che � una patch alla versione pecedente alla 6.11. Il Win98 DDK include MASM 6.11d che pu� essere scaricato da Microsoft da http://www.microsoft.com/hwdev/ddk/download/win98ddk.exe Ma ti avviso, questo � mostruosamente grande, 18.5 MB. La patch per MASM 6.13 pu� anche essere scaricata da ftp://ftp.microsoft.com/softlib/mslfiles/ml613.exe -Microsoft import libraries : Puoi usare le import libraries del Visual C++. Alcune sono incluse nel Win98 DDK. -Win32 API Reference : Puoi scaricarla dal sito della Borland: ftp://ftp.borland.com/pub/delphi/techpubs/delphi2/win32.zip Ecco una breve descrizione del processo di assembly. MASM 6.1x � fornito di due tools esseziali: ml.exe e link.exe. ml.exe � l'assembler. Esso crea dal sorgente in assembly (.asm) un file object (.obj). Un file object � un file intermedio tra il codice sorgente e il file eseguibile. Esso ha bisogno dei fixups per gli indirizzi, servizio fornito dal link.exe. Link.exe trasforma un file object in eseguibile agendo su pi� piani, come aggiungere codice da altri moduli ai file object oppure fornendo i fixups per gli indirizzi, aggiungendo le risorse, ecc. Per esempio: ml skeleton.asm ---> questo crea skeleton.obj link skeleton.obj ---> questo crea skeleton.exe Le righe sopra sono, naturalmente, delle esemplificazioni. Nel mondo reale, devi aggiungere diversi switches a ml.exe e link.exe per personalizzare la tua applicazione. Inoltre ci saranno diversi files che dovrai linkare con il file object per creare la tua applicazione. I programmi per Win32 sono eseguiti in "protected mode" che e' disponibile sin dai tempi degli 80286. Ma gli 80286 ormai sono storia. Percio' dovremo relazionarci all' 80386 e i suoi discendenti. Windows esegue ogni singolo programma Win32 in uno spazio virtuale separato e unico. Cio' significa che ogni programma Win32 avra' i suoi propri 4 GB di address space. Ogni programma e' solo nel suo address space. Cio' e' in contrasto con la situazione presente in Win16. Tutti i programmi Win16 possono *vedersi* gli uni con gli altri. Questo non accade in Win32. Tale caratteristica aiuta a ridurre la probabilita' che un programma scriva sul codice/dati di un altro. Il memory model (modello di memoria, NdT) e' parimenti drasticamente diverso dai vecchi giorni del mondo a 16-bit. In Win32, non e' piu' necessario preoccuparsi del modello di memoria o dei segmenti! C'e' solo UN modello di memoria: il Flat memory model. Non esistono piu' segmenti da 64K. La memoria e' un largo e continuo spazio di 4 GB. Questo significa anche che non bisognera' piu' giocare con i segment registers. Potrete usare qualsiasi segment register per indirizzare qualsiasi punto nello spazio di memoria. Questo e' un GRANDE aiuto ai programmatori, ed e' cio' che rende la programmazione in assembly per Win32 semplice quanto quella in C. We will examine a miminal skeleton of a Win32 assembly program. We'll add more flesh to it later. Ecco lo scheletro del programma. Se non comprendete alcune parti del codice, niente panico. Spieghero' ciascuna di esse in seguito. .386 .MODEL Flat, STDCALL .DATA ...... .DATA? ...... .CONST...... .CODE : ..... end Ecco tutto! Analizziamo questo scheletro. .386 Questa e' una direttiva per l'assembler, a cui diciamo di usare il set di istruzioni 80386. Potreste anche usare .486, .586 ma la scelta piu' sicura e' quella di utilizzare sempre .386. .MODEL FLAT, STDCALL .MODEL e' una direttiva per l'assembler che specifica il modello di memoria del nostro programma. Sotto Win32, esiste un solo modello di memoria, il modello FLAT. STDCALL comunica a MASM la convenzione nel passare i parametri. Questa convenzione specifica l'ordine con cui i parametri verranno passati, da sinistra-verso-destra o da destra-verso-sinistra, oltre a chi bilanciera' lo stack frame dopo la chiamata di una call (procedura, NdT). In Win16, ci sono due tipi di convenzioni di chiamata, C e PASCAL La convenzione di chiamata C passa i parametri da destra a sinistra, cioe', il parametro all'estrema destra e' PUSHato per primo. Il caller e' responsabile del bilanciamento dello stack frame dopo la call. Ad esempio, volendo chiamare una funzione denominata foo(int primo_param, int secondo_param, int terzo_param) con la convenzione C, il codice assomiglierebbe a questo: push [terzo_param] ; Pusha il terzo parametro push [secondo_param] ; Seguito dal secondo push [primo_param] ; E dal primo call foo add sp, 12 ; Il caller bilancia lo stack frame La convenzione PASCAL e' l'inverso di quella per il C. Essa passa i parametri da sinistra a destra, e il callee e' responsabile per il bilanciamento della stack dopo la call. Win16 adotta la convenzione PASCAL poiche' produce codici piu' piccoli. la convenzione C e' utile quando non si conosce il numero dei parametri che verranno passati alla funzione, come nel caso di wsprintf(). Nel caso di wsprintf(), la funzione non ha modo di determinare aprioristicamente il numero di parametri che verrano pushati sulla stack, percio' non puo' bilanciare lo stack frame. STDCALL e' un ibrido tra le convenzioni C e PASCAL. Essa passa i parametri da destra a sinistra ma il callee e' responsabile per il bilanciamento della stack dopo la call. La piattaforma Win32 usa esclusivamente STDCALL. Eccetto in un caso: wsprintf(). Dovete usare la convenzione C con wsprintf(). .DATA .DATA? .CONST .CODE Tutte e quattro le direttive sono cio' che viene definito SEZIONE. Non avete segmenti in Win32, ricordate ? Ma potete dividere il vostro intero address space in sezioni logiche. L'inizio di una sezione definisce la fine della sezione precedente. Ci sono due gruppi di sezioni: dati e codice. Le sezioni dei dati sono divise in 3 categorie: .DATA Questa sezione contiene i dati inizializzati del vostro programma. .DATA? Questa sezione contiene i dati NON inizializzati del vostro programma. A volte capita di voler impegnare una parte di memoria (variabli, NdT) senza inizializzarla. Questa sezione serve a tale scopo. .CONST Questa sezione contiene le costanti usate dal vostro programma. Le costanti in questa sezione non potranno mai essere modificate dal vostro programma. Sono semplicemente *costanti*. Non e' necessario utilizzare tutte e tre le sezioni nel vostro programma. Dichiarate solo la/e sezione/i che volete usare. C'e' solo una sezione per il codice: .CODE. Qui e' dove le vostre istruzioni risiedono. Ad esempio: : end ...dove e' una qualsiasi etichetta arbitraria usata per specificare l'estensione del vostro programma. Entrambe le etichette devono essere identiche. Tutto il vostro codice deve risiedere tra ed end Traduttore in lingua italiana : -NeuRaL_NoiSE ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::................................WIN32.ASSEMBLY.PROGRAMMING MessageBox Display by Iczelion In questo tutorial, creeremo un programma per Windows completamente funzionante che mostra un box con il messaggio "Win32 assembly is great!". Windows prepara una grossa quantita' di risorse per i programmi Windows. Al centro di cio' c'e' la Windows API (Application Programming Interface, Interfaccia per la Programmazione di Applicazioni, NdT). La Windows API e' un'immensa collezione di utilissime funzioni contenute in Windows stesso, pronte ad essere usate da qualsiasi programma per Windows. Queste funzioni risiedono in DLL (dynamic-linked libraries, librerie collegate dinamicamente al programma, NdT) come kernel32.dll, user32.dll e gdi32.dll. Kernel32.dll contiene funzioni API relative alla memoria e alla gestione dei processi. User32.dll controlla gli aspetti dell'interfaccia utente del vostro programma.Gdi32.dll e' responsabile per le operazioni grafiche. Oltre alle "principali tre", ci sono altre DLL che il vostro programma puo' usare, ammesso che voi possediate abbastanza informazioni riguardo alla funzione API desiderata. I programmi per Windows si linkano (collegano, NdT) dinamicamente a queste DLL, in altre parole il codice per le funzioni API non e' incluso nell'eseguibile del programma per Windows. Per comunicare al vostro programma dove trovare le funzioni API desiderate al momento dell'esecuzione, dovrete accludere tale informazione nel file eseguibile. L'informazione risiede nelle import libraries (librerie importate, NdT). Dovrete linkare il vostro programma con le corrette import libraries o esso non sara' capace di localizzare le funzioni API. Esistono due tipi di funzioni API: Uno per ANSI e uno per Unicode. Il nome delle funzioni API per ANSI e' postfissato con "A", ad esempio MessageBoxA. Quelle per Unicode sono postfissate con "W" (per Wide Char, credo). Windows 95 supporta nativamente ANSI e l'Unicode Windows NT. Ma la maggior parte delle volte, utilizzerete un file di include che puo' determinare e selezionare le funzioni API appropriate per la vostra piattaforma. Semplicemente riferitevi alla funzione API senza il postfisso. Presentero' semplicemente lo scheletro del programma qui sotto. Lo riempiremo successivamente. .386 .model flat, stdcall .data .code Main: end Main Ogni programma per Windows deve chiamare una funzione API, ExitProcess, quando vuole uscire a Windows. In quest'ottica, ExitProcess e' equivalente a int 21h, ah=4Ch in DOS. Ecco il prototipo per la funzione ExitProcess da winbase.h: void WINAPI ExitProcess(UINT uExitCode); -void significa che la funzione non restituisce nessun valore al caller. -WINAPI e' un alias della convenzione di chiamata STDCALL. -UINT e un tipo di dati, "unsigned integer", che e' un valore a 32-bits sotto Win32 (e' un valore a 16-bits sotto Win16) -uExitCode e' il codice a 32-bits di ritorno a Windows. Questo valore non e' usato da Windows al momento. Per chiamare ExitProcess da un programma in assembly, dovrete prima dichiarare il function prototype (prototipo di funzione, NdT) per ExitProcess. .386 .model flat, stdcall ExitProcess PROTO ,:DWORD .data .code Main: INVOKE ExitProcess, 0 end Main Ecco tutto. Il vostro primo programma funzionante per Win32. Salvatelo come msgbox.asm. Presupponendo che ml.exe e' nella vostra path, assemblate msgbox.asm con: ml /c /coff /Cp msgbox.asm /c dice a MASM di assemblare soltanto. Non invoca Link. /coff dice a MASM di creare un file .obj in formato COFF. /Cp dice a MASM di conservare le caratteristiche di formattazione (maiuscole/minuscole) degli identificatori (variabili, NdT) dell'utente. Quindi procedete con link: link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm611\lib msgbox.obj kernel32.lib /SUBSYSTEM:WINDOWS dice a Link che tipo di eseguibile e' questo programma. /LIBPATH: dice a Link dove sono le import libraries. Sul mio PC, sono sotto c:\masm\lib Adesso avete ottenuto msgbox.exe. Andate avanti, fatelo partire. Scoprirete che non fa niente. Beh, non ci abbiamo ancora inserito niente di interessante. Ma e' senza ombra di dubbio un programma per Windows. E osservate le sue dimensioni! Sul mio PC, il file e' lungo 1,536 bytes. La linea: ExitProcess PROTO ,:DWORD e' un prototipo di funzione. Voi dichiarate il nome della funzione seguito dalla parola chiave "PROTO", una virgola, e la lista del tipo di dati dei parametri. MASM usa il prototipo di funzione per controllare il numero e il tipo di parametri della funzione. Il miglior posto per i prototipi di funzione e' un file di include. Potete creare un file di include pieno di prototipi di funzioni e strutture di dati frequentemente usati e includerlo all'inizio del vostro programma asm. Chiamate le funzioni API usando la parola chiave INVOKE: INVOKE ExitProcess, 0 INVOKE e' in pratica una specie di call specializzata. Essa controlla il numero e il tipo di parametri e li pusha sulla stack seguendo la convenzione di chiamata predefinita (in questo caso, stdcall). Usando INVOKE invece del normale CALL, potete prevenire gli errori della stack derivanti da un passaggio di parametri incorretto. Molto utile. La sintassi e': INVOKE espressione [,argomenti] dove espressione e' un'etichetta o il nome di una funzione. Successivamente, metteremo su una message box. La dichiarazione per questa funzione e': int WINAPI MessageBoxA(HWND hwnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType); -hwnd e' l'handle della parent window (finestra-genitrice, NdT :) -lpText e' un puntatore al testo che volete mostrare nella client area (l'area a disposizione della message box, NdT) -lpCaption e' un puntatore al titolo della message box -uType specifica l'icona e il numero e tipo dei bottoni della message box Sotto la piattaforma Win32, HWND, LPCSTR, e UINT sono tutti valori della dimensione di 32 bits. Modifichiamo msgbox.asm per includere la message box. .386 .model flat, stdcall ExitProcess PROTO ,:DWORD MessageBoxA PROTO ,:DWORD, :DWORD, :DWORD, :DWORD .data MsgBoxCaption db "Iczelion Tutorial No.2",0 MsgBoxText db "Win32 Assembly is Great!",0 .const NULL equ 0 MB_OK equ 0 .code Main: INVOKE MessageBoxA, NULL, ADDR MsgBoxText, ADDR MsgBoxCaption, MB_OK INVOKE ExitProcess, NULL end Main Assemblatelo cos�: Assemble it by: ml /c /coff /Cp msgbox.asm link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib msgbox kernl32.lib user32.lib Dovrete includere user32.lib nel parametro di Link, poiche' le informazioni per linkare MessageBoxA risiedono in user32.lib Vedrete una message box che mostra il testo "Win32 Assembly is Great!". Diamo un'altra occhiata al codice. Definiamo due stringhe terminate con zero (zero-terminated) nella sezione .data. Ricordate che tutte le stringhe in Windows devono essere terminate con zero (ASCIIZ). Definiamo due costanti nella sezione .const. Utilizziamo le costanti per rendere piu' chiaro il codice. Osservate i parametri della funzione MessageBoxA. Il primo parametro e' NULL. Cio' significa che non c'e' nessuna finestra che *possiede* questa message box. L'operatore "ADDR" e' usato per passare l'indirizzo dell'etichetta alla funzione. Questo operatore � specifico di MASM. Non esiste un equivalente di TASM. Funziona come l'operatore "OFFSET" ma con alcune differenze: 1. Non accetta le forward reference. Se vuoi usare "ADDR foo", devi dichiarare "foo" prima di usare l'operatore ADDR. 2. Pu� essere usato con una variabile locale. Una variabile local � una variabile creata nello stack. L'operator OFFSET non pi� essere usato in questa situazione perch� l'assembler non conosce il reale indirizzo della variabile locale quando lo assembla. Traduttore : -NeuRaL_NoiSE ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::........................THE.C.STANDARD.LIBRARY.IN.ASSEMBLY The _itoa, _ltoa and _ultoa functions by Xbios2 ATTENZIONE I: Questo documento � basato sul Borland C++ 4.02. Quando mi � stato possibile l'ho controllato con altre librerie / programmi contenenti le funzioni specifiche, ci potrebbero comunque essere delle differenze tra questa e la tua versione di C. Inoltre questo � solo codice 32-bit, Windows compiler. Niente DOS o UNIX.] ATTENZIONE II: I confronti di grandezza sono davvero facili da compiere. I confronti di velocit� un po' meno. Le differenze di velocit� da me rilevate sono basate sui timings RDTSC, ma NON prendono in considerazione casi estremi. E' questo il motivo per cui non fornisco il numero di cicli di clock esatti. Naturalmente se hai bisogno dei cicli di clock esatti per il tuo Pentium II, puoi sempre comprarmene uno :) Il linguaggio C offre 3 funzioni per convertire un integer in ASCII: char *itoa(int value, char *string, int radix); char *ltoa(long value, char *string, int radix); char *ultoa(unsigned long value, char *string, int radix); _itoa e _ltoa fanno _esattamente_ la stessa cosa. Questo perch� un integer _�_ un long codice 32-bit. Per� sono diversi: _itoa ha del codice _completamente_ inutile in s� (nel 16bit questo codice il valore sign-extend se radix=10). Comunque il risultato � sempre lo stesso, quindi _ltoa da qui in poi significa sia _ltoa che _itoa. _ultoa esattamente uguale a _ltoa e _itoa, tranne quando radix=10 e il valore < 0. In ogni modo tutte queste funzioni fanno riferimento a questa: ___longtoa(value, *string, radix, signed, char10) I primi tre parametri sono passati 'cos� come sono', signed � settato ad 1 da _ltoa se radix=10, altrimenti � settato a 0 e char10 � il carattere corrispondente a 10 se radix>10, ed � sempre settato 'a' (___longtoa � anche utilizzato da printf, che ha un'opzione per ottenere i caratteri maiuscoli in Hex). ___longtoa esegue le seguenti (e lo fa con codice scritto male): 1. Controlla che 2<=radix<=36, se non lo � , restituisce '0' 2. Se signed=1 e value<0 aggiunge '-' alla stringa e fa 'neg' sul valore 3. Loop1: crea una pseudo-string nello stack, invertita 4. Loop2: converte e copia la pseudo-string nella string Il controllo su radix � necessario perch�: radix=0 genererebbe un INT0 (divisione per zero) radix=1 metterebbe l'applicazione in un loop infinito, distruggendo lo stack radix=37 per valore=36 restituirebbe '}', il carattere dopo 'z' I due loops sono necessari in ragione della maniera in cui la conversione � svolta. (vedi il codice dopo). Per implementare una conversione a loop-unico, il numero di caratteri dovrebbe essere calcolato in anticipo, con il risultato di un codice meno efficiente (il numero dei caratteri nel valore � n=(int)(log(value)/log(radix))+1, ma usare un loop in pi� � molto pi� veloce). Includendo il listato disassembly delle funzioni di C allungherebbe di molto l'articolo, e in ogni caso sono quelli solo esempi di codice davvero brutto. Quindi, dritti al risultato: ltoa proc cmp dword ptr [esp+0Ch], 10 sete ch mov cl, 'a'-'0'-10 jmp short longtoa ultoa: mov cx, 'a'-'0'-10 longtoa: push ebx push edi push esi sub esp, 24h mov ebx, [esp+3Ch] ; radix mov eax, [esp+34h] ; valore mov edi, [esp+38h] ; stringa cmp ebx, 2 jl short _ret cmp ebx, 36 jg short _ret or eax, eax jge short skip cmp byte ptr ch, 0 ; _ltoa ? jz short skip mov byte ptr [edi], '-' inc edi neg eax skip: mov esi, esp loop1: xor edx, edx div ebx mov [esi], dl inc esi or eax, eax jnz loop1 loop2: dec esi mov al, [esi] cmp al, 10 jl short nochar add al, cl nochar: add al, '0' stosb cmp esi, esp jg short loop2 _ret: mov byte ptr [edi], 0 mov eax, [esp+38h] add esp, 24h pop esi pop edi pop ebx ret ltoa endp C'� un 3 in 1 procedura. ltoa e ultoa prendono gli stessi parametri come le funzioni standard di C. longtoa era stato cambiato per prendere dallo stack gli stessi parametri di ltoa e ultoa, mentre signed e char10 sono passati attraverso CH e CL rispettivamente. In questo modo ltoa e ultoa 'vedono' longtoa come 'proprio' codice, e non come una procedura diversa (ci� per evitare un problema comune in C, le procedure che 'inoltrano' i loro parametri ad un'altra funzione). Questo codice si compila in 102 bytes (e potrebbe essere ottimizzato per 'grattare' altri byte), quando invece il codice standard di C impiega 270 bytes. Precisamente: function C size Asm size ------------------------------ itoa 60 0 ltoa 40 12 ultoa 27 4 longtoa 143 86 ------ ------ total 270 102 Va pure 2 volte pi� veloce di ltoa. E inoltre, questa � una versione completamente C-compatibile di ltoa e ultoa. Naturalmente potrebbe essere adattata da C-compatibile in altro per venire incontro a necessit� specifiche (p.e renderla stdcall invece di cdecl, oppure se la velocit� e la dimensione sono cruciali si pu� rimuovere il controllo per radix, e cos� via...) Ad ogni modo, � abbastanza anomalo che non userai mai valori di radix che differiscano da 2, 8, 10 o 16. Quindi se velocit� e dimensione sono l'essenza, pu� essere scritta una routine migliore e pi� specifica. Per esempio, considera questa routine che deposita il valore di EAX come un numero binario all'indirizzo specificato da EDI: ultob proc mov ecx, 32 more1: shl eax, 1 dec ecx jc more2 jnl more1 more2: setc dl add dl, '0' shl eax, 1 mov [edi], dl inc edi dec ecx jnl more2 mov [edi], al ret ultob endp Questa � 14 volte pi� veloce di ltoa in C, e 7 volte pi� veloce di ltoa in Asm, ed � di soli 29 bytes. Ma questo articolo � gi� abbastanza lungo, quindi aspetta per un altro articolo su funzioni 'ltoa' specifiche (chi lo sa, forse potrei decidere di scrivere una funzione 'printf' in Asm, che potrebbe usarle...). ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::............................................THE.UNIX.WORLD x86 ASM Programming for Linux by mammon_ Essenzialmente questo articolo � una scusa per conciliare i miei due interessi favoriti di coding: il sistema operativo Linux e la programmazione in linguaggio assembly. Tutti e due gli argomenti non necessitano (meglio, non dovrebbero) di una introduzione; come l'assembly Win32, assembly per Linux � eseguito in protected mode 32-bit... comunque ha il netto vantaggio di permetterti di chiamare le funzioni delle librerie standard C come ogni altra funzione delle normali librerie Linux "condivise". Ho cominciato con una breve introduzione sulla compilazione dei programmi in assembly language per Linux; per una migliore leggibilit� potresti bypassarla e andare direttamente alla sezione su "Le Basi". Compiling e Linking --------------------- I due assemblers principali per Linux sono Nasm, l'Assembler (gratis) di Netwide, e GAS, l'Assembler (pure gratis) di Gnu, integrato in GCC. Mi concentrer� su Nasm in questo articolo, lasciando GAS per un altro giorno dal momento che usa la sintassi AT&T e ci� richiederebbe una introduzione pi� prolissa. Nasm dovrebbe essere azionato con l'opzione di formato ELF ("nasm -f elf hello.asm"); l'object che ne deriva � poi linkato con GCC ("gcc hello.o") per creare il binario ELF finale. Lo script seguente pu� essere usato per compilare moduli ASM; l'ho scritto in modo che sia molto semplice, quindi tutto ci� che fa � prendere il primo filename passatogli (io consiglio di chiamarlo con una estensione ".asm"), lo compila con nasm, e lo linka con gcc. #!/bin/sh # assemble.sh ========================================================= outfile=${1%%.*} tempfile=asmtemp.o nasm -o $tempfile -f elf $1 gcc $tempfile -o $outfile rm $tempfile -f #EOF ================================================================== Le Basi ---------- La cosa migliore per partire, naturalmente, � un esempio, prima di immergerci nei dettagli dell'OS. Ecco qui un programma "hello-world" davvero semplice: ; asmhello.asm ======================================================== global main extern printf section .data msg db "Helloooooo, nurse!",0Dh,0Ah,0 section .text main: push dword msg call printf pop eax ret ; EOF ================================================================= Una spiegazione veloce: il "global main" deve essere dichiarato global�-e dal momento che stiamo usando il linker GCC, l'entrypoint deve essere chamato "main"--per il loader dell'OS. L'"extern printf" � semplicemente una dichiarazione per la call successiva nel programma; nota che questo � tutto il necessario; non � necessario dichiarare le dimensioni dei parametri. Ho diviso questo esempio nelle sezioni standard .data e .text, sebbene ci� non sia strettamente necessario �-chiunque potrebbe svignarsela con il solo segmento .text, proprio come in DOS. Nel corpo del codice, nota che devi pushare i parametri alla call, e in Nasm devi dichiarare la dimensione di tutti i dati ambigui (p.e. non-register): di qui il qualificatore "dword". Nota che come in altri assemblatori, Nasm assume che ogni reference memory/label � volta a significare l'indirizzo della locazione di memoria o della label, non il loro contenuto. Perci�, per specificare l'indirizzo della stringa 'msg' scriveresti 'push dword msg', mentre per specificare il contenuto della stringa 'msg' scriveresti 'push dword [msg]' (nota che questo conterr� solo i primi 4 bytes di 'msg'). Dal momento che printf richiede un pointer alla string, specificheremo l'indirizzo di 'msg'. La call a printf � abbastanza lineare. Considera che pulire lo stack dopo ogni call che esegui (vedi sotto); quindi, avendo PUSHato una dword, POPpiamo una dword dallo stack in un registro "da cestinare". I programmi Linux si chiudono semplicemente con una RET all'OS, dato che ogni processo � aperto dalla shell (o PID 1 ;) e finisce restituendogli il controllo. Nota che in Linux fai uso delle librerie standard condivise fornite con l'OS in luogo di una "API" di degli Interrupt Services. Tutte le reference esterne saranno risolte dal linker GCC, in modo da alleggerire buona parte del carico di lavoro del programmatore asm. Una volta che ti sei abituato alle stranezze di base, il coding in assembler in Linux � davvero pi� semplice di quello su una macchina DOS-based! La sintassi di chiamata C -------------------- Linux usa la convenzione di chiamata C -� ci� significa che gli argomenti sono pushati nello stack in ordine inverso (l'ultimo arg per primo), e che il caller deve pulire lo stack. Puoi far ci� o poppando i valori dallo stack: push dword szText call puts pop ecx o modificando direttamente ESP: push dword szText call puts add esp, 4 I valori restituiti dalla call si trovano in eax o in edx:eax se il valore � pi� grande di 32-bit. EBP, ESI, EDI, e EBX sono tutti salvati e ripristinati dal caller. Nota che devi conservare tutti i registri che usi come illustra il codice seguente: ; loop.asm ================================================================= global main extern printf section .text msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0 main: mov ecx, 0Ah push dword msg looper: call printf loop looper pop eax ret ; EOF ====================================================================== A primo acchito questo sembra molto semplice: dal momento che stai per usare la stringa nelle call 10 printf(), non hai bisogno di ripulire lo stack. Tuttavia quando lo compili, il loop non si ferma mai. Perch�? Perch� da qualche parte nella call printf()ECX � usato e non salvato. Quindi per far funzionare il tuo loop a dovere, devi salvare il valore del contatore in ECX prima della call e ripristinarlo dopo, cos�: ; loop.asm ================================================================ global main extern printf section .text msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0 main: mov ecx, 0Ah looper: push ecx ;salva Count push dword msg call printf pop eax ;pulisce lo stack pop ecx ;ripristina Count loop looper ret ; EOF ====================================================================== Programmazione della Porta I/O -------------------------------- E per avere un accesso diretto all'hardware? In Linux hai bisogno di un driver kernel-mode per fare ogni cosa che sia davvero ingegnosa... ci� significa che il tuo programma finir� per essere di due parti, una kernel-mode che fornisce le funzionalit� direct-hardware, l'altra user-mode per una interface. La buona notizia � che puoi ancora accedere alle porta usando i comandi IN/OUT da un programma user-mode. L'accesso alle porte I/O al tuo programma deve essere concesso da un permesso dell'OS; per far ci�, devi compiere una call ioperm(). Questa funzione pu� essere chiamata solo da un utente root, quindi devi o setuid() il programma come root oppure eseguire il programma da root. La ioperm() ha la sintassi seguente: ioperm( long StartingPort#, long #Ports, BOOL ToggleOn-Off) dove 'StartingPort#' specifica il numero della prima porta da accedere (0 is port 0h, 40h is port 40h, etc), '#Ports' specifica quante porte accedere (i.e., 'StartingPort# = 30h' e '#Ports = 10' concederebbero l'accesso alle porte 30h-39h), e 'ToggleOn-Off' consente l'accesso se TRUE (1) o lo disabilita se FALSE (0). Una volta che la call a ioperm() � compiuta, si pu� accedere alle porte richieste come normal. Il programma pu� chiamare ioperm() un qualsivoglia numero di volte e non ha bisogno di fare un successiva call ioperm() (anche se l'esempio sotto lo fa) [siccome l'OS si curer� di ci�]. ; io.asm ==================================================================== BITS 32 GLOBAL szHello GLOBAL main EXTERN printf EXTERN ioperm SECTION .data szText1 db 'Enabling I/O Port Access',0Ah,0Dh,0 szText2 db 'Disabling I/O Port Acess',0Ah,0Dh,0 szDone db 'Done!',0Ah,0Dh,0 szError db 'Error in ioperm() call!',0Ah,0Dh,0 szEqual db 'Output/Input bytes are equal.',0Ah,0Dh,0 szChange db 'Output/Input bytes changed.',0Ah,0Dh,0 SECTION .text main: push dword szText1 call printf pop ecx enable_IO: push word 1 ; enable mode push dword 04h ; 4 porte push dword 40h ; inizia dalla porta 40 call ioperm ; Deve essere SUID "root" per questa call! add ESP, 10 ; pulisci lo stack (metodo 1) cmp eax, 0 ; controlla i risultati di ioperm() jne Error ;---------------------------------------Port Programming Part-------------- SetControl: mov al, 96 ; R/W low byte di Counter2, mode 3 out 43h, al ; porta 43h = control register WritePort: mov bl, 0EEh ; valore da inviare allo speaker timer mov al, bl out 42h, al ; porta 42h = speaker timer ReadPort: in al, 42h cmp al, bl ; il byte dovrebbe essere cambiato--questo E' un timer :) jne ByteChanged BytesEqual: push dword szEqual call printf pop ecx jmp disable_IO ByteChanged: push dword szChange call printf pop ecx ;---------------------------------------End Port Programming Part---------- disable_IO: push dword szText2 call printf pop ecx push word 0 ; disable mode push dword 04h ; 4 porte push dword 40h ; parte dalla porta 40h call ioperm pop ecx ;pulisci lo stack (metodo 2) pop ecx pop cx cmp eax, 0 ; controlla i risultati di ioperm() jne Error jmp Exit Error: push dword szError call printf pop ecx Exit: ret ; EOF ====================================================================== Usare gli Interrupts In Linux ------------------------- Linux � un ambiente shared-library in protected mode, il che significa che non ci sono i servizi interrupt. Giusto? Sbagliato. Ho notato una call a INT 80 sul codice di alcuni esempi GAS con il commento "sys_write(ebx, ecx, edx)". Questa funzione � parte della syscall dell'interfaccia di Linux, e cio� l'interrupt 80 deve essere un gate ai servizi di syscall. Girovagando nel codice sorgente di Linux (e ignorando gli avvisi di NON USARE MAI l'interface INT 80 siccome i numeri della funzione potrebbere essere cambiati all'improvviso), ho trovato i "system call numbers" �-che indicano la funzione da passare a INT 80 per ogni routine di syscall�- nel file UNISTD.H. Ce ne sono 189, quindi non li elencher� qui... ma se ti accingi a programmare in Linux assembly, fa' un favore a te stesso e stampa questo file. Quando chiami INT 80h, eax deve contenere il numero della funzione desirata. Tutti i parametri alla routine syscall devono trovarsi nei seguenti registri in questo ordine: ebx, ecx, edx, esi, edi quindi il parametro uno si trova in ebx, il parametro 2 in ecx, ecc. Nota non si usa lo stack per passare i valori alla routine syscall. Il risultato della call sar� restituito in eax. Inoltre, l'interfaccia INT 80 � uguale ad una normale call (solo un po' pi� divertente ;). Il programma seguente dimostra una semplice call a INT 80h in cui il programma controlla e visualizza la sua PID. Nota l'uso del formato della stringa di printf() �-� meglio psuedocodarlo come una call C prima, poi rendere il formato della stringa DB e pushare ogni variabile passata (%s, %d, ecc). La struttura C per questa call sarebbe printf( "%d\n", curr_PID); Nota anche che le sequenze di escape ("\n") non sono tutte davvero attendibili in assembly; ho dovuto usare i valori hex (0Ah,0Dh) per il CR\LF. ;pid.asm==================================================================== BITS 32 GLOBAL main EXTERN printf SECTION .data szText1 db 'Getting Current Process ID...',0Ah,0Dh,0 szDone db 'Done!',0Ah,0Dh,0 szError db 'Error in int 80!',0Ah,0Dh,0 szOutput db '%d',0Ah,0Dh,0 ;la strana formattazione � per printf() SECTION .text main: push dword szText1 ;messaggio di apertura call printf pop ecx GetPID: mov eax, dword 20 ; getpid() syscall int 80h ; syscall INT cmp eax, 0 ; non sar� mai PID 0 ! :) jb Error push eax ; passa il valore restituito a printf push dword szOutput ; passa il formato della stringa a printf call printf pop ecx ; pulisci lo stack pop ecx push dword szDone ; messaggio di chiusura call printf pop ecx jmp Exit Error: push dword szError call printf pop ecx Exit: ret ; EOF ===================================================================== Ultime considerazioni ----------------------- Il pi� dei problemi deriver� dall'abituarsi a Nasm stesso. Mentre nasm non � fornito di una man page, non la installa per default, quindi devi spostarla (cp or mv) da /usr/local/bin/nasm-0.97/nasm.man in /usr/local/man/man1/nasm.man La formattazione � un po' incasinata, ma � facilmente risistemata usando le direttive nroff. Non ti d� ancora tutta la documentazione di Nasm, comunque; per questo, copia nasmdoc.txt da /usr/local/bin/nasm-0.97/doc/nasmdoc.txt in /usr/local/man/man1/nasmdoc.man Ora puoi chiamare man page di nasm con 'man nasm' e la documentazione di nasm con 'man nasmdoc'. Per ulteriori informazioni, controlla i seguenti: Linux Assembly Language HOWTO Linux I/O Port Programming Mini-HOWTO Jan's Linux & Assembler HomePage (bewoner.dma.be/JanW/eng.html) Devo anche dei ringraziamenti a Jeff Weeks alla code^x software (gameprog.com/codex) per avermi inviato un paio di hello-world di GAS nelle giornate nere, prima che trovassi la pagina di Jan. ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::...........................................ISSUE.CHALLENGE 11-byte Program Displays Its Command-Line by Xbios2 La Sfida ----------- Scrivi un programma di 11 byte che visualizzi la sua command line. La Soluzione -------------- Prima di dire che questi programmi non funzionano, provali. Alcuni di loro funzionano solo dopo averli avviati due volte. In ogni caso, sono stati testati sia sotto Windows che in DOS puro, e funzionano. Che tu ci creda o no, questi sono i primi programmi che ho scritto in DOS, quindi ho solo provato alcune idee finch� alcune hanno funzionato, anche se ho pensato che non potessero... :) La command line in DOS si trova nel PSP (Program Segment Prefix, Prefisso di Segmento del Programma, NdT) che nei file .COM occupa i primi 100h bytes nel segmento. All'offset 80h, una stringa (il primo byte � la lunghezza della stringa, e n bytes seguono) contiene tutto ci� che � stato digitato dopo il nome del file. L'ultimo carattere nella stringa � CR (carriage return, invio NdT). I programmi richiesti dovrebbero essere composti di tre parti: 1. settaggio dei pointers ai dati 2. visualizzazione dei dati 3. uscita In effetti tutti i programmi seguenti NON includono la parte 3, ma continua a leggere. I dati (command line) possono essere scritti o come una singola stringa, o carattere per carattere. APPROCCIO 1: Scrivi una singola stringa ------------------------------------------ Per il primo approccio ci sono 2 interrupts: 1. INT 21, 9 ; scrivi una stringa string '$ terminated' 2. INT 21, 40 ; scrivi sul file usando un handle Nel primo caso, la parte 2 sarebbe: mov ah, 9 mov dx, 81h int 21h che sono 7 bytes, lasciando solo 4 bytes per sostituire l'ultimo CR con un '$', che sono troppo pochi. (Effettivamente, se l'utente digitasse un $ come ultimo carattere nella comand line, questo sarebbe il programma pi� piccolo possibile.) Il programma pi� piccolo che son riuscito a scrivere �: shr si,1 ; D1 EE lodsb ; AC push si ; 56 add si,ax ; 03 F0 mov byte ptr [si],'$' ; C6 04 24 xcgh bp,ax ; 95 pop dx ; 5A int 21 ; CD 21 Per il secondo caso, il pi� piccolo programma sarebbe questo: ; Solution I mov dx, 81h ; BA 81 00 mov cl, ds:[80h] ; 8A 0E 80 00 mov ah, 40h ; B4 40 int 21h ; CD 21 Le prime due righe sono la parte 1 (settaggio dei pointers) e le altre due sono la parte 2 (visualizzazione della stringa). Se pensi che manca qualcosa, hai ragione: non settiamo BX (l'handle). APPROCCIO 2: Scrivi char per char ------------------------------ Per il secondo approccio ci sono interrupts: 1. INT 21, 2 ; scrivi il char in dl 2. INT 29 ; scrivi il char in al Naturalmente il secondo interrupt � meglio, dal momento che non c'� bisogno di caricare ah con il valore di una funzione. In pi�, INT 29 legge il char da AL, quindi pu� essere usato con LODSB. Il primo modo per implementare questo approccio � ridurre la parte 2 (display loop). Un programma che fa ci� � il seguente: ; Solution II mov si, 80h ; BE 80 00 lodsb ; AC mov cl, al ; 8A C8 more: lodsb ; AC int 29h ; CD 29 loop more ; E2 FB Questo programma ha scritto CX caratteri. Il secondo modo per scrivere la stringa � scrivere fino al CR. Ecco come: ; Solution III mov si, 81h ; BE 81 00 more: lodsb ; AC int 29h ; CD 29 cmp al, 13 ; 3C 0D jne more ; 75 F9 nop ; 90 Si, l'ultima istruzione E' un NOP. Quindi abbiamo un programma di 11-byte che funziona, e ha anche un NOP in s�. Rimuovendo il NOP si crea un programma ancora pi� pazzo di 10 bytes, che visualizza la sua command line E aspetta la pressione di un tasto prima di terminare... In realt� la soluzione II, sostituendo MOV SI,80h con SHR SI,1, fa la stessa cosa (10 bytes che visualizza la command line e aspetta che l'utente prema un tasto). BTW: Davvero non so perch� questi programmi funzionano, sebbene abbia una o due teorie... La sfida per il prossimo numero --------------------------------- Scrivi un programma PE (win32) il pi� piccolo possibile che visualizzi la sua command line. ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::........................................................FIN ############################################# Per questa traduzione (e per le altre) dell'APJ mi � stata lasciata l'autorizzazione personale di +mammon Colgo l'occasione per salutare tutto il crew di RingZ3r0 e di #crack-it , particolari ringraziamenti a Neural_Noise per avermi concesso le sue traduzioni dei tutorials di Iczelion (disponibili nelle pagine di ringzer0) Little-John #############################################