Winamp 5.05 Code injection

Data

by bender0

 

31/10/2004

UIC's Home Page

Published by Quequero

avvertenza: è un essay. può provocare conoscenza.


Ben di nuovo complimenti, il code injection e' sempre un argomento MOLTO interessante e molto utile, grazie del tute.

non somministrare agli haX0rz di età inferiore ai 1337 anni.

....

Home page: http://bender0.altervista.org
E-mail: [email protected]
bender0 su irc.azzurra.org, #crack-it, #cryptorev, #asm, #c, #slackware

....

Difficoltà

( )Leim ( )NewBies ( )Intermedio (x)Avanzato ( )Master ( )Leet

 
 
 

Introduzione

in un essay precedente ho parlato di winamp e della sua protezione; qui mi limito a dire che sono passato alla 5.05, pi� sicura (tanto usavo solo la modern skin ;) e, speriamo, definitiva. nota bene: se chiudete un'occhio sugli offset qualsiasi versione 5.xx andr� benone.

� caldamente consigliato (vedi: obbligatorio) leggere l'essay di faina su AVI/MPEG/ASF/WMV Splitter.

Tools usati

un compilatore e una buona api reference
ollydbg o un debugger qualunque

URL o FTP del programma

www dot winamp dot com

Notizie sul programma

winamp, sempre quello.

Essay

ho scelto winamp come target di questo essay perch� � diffuso e perch� ne ho gi� parlato. in effetti questo essay non spiega come superare un certo tipo di protezione, ma come applicare una tecnica generica; quindi il target dell'essay avrebbe potuto essere un qualsiasi eseguibile.

sappiamo gi� che winamp non � packato, quindi abbiamo campo libero per sperimentare senza troppi problemi; nonostante questo c'� da dire che la code injection � particolarmente d'aiuto contro i packers.

cosa vuol dire code injection, al di l� della traduzione letterale? dicesi ;) code injection l'inserimento di codice (malvagio e/o benigno) nella memoria di un processo, in modo da alterare la sua normale esecuzione senza tuttavia modificare un solo byte del modulo associato al processo. imho � una tecnica molto elegante, senza contare i vantaggi che offre: il modulo eseguibile rimane intatto, quindi si eludono i crc sulla memoria fisica e con qualche accorgimento anche quelli sulla memoria del processo; se il file � packato si pu� agire direttamente sul codice gi� unpackato in memoria; si pu� inserire del codice, eseguirlo e ripulire tutto a totale insaputa del processo target; si possono fare insomma molte cose simpatiche soffrendo di meno vincoli.

non mi soffermer� qui a parlare del debugging in generale; ne ho gi� parlato nell'essay su CloneDVD. andiamo pi� nello specifico.


l'essay � diviso in quattro sezioni:
phase#1 :: simple injection
phase#2 :: thread injection
phase#3 :: triggered injection
phase#4 :: api hijacking




~simple injection, #1

la forma pi� banale di code injection che mi possa immaginare. carichiamo il target con CREATE_SUSPENDED, patchiamo qua e l� ed eseguiamo. questo metodo fallisce se � presente un check di integrit� della memoria, certo anche quello si pu� patchare, ma cosa succede se il crc � in mezzo a un packer ingarbugliatissimo sotto layers e layers di crittazione? possiamo dichiararci TFU (totally fucked up). ma vediamo come si realizza una memory patch del genere.

potremmo patchare qualsiasi cosa, ma voglio solo modificare una stringa in modo che sul balancing al posto di "Balance: Center" mostri "Balance: Bender" (lol). quindi dobbiamo solo applicare una patch di quattro byte (dalla 'B' alla 'd'), niente di pi� facile. una piccola ricerca in memoria della stringa originale ci rivela che i byte da modificare si trovano a 459461. ecco il sorgente:
 
--- phase1.cpp start ---
#include <windows.h>
#include <stdio.h>

char filename[] = "winamp.exe";
char data[] = "Bend";

void main()
{
    STARTUPINFO sI;
    PROCESS_INFORMATION pI;
    ZeroMemory( &sI, sizeof(sI) );
    sI.cb = sizeof(sI);
    ZeroMemory( &pI, sizeof(pI) );

    // creiamo il processo con CREATE_SUSPENDED
    // per fare le nostre modifiche
    CreateProcess(
        filename,
        NULL,NULL,NULL,
        false,
        DEBUG_ONLY_THIS_PROCESS|CREATE_SUSPENDED,
        NULL,NULL,
        &sI,&pI);

    HANDLE hProc = pI.hProcess;
    HANDLE hMainT = pI.hThread;
   
    // applichiamo la patch
    DWORD junk;
    WriteProcessMemory(hProc,(LPVOID)0x459461,(LPVOID)&data,4,&junk);

    // ripristiniamo l'esecuzione
    ResumeThread(hMainT);

    // ci separiamo da winamp
    DebugActiveProcessStop(pI.dwProcessId);
}
--- phase1.cpp end ---
compilo, avvio... azz non funziona. le stringhe della modern skin sono contenute in essa, quindi cosa abbiamo modificato? le stringhe della classic skin. infatti con quella funziona :)



~thread injection, #2

tanto per contraddirmi, ora lavoriamo sulla protezione. per i dettagli sulla protezione vi rimando (ancora) all'essay su winamp 5.04. la routine di check comincia a 444633, breakkiamo qui e ritorniamo; ci troviamo a 444619, dove possiamo vedere che la flag globale che controlla la registrazione � all'indirizzo 46A754. l'idea (stupida in s�, ma valida come esempio) � di creare un nostro thread all'interno del processo target, che setti a 3 (perch� 3? perch� no? :) quella flag ogni secondo. per controllare che il tutto funzioni possiamo attacharci con il debugger e breakkare sul nostro thread, o pi� semplicemente togliere il valore regkey dal registro (HKLM/Software/Nullsoft/Winamp) e vedere come winamp creda lo stesso di essere registrato.

per prima cosa troviamo uno spazio libero per aggiungere il codice del nostro thread: all'indirizzo 452D00 abbiamo spazio a volont�. allora a quell'indirizzo assembliamo:

mov eax,46A754
mov dword ptr ds:[eax],3
push 3E8
call Sleep
jmp 452D00

e vediamo che l'assembler genera questi byte, che poi scriveremo nella memoria di winamp:
0xB8,0x54,0xA7,0x46,0x00,0xC7,0x00,0x03,0x00,0x00,0x00,0x68,
0xE8,0x03,0x00,0x00,0xE8,0xD1,0xEE,0x9E,0x77,0xEB,0xE9

una volta scritti questi byte dobbiamo creare un thread remoto, ovvero residente nel processo target. per far questo abbiamo a disposizione l'api CreateRemoteThread; ecco sintassi e parametri:

HANDLE CreateRemoteThread(
  HANDLE hProcess,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  SIZE_T dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID lpParameter,
  DWORD dwCreationFlags,
  LPDWORD lpThreadId
);

hProcess
[in] handle del processo target.
lpThreadAttributes
[in] descrittore di sicurezza, un NULL andr� pi� che bene :).
dwStackSize
[in] size iniziale dello stack del thread, specifichiamo 0 come default.
lpStartAddress
[in] puntatore all'inizio del thread nella memoria del processo target.
lpParameter
[in] puntatore a un parametro da passare al thread, se non ci serve passiamo NULL.
dwCreationFlags
[in] flags di creazione. possiamo passare CREATE_SUSPENDED se vogliamo un thread fermo o 0 per avviarlo.
lpThreadId
[out] puntatore a una dword che riceve l'id del thread. se non ci serve passiamo NULL.

dopo di ci� come al solito ci separiamo da winamp e ci terminiamo. ecco il sorgente:

--- phase2.cpp start ---
void main()
{
    STARTUPINFO sI;
    PROCESS_INFORMATION pI;
    ZeroMemory( &sI, sizeof(sI) );
    sI.cb = sizeof(sI);
    ZeroMemory( &pI, sizeof(pI) );

    // creiamo il processo con CREATE_SUSPENDED
    // per fare le nostre modifiche
    CreateProcess(
        filename,
        NULL,NULL,NULL,
        false,
        DEBUG_ONLY_THIS_PROCESS|CREATE_SUSPENDED,
        NULL,NULL,
        &sI,&pI);

    HANDLE hProc = pI.hProcess;
    HANDLE hMainT = pI.hThread;
   
    // inseriamo il codice del nostro thread
    DWORD junk;
    WriteProcessMemory(hProc,(LPVOID)0x452d00,(LPVOID)&data,sizeof(data),&junk);

    // creaiamo il nostro thread
    CreateRemoteThread(hProc,NULL,0,(LPTHREAD_START_ROUTINE)0x452d00,NULL,0,NULL);

    // ripristiniamo l'esecuzione
    ResumeThread(hMainT);

    // ci separiamo da winamp
    DebugActiveProcessStop(pI.dwProcessId);
}
--- phase2.cpp end ---

et voil� ;)



~triggered injection, #3

per triggered injection intendo code injection "on request"; ovvero code injection applicata in risposta a degli eventi precisi. ad esempio possiamo applicare la nostra memory patch appena un target packato si � unpackato in memoria, o rispondere a un nostro breakpoint per fare una piccola modifica e ripristinare il codice originale appena dopo la sua esecuzione.

in questo caso, per rispettare il carattere "teorico" dell'essay, faremo un'altra cosa inutile ma esplicativa. proviamo a fare una modifica stealth: il target arriva al punto da patchare, noi interveniamo, il target esegue la patch, noi ripuliamo tutto.

ci mettiamo dunque a traceare a partire dall'entry point. l� troviamo le solite istruzioni di avvio, quindi steppiamo fino alla call a 41EFB0. qui dentro c'� qualcosa di strano: ci sono delle call e dei loop che sembrano fare della crittografia, ma perch�? boh, steppiamoli tutti. quando saremo a 41F0D2 vedremo che all'indirizzo 45A4B0 viene spostata una stringa, la seguente:
http://genghis.winamp.com/~christophe/egg.mp3
uhm... bene, a quell'indirizzo strambo sotto winamp.com troviamo un mp3 di nome egg. wheee! abbiamo scoperto un uovo di pasqua :)
se vi interessa nell'mp3 troverete 3:13 minuti di drum and bass contornati da urla di piacere erotico (da qui il titolo "Porn Egg").

ma lasciamo perdere le uova di pasqua (� ottobre dopotutto) e torniamo a steppare. poche linee pi� avanti viene chiamata la wsprintfA, che printa la stringa "Winamp 5.05": in seguito verr� usata (almeno) come nome nella taskbar (o come tooltip dell'icona del tray, se usate questa opzione). ora supponiamo di voler cambiare la stringa: questa volta patchiamo il codice. dobbiamo quindi patchare le istruzioni che caricano la stringa per constringere winamp a caricare la nostra. vediamo come viene caricata:
 
0041F0D8 MOV EAX,DWORD PTR DS:[46B504] -> carica l'indirizzo di "Winamp" + 10h
[...]
0041F0EF SUB EAX,10 -> toglie i 10h
0041F0F2 PUSH ECX -> pusha "5.05"
0041F0F3 MOV DWORD PTR DS:[461AF0],EDX
0041F0F9 MOV EDX,DWORD PTR DS:[45A7FC]
0041F0FF PUSH EAX -> pusha "Winamp"
0041F100 PUSH winamp.004590F4 -> pusha la stringa di controllo formattazione, "%s %s"
0041F105 PUSH winamp.0046B820 -> questo � il buffer che conterr� la stringa formattata
0041F10A MOV DWORD PTR DS:[46B504],EAX
0041F10F MOV DWORD PTR DS:[461AF8],EDX
0041F115 CALL DWORD PTR DS:[<&USER32.wsprintfA>]
sar� quindi sufficiente patchare all'indirizzo 41F0D8, assemblando un
MOV EAX,indirizzo_della_nostra_stringa+10h.

riassumendo: fermiamo winamp all'entry point e scriviamo la nostra stringa in un'area vuota di memoria (452d00, come prima); settiamo un breakpoint a 41F0D8, dove poi andremo a patchare; quando scatta il breakpoint patchiamo; settiamo un breakpoint a 41F0DD; quando scatta il secondo breakpoint rimettiamo a posto il codice, facciamo ripartire winamp e ci separiamo.

ecco il sorgente (se non capite qualcosa, LEGGETE IL TUTE DI FAINA):

 
--- phase3.cpp start ---
#include <windows.h>
#include <stdio.h>

char filename[] = "winamp.exe";
unsigned char patchdata[] = {0xB8,0x10,0x2D,0x45,0x00,0xCC}; // MOV EAX,winamp.00452D10; int 3
unsigned char appname[] = "BENDamp";

bool BpWait(DWORD addr, DEBUG_EVENT &DebugEv)
{
bool searching = true;
    DWORD dwContinueStatus = DBG_CONTINUE;

while (searching)
{
if (!WaitForDebugEvent(&DebugEv,INFINITE)) return false;

if (DebugEv.dwDebugEventCode == EXCEPTION_DEBUG_EVENT)
{
if (DebugEv.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT)
dwContinueStatus = DBG_CONTINUE;
            else dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
}

if (DebugEv.u.Exception.ExceptionRecord.ExceptionAddress == (void*)addr)
searching = false;
else
ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);
}
    return true;
}

void main()
{
    STARTUPINFO sI;
    PROCESS_INFORMATION pI;
    ZeroMemory( &sI, sizeof(sI) );
    sI.cb = sizeof(sI);
    ZeroMemory( &pI, sizeof(pI) );

    // creiamo il processo con CREATE_SUSPENDED
    // per fare le nostre modifiche
    CreateProcess(
        filename,
        NULL,NULL,NULL,
        false,
        DEBUG_ONLY_THIS_PROCESS|CREATE_SUSPENDED,
        NULL,NULL,
        &sI,&pI);

    HANDLE hProc = pI.hProcess;
    HANDLE hMainT = pI.hThread;
    
    DEBUG_EVENT DebugEv;

    // inseriamo la nostra stringa nella memoria del processo
    DWORD junk;
    WriteProcessMemory(hProc,(LPVOID)0x452d00,(LPVOID)&appname,sizeof(appname),&junk);

    // piazziamo il breakpoint e ripristiniamo l'esecuzione
    BYTE bp = 0xCC;
    WriteProcessMemory(hProc,(LPVOID)0x41F0D8,(LPVOID)&bp,1,&junk);
    ResumeThread(hMainT);

    // aspettiamo il breakpoint
    if (!BpWait(0x41F0D8,DebugEv)) exit (0);

    // patchiamo, riallineiamo eip e piazziamo il secondo breakpoint (la patch sovrascrive anche il bp)
    WriteProcessMemory(hProc,(LPVOID)0x41F0D8,(LPVOID)&patchdata,sizeof(patchdata),&junk);
    CONTEXT tc;
    tc.ContextFlags = CONTEXT_ALL;
    GetThreadContext(hMainT,&tc);
    tc.Eip--;
    SetThreadContext(hMainT,&tc);

    ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, DBG_CONTINUE);
    ResumeThread(hMainT);

    // al secondo breakpoint risistemiamo tutto
    if (!BpWait(0x41F0DD,DebugEv)) exit (0);

    BYTE oldbyte = 0x8B;
    WriteProcessMemory(hProc,(LPVOID)0x41F0DD,(LPVOID)&oldbyte,1,&junk);
    tc.ContextFlags = CONTEXT_ALL;
    GetThreadContext(hMainT,&tc);
    tc.Eip--;
    SetThreadContext(hMainT,&tc);

    ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, DBG_CONTINUE);
    ResumeThread(hMainT);

    // ci separiamo da winamp
    DebugActiveProcessStop(pI.dwProcessId);
}
--- phase3.cpp end ---
bene, la patch funziona, se chiudiamo un occhio sul fatto che winamp perde (temporaneamente) tutta la configurazione :) infatti lanciando la nostra phase3 partir� con la skin di default e ci far� strane richieste di invio di feedback.

~api hijacking, #4

api hijacking, dirottare le api? chevvord�? semplicemente intercettare le chiamate alle api ponendoci nel mezzo, come un layer tra il target e il sistema, con tutta la libert� di agire sui parametri da passare ultimamente alla vera api, neutralizzare la chiamata, o eseguire del nostro codice al posto di quello della api. e a che serve? mettiamo il caso che il target chiami GetLocalTime per sapere che giorno � e si basi su questa informazione per decidere se il trial � scaduto... capito no? :)

in questo esempio scriviamo un logger: l'output del nostro programma conterr� tutte le chiamate a RegCreateKeyA, con tanto di parametri.

in che modo possiamo intervenire ogni volta che viene chiamata RegCreateKeyA? ma con un breakpoint ovviamente :) � opportuno fare uno schemino per capire bene come funziona il giochetto:

1. al system breakpoint leggiamo dalla IAT l'indirizzo al quale RegCreateKeyA � mappata nell'address space di winamp.
2. sempre al system bp scriviamo un int 3 sul primo byte di RegCreateKeyA, e stiamo ad aspettare che scatti il breakpoint...

3. quando scatta il bp su RegCreateKeyA scriviamo il byte originale al posto dell'int 3 e risistemiamo eip,
4. poi settiamo la trap flag che scatter� alla seconda istruzione di RegCreatekeyA;
5. ora leggiamo e stampiamo a schermo i parametri della funzione;
*. qui volendo potremmo modificare a nostro piacimento i parametri ;)
6. infine aspettiamo che scatti la trap (punto 7).

7. quando scatta la trap riscriviamo l'int 3 alla prima istruzione di RegCreateKeyA,
8. poi rimuoviamo la trap flag,
9. e aspettiamo che scatti il breakpoint (punto 3)

ecco la sintassi di RegCreateKey:

LONG RegCreateKey(
  HKEY hKey,
  LPCTSTR lpSubKey,
  PHKEY phkResult
);

hKey
[in] handle di una chiave aperta (un numero di identificazione) o una delle chiavi predefinite: HKEY_CLASSES_ROOT, HKEY_CURRENT_CONFIG, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE o HKEY_USERS.
lpSubKey
[in] stringa che contiene il nome di una sottochiave di hKey.
phkResult
[out] puntatore a una variabile che ricever� l'handle della chiave che stiamo aprendo.

bene, traduciamo in c++ tutto quello che abbiamo detto :)
 
--- phase4.cpp start ---
#include <windows.h>
#include <stdio.h>

char filename[] = "winamp.exe";
char remote[300];
BYTE bp = 0xCC;        // int 3
BYTE ob;               // il byte che rimpiazzeremo con l'int 3
BYTE chr;

void main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory( &si, sizeof(si) );
    si.cb = sizeof(si);
    ZeroMemory( &pi, sizeof(pi) );

    CreateProcess(
        filename,
        NULL,NULL,NULL,
        false,
        DEBUG_PROCESS,
        NULL,NULL,
        &si,&pi);

    HANDLE hProc = pi.hProcess;
    HANDLE hMainT = pi.hThread;

    DEBUG_EVENT DebugEv;
    DWORD dwContinueStatus;
    int bpcounter = 0;
    int rc = 0;

    DWORD junk;
    DWORD regcreatekey = 0;

    DWORD code;
    void* addr;

    DWORD temp;

    CONTEXT tc;

    for(;;)
    {
        dwContinueStatus = DBG_CONTINUE;
        WaitForDebugEvent(&DebugEv, INFINITE);

        code = DebugEv.u.Exception.ExceptionRecord.ExceptionCode;
        addr = DebugEv.u.Exception.ExceptionRecord.ExceptionAddress;

        switch (DebugEv.dwDebugEventCode)
        {
            case EXCEPTION_DEBUG_EVENT:
            {
                if (code == EXCEPTION_BREAKPOINT)
                {
                    if (bpcounter == 0) // system breakpoint
                    {
                        printf("system breakpoint hit (%u) at [%.8X]\n",bpcounter,addr);

                        // leggiamo l'indirizzo di RegCreateKey
                        ReadProcessMemory(hProc,(void*)0x453000,&regcreatekey,4,&junk);
                        printf("RegCreateKeyA's address: 0x%.8X\n\n",regcreatekey);

                        // salviamo il byte e settiamo il breakpoint su RegCreateKey
                        ReadProcessMemory(hProc,(void*)regcreatekey,&ob,1,&junk);
                        WriteProcessMemory(hProc,(void*)regcreatekey,&bp,1,&junk);

                    }
                    else if (addr == (void*)regcreatekey) // breakpoint su RegCreateKey
                    {
                        // riscriviamo il byte originale
                        WriteProcessMemory(hProc,(void*)regcreatekey,&ob,1,&junk);

                        // riallineiamo eip e attiviamo la trap flag
                        tc.ContextFlags = CONTEXT_ALL;
                        GetThreadContext(hMainT,&tc);
                        tc.EFlags |= 0x100;
                        tc.Eip--;

                        printf("*** breakpoint on RegCreateKey, realigned and trap flag set\n");

                        // leggiamo i parametri di RegCreateKey
                        // esp+4 = hKey
                        // esp+8 = lpSubKey
                        // esp+C = phkResult

                        // leggiamo hKey
                        ReadProcessMemory(hProc,(void*)(tc.Esp+4),&temp,4,&junk);

                        // se � un valore noto stampiamo il nome, altrimenti il numero
                        switch (temp)
                        {
                            case HKEY_CLASSES_ROOT:
                                printf("* hKey: HKEY_CLASSES_ROOT\n");
                                break;
                            case HKEY_CURRENT_CONFIG:
                                printf("* hKey: HKEY_CURRENT_CONFIG\n");
                                break;
                            case HKEY_CURRENT_USER:
                                printf("* hKey: HKEY_CURRENT_USER\n");
                                break;
                            case HKEY_LOCAL_MACHINE:
                                printf("* hKey: HKEY_LOCAL_MACHINE\n");
                                break;
                            case HKEY_USERS:
                                printf("* hKey: HKEY_USERS\n");
                                break;
                            default:
                                printf("* hKey: %.8X\n",temp);
                        }

                        // leggiamo lpSubKey
                        ReadProcessMemory(hProc,(void*)(tc.Esp+8),&temp,4,&junk);

                        // printiamo l'indirizzo della stringa
                        printf("* lpSubKey: %.8X -> ",temp);

                        // leggiamo la stringa e stampiamola
                        for (rc = 0; rc<300; rc++)
                        {
                            ReadProcessMemory(hProc,(void*)(temp+rc),&chr,1,&junk);
                            if (chr == 0)
                            {
                                printf("\n");
                                break;
                            }
                            printf("%c",chr);
                        }

                        // leggiamo phkResult e printiamolo
                        ReadProcessMemory(hProc,(void*)(tc.Esp+12),&temp,4,&junk);
                        printf("* phkResult: %.8X\n",temp);

                        // settiamo le modifiche al context
                        SetThreadContext(hMainT,&tc);
                    }
                    bpcounter++;
                }

                if (code == EXCEPTION_SINGLE_STEP) // trap flag
                {
                    // ripiazziamo il breakpoint su RegCreateKey
                    WriteProcessMemory(hProc,(void*)regcreatekey,&bp,1,&junk);

                    // rimuoviamo la trap flag
                    tc.ContextFlags = CONTEXT_ALL;
                    GetThreadContext(hMainT,&tc);
                    tc.EFlags &= 0xFFFFFEFF;
                    SetThreadContext(hMainT,&tc);

                    printf("*** trap, breakpoint set\n\n");
                }
            break;
            }

            case EXIT_PROCESS_DEBUG_EVENT:
            ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);
            return;
            break;
        }
        ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);
    }
}
--- phase4.cpp end ---
vediamo i risultati: durante l'avvio di winamp RegCreateKey viene chiamata una sola volta. ma se andiamo nelle "Preferences"(ctrl+P)->"General Preferences", clickiamo su "File Types" e poi su un'altra voce, vedremo molti accessi a HKEY_CLASSES_ROOT con cui winamp controlla le associazioni delle diverse estensioni.

Note finali

un grazie a faina (come non ci conosciamo?!? :) che ha scritto un gran tute sull'argomento che mi ha invogliato a completare il mio :)
e poi grazie Qu� che questa idea delle boxes per il codice � una gran trovata :)
ovviamente per chiarimenti, migliorie o correzioni mandatemi una mail.
ci vediamo al prossimo essay... bye.

bender0

Disclaimer

Vorrei ricordare che il software va comprato e  non rubato, dovete registrare il vostro prodotto dopo il periodo di valutazione. Non mi ritengo responsabile per eventuali danni causati al vostro computer determinati dall'uso improprio di questo tutorial. Questo documento � stato scritto per invogliare il consumatore a registrare legalmente i propri programmi, e non a fargli fare uso dei tantissimi file crack presenti in rete, infatti tale documento aiuta a comprendere lo sforzo che ogni sviluppatore ha dovuto portare avanti per fornire ai rispettivi consumatori i migliori prodotti possibili.

Reversiamo al solo scopo informativo e per migliorare la nostra conoscenza del linguaggio Assembly.