__ ___ __ _ __ ___ __ _ | _/ _ \_ | _ __ __| | __ _| _/ _ \_ |_ _ __ _ __| |_ __ __ _ | | | | | || '_ \ / _` |/ _` | | | | | | | | |/ _` |/ _` | '__/ _` | | | |_| | || | | | (_| | (_| | | |_| | | |_| | (_| | (_| | | | (_| | | |\___/| ||_| |_|\__,_|\__,_| |\__\_\ |\__,_|\__,_|\__,_|_| \__,_| |__| |__| |__| |__| .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [OQ20031122 0X0A] :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: http://www.ondaquadra.org - articoli@ondaquadra.org :: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: "Tutto nel ciberspazio e' scandito dalla squarewave dei micro-processori Il clock dei micro e' come un battito cardiaco elettronico..." .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [OQ20031122 0X0A] :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. :: 0x00 [L0GiN] LA JiHAD [oq~staff] :: 0x01 [PAGiNAZER0] CRED0 [Pupi] :: 0x02 [CRYPT0] RSA REVERSiNG [Evilcray] :: 0x03 [NETW0RKiNG] NEL R0UTER CiSC0 [Cartesio] :: 0x04 [C0DiNG] 0PENSSL E RSA C0DiNG [binduck] :: 0x05 [C0DiNG] DiAL0G0 S0PRA i DUE MASSiMi FiLESYSTEM [eazy] :: 0x06 [SECURITY] SQL-iNJECTi0N [Master^Shadow] :: 0x07 [SECURITY] SAM CRACKiNG [h23] :: 0x08 [APPRENDiSTA STREG0NE] C0DiCE iNVERS0: CRiTT0GRAFiA [Zer0] :: DiGiTALE AVANZATA PARTE 7 :: 0x09 [ViSi0Ni] ESTASi Di UN BLiTTER iMPAZZiT0 [Arkanoid] :: 0x0A [SHUTD0WN] EL0Gi0 DELLA P0VERTA' [Tritemius] [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: 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 [0X0A] OQ20031122[0A] :: [0x00][L0GiN] LA JiHAD [OQ] [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. Sono molte le battaglie che vedono impegnati gli hacker su tutti i fronti. Intanto ci sono le minacce provenienti dalle celebri leggi americane ed europee (DMCA, ECMA) sul copyright digitale; minacce messe in atto da potenti organizzazioni (vedi la RIAA) con le loro iniziative aggressive. Per non dimenticare i problemi europei sul brevetto, le iniziative della BSA, il caso SCO/Linux. Lo sviluppo della situazione internazionale, precipitata drammaticamente l' 11 settembre 2001 ed evoluta verso guerre di cui vediamo l'esito tutti i giorni, ha fornito ad alcuni l' alibi per auspicare un giro di vite sul controllo e la riduzione dei diritti civili e della privacy, tutto ciò spinto dall' onda emotiva scaturita dalla reale minaccia terroristica. E in questo scenario globale si va ad inserire la nostra situazione, la realtà italiana; una realtà in evoluzione verso lo sviluppo di una societa' multirazziale, multiculturale e multireligiosa. Evoluzione (o trasformazione) che inevitabilmente avviene tra tensioni, polemiche, drammi umanitari. L' hacking da sempre si pone come la "religione laica" della libertà dell' informazione, l' apertura mentale in senso generale, l' apertura verso culture diverse; l' hacking ha anche sempre sostenuto e promosso l' utilizzo della tecnolgia per migliorare la vita di tutti, senza mai dimenticare l' importanza della cretività, anzi dandole un ruolo centrale. In questo panorama anche Ondaquadra deve evolvere ed adattarsi a questo nuovo mondo; si trasforma ritornando alle origini, alla semplicità. Ondaquadra diventa più agile e leggera apre anche ai grandi temi della nostra epoca. E allora in segno di apertura verso le culture, ci appropriamo di un termine così mal compreso in Occidente, spesso erroneamente tradotto come "guerra santa". Proclamiamo la nostra "Jihad", una battaglia fatta di idee, sogni e fantasia; una battaglia per la difesa del nostro modo di vedere la realtà. Una battaglia condotta in "clandestinita'", condotta lontano dai riflettori. Esercitiamo la nostra volontà di potenza come scomparsa. Andiamo nella Foresta e combattiamo come fantasmi, invisibili come il Vento. La Jihad della libertà digitale è proclamata. Jihad che combatteremo fino all' ultimo bit... :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [0X0A] OQ20031122[0A] :: [0x01][PAGINAZER0]CRED0 [Pupi] [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. Credo negli amici, quelli veri, quelli che si caramellano le palle per sei ore ascoltando tutti i tuoi sfoghi e non se ne vanno perchè sanno che tu hai bisogno di sfogarti e concludono con un "Non ci pensare, la vita va avanti" e tu vorresti ammazzarli e non ti rendi conto che in fondo hanno ragione... Credo nell'amore, l'amore cieco, l'amore contro cui vai a sbattere la testa mille e mille volte e nonostante tutte le sofferenze sei sempre pronto a ripartire, prendere la rincorsa e tirare ancora un'altra testata in quel muro che è, l'amore... Credo nei momenti solitari di cui ogni uomo ha bisogno, durante i quali non riesci a lasciarti in pace e mille pensieri torturano la tua mente fino a farti impazzire, ti assale la malinconia e finisci sempre col ritrovarti in lacrime per qualcosa che non hai ben capito cos'è... Credo nelle cazzate che si fanno solo quando si è ragazzi, e ti dici "Tanto non mi possono fare niente, sono minorenne, quando divento grande poi smetto" e una volta grande continui a fare cazzate perchè, nonostante tutto, continui a credere in tutto questo... Credo che ognuno dovrebbe lottare per ciò in cui crede e credo che per quanto possa apparire solitaria e senza speranza, ogni battaglia, se motivata da veri ideali e non da futili affari, merita di essere combattuta... Credo che i miliardi di dollari spesi in armamenti per assoggettare il mondo al proprio volere non valgano neppure mezza di tutte le vite finite nei grandi mattatoi delle guerre... Credo che il mondo non abbia colori e che chiunque, in ogni parte del globo, possa essere considerato un proprio fratello... Credo che il luogo di nascita sia una mera e stupida casualità, in quanto credo di essere, prima di tutto, figlio del mondo... Credo in chi si sforza davvero per rendere migliore questo mondo e credo che ognuno di noi dovrebbe fare quel poco che nel proprio piccolo possiamo fare, nella speranza che un giorno qualcuno possa davvero godere dei nostri sforzi uniti... Credo nella razza umana, nonostante abbia mostrato negli anni quanto di marcio nasconda fra le sue piaghe, e credo che valga la pena morire sapendo che questo mio gesto servirà a far vivere chissà chi, chissà dove, chissà quando... Credo nella vita, e anche davanti alla morte pianterò alberi in modo che la signora delle tenebre non abbia mai la meglio sulla nostra razza... Credo nelle canzoni che si trovano proprio su quel cd che non doveva essere nello stereo in quel momento perchè ti fanno pensare ancora di più a cose a cui vorresti non pensare e sono proprio quelle canzoni che un giorno ti strapperanno un sorriso quando le riascolterai e penserai a ciò che fu... Credo nella ragazza che hai amato per tutta la vita e con cui sei stato, ti sei lasciato, sei tornato, ti sei lasciato di nuovo e via dicendo e adesso è sposata con lo stupido di turno e quando ti vede sorride con dolore e abbassa gli occhi e ti rendi conto che hai lasciato un segno indelebile nel cuore di chi non ti ha voluto... Credo che sia inutile battersi per tenersi stretto qualcuno che non desidera altro che uscire per sempre dalla tua vita e credo fermamente nel motto "Chi non mi vuole, non mi merita" che suona tanto come una cazzata ma che aiuta a buttare giù, talvolta, bocconi altrimenti troppo amari da digerire... Credo nelle frasi fatte, perchè se sono fatte e mantengono vivo il loro significato vuol dire che sotto sotto nascondono un fondo di verità che sta ad ognuno di noi trovare... Credo nelle cose dette e non dette, che non significano niente, ma che lasciano alla tua fantasia l'interpretazione, così che tu possa lasciar volare i pensieri, anche solo per un attimo, sulle ali dei sogni... Credo nei sogni, credo in chi ce la mette tutta per ottenere onestamente ciò che vuole e credo che realizzare i propri sogni valga molto più di migliaia e migliaia di euro... Credo in chi non si arrende, in chi ha battuto centinaia di volte il culo a terra ed ogni volta, impassibile, si è rialzato ed è ripartito, pronto a combattere ancora... Credo in quei film poco famosi che solo tu ed altri due o tre vedono e che sembrano descrivere in tutto e per tutto la storia della tua vita e allora ti senti per un attimo un pò protagonista di uno strano "Truman show"... Credo che la vita sia una e che non ci sia granché al di fuori di essa e per questo credo che sia stupido sprecarla per rimpiangere in quei pochi giorni guadagnati di non averli persi godendo della propria gioventù... Credo in cose forse più grandi di me e in cui, qualcuno potrebbe dirmi, è stupido credere, ma credo che sia proprio credendo in queste cose che riesco a dare un significato e un motivo alla mia esistenza... Ma soprattutto credo che sia inutile piangere l'estinzione degli "Hacker" e starsene lì come scemi a rimproverarsi di non essersi dati abbastanza da fare, di non essersi mossi in tempo... Credo che ognuno di noi nasconda in sè una personalità molto vicina a quella del vero hacker, chi più, chi meno, e credo che sarebbe quasi arrivata l'ora di darsi una svegliata... Credo che il futuro sia in mano a gente come noi, a gente normale, che lavora, si impegna, cerca di migliorarsi, di capire, ride e piange quando serve, cerca di assaporare la propria vita per quella che è, senza chiedere di più, ha dei veri sentimenti, non ha lasciato che il denaro e il frenetico tam-tam dell'era moderna appiattissero il proprio Io, ma anzi conserva una propria inestimabile, imprevedibile, inarrestabile ed irriproducibile personalità... Credo in gente che crede, nonostante tutto, in qualcosa... Credo in tutti voi, chiunque siate, ovunque siate e per qualsiasi cosa vi stiate battendo... Credo... ###PUPI### :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [0X0A] OQ20031122[0A] :: [0x02][CRYPT0] RSA REVERSiNG [Evilcry] [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. -Hiew -SoftICE o qualunque altro debugger -RSA Tool 2 -Calcolatrice -Qualche tool per il decripting (tipo powmod) URL o FTP del programma www.google.it Notizie sul programma Lockless 3 CM (se volete fare pratica) Essay RSA overview Da qualche tempo a questa parte, ho iniziato ad interessarmi di RSA, sia perchè è interessante da studiare come sistema di crittografia, sia perchè mi è capitato in più di una protezione, ed inizialmente non sapevo nemmeno che cosa fosse!. L' Rsa è un cifrario a chiave pubblica nato al M.I.T. nel 1978, ad opera di Ron Rivest, Adi Shamir e Les Andleman (RSA deriva infatti dal cognome di questi 3 ricercatori). L' Rsa è un crittosistema asimmetrico che si basa su alcune proprietà particolari che hanno i numeri primi. Questo sistema ha trovato un vasto campo di utilizzo, algoritmi come El-Gamal (anche El-Gamal è un sistema asimmetrico e quindi abbastanza sicuro) sono diventati più rari, dato il loro massiccio utilizzo in passato. Vediamo adesso un' ipotetica sessione di crypting: nella quasi totalità dei casi il messaggio viene trasformato in Int cioè in numero intero, e poi si tratta soprattutto di un discorso matematico. Il primo passo per criptare un messaggio è : n=p*q N come si può notare dall' equazione ( meglio relazione matematica), è il prodotto tra p e q che sono 2 numeri primi. Con la dovuta cautela si può affermare che più piccoli sono P e Q è minore è la sicurezza, quindi è preferibile usare un modulo N abbastanza grande. Adesso va calcolato Ô(n). Per far ciò si usa: Ô(n)=(p-1)*(q-1) Questa è un' implementazione o più precisamente estensione del teorema di Eulero. In pratica Ô(n), è un numero 'RELATIVAMENTE' primo a (p-1)*(q-1). Ma a questo punto qualcuno potrebbe chiedersi, che significa "relativamente primo": un numero è relativamente primo ad un altro quando questi due numeri non hanno fattori in comune, tranne ovviamente 1. Ad esempio 8 e 21 non sono numeri primi, però 8 e 21 saranno relativamente primi ad un altro numero cioè 1, dato che l' unico fattore comune a 8 e 21 è 1. Precisazione (i fattori di 8 sono 1,2,4,8, quelli di 21 sono 1,3,7,21). Spero di aver chiarito un pò questo concetto. {{{ Tnx fly to CyberLaw}}}. Un altro passo e siamo apposto. Molto semplicemente 'E' si ricava da: e =Ô(n) E rappresenta la chiave (o meglio l' Esponente pubblico) con cui sarà criptato il nostro messaggio. Perciò E dev' essere un numero primo. La chiave di decrittazione d si ottiene attraverso: d = e^(-1) mod ((p-1)*(q-1)) ------->Perciò dobbiamo conoscere 'e', 'p' e 'q' Ok, adesso sappiamo più o meno come avviene una sessione Rsa, come avrete certamente notato durante quest' operazione ci sono dei valori che conosciamo e altri che vanno scoperti (nel caso di un attacco). Possiamo crearci quindi una tabella che in seguto ci potrebbe essere d' aiuto. P e Q Sono numeri primi Incogniti N N è il prodotto di p*q Conosciuto Ô(n) Ô(n)=(p-1)*(q-1) Incognito e Chiave di cripto Incognita d Chiave di decripto Conosciuta A questa tabella si potrebbe aggiungere x (messaggio in chiaro) ed y (messaggio criptato). Adesso siamo arrivati al cuore dell' algoritmo cioè alla strafamosa: C=M^e mod n Dove M è il messaggio da criptare e C è il messaggio criptato. Cercate di ricordarla dato che è il nucleo dell' algoritmo. Spero che sappiate ricavarivi la formula inversa in modo da ottenere il messaggio originale. La formula è la seguente: M=C^d mod n Siamo alla fine della nostra panoramica sul Rsa, come avrete sicuramente notato si tratta di un discorso principalmente matematico, ho cercato di sintetizzare al massimo il discorso, vi ho risparmiato tutti i retroscena (teorema di Eulero, Fermat) o si sistemi di fattorizzazione tipo il Metodo della Curva Ellittica (buon metodo per numeri piccoli, se però siamo di fronte a numeri grandi, risulta molto lento), poi ci sono: Pollard's Monte Carlo Algorithm, Continued Fraction Algorithm, Trial Division (questo è il più vecchio metodo di fattorizzazione che conosco). In questo periodo non c'è neanche bisogno di sbattersi molto con calcoli, dato che ci sono tanti programmi (specialmente dopo MIRACL) che fanno questo per noi. Pratical RSA In questa sezione iniziamo a fare pratica con il programma RsaTool2 (dei The Egoiste TMG) che ci sarà molto utile per il reversing. Per prima cosa, dopo aver aperto il programma settate il Numer Base) a 10 (cioè lavoriamo in base decimale) , e verifichiamo la veridicità della formula: n=p*q Nel riquadro "Modulus N", inserite 47 e premete "Factor N", questo dovrebbe dirvi che è un numero primo, stessa cosa con 53. Adesso otteniamo N=2491 da una semplice moltiplicazione (47*53), facendo finta di non conoscere P e Q, mettiamo 2491 in "Modulus N", e vediamo cosa si ottiene se lo fattorizziamo. Il programma ritorna esattamente in valori P e Q. Se andiamo un attimo a vedere la formula: d = e^(-1) mod ((p-1)*(q-1)) vediamo che possiamo ricavarci anche la chiave di decripto D, avendo però P e Q, mettiamo i valori P e Q nei loro relativi fields, e premiamo "Calc D", adesso sappiamo anche la chiave di decriptazione. Avendo il messaggio criptato, adesso possiamo decrittarlo. Per far questo basta applicare la formula: M=(C^d) mod n Possiamo usare la stessa calcolatrice di windows per decriptare numeri relativamente piccoli, o meglio costruirci un programma che faccia questo per noi. La fattorizzazione (addendum) Questa piccola sezione, non è di fondamentale importanza per capire l' algoritmo, ma potrebbe aiutarvi a capire quali sono le problematiche più importanti di un attacco a quest' ultimo. Come spero abbiate capito, per riuscire a violare un codice protetto con Rsa dobbiamo FATTORIZZARE il modulo N. Fin quando si tratta di fattorizzare numeri piccoli non ci sono problemi, ma i problemi sorgono quando si hanno davanti moduli N molto grossi, in questo caso vanno usati algoritmi di tipo probabilistico. Gli algoritmi per la fattorizzazione si dividono in due categorie: -Algoritmi deterministici. -Algoritmi di Montecarlo (si basano su una scelta semi-casuale dei dati di partenza). Più precisamente gli algoritmi deterministici sono a loro volta divisi in tre categorie: -Algoritmi che restituiscono ad ogni tentativo riuscito un divisore proprio (sia o non sia un numero primo) -Algoritmi che per ogni tentativo riuscito restituiscono SEMPRE un divisore primo. -Algoritmi che ad ogni tentativo riuscito restituiscono un divisore comune al numero dato. L' algoritmo o teorema di Euclide, è quello che viene più usato per gli attacchi. La formula comunemente utilizzata è: x=(x.a)(x.b)/(x.a.b) mod N Sia N=777 se si divide per 3 otteniamo 259 (che è >32), 259 si può dividere per 7 e si ottiene 37 (che è < 72). I divisori primi di 259 perciò sono: 3, 7, 37. Provate a fattorizzarlo con RsaTool2 ed otterrete gli stessi fattori. Questo genere di algoritmi deterministici possono essere buoni (per buoni si intende la rapidità di risoluzione) per numeri piccoli, per risolvere numeri molto grandi vengono invece usati gli algoritmi di Montecarlo, cioè di tipo probabilistico. {{Guardare le referenze a fine art.}} Reversing corner Ora che bene o male dovremmo aver acquistato una certa familiarità con questo algoritmo, iniziamo a vedere qualche esempio pratico. Iniziamo con un crackme abbastanza semplice ed utile. Io ho scelto il lockless 3CM, reperibile sul sito dei lockless. Per prima cosa mettiamo due bpx a getdlgitemtexta ed a getwindowtexta. Dopo che sice ha poppato dovremmo essere qui: Reference To: USER32.GetDlgItemTextA 004137FE Call dword ptr [00428910] 00413804 jmp 00413819 ;Salta sotto Referenced by a (U)nconditional or (C)onditional Jump at Address: 00413819 pop ebp 0041381A ret 000C Dopo la chiamata a getdlgitemtexta e dopo il ret ci troveremo qui: 004029B0 push FFFFFFFF 004029B2 push 00419E18 004029B7 mov eax, dword ptr fs:[00000000] 004029BD push eax 004029BE mov dword ptr fs:[00000000], esp 004029C5 sub esp, 00000650 004029CB push esi 004029CC push edi Possible StringData Ref from Data Obj ->"9901" ; Questo è un valore molto importante, 004029CD push 004200DC 004029D2 lea ecx, dword ptr [esp+000000E4] 004029D9 call 00401130 ;In questa call il numero 9901 viene "copiato" in un' altra locazione Possible StringData Ref from Data Obj ->"12790891"; Anche questo è importantissimo 004029DE push 004200D0 004029E3 lea ecx, dword ptr [esp+1C] 004029E7 mov dword ptr [esp+00000664], 00000000 004029F2 call 00401130 ;Chiamata alla solita call, che ora trasferisce 12790891 Possible StringData Ref from Data Obj ->"8483678" ; Altro valore di grande importanza 004029F7 push 004200C8 004029FC lea ecx, dword ptr [esp+00000274] 00402A03 mov byte ptr [esp+00000664], 01 00402A0B call 00401130 ;Idem come sopra Possible StringData Ref from Data Obj ->"5666933" ;Ultimo valore ma non di meno importante 00402A10 push 004200C0 00402A15 lea ecx, dword ptr [esp+000001AC] 00402A1C mov byte ptr [esp+00000664], 02 00402A24 call 00401130 ;viene richiamata la solita call 00402A29 mov edx, dword ptr [esp+00000668] ;Indirizzo relativo al serial 00402A30 or esi, FFFFFFFF ; 00402A33 mov edi, edx ;| Metodo molto comune usato per calcolare la lunghezza di una stringa 00402A35 mov ecx, esi ;| in questo caso del nostro serial 00402A37 xor eax, eax ;| 00402A39 mov byte ptr [esp+00000660], 03 ;| 00402A41 repnz ;| 00402A42 scasb ;| 00402A43 not ecx ;| 00402A45 dec ecx ;| 00402A46 cmp ecx, 0000000E ;se il serial è diverso da Eh (cioè 14 chars) 00402A49 jne 00402BB2 ;Salta alla beggar off, sennò continua 00402A4F xor ecx, ecx 00402A51 mov al, byte ptr [ecx+edx] ;char relativo al serial 00402A54 cmp al, 30 00402A56 jl 00402BB2 ;Salta alla beggar off, se è più piccolo di 30, cioè 0 00402A5C cmp al, 39 00402A5E jg 00402BB2 ;Salta alla beggar off, se è più grande di 39 cioè 9 00402A64 inc ecx ;Incrementa il contatore 00402A65 cmp ecx, 0000000E 00402A68 jl 00402A51 ;Se è più piccolo di Eh, vai al prossimo ciclo Adesso soffermiamoci per a fare qualche considerazione. All' inizio viene preso il serial, invece che il name, e questo dovrebbe farci un pò pensare, perchè se quello che viene manipolato è il serial, allora dobbiamo prepararci all' analisi dell' algoritmo.In questo caso fortunatamente sappiamo per certo che si tratta di rsa, e la cosa ci avvantaggia molto. Poi notiamo anche la presenza di quattro numeri fissi, che ci fa pensare (intuitivamente) a E ed N. Vi ricordo la formula: C=M^e mod n Inoltre a 00402a51 che inizia una routine che controlla se il seral inserito sia un numero, se non lo è salta ad errore. Da questo ciclo capiamo anche che bisogna lavorare (operazioni di fattorizzazione e varie) in base 10. Arrivati a questo punto, è meglio se tralasciamo l' analisi della routine che segue, poichè è abbastanza lunga ed ingarbugliata. Cercherò quaindi di riassumervi un pò cosa succede: -Il serial dev' essere di 14 cifre, e poi durante l' algoritmo questo numero verrà "spezzato" in due parti uguali composte da 7 cifre. Ad ognuno di questi "pezzi" di serial saranno applicate una seria di operazioni, che coinvolgono anche in numeri visti sopra (9901 e 12790891). Alla fine, le due parti di serial vengono riunite e poi avviene il check finale, che ci dirà se il serial è giusto o sbagliato. Dalle operazioni che vengono fatte, possiamo capire che 12790891 rappresenta il modulo N e che 9901 E. Adesso ci tocca riuscire a decriptare le due parti di serial. Seguendo la routine, si può notare inoltre che i valori: 5666933 e 8483678, sono le due parti di serial corretto, ma purtroppo sono criptati. Inoltre se avete notato entrambi i valori sono di 7 cifre ciascuno il che dovrebbe insospettirci. Quindi arrivati a questo punto, facciamoci uno schema della situazione e vediamo cosa possiamo fare: -Conosciamo il seriale corretto ma criptato, cioè conosciamo il parametro C. -Conosciamo il modulo N con cui sono stati criptati i due valori. -Conosciamo E (Con E e con P e Q si può trovare il parametro D). Abbiamo tutto per un corretto decripting, ma dirvi soltanto quale bottoni premere non sarebbe utile a nulla. Quindi cerchiamo di arrivare alla soluzione: per poter decriptare i due spezzoni di serial dobbiamo avere oltre al messaggio criptato (C) , il modulo N ed D. Se vi ricordate N è dato da P*Q che sono due numeri primi, quindi fattorizzandolo dovremmo riuscire a trovare P e Q (2 numeri in questo caso relativamente piccoli). Aprite RsaTool2, ed impostatelo a base 10, dato che come abbiamo precendente visto i valori sono tutti in base 10. Scrivete nell' edit box "Modulus N" e fattorizzatelo "Factor N", il programma vi dirà che questo numero è primo, questo significa che non può essere il modulo N (che dovrebbe essere composto dal prodotto di due numeri primi), però dato che è primo può essere il nostro Esponete Pubblico E. Ora proviamo a fattorizzare 12790891, e da cui otteniamo P (1667) e Q( 7673).. A questo punto dato che abbiamo P, Q e l' esponete pubblico E, possiamo calcolarci la chaive di decripto D, che dipende dalla seguente formula: d = e^(-1) mod ((p-1)*(q-1)) RsaTool2 ci permette anche di calcolarci D, ricordatevi di mettere 9901 in Public Exp, e poi premete "Calc D". Il valore di D sarà 10961333, adesso abbiamo tutti gli elementi per poter applicare la formula: M=C^d mod n Non sognatevi nemmeno di usare la calcolatrice di windows, esistono dei programmi apposta per poter decriptare un numero. Vi consiglio di non effettuare il decripting su un computer lento, ci vorrrebbe molto tempo. Questi sono i valori da usare per il decrypt: M1=(8483678^10961333) mod 12790891 M2=(5666933^10961333) mod 12790891 Da cui si ottiene: M1=7167622 M2=3196885 Queste sono le due parti di serial decriptatato, se ricordate, l' algoritmo richiedeva un serial di 14 cifre e successivamente veniva diviso in due, M1 ed M2 rappresentano i 2 pezzi di serial. Quindi per ottenere il seriale corretto M1M2, vanno uniti. Il valore così ottenuto sarà 71676223196885. Riconoscere l' Rsa. Riconoscere l' Rsa negli schemi di protezione, non è molto semplice, poichè può presentarsi sotto numerose forme, o peggio può essere "riadattato". Ma ho pensato di elencarvi in modo molto generico le principali caratteristiche che *dovrebbe* avere: -Viene preso il seriale e viene criptato, solitamente con la formula C=(M^E) mod N, (M non è altro che il serial). -Di conseguenza dovremo avere 2 valori fissi, che rappresentano il modulo N e l' esponente pubblico E. -Poi dovrebbe avvenire un confronto tra il serial criptato ed il nome. Adesso voglio riportarvi un spezzone di codice preso da un crackme, che potrebbe chiarirvi un pò le idee. 0040118D sub_40118D proc near 0040118D mov ebx, eax ;In eax abbiamo il nostro serial, che ora vine messo in ebx 0040118F mov ecx, dword_4030A8 ;in ecx viene messo un valore fisso 00401195 mov esi, dword_4030A4 ; in esi viene messo un altro valore fisso 0040119B mov eax, 1 ; mette 1 in eax 004011A0 loc_4011A0: 004011A0 cdq ;ConvertDoubleToQuad, si prepara ad un divisione 04011A1 mul ebx ; Moltiplica ebx per eax 004011A3 div esi ;divide esi per eax (in esi si trova uno dei valori fissi) 004011A5 mov eax, edx ;Sposta il resto della divisione in edx 004011A7 loop loc_4011A0 ; mette in loop queste operazioni, questo ciclo sarà ripetuto 1109 volte, dato che ecx contiene proprio 1109! dopo abbiamo un confronto tra il serial corretto ed il seriale inserito da noi e criptato. VALORI: 004030A4= 0EAD2C511h cioè 3939681553 004030A8=455h cioè 1109 La routine è abbastanza semplice, in ebx abbiamo il serial inserito da noi, il quale viene moltiplicato per eax (che la prima volta contiene 1) e poi viene diviso per esi (in esi abbiamo il valore 3939681553). Poi viene preso il resto della divisione (quindi possiamo considerarlo come un MOD!!!) e spostato in eax, tutte queste operazioni vengono ripetute 1109 volte!. Ormai avreste già dovuto capire di che operazione si tratta, ma comunque vediamo un pò analiticamete cosa avviene: eax=eax * ebx dato che viene iterato per 1109 volte equivale a: eax=(ebx ^ 1109) di seguito abbiamo eax = eax / esi , ma poichè viene preso solo il resto può essere considerato come: eax MOD esi (da ricordare che esi è 3939681553) Quindi da tutto questo si ricava che, C=(Seriale ^ 1109) MOD 3939681553 Bene! abbiamo finito, ho cercato di scrivere qualcosa di sintetico e completo allo stesso tempo, spero di esserci riuscito. Nei vari schemi di protezione l' Rsa si può presentare in modo molto vario, noi abbiamo esaminato solo uno dei casi più semplici. La prossima volta (se ho tempo) scriverò qualcosa su come viene usato l' rsa a 128, 256 e 512 bit. References: -Cenni di Aritmetica Superiore per la Crittologia, di Marco Frigerio -Vari docs letti in giro. Note finali In fine vorrei porgere i miei più cari saluti a chi mi ha insegnato qualcosa e chi mi ha aiutato a capire, o più semplicemente alle persone con le quali ho passato delle ore piacevoli. Disclaimer Noi reversiamo al solo scopo informativo e di miglioramento del linguaggio Assembly . Capitoooooooo????? Bhè credo di si ;)))) Autore: Evilcry E-Mail: evilcry@virgilio.it :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [0X0A] OQ20031122[0A] :: [0x03][NETW0RKiNG] NEL R0UTER CiSC0 [Cartesio] [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. Tutto ciò che è contenuto in questo articolo non è da utilizzare in maniera lucrosa e/o in maniera lamerosa, ma è semplicemente un testo per porre un po' di risposte a quei perché di tutti i giorni. Non vuole essere una guida approfondita..a questo possiamo arrivare semplicemente se ne viene richiesta la necessità. Ma sopratutto perché questo carattere del "gedit" è così piccolo.. mi sto fottendo gli occhi, mi conviene cambiare utility. Ecco le cose fondamentali da fare prima di leggere questo articolo: 1) Visualizzare un carattere più grande. 2) Avere una bella "Fi*a" di fianco a voi. 3) essere assolutamente rilassati. 5) il punto 4 lo salto. Cominciamo.("occhio che partiremo da molto lontano") Quante volte scrivendo c:\>tracert IP_HOST oppure [localhost@root]# traceroute IP_HOST vi è capitato di vedere una tabella simile: 1 10.1.1.1 0.675 ms 1.829 ms 0.476 ms 2 10.1.7.22 5.351 ms 4.559 ms 1.258 ms 3 10.1.11.1 3.395 ms 10.3.7.1 (10.3.7.1) 6.390 ms 1.985 ms 4 10.1.15.1 1.529 ms 2.399 ms 10.14.1.1) 1.174 ms 5 10.1.13.1 5.272 ms 10.946 ms 5.307 ms 6 14.4.10.1 6.219 ms 5.977 ms 5.700 ms 7 12-1-1-0.mil11.it 6.519 ms 6.651 ms 7.738 ms 8 23.14.1.2.net 102.337 ms 102.567 ms 102.084 ms 9 124.12.1.1 103.003 ms 103.558 ms 103.424 ms "Che ca**o sono sti "zagareddri" --> "cosi !".. ? il comando traceroute o tracert è un'applicazione che invia un pacchetto e TRACCIA i passaggi che effettua tale pacchetto per raggiungere la destinazione impostata nella richiesta. Alcuni utilizzano questa applicazione per effettuare un controllo generale del funzionamento della rete ed individuare eventuali errori o per schematizzare l'intero percorso della rete. Tutto è nato da qua, mentre eseguivo il traceroute, avevo deciso di localizzare ogni ip, e costruire a mano la mappa della Rete, disegnando e disegnando mi son trovato dinnanzi ad un IP del genere "gw-3-0-0.ip.net", un GATEWAY (=ponte), cosa per me indigena, alquanto aliena, era proprio come un UFO per i miei occhi, la sera discutendo amichevolmente, scoprii che quel GATEWAY era un "ROUTER", cosa ancora più ignota. E' nato da li un fervido interesse verso tali dispositivi che stanno alla base delle LAN e WAN (guardare l'articolo di Spy e di l1l0, che hanno fornito le necessarie nozioni). Per continuare l'articolo dovrei fare una piccola introduzione allo stack TCP/IP. Lo stack "TCP/IP" prende il nome dai due protocolli più importanti e vale a dire il Transfer Control Protocol (TCP) e IP (Internet Protocol)ma lo stack TCP/IP comprende altri sottoprotocolli...un terzo protocollo è l'UDP (User Datagram Protocol) non sto a narrararvi tutta la vicenda, ne conseguirebbe un ulteriore articolo, di conseguenza torniamo al riassuntino: Il modello TCP/IP si è formato basandosi sullo standard ISO/OSI (International Standards Organization/Open Systems Interconnection) uno standard formato parecchi anni fa (io non ero nato) per far comunicare computer in reti di grandi dimensioni. La prima Rete ARPANET mamma di tutte le reti, un po' come i LED ZEPPELIN per il rock, i Metallica per il Metal...La FI*A per le femmine !... dai un po' di pazienza allegeriamo questo articolo. Tornando al modello "OSI" esso è costituito da 7 Livelli, viene rappresentato come una pila.. (chissà VoLTa se conosceva lo standard.. BATTUTONA, spero che qualcuno l'abbia capita) Livello 7 APPLICAZIONE = che equivale alle applicazioni che si eseguono sul computer per poi fornire dei servizi alle reti... un modo per ricordare questo livello è pensare al "ftp, telnet, e-mail, web browser... etc." Livello 6 PRESENTAZIONE = è il livello che si occupa della presentazione dei dati per il livello superiore.. cioè se io ti invio un file di Testo .Doc che abbia una certa topoligia di intestazione.. e poi ti viene rappresentato come file AUDIO .. vuol dire che qualcosa non è stata eseguita per il verso giusto. Livello 5 SESSIONE = si occupa ti aprire mantenere e chiudere una sessione.. che viene utilizzata dai livelli superiori Livello 4 TRASPORTO = Provvede all'affidabilita', al trasporto effettivo dei dati da una macchina sorgente a quella di destinazione. I Livello 3 RETE = rete è un livello fondamentale la comunicazione in rete.. in questo Livello vengono collocati gli indirizzi IP. Il Livello Rete consente di mettere in comunicazione due sistemi di elaborazione che possono essere localizzati anche su reti geograficamente diverse. (cioè se io sto in cucina.. e mia nonna sta in bagno, posso comunicarle che è finita la cartaigenica grazie al livello rete) Livello 2 DATA LINK(lo lascio in inglese che fa più colpo) = si occupa del collegamento e del flusso dati. Livello 1 FISICO = lo dice la parola stessa.. "se non hai il PC accesso.. non comunichi ". N.B: Ogni Livello comunica con l'entità di pari livello (peer) tramite il PDU protocol data unit. Dopo questa BREVISSIMA descrizione del modello OSI, proprio breve.. magari qualcuno mi può prendere superficialmente e considerarmi un ignorante.. ! passiamo al.. ######################################################################## Il Router: E' un dispositivo di rete che si colloca al livello 3 del modello OSI (quindi.. vedi su ^ .. livello rete). Il suo compito è di potere decisionale basandosi sul protocollo di terzo livello, il protocollo IP, indi per cui decide quale strade far prendere ad un pacchetto per arrivare a tale destinazione.. Esempio pratico: se io voglio comunicare con un biglietto scritto a mia nonna che è in bagno ma è finita la cartaigenica il router deciderà da dove far passare l'informazione, il router dirà "do il messaggio alla sorella di Cartesio che poi lo da al papà che lo consegna alla nonna..ma il papà di cartesio è a lavoro, quindi mi tocca lasciare il biglietto sotto la porta del bagno così la nonna appena si accorgerà di questo biglietto lo leggerà e scoprirà di essere senza carta igenica. Il router prende tale decisione attraverso i protocolli di routing. il RIP (Routing Information Protocl) IGRP (Interior Gateway Router Protocol) EIGRP (Enhanced IGRP), OSPF (Open Shortest Path First). Il RIP è un protocollo che seleziona il percorso basandosi sul numero di HOP (salti) che deve fare un pacchetto per arrivare a destinazione, non è detto che la via che scelga il RIP sia la più veloce.. ma certamente è quella che comprende meno passaggi.. IGRP A differenza del protocollo RIP, dove l'unica metrica possibile è l"hop count" e si ha un limite massimo di 15 hop count, in IGRP le metriche si basano su 4 parametri: Bandwidth, Delay, Reliability, Load. EIGRP e' stato sviluppato da CISCO a partire dalla release software 9.21 sulle basi del protocollo IGRP, rispetto al quale sono stati introdotti dei miglioramenti. OSPF Open Shortest Path First... detto tutto. Stiamo arrivando al router !!! GIOVANI... mentre mia nonna è ancora in bagno che chiede la cartaigenica. ######################################################################## Struttura del Router Un router è un dispositivo di rete che ha le stessa caratteristiche di un pc, e cioè il router ha una Rom, Ram, interfacce, CPU, schedamadre... etc.. etc... Prenderemo in esame un esempio di router Cisco. RAM: nella ram risiede la configurazione corrente del router e le variabili temporanee necessarei per il corretto funzionamento. Viene caricata quando viene riavviato un router. N.b: Il contenuto della RAM viene perso durante lo spegnimento del router. NVRAM: (non volatile RAM) è la memoria che non viene persa durante il riavvio del router.. e contiene il backup della configurazione. FLASH: Parte di memoria di tipo permanente in cui risiede l'IOS (Internetwork Operating System).. tutte stè parole per dire .. che nella memoria FLASH ci sta il sistema operativo. ROM: è la memoria in cui si trvoa il software di diagnostica.. come nel PC... CONSOLE: è una interfaccia seriale AUX: è una porta seriale usata per il collegamento di altre periferiche ausiliari, difatti il nome AUX abbreviazione di AUXILIAR significa proprio ausiliaria. Interfacce di rete: sono le connessione di rete che possono essere collocati su schedamadre oppure moduli separati... Esempio Ethernet, Seriali. o per ISDN .. etc.quindi le interfacce son ciò che ti permettono di essere in rete . :P proprio una interpretazione filosofica. come dire "Cogito Ergo Sum". Un router ha anche un sistema operativo, ora comincia la parte carina... Parte fondamentale del router è la configurazione... Sapendo che il router è alla base della Rete, instrada pacchetti, seleziona percorsi, possiamo immaginare l'importanza del ruolo che esso svolge. E quanto è importante configurare nel modo esatto il router e le apposite interfacce. ######################################################################## Il router e la CLI (command line interface).. ricordo che parliamo sempre di router CISCO. Il prompt del router è: Nome_router> il simbolo > indica che si è in una modalità user in cui si possono eseguire alcuni semplici comandi di visualizzazione delle configurazioni. Ma con una configurazione non dettagliata del router, possono venire a galla alcune informazioni preziose per un utente qualsiasi ma dannose per l'amministratore della rete. Sul router può essere attivo il server http che permette la visualizzazione di alcune caratteristiche del router anche da utenti sconosciuti !.. eseguendo il comando Nome_router> show running-config si otterrà una serie di informazioni riguardo configurazioni e stato delle interfacce di rete del router, esempio: interface Ethernet 0 ip address 10.1.1.1 255.255.255.0 no ip directed-broadcast no mop enable ! poi si possono osservare informazioni riguardo password line aux 0 line vty 0 4 "cisco" password terminal login ! tramite http server attivo sul router basta scrivere ip_del_router/level/16/exec/show/config e si ottiene la stessa lista sopra elencata. Ecco che sbuca fuori la password del line vty 0 4 (line virtual terminal --> le connessioni telnet, 0 4 --> perché possono effettuarsi massimo 5 sessioni telnet contemporaneamente. di conseguenza è meglio mascherare la password e disattivare il web server per il controllo da WEB, più avanti spiegherò come. Per ottenere una lista di comandi che si possono esguire in modalità USER è possibile.. anzi consigliato eseguire il comando "?" Nome_router> ? e verrà elencata una serie di comandi che possono essere eseguiti dalla modalità USER il comando "?" può essere adoperato anche per suggerimento riguardo il completamento di un comando ad esempio Nome router> show ? verrà elencata una serie di comandi da eseguire conseguenti a show.. esempio: interfaces running-config startup-config version etc etc.. se la nonna è ancora in bagno... il comando "show interfaces" serve per mostrare lo stato e le caratteristiche di tutte le interfacce presenti nel router, quali Ethernet 0 .. Ethernet 1, Serial 0, Serial 1.. etc. Quando si parla di "stato dell'interfaccia" del router si intende lo stato fisico e lo stato di rete di tale interfaccia del router, vale a dire i "cavi, dispositivi etc.. etc.." e i protocolli "ip, tcp, ipx, appletalk, routing". Di conseguenza quando viene visualizzato nello stato di un'interfaccia dopo aver eseguito "show interfaces" `Interface Serial 0 is up, line protocol is up` vuol dire che tutto funziona per il meglio, che tutto è UP che tutto è SU che tutto funge.! Interface Serial 0 is up, line protocol is down significa che c'è un problema di rete Interface Serial 0 is down, line protocol is up significa che c'è un problema a livello fisico dell'interfaccia Interface Serial 0 is down, line protocol is down, vuol dire che è "IMBAGAGLITA, che non è tutto incasianto". Interface Serial 0 is administratively down vuol dire che l'interfaccia è SPENTA dall'amministratore della rete. Dopo aver spiegato per sommi capi la modalità USER quella limitata nell'esecuzione dei comandi, vedermo la parte privilegiata. Per entrare in modalità privilegiata bisogna digitare "enable" ed invio, e verrà richiesta una passoword per avere accesso. Tale password non viene visualizzata da un user qualunque.. però alle volte per disattenzione degli amministratori le password di "user" e di "privileged" sono le stesse. Somiglia molto ad un prompt di una shell linux difatti, l'accesso privilegiato al router è caratterizzato dallo sharp = # :) quindi il prompt del router in modalità superuser sarà `Nome_router# ` basta digitare il "?" per avere subito scarrozzata una lista di tantissimi comandi da eseguire in questa modalità.. tra ciò i comandi per la configurazione della rete e la configurazione delle password di accesso. Uno dei punti relativi alla sicurezza: le password di accesso in chiaro e criptate. Il router utilizza una opzione per il criptaggio delle password in modo tale da nascondere anche al semplice utente di guardare la password di accesso con un semplice "show running-config". o tramite un ip_del_router/level/16/exec/show/config. andiamo a disattivare il web server e criptare la password. Nome_router> enable password: Nome_router# siamo in modalità privilegiata.. la password non viene scritta neanche con gli asterischi.. in modo tale da non comprendere nanche la sua lunghezza. ora entriamo nel settore di "configurazione globale", in cui si possono effettuare una serie di configuarzioni quali..esempio hostname.. Entriamo in modalità configurazioni globali con il comando configure terminal, si otterrà un prompt del genere: nome_router(config)# hostname Mia_nonna_è_in_bagno Mia_nonna_è_in_bagno(config)# ... sarà il nome del nostro router. ecco ora..l'opzione per disattivare il server http del router in modo tale da esporlo meno ad attacchi. Mia_nonna_è_in_bagno(config)# hostname Router Router(config)# scusate ma quel prompt era troppo lungo l'ho dovuto cambiare. Router(config)# no ip http server Router(config)# il comado qui sopra ha disattivato l'http server. Da notare che nel router cisco la maggiorparte delle volte che si vuol negare un qualcosa basta mettere davanti il comando "no". infatti se noi avessimo voluto rendere attivo "http server" ci sarebbe bastato digitare Router(config)# ip http server Router(config)# in questa sezione di configurazoine globale del router si possono settare le password di accesso "line console 0" "line vty 0 4" e "enable Router(config)# enable password cisco (=equivale alla password che viene richiesta quando dalla modalità user si digita enable per entrare in modalità privilegiata) Router(config)# line console 0 Router(config-line)# login "invio" (per settare un login nullo) Router(config-line)# login Nonna (login = nonna) Router(config-line)# password cisco Router(config-line)# exit Router(config)# line vty 0 4 Router(config-line)# login Router(config-line)# password cisco per tornare indietro nel prompt non c'è cd .. oppure cd / ma basta digitare exit per tornare al prompt precedente e cioè Router(config)# oppure dicitare CTRL+Z per tornare al prompt principale e cioè Router#. Ed infine la password di enable secret, vale a dire la password criptata con l'algoritmo MD5, che permette di rendere illegibile tale password a chiunque con uno show running-config, perché il risultato dell'output sarà... service password-encryption $1812AS39012!|0$091a!"£81! ... uazhazuzhu mi sento redicolo. Ogni qualvolta si cambia qualche settaggio del router che pretende essere salvato e ricaricato al prossimo riavvio conviene copiare la configurazione che è presente attualmente "cioè in running, per questo viene chiamata running-config nella startup-config che è quella che risiede nella memoria NVRAM che passerà le informazioni alla RAM al prossimo riavvio. Il comando per copiare la configurazione in running in NVRAM è Router# copy running-config startup-config N.b: se fate alcune modifiche a parametri, e settaggi di rete, ma non eseguite un copy running-config startup-config .. appena il router viene spento e riacceso le modifiche si riveleranno nulle. Di conseguenza tocca ripetere le procedure da capo. ######################################################################## Configurazione delle interfacce del router Il router è formato da interfacce che permettono la comunicazione in rete, tali interfacce possono trovarsi su scheda madre o possono essere dei moduli aggiuntivi. Andiamo ora a configuarare una interfaccia Router> enable password: Router# andiamo in configurazione globale.. Router# configure terminal Router(config)# interface Ethernet 0 (attenzione Ethernet 0.. può cambiare a seconda delle varie IOS dei vari router.. ad esempio può essere FastEthernet 0 oppure FastEthernet 0/0, sesso discorso vale per seriale..) Router(config-if)# <-- questo prompt con l'aggiunta di if sta a simboleggiare che siamo nella configuarzione dell'interfaccia ... nel caso sopra elencato "Ethernet 0" non ci resta altro che dare un ip ed una netmask all'interfaccia Ethernet del nostro router Router(config-if)# ip address 10.1.1.1 255.255.255.0 Poi settare la nostra interfaccia UP, e cioè metterla in funzione con il comando Router(config-if)# no shutdown Router(config-if)# si può passare da una interfaccia all'altra digitando semplicemente Rotuer(config-if)# interface serial 0 (ed inautomatico.. il prompt non sembrerà cambiato.. ma siamo nella interfaccia serial 0) non tutti i router cisco.. rispondono così... di conseguenza un consiglio personale è.. digitare exit che porterà al seguente prompt: Router(config)# interface serial 0 Router(config-if)# ip address 37.1.1.1 255.255.255.0 Router(config-if)# clock rate 56000 -----> "questo comando lo rimando in un altro articolo... dipende da l'utilizzo che ne viene fatto di questo di articolo" Router(config-if)# no shutdown Router(config-if)# CTRL+Z Router# ######################################################################## IOS (Internetworking Operating System) .. Sistema operativo del router. Il sistema operativo risiede nella memoria falsh del router per ottenere inforamzioni riguardo alle memorie del router.. basti digitare uno show version e si otterrano informazioni relative a: Piattaforma sulla quale si sta operando, IOS e versione e nome del file, memorie e le capacità delle memorie. Sulla memoria flash possono essere copiate più IOS. Nei router Cisco c'è differenenza tra le realase 11.0 in giù e 11.x in su.. Occhio ragazzi .. se siete su un router non digitate mai ... dico mai .. Router# erase flash questo significherebbe cancellare il sistema operativo. Di conseguenza una reazione a catena di rompimenti di balle...E' come se io facessi ERASE CARTAIGENICA.. mia nonna potrà stare in bagno per sempre oppure ... ? ... pulirsi in qualche altro modo. Il nome del file del sistema operativo è sempre un file binario di conseguenza caratterizzato da una estenzione .bin. Anche il nome del file ha delle specifichè che indicano Piattaforma, caratteristiche, fattori di compressione... che forse il 90 % degli utenti... ignorerà. Un esempio: c7200-ajs56-mz c7200: Router Cisco Serie 7200 - a: supporto protocollo APPN; j: supporto di caratteristiche Enterprise; s: supporto di NAT,ISL,VPDN/L2F 56: supporto di crittografia a 56 bit - m: esecuzione in RAM z: file compresso con Zip. E' possibile copiare una IOS da un TFTP (trivial file transfer protocol) che utilizza il protocollo UDP non affidabile ma veloce, per trasferire il file. Perché caricare una IOS da un TFTP ? per fare eseprimenti, testando IOS, compatibilità tra interfacce e IOS, per avere a disposizione molte più IOS, difatti la memoria di un router è molto limitata e non riesce a contenere un gran numero di IOS, di conseguenza su un server TFTP la quantità e la varietà di IOS disponibili sarebbe maggiore.. Si può configurare anche il router in modo da decidere la procedura di avvio... come settare il bios del pc quando selezioni "1) floppy 2) Cd - rom 3)HD... etc.. in un router basta andare in modalità di configurazione generale...di conseguenza digitare configure terminal Router(config)# boot system flash nome_file.bin Router(config)# boot system tftp nome_file.bin ip_del_server. etc... per avere più completezza di conoscenza basta digitare Router(config)# boot system ? e comparirà una lista di comandi disponibili.. non sto ad elencarvi.. perché non sono qui.. a descrivere tutti i comandi di un router.. ma le cose essenziali.. per approcciare ad esso. ######################################################################## Siamo giunti al termine dell'Articolo, però voglio che si sappia che questo articolo.. è solo per un approccio al router e non un tutorial completo, perché ho omesso parecchie parti anche importanti ma che inserirò o aggiornerò in seguito ad eventuali richieste... Vedremo se questi accorgimenti sono serviti.. e se qualcuno si è interessato in tal modo da chiedermi un articolo più dettagliato. Per finire. Accorgimenti sicurezza: Disattivare le porte Echo UDP e Chargen UDP, possono essere causa di un DoS che provocherebbe il collasso del router. Disattivare il server http Attivare l'enable secret "password" Alcuni router sono vulnerabili ad un attacco del genere http://ip_router/%% che possono causare un blocco del sistema. scritto da "CaRTeSio".. cogito ergo sum. e-mail: comunicazione@polizia.it :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [0X0A] OQ20031122[0A] :: [0x04][C0DiNG] 0PENSSL E RSA C0DiNG [binduck] [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. Article && sources by binduck - binduck@coder.hu 0. Intro 1 RSA - Teoria 1.1 OpenSSL/RSA [genrsa,rsa,rsautl] (generare chiave,cifrare/decifrare,firmare,...) 1.1.1 GPG 1.2 Impariamo ad implementare RSA nei nostri programmi 1.2.1 RSA - Headers 1.2.2 RSA - Le chiavi 1.2.3 RSA - Cifrare e decifrare 0. Intro In questo articolo voglio illustrarvi le basi che vi permetteranno di capire e implementare il potenete algoritmo RSA. 1]Vedremo, quindi, come utilizzare il toolkit openssl (ne vedremo le principali funzioni, come creare le chiavi, come cifrare/decifrare dati). 2]Vedremo insieme le basi delle librerie openssl, e in particolare quelle che vi permetteranno di implementare l'algoritmo nei vostri programmi. Per affrontare la lettura di questo articolo avrete bisogno di conoscere: - il linguaggio C - se volete comprendere la parte matematica dovrebbe bastare l'infarinatura di matematica fornitavi nelle elementari e nelle medie. - dovreste avere openssl e le librerie (/usr/include/openssl/) se vi mancano....installatele. 1 RSA - Teoria RSA fu sviluppato da tre matematici del MIT:Rivest, Shamir e Adleman: - Cifrario a chiave pubblica (usato nel PGP e in molti altri programmi) - Si basa sulla teoria matematica dei numeri; infatti le chiavi sono costituite dal prodotto di due numeri primi molto grandi(piu' di trecento cifre) - Cifrazione/Decifrazione di dati - Firma/Controllo firma - Supporta chivi di diversa grandezza in modo da fornire vari livelli di sicurezza (grandezza minima consigliata e' 1024). Riprendiamo alcuni concetti basilari della crittografia che ci saranno utili. Un cifrario a chiave pubblica (o cifrario asimmetrico) e' basato sull'utilizzo di due chiavi,una per cifrare e una per decifrare. La prima chiave puo' essere distribuita a chi vuole farci pervenire messaggi cifrati in modo che solo noi possiamo decifrarli con la nostra chiave segreta(che DEVE rimanere segreta). Il fatto che la chiave pubblica possa essere tranquillamente distribuita anche via email, in messaggi in chiaro, elimina la necessita' di utilizzare canali altamente sicuri per lo scambio della chiave utilizzata nei cifrari a chiave semplice. Iniziamo a vedere l'algoritmo in generale ed a analizzarlo dal punto di vista matematico. 1)Il cifrario RSA e' basato sul prodotto di due numeri primi di grandi dimensioni, che possono superare le 300 cifre. Ricordo inanzitutto che un numero si dice primo solo se e' divisibile per 1 e se stesso. Noi prenderemo due numeri primi piccoli per fare un esempio: P = 7 e Q = 11 2)Ora chiamiamo N il prodotto dei due: N = P * Q = 77 3)Ora calcoliamo il valore della funzione di Eulero di N in questo modo: M = f(n) = (P-1)*(Q-1) Nel nostro caso M = 60 Vi ricordo che il valore della funzione di Eulero dovra' rimanere segreto. 4)Ora scegliamo un numero E tale che E e M siano primi tra loro. Poiche' 1 < E < M allora iniziamo a dividere M per tutti i numeri maggiori di 1 fino a che non troviamo un numero che abbia una parte decimale. 60 / 2 = 30 60 / 3 = 20 60 / 4 = 15 60 / 5 = 12 60 / 6 = 10 60 / 7 = 8.57 Quindi E = 7 5)Ora calcoliamo D usando la formula D = ((H * M) + 1) / E in modo che H sia il piu' piccolo valore per cui D sia intero. Quindi iniziamo a sostituire H con i numeri 1,2,3,... fintanto che il risultato trovato non sia intero. D = ((1 * 60) + 1) / 7 -> NO D = ((2 * 60) + 1) / 7 -> NO .... .... D = ((5 * 60) + 1) / 7 = 301 / 7 = 43 D = 43 6)Bene ora abbiamo generato la nostra chiave che useremo per crifrare/decifrare i dati. Per cifrare usiamo la formula: C = (B^E) mod N Dove C e' il testo cifrato, mentre B e' il valore decimale del testo che vogliamo cifrare. Prendiamo come esempio B = 3 -> C = (3 ^ 7) mod 77 = 31 Per decifrare usiamo la formula: L = (C^D) mod N Prendiamo ora il numero precedentemente ottenuto e decifriamolo: L = (31 ^ 43) mod 77 = 3 Nota: si potrebbe pensare che si possa semplicemente calcolare la funzione di Eulero senza utilizzare i numeri primi P e Q. Questo non e' impossibile ma richiede un ingente quantita' di tempo, avendo lo stesso grado di complessita' della fattorizzazione di N. 1.1 OpenSSL/RSA Vediamo ora come utilizzare openssl in combinazione con RSA. Creiamo inanzitutto le chiavi RSA, e per fare questo si usa openssl genrsa: openssl genrsa [-out nome_file] [-passout arg] [-des] [-des3] [-idea] [-f4] [-3] [-rand file(s)] [numbits] binduck@fuzzy:~$ openssl genrsa -des3 1024 > key.pem Con questo comando generiamo una chiave RSA di lunghezza 1024 bits e la salviamo nel file key.pem nel formato PEM. Verra' chiesto di inserire una password per cifrare la chiave con 3des prima di salvarla(possiamo sostituire -des3 con -des o -idea). Se invece non vogliamo cifrarla basta fare eliminarel'opzione -des3: binduck@fuzzy:~$ openssl genrsa 1024 > key.pem Vediamo le altre opzioni del comando openssl genrsa: -out file_out :Il nome del file di output. -passout arg :Il file sorgente delle password per l'output. -F4|-3 :L'esponente pubblico da usare(65537 o 3(default 65537)) numbits :Numero di bits(minimo consigliato 1024). PER MAGGIORI INFORMAZIONI SULLE OPZIONI: genrsa(1) Ora ricaviamo la chiave pubblica da quella privata appena generata, utilizzando openssl rsa: openssl rsa[-inform] [-outform |PEM|NET|DER] [-in filename] [-passin arg] [-out filename] [passout arg] [-sgckey] [-des] [-des3] [-idea] [-text] [-noout] [-modulus] [-check] [-pubin] [-pubout] -in file :Legge la chiave RSA contenuta nel file passato come argomento.(Per default la chiave sara' intesa come privata). -pubin :Dichiara che la chiave letta con -in file sara' pubblica. -pubout :Dichiara che l'output sara' una chiave pubblica. binduck@fuzzy:~$ openssl rsa -in key.pem -pubout > pub.pem o in maniera equivalente: binduck@fuzzy:~$ openssl rsa -in key.pem -pubout -out pub.pem Vediamo le altre opzioni interessanti: -check :Controlla la struttura di una chiave privata RSA. binduck@fuzzy:~$ openssl rsa -check -in key.pem Altre opzioni utili sono quelle che permettono di cifrare una chiave, con un algoritmo a scelta tra des, 3des, idea, in un secondo momento dalla creazione della chiave. binduck@fuzzy:~$ openssl rsa -in key.pem -des3 -out key.pem A questo punto bastera' digitare la pass phrase, confermarla e la vostra chiave sara' cifrata. Ovviamente possiamo essere interessati a rimuovere la password dalla chiave: binduck@fuzzy:~$ openssl rsa -in key.pem -out keydec.pem Dovrete inserire la pass phrase per eliminare la cifratura e il gioco e' fatto. -modulus :Stampa il modulo della chiave. binduck@fuzzy:~$ openssl rsa -in key.pem -modulus PER MAGGIORI INFORMAZIONI SULLE ALTRE OPZIONI: rsa(1) Ora passiamo a vedere come cifrare/decifrare,firmare/verificare dati usando l'algoritmo RSA. openssl rsautl [-in file][-out file][-inkey file][pubin][-certin] [-sign][-verify][-encrypt] [-decrypt][-pkcs][-ssl][-raw][-hexdump] [-asn1parse] NOTA: rsautl, poiche' usa l'algoritmo RSA direttamente puo' essere usato solo per cifrare dati di piccole dimensioni. Se tentiamo di firmare un file troppo grande verra' generato un errore che ci dira' appunto che la dimensione del file era troppo elevata per la chiave usata. -inkey file :Usa la chiave contenuta nel file passato come argomento. (Per default rsautl si aspettera' una chiave privata). -pubin :Dichiara che la password contenuta nel file letto con -inkey file e' una chiave pubblica. -certin :Dichiare che l'input sara' un certificato contentente una chiave RSA. Prendiamo come esempio un file contenente una password. echo "rsa_algorithm" > pass.txt Ora per cifrare il file digitiamo: binduck@fuzzy:~$ openssl rsautl -encrypt -in pass.txt -inkey pub.pem -pubin> crypto Ora avremo la nostra password cifrata nel file "crypto". Ovviamente pub.pem e' la chiave pubblica precedentemente generata con openssl genrsa. Vediamo ora il processo inverso, decifriamo la password: binduck@fuzzy:~$ openssl rsautl -decrypt -in crypto -inkey key.pem Questo stampera' sullo stdout la password in chiaro. key.pem e' la chiave privata, poiche' stiamo decifrando. Mettiamo il caso che vogliamo semplicemente firmare un documento usando la nostra chiave privata(anche nel caso delle firme possiamo firmare solo dati di piccole dimensioni). binduck@fuzzy:~$ openssl rsautl -sign -in small_file.txt -inkey key.pem -out signed_file Vediamo ora come verificare dei dati firmati: binduck@fuzzy:~$ openssl rsautl -verify -in signed_file -inkey key.pem PER MAGGIORI INFORMAZIONE SULLE ALTRE OPZIONI: rsautl(1) 1.1.1 GPG Se vi siete rotti delle limitazioni insite nell'uso di rsautl a causa della dimensione dei file vi consiglio di usare gpg. Pero' per quanto riguarda l'utilizzo di RSA in gpg bisogna ricordare che questo algoritmo e' usato solo per le firme. Ci sono ottime guide dedicate a questo programma, prima fra tutte il manuale che potrete trovare a questo indirizzo: http://www.gnupg.org/(en)/documentation 1.2 Impariamo ad implementare RSA nei nostri programmi. Ora passiamo alla parte piu' interessante: Impariamo come implementare RSA nei nostri programmi. 1.2.1 RSA - Headers Qui e' riportata la lista di headers che utilizzeremo per creare dei programmini di base. #include Funzioni per generare le chiavi, cifrare, decifrare, firmare, verificare dati. ----------------------------- #include Funzioni per leggere e scrivere strutture in formato PEM. Noi le utilizzeremo per salvae le chiavi RSA nei files. Ricordate openssl genrsa che salva le chiavi generate in formato PEM. ----------------------------- #include Funzioni per accedere ai codici di errore generati dalle funzioni della libreria openssl. ----------------------------- 1.2.2 RSA - Le chiavi Vediamo la struttura di una chiave RSA: struct { BIGNUM *n; //public modulus BIGNUM *e; //public exponent BIGNUM *d; //private exponent BIGNUM *p; //secret prime factor BIGNUM *q; //secret prime factor BIGNUM *dmp1; // d mod (p-1) BIGNUM *dmq1; // d mod (q-1) BIGNUM *iqmp; // q^-1 mod p }; RSA Ovviamente nelle chiavi pubbliche l'esponente privato(d) e i relativi valori segreti (p e q) sono NULL (per chiarirvi le idee potete andare a rivedervi la trattazione matematica al paragrafo 1). Nelle chiavi private p, q, dmp1, dmq1 e iqmp potrebbero anche essere NULL, ma le operazioni effettuate usando questa chiave risulterebbero piu' lente. Ricordiamo infatti che per decifrare si usa solo d (guardatevi sempre il par.1). Vediamo come creare le chiavi. Prima presentero' le funzioni necessarie e poi vedremo un semplice programma per creare le nostre chiavi. Inanzitutto dobbiamo allocare e iniziare una struttura RSA. Questo e' possibile con la funzione RSA_new(): --------------------------------------------------------------------- #include RSA * RSA_new(void); Ritorna NULL se fallisce l'allocazione; l'errore puo' essere ottenuto usando ERR_get_error() (spiegata piu' avanti). Come potete vedere dal programma per crear le chiavi usando la funzione RSA_generate_key() la struttura viene direttamente inizializzata. --------------------------------------------------------------------- RSA_free() libera la memoria occupata dalla struttura RSA, ma prima di fare cio' cancella la chiave. #include RSA_free(RSA *rsa); Ricordatevi di liberare sempre la memoria occupata dalle vostre chiavi. --------------------------------------------------------------------- Per creare le chiavi usiamo la funzione RSA_generate_key(): #include RSA *RSA_generate_key(int num, unsigned long e, void(*callback) (int,int,void *), void *cb_arg); int num: numero di bit. Come dice la man page e' meglio scegliere una chiave >= di 1024 bit. unsigned long e: e' l'esponente. Deve essere un numero dispari, tipicamente si sceglie 3, 17 o 65537. Se la funzione fallisce ritorna NULL. Il numero dell'errore puo' essere ottenuto sempre con ERR_get_error(). --------------------------------------------------------------------- #include int RSA_check_key(RSA *rsa); Questa funzione controlla se la chiave e' una vera chiave RSA. Se e' corretta allora la funzione ritorna 1, se non e' una chiave ritorna 0, mentre se la funzione fallisce ritorna -1. --------------------------------------------------------------------- Ora che sappiamo come creare la nostra chiave vediamo come scrivere la chiave privata in un file. Possiamo usare una funzione presente in pem.h: PEM_write_RSAPrivateKey(): #include int PEM_write_RSAPrivateKey(FILE *fp,RSA *x,const EVP_CIPHER *enc, unsigned char *kstr,int klen,pem_password_cb *cb,void *u); --------------------------------------------------------------------- Mentre per scrivere la chiave pubblica useremo PEM_write_RSAPublicKey(): #include int PEM_write_RSAPublicKey(FILE *fp,RSA *x); Tutte e due queste funzioni di scrittura ritornano 1 se hanno successo e 0 se falliscono. --------------------------------------------------------------------- Per leggere la chiave privata scritta in formato pem usiamo PEM_read_RSAPrivateKey(): #include RSA *PEM_read_RSAPrivateKey(FILE *fp,RSA **x,pem_password_cb *cb, void *u); --------------------------------------------------------------------- Per leggere la chiave pubblica usiamo PEM_read_RSAPublicKey(): #include RSA *PEM_read_RSAPublicKey(FILE *fp,RSA **x,pem_password_cb *cb, void *u); Le funzioni di lettura ritornano un puntatore alla struttura letta o NULL se c'e' un errore. Settiamo enc, cb, kstr e u a NULL e poniamo klen a 0 poiche' non necessitiamo di crittare le strutture PEM. --------------------------------------------------------------------- Ora sappiamo come creare e salvare le nostre chiavi. Prima di vedere un esempio pratico introduciamo anche le funzioni per il controllo degli errori. #include unsigned long ERR_get_error(void); Questa funzione ritorna il codice dell'ultimo errore. --------------------------------------------------------------------- #include char *ERR_error_string(unsigned long e, char *buf); Genera una stringa "human-readable". Un utilizzo di queste due funzioni combinate e' questo: printf("%s\n",ERR_error_string(ERR_get_error(),NULL)); --------------------------------------------------------------------- Bene ora vediamo un piccolo esempio su come creare una chiave e salvarla su dei files. -----------------------------keygen.c-------------------------------- /* Simple RSA Key generator. Saves keys in PEM format. Coded by binduck - Compile: gcc -lssl keygen.c -o keygen Usage: ./keygen Ex. ./keygen 1024 */ #include #include #include #include #define SECFILE "sec.pem" #define PUBFILE "pub.pem" int main(int argc, char *argv[]) { RSA *key; FILE *fp; int keylen=0; if(argc!=2) { fprintf(stderr,"Error: too many/few arguments.\n " "Usage: %s \n",argv[0]); exit(0); } keylen = atoi(argv[1]); if((key = RSA_generate_key(keylen,3,NULL,NULL)) == NULL) { fprintf(stderr,"%s\n",ERR_error_string(ERR_get_error(),NULL)); exit(-1); } if(RSA_check_key(key) < 1) { fprintf(stderr,"Error: Problems while generating RSA Key.\nRetry.\n"); exit(-1); } fp=fopen(SECFILE,"w"); if(PEM_write_RSAPrivateKey(fp,key,NULL,NULL,0,0,NULL) == 0) { fprintf(stderr,"Error: problems while writing RSA Private Key.\n"); exit(-1); } fclose(fp); fp=fopen(PUBFILE,"w"); if(PEM_write_RSAPublicKey(fp,key) == 0) { fprintf(stderr,"Error: problems while writing RSA Public Key.\n"); exit(-1); } fclose(fp); RSA_free(key); printf("RSA key generated.\nLenght = %d bits.\n",keylen); return 0; } --------------------------------------------------------------------- Per compilare digitate: gcc -lssl keygen.c -o keygen Usage: ./keygen binduck@fuzzy:~$ ./keygen 1024 Eseguendo il programma, questo ci creera' due files 'pub.pem' e 'sec.pem' in cuiverranno salvate le nostre chiavi RSA. Ovviamente potrete specificare la lunghezza da voi desiderata (Es. 64 o 512 o 2048). Ecco come risulterebbe una chiave privata a 1024 bit generata. -----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDWxE5+qNqE20NQWqRi71vkqroI+52CgzGmKv42BsE6kk1QiVSS 9Qm75bKfEP7Jz83NO/YO5DPHp6ZVr38nY6HCY3Ta/fNU0O/xNE3UiOQSK8dn7lbw 7itshIwjPpK09cgL6wos4Sm+lcHjdRk5DyTKcRNadWyXSxNsdrR5FWtImwIBAwKB gQCPLYmpxecDPNeK5xhB9Oftxyawp75XAiEZcf7Oryt8YYjgW423TgZ9Q8xqC1SG iokzfU60mCKFGm7jylTE7RaAXuiq84NDz71wmFjOgqGzePHl4CH5WnPi0GTwfssI cEBr5WjUu2DTNKAUdk+1jLJFXFY6UIws6tzZB1SPNNagewJBAPAvdSm765vsMcOL zfIks0rMpx9c4QZrXAgm+IoAN3EQOvZ9KNAK1xkUYDPJEyJ93GY5ZjyPaFnM9t+x n97l1r8CQQDk6GVm8oN9Z5aMPNDSzNGrj+f+xxngEjxDzcH2YfDcVy8cb8T4Daqt vWSP2JWZm4YAWJzACuuQ+zAMBAJnQ4ElAkEAoB+jcSfyZ/LL17KJTBh3hzMaFOiW BEeSsBn7BqrPoLV8pFNwirHku2LqzTC3bFPoRCZEKF+a5oiklSEVP0PkfwJBAJia 7kShrP5FDwgoizczNnJf7/8vZpVhfYKJK/mWoJLkyhL1Lfqzxx5+QwqQY7u9BAA7 Eyqx8mCndV1YAZotAMMCQQDYZg4eFgmTJuq/J/Ls7NusdBxuwh0fKRt2KPhCZw5r 5mEeCDflQ3ATBjeUt6Z77JG0aEmQUq5NOqd/ju1VtBst -----END RSA PRIVATE KEY----- Ed ecco la chiave pubblica relativa: -----BEGIN RSA PUBLIC KEY----- MIGHAoGBANbETn6o2oTbQ1BapGLvW+Squgj7nYKDMaYq/jYGwTqSTVCJVJL1Cbvl sp8Q/snPzc079g7kM8enplWvfydjocJjdNr981TQ7/E0TdSI5BIrx2fuVvDuK2yE jCM+krT1yAvrCizhKb6VweN1GTkPJMpxE1p1bJdLE2x2tHkVa0ibAgED -----END RSA PUBLIC KEY----- 1.2.3 Cifrare/Decifrare Nell'header openssl/rsa.h troviamo le funzioni di cui abbiamo bisogno: RSA_private_decrypt() e RSA_public_encrypt(). --------------------------------------------------------------------- #include int RSA_public_encrypt(int flen, unsigned char *from, unsigned char *to,RSA *rsa,int padding); unsigned char *from: stringa da crittografare. unsigned char *to: buffer dove la funzione mette l'output crittografato. RSA *rsa: chiave pubblica RSA. Come padding impostiamo RSA_PKCS1_OAEP_PADDING Ritorna la grandezza dei dati crittografati. --------------------------------------------------------------------- int RSA_private_decrypt(int flen,unsigned char *from,unsigned char *to,RSA *rsa,int padding); unsigned char *from: stringa da decrittare. unsigned char *to: buffer che conterra' l'output in chiaro. RSA *rsa: chiave privata RSA. Come padding impostiamo sempre RSA_PKCS1_OAEP_PADDING. Ritorna la grandezza del testo in chiaro. --------------------------------------------------------------------- Bene ora vediamo un programmino che cifra e decifra il contenuto di un file. RICORDO CHE STIAMO USANDO DIRETTAMENTE L'ALGORITMO RSA, QUINDI POSSIAMO INTERVENIRE SU UNA PICCOLA QUANTITA' DI DATI(ricordate rsautl nella prima parte dell'articolo?). --------------------------file_crypt.c------------------------------- /* Simple RSA encrypting/decrypting tool. Coded by binduck - Compile: gcc -lssl file_crypt.c -o file_crypt Usage: ./file_crypt -h for help */ #include #include #include #include #include #include #define SECFILE "sec.pem" #define PUBFILE "pub.pem" #define HELP 'h' #define GENKEY 'g' #define CIPHER 'c' #define DECIPHER 'd' RSA* readpemkeys(int type); void genkey(int size); int main(int argc,char *argv[]) { int ch,size=0,len=0,ks=0; FILE *fp; RSA *key; char char_opt[] = {HELP,GENKEY,CIPHER,DECIPHER}; unsigned char *out,*in,*mex; printf("RSA Cipher/Decipher Signer/Verifier.\n"); printf("Coded by Binduck - \n\n"); if (argc == 1) { printf("%s -h for help.\n",argv[0]); } while((ch=getopt(argc,argv,char_opt)) != -1) { switch(ch) { case HELP: printf(" %s -g :generates RSA keys and save them " "in PEM format.(1024 or 2048 are strongly suggested).\n",argv[0]); printf(" %s -c :crypt datas in 'file_in' " "and stores output in 'file_out'.\n",argv[0]); printf(" %s -d :decript datas in 'file_in' " "and stores output in 'file_out'.\n",argv[0]); break; case GENKEY: if(argc != 3) { fprintf(stderr,"Error: check arguments.\n%s -h for " "help.\n",argv[0]); exit(0); } printf("Generating RSA keys...\n"); size=atoi(argv[2]); printf("%d bits.\n",size); genkey(size); printf("Private Key saved in %s file.\n",SECFILE); printf("Public Key saved in %s file.\n",PUBFILE); printf("Done.\n"); break; case CIPHER: if(argc != 4) { fprintf(stderr,"Error: check arguments.\n%s -h for " "help.\n",argv[0]); exit(0); } key = readpemkeys(0); if(!(fp = fopen(argv[2],"r"))) { fprintf(stderr,"Error: No input file.\n"); exit(-1); } ks = (RSA_size(key)/2); if((mex = (char *) malloc(ks)) == NULL) { fprintf(stderr,"Error: malloc()\n"); } memset(mex,'\0',ks); fread(mex,1,ks,fp); fclose(fp); if((out = (unsigned char*)malloc(RSA_size(key))) == NULL) { fprintf(stderr,"Error: malloc()\n"); exit(-1); } memset(out,'\0',RSA_size(key)); len = strlen(mex)*(sizeof(char)); printf("Encrypting max %d bytes from '%s' file.\n",ks,argv[2]); if(RSA_public_encrypt(len,mex,out,key,RSA_PKCS1_OAEP_PADDING)==-1) { fprintf(stderr,"%s\n",ERR_error_string(ERR_get_error(),NULL)); exit(-1); } fp = fopen(argv[3],"w"); fwrite(out,1,strlen(out),fp); fclose(fp); RSA_free(key); free(out); free(in); printf("Done.\n"); break; case DECIPHER: if(argc != 4) { fprintf(stderr,"Error: check arguments.\n%s -h for " "help.\n",argv[0]); exit(0); } key = readpemkeys(1); if((in = (unsigned char*)malloc(RSA_size(key))) == NULL) { fprintf(stderr,"Error: malloc()\n"); exit(-1); } memset(in,'\0',RSA_size(key)); fp = fopen(argv[2],"r"); fread(in,1,RSA_size(key),fp); fclose(fp); if((out = (unsigned char*)malloc(RSA_size(key)))== NULL) { fprintf(stderr,"Error: malloc()\n"); exit(-1); } memset(out,'\0',RSA_size(key)); printf("Decrypting '%s' file.\n",argv[2]); if(RSA_private_decrypt(RSA_size(key),in,out,key, RSA_PKCS1_OAEP_PADDING) == -1) { fprintf(stderr,"%s\n",ERR_error_string(ERR_get_error(),NULL)); exit(-1); } fp = fopen(argv[3],"w"); fprintf(fp,"%s",out); fclose(fp); free(out); RSA_free(key); printf("Done.\n"); } } } void genkey(int size) { RSA *key=NULL; FILE *fp; if((key = RSA_generate_key(size,3,NULL,NULL)) == NULL) { fprintf(stderr,"%s\n",ERR_error_string(ERR_get_error(),NULL)); exit(-1); } if(RSA_check_key(key) < 1) { fprintf(stderr,"Error: Problems while generating RSA Key.\n " "Retry.\n"); exit(-1); } fp=fopen(SECFILE,"w"); if(PEM_write_RSAPrivateKey(fp,key,NULL,NULL,0,0,NULL) == 0) { fprintf(stderr,"Error: problems while writing RSA Private Key.\n"); exit(-1); } fclose(fp); fp=fopen(PUBFILE,"w"); if(PEM_write_RSAPublicKey(fp,key) == 0) { fprintf(stderr,"Error: problems while writing RSA Public Key.\n"); exit(-1); } fclose(fp); RSA_free(key); printf("RSA keys generated.\n"); } RSA* readpemkeys(int type) { FILE *fp; RSA *key=NULL; if(type == 0) { if((fp = fopen(PUBFILE,"r")) == NULL) { fprintf(stderr,"Error: Public Key file doesn't exists.\n"); exit(-1); } if((key = PEM_read_RSAPublicKey(fp,NULL,NULL,NULL)) == NULL) { fprintf(stderr,"Error: problems while reading Public Key.\n"); exit(-1); } fclose(fp); return key; } if(type == 1) { if((fp = fopen(SECFILE,"r")) == NULL) { fprintf(stderr,"Error: Private Key file doesn't exists.\n"); exit(-1); } if((key = PEM_read_RSAPrivateKey(fp,NULL,NULL,NULL)) == NULL) { fprintf(stderr,"Error: problmes while reading Private Key.\n"); exit(-1); } fclose(fp); if(RSA_check_key(key) == -1) { fprintf(stderr,"Error: Problems while reading RSA Private Key in %s " "file.\n",SECFILE); exit(-1); } else if(RSA_check_key(key) == 0) { fprintf(stderr,"Error: Bad RSA Private Key readed in %s " "file.\n",SECFILE); } else return key; } } --------------------------------------------------------------------- readpemkeys() e' un esempio di come e' possibile leggere le chiavi salvate in formato PEM. Il codice e' molto semplice e quindi non ritengo necessario commenti. E con questo programmino io avrei terminato questo articolo, che spero vi abbia chiarito le idee su RSA e openssl. Per eventuali chiarimenti potete scrivermi a binduck@coder.hu Grazie a tutti e alla prossima. binduck :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [0X0A] OQ20031122[0A] :: [0x05][C0DiNG] DiAL0G0 S0PRA i DUE MASSiMi FiLESYSTEM [eazy] [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. 1. Scopo del gioco 2. Il filesystem 2.1 Struttura del filesystem 2.2 Strutture dati 3. Funzionamento di Suxfs 4. suxfs.c 1. Scopo del gioco Questo articolo si pone l'obiettivo di illustrare come sia possibile scrivere un semplice filesystem fatto in casa. Lo scopo di questo progetto è quello di permettere al lettore di analizzare la struttura e il funzionamento di un filesystem semplificato, nonchè permettere al sottoscritto di superare l'esame di sistemi operativi :) Il filesystem in questione ci permette di memorizzare porzioni di dati in file organizzati in una struttura gerarchica a directory secondo la convenzione adottata dai sistemi Unix. E' inoltre possibile effettuare altre operazioni elementari quali la rimozione di file e directory o ottenere la lista dei file presenti nel filesystem. 2. Il filesystem Cos'è un filesystem? Per filesystem si intende l'insieme delle strutture dati e i metodi di accesso utilizzati per memorizzare e accedere alle informazioni conservate in un dato supporto. Perchè scrivere un proprio filesystem? Per imparare come funziona, per venire a diretto contatto con le problematiche inerenti la progettazione e l'implementazione di un filesystem, per implementare delle funzionalità non previste. Di cosa ho bisogno? L'unica cosa indispensabile per implementare un proprio filesytem è una discreta conoscenza di un linguaggio di programmazione, nel mio caso ho utilizzato il C che si presta piuttosto bene per questo tipo di applicazioni. 2.1 Struttura del filesystem A questo punto eviterei di dilungarmi ulterioriormente in chiacchiere ma passerei all'analisi della struttura che compone il nostro filesystem. Per semplificare l'esposizione e facilitare il lettore nella comprensione dell'argomento ho scelto un approcio basato su esempi. D'ora in avanti ogni componente del filesystem verrà rappresentato per mezzo di grafica ASCII secondo un preciso modello logico che mi appresto a descrivere. Possiamo suddividere il filesystem in questione in quattro componenti principali, ognuno dei quali ricopre una particolare funzione. Segue una breve descrizione di tali componenti accompagnata dalla rappresentazione grafica del proprio modello logico: 1) puntatore al primo inode libero: la sua funzione è quella di indicare la posizione del primo inode libero all'interno dell'array di inode; _ |_| ^ | |___ puntatore al primo inode libero 2) array di inode: è un array di puntatori, ognuno dei quali può puntare al prossimo inode libero nell'array oppure ad una directory oppure ancora ad un file; _______________________ |__|__|__|__|__|__|__|__| ^ | |___ array di inode 3) mappa blocchi: è un array di interi, ogni elemento indica se il relativo blocco è libero o occupato, la mappa è composto da un numero di elementi pari al numero di blocchi, di conseguenza l'n-esimo elemento della mappa si riferisce allo stato dell'n-esimo blocco del filesystem; _________________________ |_|_|_|_|_|_|_|_|_|_|_|_|_| ^ | |___ mappa blocchi 4) array di blocchi: è un array di elementi che possono contenere dei dati (es. parte del contenuto di un file) oppure la struttura di una directory oppure ancora un array di puntatori ad altri blocchi; _____________________________ |____|____|____|____|____|____| ^ | |___ array di blocchi Non posso negare il fatto che la descrizione precedente sia alquanto imprecisa tuttavia ho preferito semplificare le cose per poi analizzarle nel dettaglio qualora ve ne fosse bisogno. Iniziamo analizzando la funzione init() alla quale viene delegato il compito di inizializzare l'intera struttura del nostro filesystem. /* Provvede all'inizializzazione delle strutture fondamentali del filesystem */ int init(char *file){ int fd, i; Ptr_Inode free_inode; Map map; Inode inode[INODE_NO]; Block blocchi[BLOCK_NO]; if( (fd = open(file, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU)) < 0){ printf("open error\n"); exit(0); } /* Inizializzo il puntatore al primo inode libero. inode[0] contiene il puntatore al directory block relativo alla root directory e risulta pertanto occupato, inode[1] e' invece libero */ free_inode = 1; lseek(fd, 0, SEEK_SET); if(write(fd, &free_inode, PTR_SIZE) < 0){ printf("write error\n"); exit(0); } . . . Come prima cosa questa funzione apre il file che conterrà il nostro filesystem virtuale (e lo crea se necessario) e che verrà utilizzato per contenerne la struttura e i dati che lo popoleranno. Successivamente impostiamo il valore del puntatore al primo inode libero, tale valore è pari a 1 ovvero il secondo inode (a partire da 0) in quanto il primo dovrà puntare al blocco contenente la directory radice del nostro filesystem. Come possiamo osservare in realtà il puntatore al primo inode altro non è che un valore int che indica la posizione dell'inode all'intero del file contenente la struttura del nostro filesystem. Ne risulta che lo stato attuale del nostro filesystem possa essere rappresentato in questo modo: _ |1| ^ | |___ puntatore al primo inode libero Prima di procede all'analisi della prossima porzione di codice è necessaria una premessa, o meglio un approfondimento riguardo la struttura dell'inode. Prendiamo il nostro array di inode: _______________________ |__|__|__|__|__|__|__|__| ^ | |___ array di inode Ogni elemento che compone l'array è una union dichiarata in questo modo: typedef union { Ptr_Inode free; Inode_Used used; } Inode; Pertanto, ogni inode può essere un puntatore al prossimo inode libero (Ptr_Inode free) oppure un inode utilizzato (Inode_Used used). Nel caso di un puntatore al prossimo inode libero avremmo qualcosa del genere: _______________________ |_1|__|__|__|__|__|__|__| | ^ |___| Nel caso, invece, in cui l'inode sia utilizzato si possono presentare due differenti configurazioni a seconda che esso si riferisca ad un file o ad una directory: typedef struct { double dimensione; Ptr_Block ptr[FILE_PTR_NO]; Ptr_Block ptr_index; } File; typedef union { File file; Ptr_Block dir; } Inode_Type; typedef struct { int flag; Inode_Type type; } Inode_Used; Come possiamo vedere Inode_Used è una struttura composta da un intero e da una union. La variabile di tipo intero flag ha la funzione di indicare se l'inode utilizzato si riferisce ad un file (flag = 0) o ad una directory (flag = 1). L'union rappresentata dal campo type sarà acceduta a seconda del valore assunto dalla variabile flag. Di conseguenza il singolo inode potrà apparire come segue nel caso di una directory: array inode array blocchi _______________________ _____________________________ |__|__|__|__|__|__|__|__| |____|____|____|____|____|____| / \ ^ /_____\ | |__1__| flag | |__0__| type.dir | | | |______________________________| oppure nel seguente modo nel caso di un file: array inode array blocchi _______________________ _____________________________ |__|__|__|__|__|__|__|__| |____|____|____|____|____|____| / \ ^ ^ ^ /_____\ | | | |__0__| flag | | | |__x__| dimensione | | | |__0__| ptr[0] ___________________| | | |__1__| ptr[1] ________________________| | ~ ~ | ~_____~ | |__N__| ptr[FILE_PTR_NO-1] ____________________________| |_NULL| ptr_index Torniamo a questo punto al nostro codice: /* Inizializzo la lista degli inode liberi */ bzero(inode, INODE_SIZE * INODE_NO); for(i = 0; i < INODE_NO; i++) inode[i].free = i + 1; /* Faccio puntare il primo inode al primo data block che conterra' le directory entry relative alla root directory */ inode[0].used.flag = 1; inode[0].used.type.dir = 0; if(write(fd, inode, INODE_SIZE * INODE_NO) < 0){ printf("write error\n"); exit(0); } . . . Vediamo che la funzione init() procede ora all'inizializzazione dell'array di inode in modo tale che ognuno di essi punti al successivo inode libero. Inoltre, il primo inode viene utilizzato e viene fatto puntare al primo blocco che conterrà la struttura della directory radice. Lo stato attuale del nostro filesystem al termine dell'esecuzione di queste poche righe di codice può essere riassunto così: ________ | | | v__ __ __ | | | | | | | |_ ___|__v__|__v__|__v____ |1| |__|_2|_3|_4|_5|_6|_7|_8| / \ | ^ | ^ | ^ ^ /_____\ |__| |__| |__| | |__1__| flag | |__0__| type.dir | | | |______________________________| /* Inizializzo a 0 il bitmap e pongo a 1 il bit relativo alla root dir */ bzero(map, MAP_SIZE); map[0] = 1; if(write(fd, map, MAP_SIZE) < 0){ printf("write error\n"); exit(0); } . . . Viene creata la mappa dei blocchi, il primo elemento della mappa viene posto a 1 per segnalare il fatto che il primo blocco dell'array di blocchi sarà occupato dalla directory radice. ________ | | | v__ __ __ | | | | | | | |_ ___|__v__|__v__|__v____ _______ |1| |__|_2|_3|_4|_5|_6|_7|_8| |1|0|0|0| / \ | ^ | ^ | ^ ^ /_____\ |__| |__| |__| | |__1__| flag | |__0__| type.dir | | | |_______________________________________| /* Inizializzo il directory block relativo alla root directory. Inserisco le entry relative alla current e alla parent dir */ bzero(blocchi, BLOCK_SIZE * BLOCK_NO); strncpy(blocchi[0].dirent[0].filename, ".", FILENAME_LEN - 1); blocchi[0].dirent[0].inode = 0; strncpy(blocchi[0].dirent[1].filename, "..", FILENAME_LEN - 1); blocchi[0].dirent[1].inode = 0; /* Inizializzo i restanti data block del filesystem */ if(write(fd, blocchi, BLOCK_SIZE * BLOCK_NO) < 0){ printf("write error\n"); exit(0); } } Infine, alloco l'array di blocchi e inizializzo la directory radice inserendo le due entry che costituiscono il default per ogni directory, ovvero l'entry . (current dir) e .. (parent dir). E' importante notare che la directory root rappresenta un caso paricolare inquanto la sua directory parent è se stessa. ________ | | | v__ __ __ | | | | | | | |_ ___|__v__|__v__|__v____ _______ ___________________ |1| |__|_2|_3|_4|_5|_6|_7|_8| |1|0|0|0| |____|____|____|____| / \ | ^ | ^ | ^ / \ /_____\ |__| |__| |__| /______\ |__1__| flag |. __|0|____ |__0__| type.dir |.. _|0|____|_ | ^ ^ |____|_| | | | | | ~ ~ | | | | | ~______~ | | | | | |____|_| | | | | | |____|_| | | | | | ^ | | | | |_____________________________________|________| | | |_______________________________________|__________| | | | | |_________________________________________| Le strutture fondamentali del nostro filesystem sono ora inizializzate a dovere, non ci resta che popolare il sistema con i nostri dati. Passiamo quindi all'analisi di una particolare porzione della funzione copy() che provvede alla copia dei dati all'interno del filesystem: /* ...altrimenti copia il file specificato nel filesystem virtuale */ else if(strncmp(dst, "VD:", 3) == 0){ dst+=3; if((fd = open(src, O_RDONLY)) <= 0){ printf("open error\n"); exit(0); } /* Ricava la directory genitore e il nome del file di destinazione della copia */ dir = getParentFromPath(fs, dst, file); . . . La funzione getParentFromPath() prende in ingresso il pathname assoluto della destinazione in cui si vuole copiare il file (dst) e restituisce due valori: un puntatore all'inode di tale directory (dir) e il nome del file di destinazione privato del proprio path (file). /* Controllo se esiste un file con lo stesso nome e interrompo la copia nel caso la destinazione contenga gia' una voce per tale file. Il controllo se pur presente nella funzione newDirEntry() viene eseguito anticipatamente per inibire l'allocazione di inode e data block che poi risulterebbero inutilizzati */ checkIfExist(fs, dir, file); /* Scrive il contenuto del file nel filsystem virtuale */ inode = writeFileToInode(fs, fd); . . . Dopo aver controllato che non esista già un file con lo stesso nome nella stessa directory, scrive il contenuto del file nell'array di blocchi e memorizza il valore dell'inode restituito dalla funzione writeFileToInode() ottenuto a sua volta con una chiamata a getFreeInode() che restituisce il primo inode libero. /* Aggiunge una directory entry per il file appena creato nella directory restituita come valore di ritorno da getParentFromPath() */ newDirEntry(fs, dir, file, inode); } . . . Infine viene aggiunta una nuova entry nella parent directory per il file appena creato, l'entry sarà costituita da due valori: il filename e il puntatore all'inode del file. Lo stato attuale del nostro filesystem viene riassunto con maggiore chiarezza nello schema seguente: ____________________________________________________________ | | | | __v__ | |__0__| flag | |__x__| dimensione | |__0__| ptr[0] _______________________________ | |__1__| ptr[1] _______________________________|____ | ~ ~ ___v____v____ | ~_____~ |conten|o nel | | |_NULL| ptr[n] |uto de|filesy| | |_NULL| ptr_index |l file|stem | | _____| | __ __ |copiat|______| | | \ /| | | | | \ / | |_ __\__/_v_|__v__|__v____ _______ ____\_________/____ | |2| |__|_2|_3|_4|_5|_6|_7|_8| |1|1|1|0| |____|____|____|____| | / \ | ^ | ^ | ^ / \ | /_____\ |__| |__| |__| /______\ | |__1__| flag |. __|0|____ | |__0__| type.dir |.. _|0|____|_ | | ^ ^ |file|1|____|_|___________| | | | ~ ~ | | | | | ~______~ | | | | | |____|_| | | | | | |____|_| | | | | | ^ | | | | |_____________________________________|________| | | |_______________________________________|__________| | | | | |_________________________________________| 2.2 Strutture dati Ora che abbiamo osservato le modalità con le quali i dati vengono memorizzati all'interno del filesystem possiamo passare all'analisi delle strutture dati da esso utilizzate. Come abbiamo avuto modo di vedere nel precedente paragrafo, durante la fase di inizializzazione verrà istanziato un file contenente le strutture dati fondamentali del nostro fs. La sua dimensione dipenderà da alcune costanti definite all'interno del file sorgente: #define BLOCK_NO 2048 il numero di blocchi presenti nel filesystem, tale valore va scelto a seconda della quantità di dati che si desidera memorizzare; #define INODE_NO 256 il numero di inode presenti nel filesystem, tale valore va scelto in funzione del numero di file e directory che si intendono creare; #define DATA_SIZE 1024 la dimensione massima di dati che ogni blocco è in grado di contenere, tale valore va scelto in base alla dimensione media assunta dai file che si desidera memorizzare nel filesystem. Prima di procedere alla compilazione del sorgente possiamo intervenire su tali parametri modificandoli in modo coerente. Possiamo inoltre calcolare la dimensione richiesta per memorizzare il nostro filesystem così: fs size = PTR_SIZE + INODE_NO * INODE_SIZE + MAP_SIZE + + BLOCK_NO * BLOCK_SIZE Il filesytem usa il file come forma di astrazione per rappresentare i dati che vengono memorizzati, questa convenzione logica permette di attribuire un nome univoco ad ogni porzione di dati memorizzata in modo da facilitarne la ricerca e la classificazione. Vediamo come si presenta la struttura di un generico file: #define FILE_PTR_NO 4 #define DATA_SIZE 1024 #define N 10 typedef char Data[DATA_SIZE]; typedef struct { double dimensione; Ptr_Block ptr[FILE_PTR_NO]; Ptr_Block ptr_index; } File; typedef union { Ptr_Block indice[N]; Dirent dirent[DIR_ENTRY]; Data data; } Block; ____________________________ | | struct File | data data data data | data data data _____ | _____________________v____________ _ _ _ |__0__| flag | |____|____|____|____|____|____|____|_ _ _ |__x__| dimensione | ^ ^ ^ ^ / \ ^ ^ ^ |__0__| ptr[0] _______|________| | | | /______\ | | | |__1__| ptr[1] _______|_____________| | | |___5__|_| | | |__2__| ptr[2] _______|__________________| | |___6__|_____| | |__3__| ptr[4] _______|_______________________| |___7__|__________| |__4__| ptr_index ____| |___8__|____________... |___9__|____________... |__10__|____________... |__11__|____________... |__12__|____________... |__13__|____________... |__14__|____________... indice Come possiamo vedere ogni file può indirizzare un massimo di quattordici blocchi, quattro ad accesso diretto e dieci ad accesso indicizzato, ognuno dei quali può contenere un massimo di 1024 byte. Ne consegue che la grandezza massiama che può assumere un file nel nostro filesystem può essere calocalta in questo modo: max file size = (FILE_PTR_NO + N) * DATA_SIZE = (4 + 10) * 1024 = 14 Kb Nel sorgente originale tali costanti sono già state modificate a volori più sensati in modo da poter ospitare file di almeno 220 Kb. Tuttavia agendo sulle costanti FILE_PTR_NO, DATA_SIZE e N è possibile adattare il filesystem alle proprie esigenze. 3. Funzionamento di Suxfs Se volete testare sulla vostra macchina il funzionamento di Suxfs dovete compilarlo con il comando: $ gcc suxfs.c -o suxfs Per prima cosa dovete provvedere all'inizializzazione del filesystem per mezzo del comando: $ ./suxfs -i Terminata la fase di inizializzazione è possibile eseguire alcune operazioni elementari come la copia di dati nel/dal filesystem, provvedere alla creazione di file e directory e alla loro rimozione, listare il contenuto dei file al suo interno. Seguono alcuni esempi: $ ./suxfs -c in.txt VD:/in.txt Se richiamato con l'opzione -c il programma suxfs copia il file in.txt nel filesystem virtuale che per convenzione viene rappresentato dalla sigla VD: (virtual disk). $ ./suxfs -c VD:/in.txt out.txt Stessa cosa ma questa volta il file viene copiato dal filesystem virtuale nel file out.txt. E' necessario precisare che in entrambi i casi il nome del file di destinazione NON può essere omesso. $ ./suxfs -l VD:/ . .. in.txt Lo switch -l ci permette di listare il contenuto di una directory, in questo caso la directory radice. $ ./suxfs -m VD:/dirname Con l'opzione -m possiamo creare una nuova directory. $ ./suxfs -r VD:/in.txt L'opzione -r, invece, ci permette di rimuovere un file o una directory insieme a tutto il suo contenuto. 4. suxfs.c /* * suxfs.c: Semplice filesystem che runna in userspace. * * * Copyright (c) 2003, eazy * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the * distribution. * * * Neither the name of the Networks Associates Technology, Inc nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. * */ #include #include #include #include #include #include #include #define BLOCK_NO 2048 #define INODE_NO 256 #define PTR_BLOCK_NULL BLOCK_NO #define PTR_INODE_NULL INODE_NO #define FILENAME_LEN 15 #define PATHNAME_LEN 255 #define DIR_ENTRY 128 #define FILE_PTR_NO 10 #define DATA_SIZE 1024 #define N 100 typedef int Ptr_Block; typedef int Ptr_Inode; typedef struct { double dimensione; Ptr_Block ptr[FILE_PTR_NO]; Ptr_Block ptr_index; } File; typedef union { File file; Ptr_Block dir; } Inode_Type; typedef struct { int flag; Inode_Type type; } Inode_Used; typedef union { Ptr_Inode free; Inode_Used used; } Inode; typedef int Map[BLOCK_NO]; typedef struct { char filename[FILENAME_LEN]; Ptr_Inode inode; } Dirent; typedef char Data[DATA_SIZE]; typedef union { Ptr_Block indice[N]; Dirent dirent[DIR_ENTRY]; Data data; } Block; #define PTR_SIZE sizeof(Ptr_Block) #define BLOCK_SIZE sizeof(Block) #define INODE_SIZE sizeof(Inode) #define MAP_SIZE sizeof(Map) void moveToBlock(int fd, Ptr_Block ptr){ lseek(fd, PTR_SIZE + INODE_SIZE * INODE_NO + MAP_SIZE + BLOCK_SIZE * ptr, SEEK_SET); } void moveToInode(int fd, Ptr_Inode ptr){ lseek(fd, PTR_SIZE + INODE_SIZE * ptr, SEEK_SET); } void moveToMap(int fd){ lseek(fd, PTR_SIZE + INODE_SIZE * INODE_NO, SEEK_SET); } /* Restituisce un puntatore al primo data block libero e aggiorna il relativo bit nella mappa dei blocchi. Se non sono data block liberi restituisce -1 */ Ptr_Block getFreeBlock(int fd){ int r; Ptr_Block i; Map bitmap; moveToMap(fd); if( (r = read(fd, bitmap, MAP_SIZE)) <= 0){ printf("read error getFreeBlock: %d\n", r); exit(0); } for(i = 0; i < BLOCK_NO; i++) if(bitmap[i] == 0) break; if(i < BLOCK_NO){ bitmap[i] = 1; moveToMap(fd); if(write(fd, bitmap, MAP_SIZE) <= 0){ printf("write error\n"); exit(0); } return i; } else return -1; } /* Restituisce un puntatore al primo inode libero e aggiorna la lista degli inode in maniera coerente. Se non ci sono inode liberi restituisce -1 */ Ptr_Inode getFreeInode(int fd){ Ptr_Inode ptr; Inode inode; lseek(fd, 0, SEEK_SET); if(read(fd, &ptr, PTR_SIZE) <= 0){ printf("read error getFreeInode\n"); exit(0); } if(ptr != PTR_INODE_NULL){ moveToInode(fd, ptr); if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error getFreeInode\n"); exit(0); } lseek(fd, 0, SEEK_SET); if(write(fd, &inode.free, PTR_SIZE) <= 0){ printf("write error\n"); exit(0); } return ptr; } else return -1; } /* Verifica se esiste un dato filename esiste in una directory. L'argomento dir specifica l'inode della directory, mentre filename e' un puntatore al nome del file di cui eseguire il check */ void checkIfExist(int fd, Ptr_Inode dir, char *filename){ int i; Inode inode; Block blocco; moveToInode(fd, dir); if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error checkIfExist\n"); exit(0); } /* Verifico che il secondo argomento passato alla funzione sia effettivamente una directory, in caso contrario restituisco un errore e termino il programma */ if(inode.used.flag == 1){ moveToBlock(fd, inode.used.type.dir); } else{ printf("checkIfExist: Impossibile creare il file\n"); exit(0); } if(read(fd, &blocco, BLOCK_SIZE) <= 0){ printf("read error checkIfExist\n"); exit(0); } /* Se esiste una directory entry il cui nome corrisponde a quello cercato restituisco un messaggio di errore e termino il programma al fine di garantire la coerenza del filesystem */ for(i = 0; i < DIR_ENTRY; i++){ if(strcmp(blocco.dirent[i].filename, filename) == 0){ printf("%s esiste gia'\n", filename); exit(0); } } } /* Dato in ingresso un pathname assoluto resituisce un puntatore all'inode del file specificato da tale pathname. Il terzo argomento passato alla funzione consiste in un valore di ritorno che restituisce il nome del file privato del proprio path, l'allocazione dello spazio necessario ad ospitare il valore di ritorno deve avvenire ad opera del chiamante */ Ptr_Inode getFileFromPath(int fd, char *pathname, char *filename){ int i; Ptr_Inode ptr = 0; char *str, path[PATHNAME_LEN], token[FILENAME_LEN]; Block blocco; Inode inode; strncpy(path, pathname, PATHNAME_LEN - 1); /* Elimina da path la prima occorrenza del carattere slash e ritorna il resto della stringa fino al prossimo slash o al fine stringa. Se path e' composto da un singolo carattere slash (root directory) o e' una stringa nulla la funzione ritorna ptr = 0 ovvero l'inode della directory root */ if( (str = strtok(path, "/")) == NULL) return ptr; strncpy(token, str, FILENAME_LEN - 1); moveToInode(fd, ptr); if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error getFileFromPath\n"); exit(0); } /* Verifica che l'inode si riferisca effettivamente ad una directory */ if(inode.used.flag == 1) moveToBlock(fd, inode.used.type.dir); else{ printf("Non e' una directory\n"); exit(0); } if(read(fd, &blocco, BLOCK_SIZE) <= 0){ printf("read error getFileFromPath\n"); exit(0); } /* Cerca nella directory root se esiste una directory entry relativa al token ottenuto dal parsing effettuato da strtok() sul pathname */ for(i = 0; i < DIR_ENTRY; i++){ /* Se esiste ne salva il valore dell'inode in ptr */ if(strcmp(token, blocco.dirent[i].filename) == 0){ ptr = blocco.dirent[i].inode; break; } } if(i == DIR_ENTRY){ printf("getFileFromPath: Path errato\n"); exit(0); } /* Chiama strtok() fino a che path contiene ulteriori token da parsare e ne copia il valore di ritorno nella variabile token. All'uscita dal while ptr puntera' all'inode del file che stiamo cercando e token ne conterra' il nome */ while( (str = strtok(NULL, "/")) != NULL && strncpy(token, str, FILENAME_LEN - 1)){ moveToInode(fd, ptr); if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error getFileFromPath\n"); exit(0); } if(inode.used.flag == 1) moveToBlock(fd, inode.used.type.dir); else{ printf("Non e' una directory\n"); exit(0); } if(read(fd, &blocco, BLOCK_SIZE) <= 0){ printf("read error getFileFromPath\n"); exit(0); } /* Cerca nella directory puntata da ptr se esiste una directory entry relativa al token ottenuto dal parsing effettuato da strtok() sul pathname */ for(i = 0; i < DIR_ENTRY; i++){ /* Se esiste ne salva il valore dell'inode in ptr */ if(strcmp(token, blocco.dirent[i].filename) == 0){ ptr = blocco.dirent[i].inode; break; } } if(i == DIR_ENTRY){ printf("getFileFromPath: Path errato\n"); exit(0); } } /* Copia nella variabile filename il token corrispondente al file specificato da pathname privato del proprio percorso assoluto */ strncpy(filename, token, FILENAME_LEN - 1); /* Vado a NULL terminare la stringa nel caso sia stata troncata a FILENAME_LEN - 1 */ filename[FILENAME_LEN - 1] = 0; return ptr; } /* Dato in ingresso un pathname assoluto resituisce un puntatore all'inode della directory genitore in cui risiede il file specificato da tale pathname. Il terzo argomento passato alla funzione consiste in un valore di ritorno che restituisce il nome del file privato del proprio path, l'allocazione dello spazio necessario ad ospitare il valore di ritorno deve avvenire ad opera del chiamante */ Ptr_Inode getParentFromPath(int fd, char *pathname, char *filename){ int i; Ptr_Inode ptr = 0; char *str, path[PATHNAME_LEN], token[FILENAME_LEN], token_next[FILENAME_LEN]; Block blocco; Inode inode; strncpy(path, pathname, PATHNAME_LEN - 1); /* Elimina da path la prima occorrenza del carattere slash e ritorna il resto della stringa fino al prossimo slash o al fine stringa. Se path e' composto da un singolo carattere slash (root directory) o e' una stringa nulla la funzione ritorna ptr = 0 ovvero l'inode della directory root */ if( (str = strtok(path, "/")) == NULL){ printf("strtok error\n"); exit(0); } /* Salva il valore del token ottenuto dalla prima chiamata a strtok() */ strncpy(token, str, FILENAME_LEN - 1); /* Chiama strtok() fino a che path contiene ulteriori token da parsare e ne copia il valore di ritorno nella variabile token_next. All'uscita dal while ptr puntera' all'inode della directory genitore del file che stiamo cercando e token conterra' il nome di tale file */ while( (str = strtok(NULL, "/")) != NULL && strncpy(token_next, str, FILENAME_LEN - 1)){ moveToInode(fd, ptr); if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error parsePath\n"); exit(0); } if(inode.used.flag == 1) moveToBlock(fd, inode.used.type.dir); else{ printf("Non e' una directory\n"); exit(0); } if(read(fd, &blocco, BLOCK_SIZE) <= 0){ printf("read error parsePath\n"); exit(0); } /* Cerca nella directory puntata da ptr se esiste una directory entry relativa al token ottenuto dalla chiamata a strtok() di DUE volte fa. Il valore di ritorno dell'ultima chiamata a strtok() si trova in token_next mentre il valore di ritorno della chiamata precedente a strtok() si trova in token ed e' di tale valore che ci serviamo per il confronto. All'uscita dal while ptr conterra' il valore dell'inode della directory genitore del file specificato da pathname e token conterra' il nome di tale file */ for(i = 0; i < DIR_ENTRY; i++){ /* Se esiste si sposta al valore specificato dal relativo inode */ if(strcmp(token, blocco.dirent[i].filename) == 0){ moveToInode(fd, blocco.dirent[i].inode); break; } } if(i < DIR_ENTRY){ if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error parsePath\n"); exit(0); } /* Il fatto che token_next non sia NULL ci assicura che il valore di token si riferisce per forza di cose ad una directory. Di consegueza verifichiamo che l'inode si riferisca effettivamente ad una directory e ne salviamo il valore in ptr */ if(inode.used.flag == 1){ ptr = blocco.dirent[i].inode; } else{ printf("Non e' una directory\n"); exit(0); } } else{ printf("getParentFromPath: Path errato\n"); exit(0); } /* Copiamo in token il valore di token_next ottenuto dall'ultima chiamata a strtok() */ strncpy(token, token_next, FILENAME_LEN - 1); } if(filename != NULL){ /* Copia nella variabile filename il token corrispondente al file specificato da pathname privato del proprio percorso assoluto */ strncpy(filename, token, FILENAME_LEN - 1); /* Vado a NULL terminare la stringa nel caso sia stata troncata a FILENAME_LEN - 1 */ filename[FILENAME_LEN - 1] = 0; } return ptr; } /* Provvede alla creazione di una nuova directory. Viene passato come secondo argomento alla funzione un puntatore all'inode della directory genitore e restituisce come valore di ritorno un puntatore al proprio inode ottenuto tramite una chiamata a getFreeInode() */ Ptr_Inode writeDirToInode(int fd, Ptr_Inode ptr_parent){ Ptr_Inode ptr; Inode inode; Block blocco; /* Inizializza il campo flag (file = 0, directory = 1) e il puntatore al data block contentente le directory entry */ inode.used.flag = 1; if( (inode.used.type.dir = getFreeBlock(fd)) < 0){ printf("No free block\n"); exit(0); } /* Ottiene un inode libero (se esiste), altrimenti restituisce un errore e termina il programma */ if( (ptr = getFreeInode(fd)) < 0){ printf("No free inode\n"); exit(0); } /* Crea le directory entry relative alla directory corrente (.) e alla directory genitore (..) e le inizializza ai relativi valori di inode */ bzero(&blocco, sizeof(Block)); strncpy(blocco.dirent[0].filename, ".", FILENAME_LEN); blocco.dirent[0].inode = ptr; strncpy(blocco.dirent[1].filename, "..", FILENAME_LEN); blocco.dirent[1].inode = ptr_parent; /* Scrive le modifiche alla lista degli inode e al nuovo data block allocato su file */ moveToInode(fd, ptr); if(write(fd, &inode, INODE_SIZE) <= 0){ printf("write error writeDirToInode\n"); exit(0); } moveToBlock(fd, inode.used.type.dir); if(write(fd, &blocco, BLOCK_SIZE) <= 0){ printf("write error writeDirToInode\n"); exit(0); } return ptr; } /* Rimuove il file o la directory (e tutto il suo contenuto) puntato da filename. La variabile parent contiene il valore dell'inode della directory genitore nella quale e' contenuto il file o la directory da rimuovere */ void rm(int fd, Ptr_Inode parent, char *filename){ int i, count; Map map; Ptr_Inode ptr, ptr_first; Ptr_Block parent_block; Inode inode, inode_list[INODE_NO]; Block blocco, index, blocco_rm; moveToInode(fd, parent); if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error rm\n"); exit(0); } /* Salvo il puntatore al blocco della directory genitore contenente la directory entry relativa al file o alla directory da rimuovere */ parent_block = inode.used.type.dir; /* Controllo che il valore specificato come inode della directory genitore si riferisca effettivamente ad una directory */ if(inode.used.flag == 1){ moveToBlock(fd, parent_block); if(read(fd, &blocco, BLOCK_SIZE) <= 0){ printf("read error rm\n"); exit(0); } /* Cerco il file da rimuovere tra tutte le directory entry della directory genitore fornita come argomento della funzione. */ for(i = 0; i < DIR_ENTRY; i++) /* Se il file o la directory da rimuovere esiste ne salvo il valore dell'inode e azzero la directory entry relativa a tale file */ if(strcmp(blocco.dirent[i].filename, filename) == 0){ ptr = blocco.dirent[i].inode; strncpy(blocco.dirent[i].filename, "", FILENAME_LEN - 1); blocco.dirent[i].inode = 0; break; } if(i < DIR_ENTRY){ moveToInode(fd, ptr); if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error rm\n"); exit(0); } /* Verifico se l'elemento da rimuovere e' un file oppure una directory */ if(inode.used.flag == 0){ /* se e' un file... */ moveToMap(fd); if(read(fd, &map, MAP_SIZE) <= 0){ printf("read error rm\n"); exit(0); } /* Vado a marcare come libero nella mask ogni data block puntato dall'indice dei blocchi diretti */ count = 0; while(count < FILE_PTR_NO && inode.used.type.file.ptr[count] != PTR_BLOCK_NULL){ map[inode.used.type.file.ptr[count]] = 0; count++; } /* Vado a marcare come libero nella mask ogni data block puntato dall'indice dei blocchi indiretti */ if(count == FILE_PTR_NO && inode.used.type.file.ptr_index != PTR_BLOCK_NULL){ moveToBlock(fd, inode.used.type.file.ptr_index); if(read(fd, &index, BLOCK_SIZE) <= 0){ printf("read error rm\n"); exit(0); } i = 0; while(i < N && index.indice[i] != PTR_BLOCK_NULL){ map[index.indice[i]] = 0; i++; } /* Dealloco il data block contenente l'indice ai blocchi indiretti */ map[inode.used.type.file.ptr_index] = 0; } } /* altrimenti se e' una directory... */ else if(inode.used.flag == 1){ moveToBlock(fd, inode.used.type.dir); if(read(fd, &blocco_rm, BLOCK_SIZE) <= 0){ printf("read error rm\n"); exit(0); } /* Richiamo ricorsivamente la funzione rm() per ogni directory entry della directory da rimuovere. Skippo i valori nulli e quelli relativi alla directory corrente e a quella genitore */ for(i = 0; i < DIR_ENTRY; i++) if(strcmp(blocco_rm.dirent[i].filename, "") != 0 && strcmp(blocco_rm.dirent[i].filename, ".") != 0 && strcmp(blocco_rm.dirent[i].filename, "..") != 0) rm(fd, ptr, blocco_rm.dirent[i].filename); moveToMap(fd); if(read(fd, &map, MAP_SIZE) <= 0){ printf("read error rm\n"); exit(0); } /* Vado a marcare come libero il data block relativo alla directory rimossa */ map[inode.used.type.dir] = 0; } else{ printf("Tipo file non supportato\n"); exit(0); } /* Provvedo ad aggiornare in maniera coerente la lista degli inode liberi inserendo l'inode liberatosi in seguito alla rimozione del file */ lseek(fd, 0, SEEK_SET); if(read(fd, &ptr_first, PTR_SIZE) <= 0){ printf("read error rm\n"); exit(0); } moveToInode(fd, 0); if(read(fd, &inode_list, INODE_SIZE * INODE_NO) <= 0){ printf("read error rm\n"); exit(0); } /* Se l'inode che ho liberato in seguito alla rimozione del file precede il primo inode libero nella lista, eseguo un inserimento in testa... */ if(ptr < ptr_first){ inode_list[ptr].free = ptr_first; ptr_first = ptr; lseek(fd, 0, SEEK_SET); if(write(fd, &ptr_first, PTR_SIZE) <= 0){ printf("write error rm\n"); exit(0); } } /* ...altrimenti eseguo un inserimento in coda */ else{ i = ptr_first; while(inode_list[i].free < ptr) i = inode_list[i].free; inode_list[ptr].free = inode_list[i].free; inode_list[i].free = ptr; } /* Scrivo su file le modifiche apportate alla mappa dei data block, alla directory genitore e alla lista degli inode */ moveToMap(fd); if(write(fd, &map, MAP_SIZE) <= 0){ printf("write error rm\n"); exit(0); } moveToBlock(fd, parent_block); if(write(fd, &blocco, BLOCK_SIZE) <= 0){ printf("write error rm\n"); exit(0); } moveToInode(fd, 0); if(write(fd, &inode_list, INODE_SIZE * INODE_NO) <= 0){ printf("write error rm\n"); exit(0); } } else{ printf("Impossibile trovare il file o la dir da rimuovere\n"); exit(0); } } else{ printf("Non e' una directory\n"); exit(0); } } /* Crea una nuova directory entry. Attraverso la variabile dir viene passato alla funzione il valore dell'inode della directory in cui si vuole creare una entry composta dalla coppia di valori passati attraverso *dst e inode_entry. I due volori rappresentano rispettivamente il nome del file e l'inode che compongono l'entry */ int newDirEntry(int fd, Ptr_Inode dir, char *dst, Ptr_Inode inode_entry){ int i; Block blocco; Inode inode; moveToInode(fd, dir); if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error newDirEntry\n"); exit(0); } /* Verifico che il valore passato alla funzione si riferisca effettivamente all'inode di una directory, altrimenti restituisco un errore e termino il programma */ if(inode.used.flag == 1){ moveToBlock(fd, inode.used.type.dir); } else{ printf("newDirEntry: Impossibile creare il file\n"); exit(0); } if(read(fd, &blocco, BLOCK_SIZE) <= 0){ printf("read error newDirEntry\n"); exit(0); } /* Se esiste una directory entry il cui nome corrisponde a quello della entry da creare restituisco un messaggio di errore e termino il programma */ for(i = 0; i < DIR_ENTRY; i++){ if(strcmp(blocco.dirent[i].filename, dst) == 0){ printf("%s esiste gia'\n", dst); exit(0); } } /* Inizializzo la prima directory entry libera nel genitore con i valori passati come argomento della funzione */ for(i = 0; i < DIR_ENTRY; i++){ if(strcmp(blocco.dirent[i].filename, "") == 0){ strncpy(blocco.dirent[i].filename, dst, FILENAME_LEN); blocco.dirent[i].inode = inode_entry; break; } } /* Se ho trovato una directory entry libera nel genitore scrivo su file le modifiche apportate */ if(i < DIR_ENTRY){ moveToBlock(fd, inode.used.type.dir); if(write(fd, &blocco, BLOCK_SIZE) <= 0){ printf("write error\n"); exit(0); } } else{ printf("nessuna entry disponibile nella directory\n"); exit(0); } } /* Crea la directory specificata da pathname */ void makedir(int fd, char *pathname){ Ptr_Inode parent, new_inode; char dirname[FILENAME_LEN]; if(pathname == NULL){ printf("list: argomento non valido\n"); exit(0); } if(strncmp(pathname, "VD:", 3) == 0) pathname+=3; else{ printf("makedir: Path errato\n"); exit(0); } /* Ricava da pathname il valore relativo all'inode della directory genitore che dovra' ospitare la directory creata dirname is a return value */ parent = getParentFromPath(fd, pathname, dirname); /* Controlla che la directory genitore non contenga gia' un file o una directory con lo stesso nome della directory da creare */ checkIfExist(fd, parent, dirname); /* Alloco l'inode e il data block necessari ad ospitare la nuova directory */ new_inode = writeDirToInode(fd, parent); /* Scrivo nella directory genitore una entry relativa alla directory appena creata, contenente il nome della directory e il proprio inode */ newDirEntry(fd, parent, dirname, new_inode); } /* Se pathaname si riferisce ad un file list() stampa il nome del file, altrimenti se si riferisce ad una directory list() stampa ogni directory entry contenuta nella directory specificata da pathname */ void list(int fd, char *pathname){ int i; Ptr_Inode ptr; Inode inode; Block blocco; char file[FILENAME_LEN]; if(pathname == NULL){ printf("list: argomento non valido\n"); exit(0); } if(strncmp(pathname, "VD:", 3) == 0) pathname+=3; else{ printf("list: Path errato\n"); exit(0); } ptr = getFileFromPath(fd, pathname, file); moveToInode(fd, ptr); if(read(fd, &inode, INODE_SIZE) <= 0){ printf("read error list\n"); exit(0); } /* Se il pathname si riferisce ad un file printa a schermo il nome del file */ if(inode.used.flag == 0) printf("%s\n", file); /* Se il pathname di riferisce ad una directory... */ else if(inode.used.flag == 1){ moveToBlock(fd, inode.used.type.dir); if(read(fd, &blocco, BLOCK_SIZE) <= 0){ printf("read error list\n"); exit(0); } /* ...printa a schermo il nome di ogni entry contenuta nella directory specificata da pathname */ for(i = 0; i < DIR_ENTRY; i++) if(strcmp(blocco.dirent[i].filename, "") != 0) printf("%s\n", blocco.dirent[i].filename); } else{ printf("list: file type not supported\n"); exit(0); } } /* Scrive un file sul filesystem virtuale preoccupandosi di allocare l'inode e i data block necessari alla sua memorizzazione */ Ptr_Inode writeFileToInode(int fd_dst, int fd_src){ Inode inode; Data buf; Ptr_Inode ptr; int k, j, r, i = 0, count = 0, bool = 1; Block blocco[FILE_PTR_NO], index, blocco_index[N]; bzero(buf, sizeof(Data)); bzero(&inode, INODE_SIZE); bzero(blocco, BLOCK_SIZE * FILE_PTR_NO); bzero(blocco_index, BLOCK_SIZE * N); /* Inizializzo a 0 (file = 0, directory = 1) il campo flag della struct Inode_Used e a PTR_BLOCK_NULL sia i puntatori a data block diretti che indiretti */ inode.used.flag = 0; inode.used.type.file.ptr_index = PTR_BLOCK_NULL; for(k = 0; k < FILE_PTR_NO; k++) inode.used.type.file.ptr[k] = PTR_BLOCK_NULL; for(k = 0; k < N; k++) index.indice[k] = PTR_BLOCK_NULL; lseek(fd_src, 0, SEEK_SET); /* Continua a leggere fino a che non raggiunge EOF */ while( (r = read(fd_src, buf, sizeof(Data))) > 0){ inode.used.type.file.dimensione += r; /* Alloca un data block e inizializza un puntatore diretto a tale blocco */ if(count < FILE_PTR_NO){ if( (inode.used.type.file.ptr[count] = getFreeBlock(fd_dst)) < 0){ printf("no free block\n"); exit(0); } /* Copia nel data block i dati precedentemente letti dal file tramite la chiamata a read() */ memcpy(blocco[count].data, buf, sizeof(Data)); bzero(buf, sizeof(Data)); count++; continue; } /* Se FILE_PTR_NO puntatori a data block diretti non dovessero bastare ad indirizzare tutti i data block necessari a contenere il file vengono allocati dei puntatori a data block indiretti */ if(bool && (inode.used.type.file.ptr_index = getFreeBlock(fd_dst)) < 0){ printf("no free block\n"); exit(0); } bool = 0; /* Se N puntatori a data block indiretti non dovessero bastare ad indicizzare tutti i data block necessari a contenere il file visualizza un messaggio di errore e termina il programma */ if(i == N){ printf("file troppo grande\n"); exit(0); } /* Alloca un data block e inizializza un puntatore indiretto a tale blocco */ if( (index.indice[i] = getFreeBlock(fd_dst)) < 0){ printf("no free block\n"); exit(0); } /* Copia nel data block i dati precedentemente letti dal file tramite la chiamata a read() */ memcpy(blocco_index[i].data, buf, sizeof(Data)); bzero(buf, sizeof(Data)); i++; } if(r != 0){ printf("read error writeFileToInode\n"); exit(0); } /* Se ho scritto almeno un data block alloco un inode per il file */ if(count != 0){ if( (ptr = getFreeInode(fd_dst)) < 0){ printf("No free inode\n"); exit(0); } } else{ printf("Nessun dato da leggere\n"); exit(0); } /* Scrivo su file il mio inode nel punto in cui lo avevo precedentemente allocato */ moveToInode(fd_dst, ptr); if(write(fd_dst, &inode, INODE_SIZE) <= 0){ printf("write error\n"); exit(0); } /* Scrivo su file i data block relativi ai puntatori diretti */ j = 0; while(j < FILE_PTR_NO && inode.used.type.file.ptr[j] != PTR_BLOCK_NULL){ moveToBlock(fd_dst, inode.used.type.file.ptr[j]); if(write(fd_dst, &blocco[j], BLOCK_SIZE) <= 0){ printf("write error\n"); exit(0); } j++; } /* Scrivo su file l'indice dei puntatori indiretti */ if(inode.used.type.file.ptr_index != PTR_BLOCK_NULL){ moveToBlock(fd_dst, inode.used.type.file.ptr_index); if(write(fd_dst, &index, BLOCK_SIZE) <= 0){ printf("write error\n"); exit(0); } /* Scrivo su file i data block relativi ai puntatori indiretti */ j = 0; while(j < N && index.indice[j] != PTR_BLOCK_NULL){ moveToBlock(fd_dst, index.indice[j]); if(write(fd_dst, &blocco_index[j], BLOCK_SIZE) <= 0){ printf("write error\n"); exit(0); } j++; } } return ptr; } /* Esporta il contenuto di un file memorizzato nel filesystem virtuale in un file specificato dall'utente */ void writeInodeToFile(int dbfd, Ptr_Inode dir, char *filename, int fd){ int i, count, dim; Inode dir_inode, inode; Block blocco, index; moveToInode(dbfd, dir); if(read(dbfd, &dir_inode, INODE_SIZE) <= 0){ printf("read error writeInodeToFile\n"); exit(0); } /* Verifico che il valore passato alla funzione si riferisca effettivamente all'inode di una directory, altrimenti restituisco un errore e termino il programma */ if(dir_inode.used.flag == 1){ moveToBlock(dbfd, dir_inode.used.type.dir); } else{ printf("Impossibile leggere la directory\n"); exit(0); } bzero(&blocco, BLOCK_SIZE); if(read(dbfd, &blocco, BLOCK_SIZE) <= 0){ printf("read error writeInodeToFile\n"); exit(0); } /* Cerco il file da copiare tra tutte le directory entry della directory genitore fornita come argomento della funzione */ for(i = 0; i < DIR_ENTRY; i++) /* Se lo trovo mi sposto al relativo inode per effettuare una lettura */ if(strcmp(blocco.dirent[i].filename, filename) == 0){ moveToInode(dbfd, blocco.dirent[i].inode); break; } if(i < DIR_ENTRY){ if(read(dbfd, &inode, INODE_SIZE) <= 0){ printf("read error writeInodeToFile\n"); exit(0); } /* Verifico che l'inode si riferisca ad un file */ if(inode.used.flag == 0){ dim = inode.used.type.file.dimensione; /* Scrivo sul file il contenuto dei data block relativi ai puntatori diretti */ count = 0; while(count < FILE_PTR_NO && inode.used.type.file.ptr[count] != PTR_BLOCK_NULL && dim > 0){ moveToBlock(dbfd, inode.used.type.file.ptr[count]); bzero(&blocco, BLOCK_SIZE); if(read(dbfd, &blocco, BLOCK_SIZE) <= 0){ printf("read error writeInodeToFile\n"); exit(0); } if(dim >= sizeof(Data)){ if(write(fd, &blocco.data, sizeof(Data)) <= 0){ printf("write error writeInodeToFile\n"); exit(0); } dim -= sizeof(Data); } else{ if(write(fd, &blocco.data, dim) <= 0){ printf("write error writeInodeToFile\n"); exit(0); } dim = 0; } count++; } /* Se non ci sono puntatori diretti inutilizzati ed e' stato allocato un indice dei puntatori indiretti, scrivo sul file il contenuto dei data block indicizzati dai puntatori indiretti */ if(count == FILE_PTR_NO && inode.used.type.file.ptr_index != PTR_BLOCK_NULL && dim > 0){ moveToBlock(dbfd, inode.used.type.file.ptr_index); if(read(dbfd, &index, BLOCK_SIZE) <= 0){ printf("read error writeInodeToFile\n"); exit(0); } i = 0; while(i < N && index.indice[i] != PTR_BLOCK_NULL && dim > 0){ moveToBlock(dbfd, index.indice[i]); if(read(dbfd, &blocco, BLOCK_SIZE) <= 0){ printf("read error writeInodeToFile\n"); exit(0); } if(dim >= sizeof(Data)){ if(write(fd, &blocco.data, sizeof(Data)) <= 0){ printf("write error writeInodeToFile\n"); exit(0); } dim -= sizeof(Data); } else{ if(write(fd, &blocco.data, dim) <= 0){ printf("write error writeInodeToFile\n"); exit(0); } dim = 0; } i++; } } } else{ printf("Impossibile leggere il file\n"); exit(0); } } else{ printf("File inesistente\n"); exit(0); } } /* Gestisce la copia di file da e verso il filesystem virtuale */ int copy(char *dbfile, char *src, char *dst){ int fd, fs; char file[FILENAME_LEN], src_token[4], dst_token[4]; Ptr_Inode inode, dir; if(src == NULL || dst == NULL){ printf("copy: argomento non valido\n"); exit(0); } if((fs = open(dbfile, O_RDWR)) <= 0){ printf("open error\n"); exit(0); } /* Se la sorgente e' il filesystem virtuale scrivi sul file specificato... */ if(strncmp(src, "VD:", 3) == 0){ src+=3; if((fd = open(dst, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU)) <= 0){ printf("open error\n"); exit(0); } /* Ricava la directory genitore e il nome del file da esportare dal filesystem virtuale */ dir = getParentFromPath(fs, src, file); /* Copia il contenuto del file contenuto nel filesystem virtuale su un file di destinazione */ writeInodeToFile(fs, dir, file, fd); } /* ...altrimenti copia il file specificato nel filesystem virtuale */ else if(strncmp(dst, "VD:", 3) == 0){ dst+=3; if((fd = open(src, O_RDONLY)) <= 0){ printf("open error\n"); exit(0); } /* Ricava la directory genitore e il nome del file di destinazione della copia */ dir = getParentFromPath(fs, dst, file); /* Controllo se esiste un file con lo stesso nome e interrompo la copia nel caso la destinazione contenga gia' una voce per tale file. Il controllo se pur presente nella funzione newDirEntry() viene eseguito anticipatamente per inibire l'allocazione di inode e data block che poi risulterebbero inutilizzati */ checkIfExist(fs, dir, file); /* Scrive il contenuto del file nel filsystem virtuale */ inode = writeFileToInode(fs, fd); /* Aggiunge una directory entry per il file appena creato nella directory restituita come valore di ritorno da getParentFromPath() */ newDirEntry(fs, dir, file, inode); } else{ printf("cp: argomenti comando errati\n"); exit(0); } } /* Provvede all'inizializzazione delle strutture fondamentali del filesystem */ int init(char *file){ int fd, i; Ptr_Inode free_inode; Map map; Inode inode[INODE_NO]; Block blocchi[BLOCK_NO]; if( (fd = open(file, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU)) < 0){ printf("open error\n"); exit(0); } /* Inizializzo il puntatore al primo inode libero. inode[0] contiene il puntatore al directory block relativo alla root directory e risulta pertanto occupato, inode[1] e' invece libero */ free_inode = 1; lseek(fd, 0, SEEK_SET); if(write(fd, &free_inode, PTR_SIZE) < 0){ printf("write error\n"); exit(0); } /* Inizializzo la lista degli inode liberi */ bzero(inode, INODE_SIZE * INODE_NO); for(i = 0; i < INODE_NO; i++) inode[i].free = i + 1; /* Faccio puntare il primo inode al primo data block che conterra' le directory entry relative alla root directory */ inode[0].used.flag = 1; inode[0].used.type.dir = 0; if(write(fd, inode, INODE_SIZE * INODE_NO) < 0){ printf("write error\n"); exit(0); } /* Inizializzo a 0 il bitmap e pongo a 1 il bit relativo alla root dir */ bzero(map, MAP_SIZE); map[0] = 1; if(write(fd, map, MAP_SIZE) < 0){ printf("write error\n"); exit(0); } /* Inizializzo il directory block relativo alla root directory. Inserisco le entry relative alla current e alla parent dir */ bzero(blocchi, BLOCK_SIZE * BLOCK_NO); strncpy(blocchi[0].dirent[0].filename, ".", FILENAME_LEN - 1); blocchi[0].dirent[0].inode = 0; strncpy(blocchi[0].dirent[1].filename, "..", FILENAME_LEN - 1); blocchi[0].dirent[1].inode = 0; /* Inizializzo i restanti data block del filesystem */ if(write(fd, blocchi, BLOCK_SIZE * BLOCK_NO) < 0){ printf("write error\n"); exit(0); } } void print_usage(char *prog){ printf("Usage: %s [ -h ] [ -i ] [ -c [VD:]/path [VD:]/path ]", prog); printf(" [ -l VD:/path ] [ -m VD:/path ] [ -r VD:/path ]\n"); printf("\t-h questo help\n"); printf("\t-i inizializza il filesystem\n"); printf("\t-c copia un file nel/dal filesystem\n"); printf("\t-l elenca i file presenti nella directory\n"); printf("\t-m crea una nuova directory\n"); printf("\t-r rimuove il file o la directory e tutto il suo"); printf(" contenuto\n"); } int main(int argc, char **argv){ int fd, i; Ptr_Inode parent; Block blocco; char opt, filename[FILENAME_LEN]; opterr = 0; if( (opt = getopt(argc, argv, "chilmr")) != -1){ switch(opt){ case 'c': argc -= optind; argv += optind; copy("dbfs", argv[0], argv[1]); break; case 'h': print_usage(argv[0]); break; case 'i': init("dbfs"); break; case 'l': argc -= optind; argv += optind; if( (fd = open("dbfs", O_RDONLY)) < 0){ printf("open error\n"); exit(0); } list(fd, argv[0]); break; case 'm': argc -= optind; argv += optind; if( (fd = open("dbfs", O_RDWR)) < 0){ printf("open error\n"); exit(0); } makedir(fd, argv[0]); break; case 'r': argc -= optind; argv += optind; if( (fd = open("dbfs", O_RDWR)) < 0){ printf("open error\n"); exit(0); } if(argv[0] == NULL){ printf("rm: argomento non valido\n"); exit(0); } if(strncmp(argv[0], "VD:", 3) == 0) argv[0]+=3; else{ printf("rm: Path errato\n"); exit(0); } parent = getParentFromPath(fd, argv[0], filename); rm(fd, parent, filename); break; default: print_usage(argv[0]); } } else print_usage(argv[0]); } :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [0X0A] OQ20031122[0A] :: [0x06][SECURITY] SQL-iNJECTi0N [Master^Shadow] [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. Indice * Introduzione * Capire il problema * Metodologie di testing * I problemi più diffusi o Accessi non desiderati o Visualizzare dati non accessibili o Analisi delle query tramite l'introduzione di errori o Advanced SELECT Injection o INSERT Injection o STORED PROCEDURES Injection o Le feature e i problemi dei DBMS più diffusi * Scrivere codice sicuro [Introduzione] L'evoluzione delle esigenze del pubblico internauta ha comportato l'introduzione di tecnologie dinamiche (creazione di contenuti "on demand") nei più disparati servizi offerti dal web. Sono sempre più numerosi i server, anche gratuiti, che offrono la possibilità di elaborare script per la creazione di contenuti web e sempre più utenti stanno convertendo i propri siti statici in vere e proprie applicazioni residenti sul web. Tali applicazioni utilizzano, nella maggioranza dei casi, dati provenienti dall'utente ed introducono quindi la possibilità di modifica arbitraria delle variabili utilizzate dallo script, rendendo così possibile l'insorgere di vulnerabilità sfruttabili da remoto.In questo paper analizzeremo una fra le più diffuse vulnerabilità delle applicazioni web-related: la vulnerabilità da SQL Injection. [Capire il problema] La maggior parte delle applicazioni web vengono realizzate interfacciando l'applicazione stessa ad un server DBMS ed interagiscono con tali server tramite l'utilizzo del linguaggio standard SQL. SQL Injection è una tecnica di exploiting delle web application che utilizzano dati provenienti dai client nelle query, senza controllare la presenza di caratteri potenzialmente pericolosi. Contrariamente a quello che si può pensare, questo tipo di vulnerabilità è molto diffusa e tale diffusione è spesso dovuta alla mancanza di professionalità di molti programmatori, spesso improvvisati, e alla mancanza di solide basi di programmazione sicura. [Metodologie di testing] Controllare le applicazioni per trovare eventuali vulnerabilità dovute all'SQL Injection potrebbe essere molto complesso ma ci sono casi dove la ricerca di script potenzialmente vulnerabili è molto semplice. Se, ad esempio, l'aggiunta di un apice in un form ritorna una pagina bianca o piena di errori riportati dal server DBMS siamo già ad un buon punto di partenza per quanto comporta la ricerca delle vulnerabilità. Un buon programmatore dovrebbe controllare l'input di ogni variabile e non pensare che l'utente, utilizzatore della web application, formatti correttamente le variabili. Ogni possibile campo dovrebbe essere testato per tutte le vulnerabilità che potrebbero coinvolgerlo, in modo da evitare ripercussioni in tutto lo script, spesso legato ad altri.Un buon metodo di controllo delle applicazioni web potrebbe essere quello di inserire in ogni campo una comando tipico di SQL preceduto da un apice, cercando così eventuali problemi sulle singole variabili. Dopo aver provato i campi uno alla volta potrebbe essere utile riempire il form con dati formalmente corretti e ripetere la procedura "apice+comando" per ogni campo, mantenendo formalmente corretti gli altri campi. Ipotizziamo di passare ad uno script questi parametri formalmente corretti: script.php?nome=pinco&cognome=pallino&email=pinco@pallino.ext Lo script funziona alla perfezione. Proviamo ora a modificare la stringa con un apice nel parametro "nome": script.php?nome='pinco&cognome=pallino&email=pinco@pallino.ext Ipotizzando che lo script sia vulnerabile, siamo andati a modificare i parametri inviati al server DBMS dalla query, modificandone strutturalmente la sintassi (ricordo che l'apice (') è un operatore SQL). Cosa potrebbe accadere utilizzando una stringa di parametri come quella proposta dall'esempio? I risultati potrebbero essere molteplici: dalla semplice pagina bianca ai più disparati errori provenienti dal server DBMS. Potrebbe riportare l'errore che gli altri paramentri non sono stati inseriti o addirittura mostrare dati che non dovrebbero essere visualizzati con quella query. Dopo aver trovato una possibile vulnerabiltà da SQL Injection, la parte più importante è l'interpretazione degli errori. Se l'errore viene generato del server DBMS siamo sicuramente davanti ad una vulnerabilità ad SQL Injection ma gli errori spesso sono tutt'altro che ovvi. Controlliamo sempre gli errori che si riferiscono ad ODBC, alla sintassi, al server SQL. Bisogna inoltre prestare attenzione alle più minimali modifiche della pagina, segno di SQL Injection Exploiting, poichè molti programmatori possono nascondere le informazioni mettendo, ad esempio, eventuali errori negli header del documento HTML e non mostrandone traccia nel body, la parte visualizzata dal browser.Non bisogna fermarsi all'analisi della singola pagina colpita da SQL Injection ma è buona abitudine seguire i link presenti nella pagina, alla ricerca di eventuali ripercussioni sull'intera applicazione ed è importante seguire anche eventuali redirect verso una pagina di errore predefinita, spesso preceduta da una schermata di errore propria del database. [I problemi più diffusi] Passiamo ora all'analisi dei più diffusi problemi di sicurezza dovuti alla vulnerabilità da SQL Injection. [Accessi non desiderati] L'autenticazione delle web application è spesso delegata ad uno script di login che ha il compito di processare la coppia login/password proveniente dal client e di confrontarla con le coppie presenti nel database. In caso di corrispondenza lo script setterà gli appositi flag per consentire il nostro accesso o, nel caso opposto, ci vieterà l'accesso. Consideriamo il seguente codice PHP: $user = $_GET["nome"]; $passwd = $_GET["password"]; $dati = mysql_query("SELECT * FROM utenti WHERE user='".$user."'... ...AND passwd='".$passwd."'"); if (mysql_num_rows($dati) == 0) $logged = 0 else $logged = 1 Questo breve script prende i dati dalla querystring e li mette nella query senza controllare la presenza di eventuali caratteri pericolosi. Ipotizziamo di modificare entrambi gli argomenti passati allo script in " ' OR ''='' " e di far processare la pagina al server. La parte condizionale della query passata al server DBMS (in questo caso MySQL) diventa: ...WHERE user='' OR ''='' AND passwd='' OR ''='' Come potrete ben capire entrambe queste condizioni sono sempre verificate e mysql_num_rows() restituirà un valore sicuramente diverso da zero (se la tabella contiene dati) consentendo così il login a qualsiasi persona a conoscenza di questo problema. [Visualizzare dati non accessibili] Il mancato parsing dei parametri per caratteri maligni ha introdotto la possibilità di editare, a piacimento dell'attaccante, la query verso il database. Abbiamo appena visto che è possibile entrare con il massimo dei privilegi in un'applicazione web ma potremmo decidere di accedere a dati non direttamente accessibili dall'applicazione stessa. Lo standard SQL permette la creazione di SELECT multiple tramite il comando UNION e tale fatto può essere sfruttato per gli scopi dell'attaccante. Prendiamo in esame la seguente query: SELECT nome FROM users WHERE paese='".$var."'" La variabile $var dovrebbe contenere il paese di provenienza degli utenti, dei quali stiamo cercando il nome ma, su di essa, non viene fatto nessun controllo ed è quindi possibile scrivere codice SQL direttamente nella variabile. Nel nostro caso $var conterrà: ' UNION ALL SELECT nome, passwd FROM users WHERE ''=' Vediamo il contenuto della query una volta settata $var: SELECT nome FROM users WHERE paese='' UNION ALL ... ... SELECT nome, passwd FROM users WHERE ''='' Evidentemente la prima SELECT non restituirà nessun record (supponendo che nessun utente ha il campo paese vuoto) mentre la seconda SELECT è incondizionata e restituirà tutte le coppie nome/password. Il problema è quello della visualizzazione dei dati: i dati ora sono stati estratti dal database ma l'applicazione considera solo il campo ""nome"" e non ""password"". Fortunatamente (!) il linguaggio SQL permette l'aliasing dei campi tramite il comando AS che può essere sfruttato per fare l'output dei dati non visualizzabili ordinariamente. [Analisi delle query tramite l'introduzione di errori] Molti server web restituiscono parte delle query in caso di errore. Normalmente questa funzionalità è utile, anzi direi necessaria, durante il debugging delle applicazioni web ma può essere usata impropriamente per analizzare le query e quindi carpire informazioni sulla realizzazione di una web application vulnerabile da SQL Injection. E' sempre utile testare un'applicazione inserendo volutamente errori di sintassi nei campi che interagiscono con l'utente, magari utilizzando costrutti SQL incompleti come "valore'", "'", "'valore", "' OR '", ";" e "0,1,2". [Advanced SELECT Injection] Non sempre le web application elaborano query semplici e lineari. Capita a volte che siano presenti istruzioni racchiuse da parentesi, selezione sulla base di wildcards e campi non direttamente modificabili. Iniettare codice sintatticamente corretto e funzionante all'interno di queste tipologie di query potrebbe richiedere l'utilizzo di piccoli accorgimenti, a volte non immediati. SELECT nome FROM utenti WHERE (paese='campovariabile') Estrapolare dati da una query di questo tipo non può essere fatto con il metodo visto nelle pagine precedenti poichè ci verrebbero segnalati diversi errori di sintassi. Occorre quindi modificare la query utilizzando un campo di questo genere ') UNION ALL SELECT campo FROM altraTabella WHERE (''=' che, sostituita nella precedente, soddisfa perfettamente la sintassi SQL: SELECT nome FROM utenti WHERE (paese='') UNION ALL ... ... SELECT campo FROM altraTabella WHERE (''='') Il trucco sta semplicemente nel completare parentesi e apici con piccoli trucchi in modo da far risultare la sintassi corretta e impostare correttamente le variabili booleane di confronto. Altra sintassi che potrebbe creare problemi è quella dovuta al costrutto di confronto LIKE. SELECT nome FROM utenti WHERE nome LIKE '%campovariabile%' I simboli di percentuale funzionano nelle query SQL come wildcards e un eventuale completamento %% ritornerebbe tutti i record e quindi non sarebbe applicabile il costrutto UNION. Bisogna quindi pensare di inserire una stringa che non risulti in nessuno dei record, come potrebbe essere ad esempio "!?!". Un campo tipico per iniettare codice in questa query potrebbe essere il seguente: !?!% UNION ALL SELECT campo FROM tab WHERE campo LIKE '% Sostituendo la query SQL diventa: SELECT nome FROM utenti WHERE nome LIKE '%!?!% UNION ALL ... ... SELECT campo FROM altraTabella WHERE campo LIKE '%%' [INSERT Injection] Finora abbiamo considerato SQL Injection un problema legato al costrutto SELECT ma possiamo tranquillamente dire che questa vulnerabilità si può estendere a qualsiasi query contentente input dell'utente non appositamente controllato. Un altro costrutto potenzialmente vulnerabile è INSERT, necessario per l'inserimento di nuovi record all'interno di una tabella. Controllare la vulnerabilità di una query di inserimento comporta le stesse tecniche viste per la query di selezione ma la forzatura di queste query potrebbe segnalarci all'amministratore dell'applicazione in quanto verrebbero riportate nei record parti di sintassi SQL. Iniettare codice nella query INSERT consente all'utente smaliziato di prelevare dati da un database e utilizzarli per la propria registrazione. Ad esempio è possibile registrarsi al posto di un altro utente senza aver nessun dato su di lui. L'unico trucco necessario per assicurarsi dell'effettiva estrazione dei dati è quella di visualizzarli (ad esempio tramite il pannello di controllo). Per portare a termine questo tipo di exploit il server DBMS deve supportare le SUBSELECT (ad esempio MySQL non supporta questa feature) e si devono conoscere i nomi di campi e tabella, ricavabili con un po' di reverse engineering. Ipotizziamo di avere la seguente query di inserimento: INSERT INTO tab (nome, cognome) VALUES ('campo1', 'campo2') Se al posto di campo1 inseriamo ' + SELECT nome FROM tab LIMIT 1 + ' e al posto di campo2 inseriamo ' + SELECT cognome FROM tab LIMIT 1 + ' otteniamo una perfetta replica di un record esistente all'interno del database poichè la query in questione diventa: INSERT INTO tab (nome, cognome) VALUES ('' + SELECT nome FROM tab LIMIT 1 + '', '' + SELECT cognome FROM tab LIMIT 1 + '') Per cambiare l'utente da selezionare basta scorrere i record con l'offset offerto dall'istruzione LIMIT o, eventualmente, usare la sintassi NOT IN () per ciclare i record. [STORED PROCEDURES Injection] Exploitare le stored procedures è generalmente molto più semplice che agire sul costrutto SELECT. Le stored procedures sono parti di codice SQL richiamabili nei costrutti SQL tramite l'utilizzo di EXEC e sono praticamente degli script batch, atti ad effettuare operazioni direttamente all'interno del server DBMS. Hanno la particolarità di essere abbastanza veloci e sono molto utilizzate per le operazioni transizionali. Non tutti i server DBMS permettono l'esecuzione delle stored procedures e, per richiamare l'esecuzione delle stesse, è necessario che lo stesso server permetta l'utilizzo di statement multipli (istruzioni SQL distinte separate da un punto e virgola). Diversi server DBMS hanno comunque queste feature e possiedono, inoltre, diverse Stored Procedures predefinite, molte delle quali possono compiere operazioni molto interessanti per un attaccante. Esaminiamo la vulnerabilità più evidente nel server DBMS di MicroSoft, MS SQL Server. Tale server attiva di default innumerevoli stored procedures, fra le quali troviamo xp_cmdshell ovvero un frontend per l'interprete dei comandi dei sistemi basati su kernel NT. Il server in questione supporta statement multipli e basta quindi una qualsiasi vulnerabilità da SQL Injection per accedere al sistema con i permessi del server SQL. Consideriamo la seguente query: SELECT * FROM tab WHERE nome='campovariabile' Terminando il primo costrutto con un nome arbitrario ed applicando la sintassi di EXEC sulla procedura xp_cmdshell otteniamo la sequente query: SELECT * FROM tab WHERE nome='ed';EXEC master.dbo.xp_cmdshell 'cmd.exe comando' Al posto di "comando" possiamo immettere qualsiasi stringa di comando interpretabile dalla shell dei sistemi NT ovvero il sistema è in balia dei nostri comandi. Ovviamente il sistema server SQL di Microsoft non è l'unico ad offrire stored procedures predefinite ma è stato preso in considerazione per la gravità della situazione che combinata con errori di programmazione web permette l'accesso a persone non desiderate. E' quindi bene disabilitare, o rimuovere, le stored procedures non necessarie al corretto funzionamento del server e assicurarsi che i permessi sulle parti vitali del sistema siano più restrittivi possibile. [Le feature e i problemi dei DBMS più diffusi] MySQL Supporta 'INTO OUTFILE' Supporta 'UNION' (dalla versione 4.x) Spesso gira come ""root"" Molti moduli e librerie non supportano statement multipli Oracle Supporta le SubSelect Supporta 'UNION' Supporta le stored procedures Non supporta statement multipli Molte stored procedures preimpostate, alcune delle quali pericolose DB2 Supporta le SubSelect Supporta 'UNION' Supporta le stored procedures Non supporta statement multipli Postgres Supporta 'COPY' se fatto girare come superutente Supporta le SubSelect Supporta 'UNION' Supporta le stored procedures Supporta gli statement multipli MS SQL Server Supporta le SubSelect Supporta 'UNION' Supporta le stored procedures Supporta gli statement multipli Molte store procedures preimpostate sono pericolose [Scrivere codice sicuro] Ogni web application che si rispetti deve seguire piccole norme di programmazione per elevare la sicurezza al massimo possibile. Se l'applicazione riceve input dall'utente questo deve essere processato appropriatamente in modo da escludere eventuali caratteri maligni, che potrebbero interferire con le query previste dai programmatori. Raddoppiare gli apici o farli precedere dal carattere backslash "\" spesso è poco utile e si può trovare il modo di aggirare l'ostacolo. E' quindi necessario lasciar passare solo i caratteri necessari e filtrare tutti gli indesiderati tramite l'utilizzo di parser appositi (magari una regular expression ben congengnata). Ad esempio per lasciar passare solo i caratteri alfabetici la regexp seguente è l'ideale: s/^a-zA-z//g mentre per far passare i soli numeri si può applicare: s/^0-1//g Lasciare solo i caratteri alfanumerici è quasi sempre un'ottima soluzione al nostro problema ma può capitare di aver bisogno di altri tipi di caratteri. In questo caso è bene sostituire ai caratteri la loro codifica nello standard UniCode o nello standard HTML. Ovviamente è sempre bene utilizzare il minimo numero possibile di caratteri non alfanumerici per evitare potenziali problemi di iniezione. Altra feature interessante per aumentare la sicurezza delle web application potrebbe essere l'individuazione di tentativi di iniezione del codice prima del passaggio a stored procedures e a query. All'interno delle stored procedures è bene non creare le query dinamicamente ma cercare di mantenere fisso più codice possibile. Realizzare una procedura di questo tipo richiede la creazione di una procedura contentente la sola query statica ed il semplice passaggio delle variabili come parametri. Ad esempio: CREATE PROC usr_prendiUtentedaNick @nick nvarchar(255) AS SELECT nome, cognome, indirizzo, telefono FROM utenti WHERE nick = @nick RETURN La configurazione del server deve essere più restrittiva possibile e deve evitare la possibilità di accedere liberamente a parti del sistema non normalmente disponibili. Disabilitare le stored procedure non necessarie agli utenti potrebbe essere la soluzione ai nostri problemi ma un'attenta policy di permessi potrebbe evitare la mutilazione eccessiva del server DBMS. [Ringraziamenti] Ringrazio tutti gli amici di RoLUG, HX, OndaQuadra, S.P.I.N.E. e tutti gli sviluppatori di software libero del mondo. Happy hacking to everybody! Master^Shadow :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [0X0A] OQ20031122[0A] :: [0x07][SECURITY] SAM CRACKiNG [h23] [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. what: HOW TO - SAM cracking who: h23 where: interno-notte when: notte a cavallo tra 10&11 ottobre 2003 why: just to share knowledge :) update: 13 ottobre 2003 _________________________ | | | PREFAZIONE/DISCLAIMER | |_________________________| "Considerate la vostra semenza: fatti non foste a viver come bruti, ma per seguir virtute e canoscenza" Dante Alighieri, Inferno Canto XXVI Questo ragazzo era proprio un genio (come ama ripetere - piu' che giustamente -la mia prof di letteratura italiana) e, proprio a causa della sua genialita', conclude questo canto facendo dire ad Ulisse "Tre volte il fe' girar con tutte l'acque: e la quarta levar la poppa in suso e la prora ire in giu', come altrui piacque, infin che 'l mar fu sovra noi richiuso" Dante Alighieri, ibidem MORALE: nonostante la sua intelligenza, la sua sagacia, la sua abilita', anche Ulisse colo' a picco a causa del suo peccato di iubris (orgoglio). Tutto questo giro di parole per dire cosa? La sete di conoscenza e' legittima, ma occhio: si puo' restare fregati. Quindi io me ne tiro fuori. Se un giorno questo HOW TO andra' in giro, sappiate che io sto scrivendo solo per soddisfare l'arsura del vostro cervello assetato di conoscenza, ma non mi rendo minimamente responsabile per l'uso che saprete/vorrete farne. ____________ | | | READY?!? | |____________| | +-INTRO | | Come saprete (visto che state leggendo questo documento), i file | SAM sono quelli che contengono le informazioni | rigurdanti username(s) e password(s) nei sistemi operativi Windows | NT, Windows 2000, Windows XP. | Quando lo zio Bill, o chi per lui, li creo', sostenne che erano si | curi poiche', a partire dall'hash della password | contenuto nel file SAM, non era possibile risalire alla password | originaria. | Gli dimostrarono che tramite un attacco brute-force si poteva | faclmente ovviare al problema cosi', da Windows | 2000 in poi, decise di attivare di default una protezione aggiunti | va (chiamata SYSKEY) la cui abilitazione su NT | era a discrezione dell'utente. | Bene, noi qui non vogliamo far altro che dimostrare che anche | questo tipo di protezione puo' essere aggirata | (e neanche troppo difficilmente). | Ultima nota: la tipologia di cracking qui descritta implica | l'avere accesso fisico al computer di cui si vuole | trovare la password. | Tra i tanti metodi possibili, questo HOW TO descrive quello in cui | si lavora per il minor tempo sulla macchina | vittima: da quest'ultima ricaveremo solo le informazioni | indispensabili. Il resto (il cracking vero e proprio) | dovremo farlo su un secondo computer. | | +-INGREDIENTI (ossia cio' di cui abbiamo bisogno prima d'iniziare) | | I programmi di cui abbiamo bisogno non sono molti: | | 1)cosa: SAMInside (24KB) - purtroppo e' disponibile solo | in versione demo | dove: http://www.insidepro.com/eng/saminside.shtml | note: sara' la nostra punta di diamante ;) | | 2)cosa: RAR 3.20 for DOS and OS/2 (427KB) | dove: http://www.rarlab.com/download.htm | note: in alternativa va bene qualsiasi compressore purche' | funzioni da linea di comando | | 3)cosa: L0phtCrack (~3940KB) - anche questo e' disponibile solo | "free to try" | dove: http://www.atstake.com/research/lc/ | note: nel campo dei software adibiti a sferrare brute force e' | senz'altro uno dei migliori; | nel nostro caso, pero', puo' essere usato insieme a SAMInside, | o se ne puo' fare del tutto a meno | | 4)cosa: disco di boot NTFS | io mi sono trovato molto bene usando per il boot il floppy di | avvio di win98 (semplicemente perche' | ce l'avevo gia' pronto) e poi "montando" una partizione NTFS | tramite NTFSDOS (39KB) | dove: http://www.sysinternals.com/ntw2k/freeware/NTFSDOS.shtml | note: come ho gia' detto sopra va bene un qualsiasi altro disco | di boot. | Il nostro scopo e' poter accedere all'hard disk (nel 99% con | file system NTFS) senza dover passare | per Windows (per il perche' di questa stranezza basta continuare | a leggere) | | +-OPERAZIONI PRELIMINARI Poiche' per motivi di sicurezza Windows preclude ogni accesso in lettura/scrittura ai files che ci servono (questo vuol dire che non e' possibile nemmeno copiarli, rinominarli etc.), dovremo avviare il computer in modo che win non venga mai lanciato. Ora si capisce il perche' del floppy di boot. Come detto sopra, sul computer vittima faremo solo parte del lavoro. Questo vuol dire che, quando smanetteremo su quella povera macchina, non ci serviranno tutti i programmi elencati sopra. Per l'occasione ho provveduto a preparare tre floppy (la scelta del dischetto e' dovuta principalmente alla possibilita' di scrittura) floppy 1: disco di boot NTFS floppy 2: RAR* floppy 3: -vuoto- *N.B. Nel floppy 2 non va inserito il file rarx320.exe scaricato dal sito rarlab.com: quel file va avviato sul proprio computer, il che generera' una cartella di nome RAR che conterra' il programma vero e proprio. Sara' quella cartella a dover essere copiata sul floppy 2. Ora abbiamo tutto il necessario per cominciare! :) _______________ | | | LET'S GO!!! | |_______________| | +-PARTE 1: operazioni da compiere sul computer vittima | | Inserite il disco di boot (floppy 1) e avviate il comp. | Durante il caricamento date un'occhiata alla lettera che viene | assegnata al | vostro hard disk (ad esempio, nel caso di NTFSDOS se il vostro hd | era C:\ ora e' accessibile come D:\). | Togliete dal drive del floppy il disco 1, ed inserite il disco 3 | (quello vuoto). | Dato che la posizione canonica del nostro SAM e' | C:\WINDOWS\SYSTEM32\CONFIG\SAM [per windows xp] * | C:\WINNT\SYSTEM32\CONFIG\SAM [per windows nt/2000] * | [*N.B. Il file SAM e' privo di estensione!] | lanciamo un bel copy | copy C:\WINDOWS[o WINNT, a seconda dei casi]\SYSTEM32\CONFIG\SAM A:\ | ... | 1 file/s copiato/i | Voila', meta' del lavoro e' compiuto. | Se il file SAM non fosse criptato SYSKEY, il nostro compito sarebbe | finito qui; ma dato che lo e', dobbiamo | eseguire qualche operazione in piu'. | Togliete il disco 3 (che ora conterra' il SAM) e inserite il floppy | 2 (quello con RAR). | Per craccare questa ulteriore forma di protezione abbiamo bisogno | di prelevare un altro file. | Piu' precisamente il SYSTEM contenuto nella stessa cartella. | Il problema e' che questo file e' abbastanza pesante (generalmente | si aggira intorno ai 3 Mb), da qui la necessita' | di usare un compressore come RAR. | Portatevi nel floppy e aprite la directory RAR (che dovrebbe essere | quella contenente il programma dopo averlo | estratto - ricordate le OPERAZIONI PRELIMINARI? - ) | A:\cd RAR | A:\RAR\ | La directory RAR occupa 852KB. Questo vuol dire che sul floppy ne | avremo poco piu' di 570 a nostra disposizione. | Dai test effettuati ho visto che RAR comprime un file SYSTEM fino | all'83-84%. | Questo vuol dire che un file da 2800KB diventa da circa 470KB: | pienamente compatibile, quindi, con il nostro | floppy 2! | A questo punto da | A:\RAR\ | eseguiamo | RAR32 a -s A:\system.rar C:\WINDOWS[o WINNT, a seconda dei casi] | \SYSTEM32\CONFIG\SYSTEM | vederemo la percentuale aumentare e, alla comparsa dell'OK, sapremo | che sul dischetto 2, insieme al rar, c'e' il | file SYSTEM compresso. | [N.B. Anche il file SYSTEM e' privo di estensione] | Parte 1 conclusa. | Riprendetevi tutti i floppy, spegnete il comp e andatevene. | Se invece siete fanatici di mission impossible, ricordatevi di | cancellare le impronte digitali sulla tastiera, | riattivare le telecamere perimetrali che avevate disinserito e | togliere il guardiano tramortito da quell'angusto | armadietto (ricordate, voi siete i buoni, e i buoni non uccidono | la gente ;) | | +-PARTE 2: operazioni da effettuare sul vostro computer di fiducia ( possibilimente abbastanza potente, altrimenti non finirete mai col brute force!) Bene, se tutto e' andato liscio ora dovreste essere davanti al vostro schermo, leggendo queste parole e, soprattutto, avendo 3 dischetti in mano, 2 dei quali dovrebbero contenere informazioni vitali per quello che ci siamo prepostidi fare. La prima cosa da fare e' crackare syskey. Per fare questo basta aprire SAMInside ed importare il SAM File -> ImportSAM... Il programma riconoscera' automaticamente la protezione SYSKEY e chiedera' il file SYSTEM che voi avrete precedentemente decompresso in un qualsiasi luogo del vostro comp. Vedrete quindi comparire nella schermata azzurrina gli username e i relativi hash delle password privi di crittazione SYSKEY. A questo punto potete seguire due strade: 1) ordinate a SAMInside di eseguire il bruteforce, vi mettere l'anima in pace e aspettate 2) nel caso in cui abbiate scaricato LC4, potreste copiare uno per uno i caratteri dell'LM hash e dell'NT hash, unirli insieme mediante questa sintassi (nel caso di administrator) "Administrator:500:" + LMhash + ":" + NThash + ":::" [anche se a questi livelli non dovrei nemmeno dirlo, ribadisco il concetto che la stringa vada copiata SENZA virgolette] salvarli come *.txt e darli in pasto al LOphtCrack, il quale sferrera' anche lui un egregio attacco di forza bruta. [Finezza vorrebbe che, al posto di copiare 64 caratteri, consultaste il file SAMInside.ini...] Non mi dilunghero' sull'argomento LMhash NThash (ci sono una infinita' di documenti in proposito). Sappiate solo che, quando sara' emersa la pass in chiaro, dovrete tener conto della NT password e non della LM password (basta anche un minimo di osservazione per rendersene conto!) _________ | | | OUTRO | |_________| | +-CONCLUSIONI | | Che dire gente, se tutto e' andato per il meglio ora dovreste aver | e per le mani la password di quel computer che | bramavate tanto. | | +-BENCHMARKS | | Come vi ho detto nella PARTE 2, avete due possibilita' per sferrar | e il brute force: via SAMInside o via LOpht Crack. | Riporto qui il tempo che ho impiegato con i due programmi per trov | are una stessa password (9 lettere, no numeri, | no caratteri speciali). | SAMInside on P4 @ 2GHz: 21 minuti | LOphtCrack on P4 @ 2GHz: 22 minuti | Come potete vedere non c'e' una grande differenza tra i due... | | *** Benchmarks estremi *** | Per darvi un'idea di quanto potrebbe durare il vostro cracking (e | per capire se e' una cosa fattibile o meno), | aggiungo delle tabelle che, ad ogni tot di caratteri, fanno corris | pondere il tempo stimato per risalire alla | password. | ATTENZIONE: Questi dati NON li ho sperimentati di persona (li ho t | rovati in rete) ma, a giudicare dai risultati che | ho ottenuto nei miei test, li ho reputati attendibili. | | solo lettere: | | LUNGHEZZA PERMUTAZIONI AMD ATHLON @ 1GHz | 01 26 < 1 secondo | 02 676 < 1 secondo | 03 17,576 < 1 secondo | 04 456,976 < 1 secondo | 05 11,881,376 04 secondi | 06 308,915,776 02 minuti 19 secondi | 07 8,031,810,176 01 ora 06 minuti 08 secondi | 08 208,827,064,576 01 ora 06 minuti 05 secondi | 09 5,429,503,678,976 01 ora 06 minuti 02 secondi | 10 141,167,095,653,376 01 ora 06 minuti 05 secondi | 11 3,670,344,486,987,776 01 ora 06 minuti 03 secondi | 12 95,428,956,661,682,176 01 ora 06 minuti 05 secondi | 13 2,481,152,873,203,736,576 01 ora 06 minuti 12 secondi | 14 64,509,974,703,297,150,976 01 ora 10 minuti 14 secondi | | | lettere e numeri: | | LUNGHEZZA PERMUTAZIONI AMD ATHLON @ 1GHz | 01 36 < 1 secondo | 02 1,296 < 1 secondo | 03 46,656 < 1 secondo | 04 1,679,616 < 1 secondo | 05 60,466,176 24 secondi | 06 2,176,782,336 16 minuti 18 secondi | 07 78,364,164,096 10 ore 41 minuti 57 secondi | 08 2,821,109,907,456 10 ore 41 minuti 38 secondi | 09 101,559,956,668,416 10 ore 41 minuti 48 secondi | 10 3,656,158,440,062,976 10 ore 42 minuti 43 secondi | 11 131,621,703,842,267,136 10 ore 41 minuti 48 secondi | 12 4,738,381,338,321,616,896 10 ore 43 minuti 04 secondi | 13 170,581,728,179,578,208,256 10 ore 44 minuti 33 secondi | 14 6,140,942,214,464,815,497,216 11 ore 22 minuti 48 secondi | | | lettere, numeri e caratteri speciali (uk)** | | LUNGHEZZA PERMUTAZIONI AMD ATHLON @ 1 GHz | 01 71 < 1 secondo | 02 5,041 < 1 secondo | 03 357,911 < 1 secondo | 04 25,411,681 09 secondi | 05 1,804,229,351 12 minuti 27 secondi | 06 128,100,283,921 16 ore 20 minuti 47 secondi | 07 9,095,120,158,391 circa 52 giorni, non testato completamente | | ** N.B. questo test e' stato effettuato con i caratteri speciali | di una tastiera inglese, escluso l'euro, che sono | !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~£¬ | C'e' da far notare che, pero', nella tastiera italiana il range | si allarga notevolmente perche' si | possono utilizzare anche le lettere accentate che, sulle | tastiere straniere, sono digitabili solo grazie | alla combinazione ALT + sequenza_numerica | | +-THANKS TO | | zi' Tufa -> per tutto il tempo buttato assieme dietro ad uno schermo | BianConiglio -> il Prof, colui che mi ha indicato la Via. | isa -> semplicemente per cio' che e' | | +-IPSE DIXIT "Salimmo su', el primo e io secondo, tanto ch'i' vidi de le cose belle che porta 'l ciel, per un pertugio tondo. E quindi uscimmo a riveder le stelle" Dante Alighieri, Inferno Canto XXXIV h23 :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [0X0A] OQ20031122[0A] :: [0x08][APPRENDiSTA STREG0NE] C0DiCE iNVERS0: CRiTT0GRAFiA [Zer0] :: DiGiTALE AVANZATA PARTE 7 :: [.]::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::. Siamo cosi' giunti alla fine di questo lunghissimo articolo, in cui illustrero' alcune applicazioni "pratiche" di crittografia & related. Vi anticipo subito inoltre che visto che la sola introduzione alle curve ellittiche richiederebbe un capitolo a parte NON le trattero' in questa sede. Non sono poi nulla di troppo fondamentalmente diverso da quel che abbiamo visto finora, ma potrebbero creare moltissima confusione su chi non abbia una preparazione abbastanza "matematica"... Dal momento inoltre che ho notato il fiorire di oscenita' sull'argomento crypto che stanno spuntando come funghi ovunque in questi ultimi tempi, faccio presente che NON mi dilunghero' nel commentare tali oscenita'. Non ne ho tempo ne' voglia. Ma checcazzo, e' scoppiata una moda?! Non vi nascondo che sono lieto di mettere finalmente la parola fine a quest'opera *mastodontica*, per tre motivi: Il primo e' che il tempo e' denaro ma soprattutto e' tempo. Il secondo e' che, ci crediate o no, non sono contento di cio' che ho scritto e di come lo ho scritto. Dipende sicuramente dal fatto che sono ormai piu' di due anni che la storia va avanti. Io mi evolvo, e CODiCE iNVERSO si evolve con me. Non illudetevi neanche lontanamente di aver capito di cosa sto parlando. Non lo capite, e non mi aspetto che lo capiate. A darmi fastidio e' principalmente il fatto che l'articolo avrebbe potuto essere trattato molto meglio sin dall'inizio, salvo che poi alla fine avrei voluto ricominciare di nuovo tutto da capo. Il terzo motivo e' che, come mi sembra di aver gia' detto, mi sento pericolosamente braccato dalla banalita' del mondo. Il leggere tra i titoli di una rivista per computer (una di quelle stronzate tipo "pc facile" o roba del genere) una scrittona gigantesca "SPECIALE CRITTOGRAFIA: TUTTI I SEGRETI PER TENERE A BADA GLI HACKERS" mi fa accapponare la pelle. Cristo, ma cosa sta succedendo? Giorno dopo giorno sento il fiato del Sistema sul collo. Io non vorrei che leggiate questo articolo, non ve lo meritate. Almeno, il 90% di voi non se lo meritano, e non e' gratificante sapere di scrivere solo per quattro stronzi che non vogliono arrendersi e che vogliono continuare a lottare. Ma io continuero' a lottare con voi. Non e' una promessa, e' di piu'. E' un sacrifico, un gesto di fratellanza. E' la mia, la nostra scelta. La scelta di chi ha deciso che continuera' a combattere anche quando rimarra' da solo. E se sono solo, che scrivo a fare? Ma un impegno e' un impegno, ed io ho preso l'impegno, una sera su IRC di tanto tempo fa, di finire questo articolo senza lasciarlo a meta', ed e' quello che faro'. Temo che, di tutte le cose che ci spingono a vivere, il senso di responsabilita' sia quello piu' intenso e irrazionale. Ma e' anche una di quelle cose, di quelle poche cose, per cui vale la pena vivere... Cap.2: le matrici si scrivono tra tonde, non si smettera' mai di fare confusione. Funzione lineare significava in effetti "con tutti i termini di grado 1" (e finiamola qui, anche se non sarebbe correttissimo). In generale tutto il capitolo 2 e' un grande cumulo di immondizia e nulla di piu'. Molti di voi se ne saranno gia' accorti a suo tempo. Esso aveva comunque solo lo scopo di far capire al newbie i concetti di base, e non di fornire all'esperto spiegazioni dettagliate, cha tralaltro potra' trovare in qualsiasi buon testo di algebra. Spero che lettori con una discreta preparazione matematica lo abbiano semplicemente ignorato. Cap.9: paragrafo "generazione di numeri primi nel DSA" l'algoritmo che ho riportato io e' sbagliato. Cioe', la procedura e' giusta, ma l'algoritmo: W = V[0] for i = 1 to (n - 1): W = W (V[i] << (160 * n)) next i W = W (V[n] && ((1 << b) - 1)) va sostituito con questo (in pratica mi ci e' finita una "n" al posto di una "i") : W = V[0] for i = 1 to (n - 1): W = W (V[i] << (160 * i)) next i W = W (V[n] && ((1 << b) - 1)) +----------------------------------------------------------------------+ APPLICAZIONI DELLA CRITTOGRAFIA . . . . . . *--------------------------------------------------------+ Capitolo 10 / Ha la forza di undici aquile... +-----------* Orbene, giunti a questo punto siamo consapevoli del fatto di aver fatto una bella sgobbata, di esserci accresciuti "culturalmente" e di aver perso una buona parte di quei 2^8 neuroni pronosticati nell'introduzione. Ma forse avete la sensazione che manchi ancora qualcosa... L'ingrediente che manca e', naturalmente, il "cemento", ovvero qualcosa che ci permetta di solidificare tante belle nozioni informatico-matematiche in qualcosa di concreto... In un'arma magari. E' per questo che sarebbe stato sciocco non trattarlo nell'ULTIMO capitolo. Questo "qualcosa" sono le "istruzioni pratiche", e cioe' le direttive di base per realizzare un programma di crittografia, per creare comunicazioni schermate dall'esterno, per difendersi dagli attacchi che attivamente possono essere portati alle infrastrutture a cui affidiamo la nostra difesa, nonche' le indicazioni topografiche per orientarsi nel mondo dei codici e dell'informazione occulta, un mondo pieno di fantasmi e luoghi comuni. =========================== *** SISTEMI DI ARMORING *** =========================== Avevo gia' accennato al cap.5 gli algoritmi di armoring: servono a trasformare un qualsiasi file (tipicamente file binario) in un file composto esclusivamente di caratteri stampabili, allo scopo di poterlo spedire per posta ad esempio senza che si verifichino problemi di malinterpretazione dovuti ai diversi codici di controllo che cambiano da sistema a sistema. Un elementare algoritmo di armoring potrebbe essere la codifica esadecimale: le 16 cifre dell'hex sono tutte appartenenti al range "stampabile" del charset ascii (uso i termini "stabile" e "stampabile" piu' o meno come sinonimi), e infatti andrebbero bene al nostro scopo. Solo che, come avevo gia' fatto osservare, per ogni byte servirebbero DUE caratteri hex, quindi le dimensioni del messaggio raddoppierebbero. Si puo' pero' tentare di fare una cosa simile tentando di "mitigare" questo fenomeno di aumento delle dimensioni, con la seguente considerazione: nel caso esadecimale porto un messaggio da pacchetti di 8 bit (byte) a pacchetti di 4 bit (nibble), ognuno dei quali viene di nuovo codificato in un pacchetto di 8 bit "stabile" (cifra esadecimale). Cio' lo posso fare perche' esistono almeno 2^4 = 16 caratteri "stabili" nel charset ascii. Il rapporto dimensioni e' di 8 a 4, cioe' raddoppia. Ma nel charset ascii esistono ben piu' di 16 caratteri stabili, perche' non sfruttarli? Vediamo... riesco a trovare almeno 32 caratteri stabili nel charset ascii? Certo che si', ad esempio le 10 cifre e le prime 22 lettere minuscole. Allora bingo! Posso creare un algoritmo simile che prenda pacchetti di 8 bit, li "spezzi" in pacchetti di 5 bit (perche 2^5 = 32) e li ricodifichi poi ciascuno con un singolo carattere ascii tra quei 32 che ho scelto. Visto che 5 non divide 8 dovremo prendere 40 bit per volta (5 pacchetti da 8 bit) e splittarli in 8 pacchetti da 5 bit (perche' il minimo comune multiplo tra 5 e 8 e' 40). In questo caso il rapporto dimensioni e' di 8 a 5, cioe' e' MENO del doppio della dimensione originale, quindi gia' ho ottenuto qualcosa di piu' vantaggioso rispetto all'hex. Ma come avrete capito si puo' fare di meglio. Ne riesco a trovare 64 di caratteri stabili? Si', ad esempio: - le 10 cifre - le 26 lettere minuscole - le 26 lettere maiuscole - altri due caratteri scelti tra quelli stampabili, ad esempio "+" e "/" In questo caso il rapporto e' di 8:6, cioe' 4:3, cioe' un messaggio codificato cosi' aumenta solo di un terzo le sue dimensioni! Visto che m.c.m.(8, 6) = 24 bisognera' prendere 3 pacchetti da 8 bit e splittarli in 4 pacchetti da 6 bit, per poi ricodificarne ciascuno con uno dei 64 caratteri stabili da 8 bit che abbiamo scelto. Ecco, questa che ho descritto e' esattamente la codifica Base64 usata dal PGP e dalla maggior parte dei client di posta elettronica. Ecco la tabella per la codifica: 0 = A 16 = Q 32 = g 48 = w 1 = B 17 = R 33 = h 49 = x 2 = C 18 = S 34 = i 50 = y 3 = D 19 = T 35 = j 51 = z 4 = E 20 = U 36 = k 52 = 0 5 = F 21 = V 37 = l 53 = 1 6 = G 22 = W 38 = m 54 = 2 7 = H 23 = X 39 = n 55 = 3 8 = I 24 = Y 40 = o 56 = 4 9 = J 25 = Z 41 = p 57 = 5 10 = K 26 = a 42 = q 58 = 6 11 = L 27 = b 43 = r 59 = 7 12 = M 28 = c 44 = s 60 = 8 13 = N 29 = d 45 = t 61 = 9 14 = O 30 = e 46 = u 62 = + 15 = P 31 = f 47 = v 63 = / Quando prendo un messaggio codificato in questo modo e vado in lettura, dovro' prendere 4 caratteri per volta (32 bit), riportarli ciascuno all'equivalente di 6 bit (24 bit in tutto), quindi risegmentarli nei 3 byte originali. Problema 1: il messaggio cosi' facendo dovra' avere un numero di byte multiplo di 3, perche' ne prendo 3 per volta. Analizziamo i casi possibili considerando "l" = lunghezza in byte del messaggio: a) l Mod 3 = 0 : In questo caso il messaggio e' multiplo di 3 byte, quindi non ci sono problemi e il codice in output sara' multiplo di 4 byte. b) l Mod 3 = 1 : c'e' un byte "extra" rimasto, in questo caso bisogna aggiungere in coda a questi 8 bit 4 zeri (SHL 4), in modo da avere 12 bit. Poi spezzo in due blocchi da 6 bit e li codifico normalmente. In questo caso il messaggio in output sara' multiplo di 4 byte, piu' due byte (caratteri) extra. c) l Mod 3 = 2 : analogamente: ci sono 2 byte "extra". Bastera' aggiungere 2 zeri in coda (SHL 2) per ottenere un blocco da 18 bit, da segmentare poi in 3 blocchi da 6 bit e codificarli normalmente. In questo caso il testo in output sara' multiplo di 4 piu' tre caratteri extra. Problema 2: se prendo un messaggio codificato Base64 dovro' accertarmi che la sua lunghezza in byte sia multiplo di 4, oppure analizzare i vari casi, indicando con "L" la lunghezza del testo codificato: a) L Mod 4 = 0 : nulla da segnalare, il testo originale era multiplo di 3 byte, quindi posso prendere 4 byte per volta e decodificarli normalmente. b) L Mod 4 = 1 : se guardate i casi precedenti vi accorgete che questa eventualita' non era contemplata (puo' esserlo solo se invece che byte state codificando una sequenza di bit non multipla di 8 bit, ma in questo caso il tutto diventa un po' piu' complicato), quindi significa che si e' verificato un errore di trasmissione! c) L Mod 4 = 2 : questo sarebbe il caso b) riportato per la codifica: dopo aver decodificato i due byte extra in un blocco da 12 bit dovrete togliere i 4 zeri finali ed ottenere un unico blocco da 8 bit. d) L Mod 4 = 2 : analogamente: basta invertire il passo c) della fase di codifica. Lo standard Base64 inoltre prevede che il testo codificato sia SEMPRE multiplo di 4 byte, per farlo basta aggiungere in coda al testo codificato ottenuto tanti caratteri "=" (segno uguale) quanti ne occorrono per arrivare a lunghezza multipla di 4 (quindi uno o due). Tali caratteri "=" saranno semplicemente ignorati in fase di decodifica. E questo era il Base64. Resta una domanda: non si poteva fare di meglio? Non era possibile una codifica a 7 bit? La risposta e' no, perche' purtroppo non riesco a trovare 2^7 = 128 caratteri ascii "stabili". O meglio, alcuni sistemi di trasporto prevedono anche una codifica a 7 bit, ma e' valida solo in determinate circostanze (es.: conversione automatica tra sistemi diversi), quindi non e' un vero e proprio sistema di armoring, non del tutto versatile almeno. Un altro sistema molto diffuso e' l'Uuencode, ma e' cmq funzionalmente uguale al Base64. Sempre in appendice troverete delle routine per la conversione e deconversione dei dati in hex e base64 in VB (e per il base64 anche in C) che ho codato nelle notti insonni. Per le specifiche tecniche vi rimando al readme allegato. ========================================== *** GENERATORI DI NUMERI PSEUDOCASUALI *** ========================================== Come dovreste ormai aver capito, il cuore di ogni sistema crittografico sicuro e' dato dal generatore di numeri random. La crittografia infatti non "vive" senza caos, la scelta di un buon RNG e' la prima cosa da fare per progettare un crittosistema. e' da un pezzo che lo ripeti, non gli starai dando un po' troppa importanza a sta' cosa? Assolutamene no! La generazione di dati random e' alla base di TUTTO, tuttavia e' anche la cosa meno banale. I sistemi di generazione che trovate nella maggior parte delle applicazioni infatti (cito, ad esempio, la funzione Rnd() del VB) sono poco piu' che giocattoli, del tutto inadeguati ad applicazioni crittografiche, ma comunque validi per i giochi ad esempio, o per la maggior parte delle simulazioni scientifiche. Quello che serve nella crittografia e' un generatore random progettato per uso espressamente crittografico, che deve avere quindi proprieta' diverse da quelle di un comune RNG. Come gia' accennato al cap.4 naturalmente, l'esistenza di un vero generatore di numeri random e' una mera utopia. Questo perche' un computer (qualsiasi computer) e' una macchina deterministica, che puo' assumere solo un numero finito di stati: esso accetta in ingresso degli input, effettua su di essi delle operazioni algebriche totalmente prevedibili e da' in uscita un output che e' diretta conseguenza dei processi che sono avvenuti all'interno di esso. Jon Von Neumann affermava che "chiunque tenti di riprodurre numeri casuali tramite metodi algebrici, evidentemente vive nel peccato". Questa frase riassume bene l'impossibilita' di ottenere dati veramente random da un computer: come gia' detto, per avere anche solo una parvenza di casualita' bisogna inevitabilmente appoggiarsi all'ambiente esterno. Il punto e': a scopo crittografico (in generale per qualsiasi scopo) non c'e' bisogno di avere dati strettamente random, in pratica e' sufficiente che tali dati SI COMPORTINO come se fossero random. Qui andiamo un po' nel filosofico, ma e' lo stesso discorso che si faceva a proposito del fatto che il Caos deriva semplicemente dall'incapacita' dell'essere umano di comprendere relazioni complesse. Se una sequenza di dati random generata con un computer (e quindi NON E' random...) appare agli occhi di chiunque, persino a quelli di un altro computer, totalmente casuale, passa tutti i test statistici di rumore, ha entropia del 100% etc., allora posso affermare che e' abbastanza casuale da poterla considerare random. In effetti in questi casi e' piu' giusto parlare di dati (e quindi di generatore) PSEUDORANDOM, cioe' QUASI random. In pratica un generatore si dice pseudorandom se ha la seguente proprieta': - Sembra random. Cioe' i dati prodotti passano tutti i test di casualita'. Ecco alcuni test tipici: - misura dell'entropia (deve essere del 100% o quasi) - bilanciamento 0-1 (cioe' devono esserci circa meta' zeri e meta' 1) - controllo del periodo (un generatore e' per forza di cose, periodico, ma il suo periodo deve essere grandissimo) - distribuzione run lenght (run lenght e' la lunghezza di una stringa di zeri adiacenti, oppure di uni adiacenti, es: la stringa "01100001" ha una zero run lenght massima di 4 ed una one run lenght massima di 2. In una sequenza pseudorandom la meta' delle run lenght deve essere uno, un quarto deve essere due, un ottavo deve essere tre etc... Inoltre la distribuzione deve essere la stessa per gli zero e gli uno) Questo non e' ancora sufficiente per la crittografia. Un generatore si dice "generatore crittografico sicuro" se ha la seguente proprieta': - E' imprevedibile. Deve essere arduo conoscere quale sara' il prossimo bit in output, anche conoscendo tutti i bit di output usciti finora. Infine, un generatore ideale e' quello "veramente random", che ha questa proprieta': - Non puo' essere riprodotto, cioe' se metti gli stessi identici input in uno stesso identico generatore dello stesso tipo ottieni output totalmente diversi. La dicitura "stesso identico" e' filosoficamente controversa, e non solo filosoficamente. Anche in termini fisicamente teorici non avro' mai il modo di dire che due oggetti sono "identici", poiche' non posso misurarne tutti gli stati contemporaneamente (Heisenberg rulez...). Per quel che se ne sa questa ultima citata e' una proprieta' tipica soltanto di sistemi complessi e delicati al punto di essere molto sensibili alle fluttuazioni quantistiche ambientali, parafrasando significa che per ottenerla e' necessario l'intervento di Dio (e' esattamente cio' che in pratica afferma la fisica quantistica, ovvero che "da qualche parte la' fuori c'e' una fonte di Caos"). Queste fluttuazioni non sono apprezzabili nei moderni chip in silicio, che anzi sono studiati per essere insensibili all'effetto tunnel ad esempio, per ottenere questa proprieta' e' necessario appoggiarsi ad un qualche rumore di fondo, misurando gli intervalli di tempo dei keystrokes, degli spostamenti del mouse, l'uso della cpu o della memoria, lo spazio utilizzato su disco, l'entropia dell'intero hard disk, il noise ambientale etc. Riguardo al noise un metodo interessante ed usato in applicazioni professionali e' quello di catalogare la corrente di fondo prodotta da un dispositivo hardware "sensibile", come un contatore Geiger o un diodo Zener. Per ottenere dei buoni RNG bisogna prima catalogare una buona quantita' di questi dati, poi processarli tramite funzioni irreversibili per "distillarli" (tipo con una funzione hash), poi magari ricombinarli con i dati prodotti da dispositivi "logici" come i LFSR (Linear Feedback Shift Register) o cifrari a flusso, o generatori modulari etc... E poi magari riprocessare e ridistillare il tutto tramite funzioni irreversibili. Ogni volta che avete bisogno di dati random ricaricate la vostra "pool" di noise ambientale, riprocessate i dati random che avevate salvati la volta precedente, reinizializzate tutti gli IV, tutte le chiavi, mixate il tutto... Una volta finito salvate su disco lo "stato interno" del generatore in un file di randseed, crittate il randseed. La prossima volta ri-decrittatelo, raccogliete nuovamente rumore di fondo, ri-processate, etc... Come al solito' pero' la complessita' non e' sinonimo di sicurezza, quindi diffidate dei generatori fatti in casa. Tra i generatori random ad uso crittografico piu' conosciuti cito il "Marsaglia's mother of all RNG" e lo Yarrow, della Counterpane. ========================== *** TEST DI PRIMALITA' *** ========================== Al cap.2 avevo descritto il Test di Fermat (ora che ci penso, si legge "ferma'"...) per la primalita', ma poi avevo anche accennato che quello era uno dei piu' elementari, e che in genere erano altri i test di primalita' utilizzati normalmente per la generazione di numeri primi. Per approfondire alcuni di questi test servirebbe di fare un'aggiunta, anche abbastanza sostanziosa, al cap.2, cosa che a grande richiesta del pubblico NON FARO'. In compenso ce n'e' uno di questi test che sara' possibile trattare qui, che tralaltro e' anche il migliore di tutti, ed e' quello che viene comunemente usato nelle librerie aritmetiche multiprecisone come la GMP e da applicazioni come PGP. E' il Test di Rabin-Miller: 0) Scegli un numero random "p" da testare. Calcola "b" = numero di volte che 2 divide (p - 1). Poi calcola "m" tale che p = m * 2^b + 1 (nella pratica: setta il bit low-end di p a zero, b e' il numero di zeri low-end di quel che ottieni ed m e' p >> b). 1) scegli un numero random "a", maggiore di 1 e minore di p. 2) setta le variabili "j" = 0 e "z" = a^m (mod p). 3) se z = 1 oppure z = p - 1, allora p passa il test e potrebbe essere primo. 4) se j > 0 e z = 1 allora p non e' primo. 5) incrementa j (j = j + 1). Se j < b e z != (p - 1) allora: - setta z = z^2 (mod p) - torna al punto 4). Se z = p - 1, allora p passa il test e potrebbe essere primo 6) se j = b e z != (p - 1), allora p non e' primo. L'efficienza di questo test e' doppia rispetto a quella degli altri test standard: la possibilita' che un numero composto passi il test e' solo di un quarto per ogni test, quindi facendo "n" tentativi su un singolo numero, se questo li passa tutti la possibilita' che questo non sia primo e' solo di 1 / 4^n. Nella pratica, per generare un numero primo di lunghezza "l" si fa cosi': - genera una sequenza random di l bit - setta il bit high-end a 1 (per assicurarsi che il numero sia della dimensione voluta) - setta il bit low-end a 1 (per assicurarsi che il numero sia dispari) (trucco: se volete generare un DH-prime settate i DUE bit low-end a 1, e non solo l'ultimo bit, lascio al lettore lo spippolamento matematico per comprendere il perche' di questa scelta, cosa peraltro ESTREMAMENTE elementare) - controllane la non-divisibilita', tramite trial division, per i primi piccoli numeri primi da 3 in su (Lenstra consiglia fino a 1999) - se il numero supera questa prima fase fai alcuni test di Rabin-Miller (5 o 6 in genere bastano), se il numero li passa tutti, allora e' probabilmente primo, altrimenti generane un altro (oppure incrementa di due il numero appena testato e ricontrolla, questa opzione di ricerca incrementale e' in genere piu' veloce anche se a volte meno sicura) La trial division dei primi numeri primi (ops! :P) e' opzionale ma e' un'ottima idea: una semplice divisione e' molto piu' veloce del test di Rabin-Miller, e scarta a priori la maggior parte dei candidati. Esempio: testare la non-divisibilita' per 3, 5 e 7 elimina il 54% dei numeri dispari, testarla per i numeri primi minori di 256 ne elimina l'80% etc... =================================== *** DISTRUZIONE SICURA DEI DATI *** =================================== Quando eliminate un file, questo non viene effettivamente cancellato da disco, bensi' ne viene rimossa la relativa voce nel file system. In questo modo, anche se i dati risiedono ancora fisicamente sul supporto, il sistema considera la relativa area di memorizzazione come se fosse vuota, e quindi alla prima occasione in cui avra' bisogno di nuovo spazio per altri dati sovrascrivera' quelli vecchi senza curarsene. Questo sistema e' molto rapido, ma come avrete capito e' anche molto pericoloso. Infatti dal momento in cui un file viene cancellato al momento in cui il sistema ci scrive sopra qualcos'altro possono passare giorni, settimane, mesi. Se il file da eliminare contiene informazioni *delicate* questo puo' essere un problema. Potreste ricevere inaspettatamente a casa vostra la visita di alcuni gentili signori in giacca e cravatta i quali, utilizzando una semplice utility per il recupero dei dati, cavano fuori dal vostro pc la lettera di auguri che vi aveva spedito il vostro amico Osama (quello dei pennarelli) tempo addietro, e che eravate convinti di aver cancellato. Piuttosto seccante. Utility del genere si trovano facilmente via Internet o su riviste di computer e sono alla portata di chiunque. Crittare i file o le e-mail e' del tutto inutile se poi dovete formattare il disco ogni volta che avete bisogno di cancellare qualcosa di "scottante". Fortunatamente c'e' la soluzione a questo problema, ed e' inclusa in pacchetti tipo PGP o GPG. La soluzione si chiama WIPE ("uaip" :P). In pratica consiste nella cancellazione sicura dei dati. Se avete bisogno di eliminare un file in maniera sicura, utilizzando lo wipe del PGP ad esempio, il file verra' PRIMA sovrascritto con dati random, e POI cancellato dal file system. Naturalmente dovrete usarlo con attenzione perche' se vi cancellate per sbaglio qualcosa di utile con questo sistema, tipo le vostre foto porno di Daria Bignardi, poi dovrete chiamare Dio per riesumarle dal vostro pc. *** ATTACHI ALLO WIPE *** Qui andiamo in piena paranoia, cio' significa che questi attacchi sono attualmente tra i piu' fantascientifici che si e' riusciti a immaginare (anche se, come vedrete alla sezione del brute-forcing c'e' di peggio ;) ed e' poco probabile che possano essere sfruttati con successo. Tuttavia e' girata voce tempo addietro che l'FBI disponeva di una macchina in grado di recuperare dati sovrascritti, motivo per cui non sara' superfluo prendere le dovute precauzioni. E' facile immaginare che agenzie molto potenti abbiano investito fondi nella ricerca di sistemi per recuperare dati distrutti in questo modo. Al momento sono state fatte delle ipotesi per lo sfruttamento teorico di un paio di fenomeni che potrebbero in effetti riuscire nell'intento (si tratterebbe comunque di tecnologie costosissime). Il primo fenomeno si basa sull'allineamento delle testine. In un hard-disk i dati vengono scritti su un piatto di materiale magnetizzabile da delle minuscole testine, le quali seguono una traccia circolare sulla superficie. E' logico aspettarsi che l'allineamento di queste testine cambi nel tempo a causa dell'uso, anzi, teoricamente potete immaginare che una testina non percorre mai due volte la stessa identica traccia. Sarebbe quindi possibile utilizzare un "microscopio magnetico" per osservare il bordo delle tracce recenti ed osservare le vecchie tracce, le quali non sono state del tutto "ricoperte" da quelle nuove. Il secondo fenomeno riguarda la magnetizzazione delle aree. Un singolo bit e' rappresentato in una minuscola area dell'hard disk magnetizzandola con una certa polarita' (es.: "1" per polarita' positiva, "0" per polarita' negativa). Nella pratica il livello di magnetizzazione della superficie e' una funzione continua, quindi ci possono essere dei valori intermedi. Il circuito dell'hard-disk naturalmente opera una "quantizzazione" di questo segnale, tollerando una certa soglia d'errore rispetto alla polarita' prevista, allo scopo di fornire al computer un'informazione discreta, cioe' 0 o 1 (ne avevamo gia' parlato al cap.3). Nel momento in cui la testina vada a sovrascrivere un bit, essa cambia la polarita' della relativa superficie. Ora, se il bit da sovrascrivere e' uguale a quello che c'era prima, la testina "spingera'" ancora di piu' la polarita' verso una determinata direzione (ad esempio, se la polarita' era negativa, e' logico aspettarsi che la rendera' ancora un po' piu' negativa). Se invece il bit era diverso, essa ne invertira' la polarita' e quindi fara' *piu' fatica*. In pratica, sara' logico aspettarsi di trovare un "rumore di fondo" nella magnetizzazione delle aree dovuta a piccole differenze di valore, a seconda che l'area abbia assunto spesso uno stesso stato oppure che abbia cambiato frequentemente di polarita'. Utilizzando un dispositivo molto sensibile potrebbe essere in teoria possibile "leggere in trasparenza" i vecchi bit mediante un'approfondita analisi di questo rumore. *** CONTROMISURE *** Fortunatamente e' abbastanza facile proteggersi anche da queste alienita'. Il metodo piu' semplice e' quello di sovrascrivere PIU' VOLTE con dati random il file interessato prima di cancellarlo dal file system. Questo va a scapito della velocita' ma aumenta la sicurezza, spettera' all'utente trovare il giusto compromesso. ===================== *** STEGANOGRAFIA *** ===================== La parola "steganografia" ha un'etimologia molto simile a quella di "crittografia": significa qualcosa del tipo "occultamento del messaggio". In effetti questa tecnica si e' diffusa molto con l'avvento della crittografia digitale, ma va troppo spesso confusa. Mentre lo scopo della crittografia e' quello di rendere un messaggio illeggibile agli occhi di chi non e' autorizzato, lo scopo della steganografia e' quello di fare in modo che una persona non autorizzata NON SI RENDA NEMMENO CONTO dell'esistenza del messaggio. Alcuni esempi: gli inchiostri invisibili (tipo il succo di limone, che diventa visibile se avvicinato a una fonte di calore), gli acrostici (come un testo in cui le iniziali di ogni capoverso lette in sequenza formano un messaggio nascosto), ma la steganografia e' una tecnica molto antica. Si racconta di un antico re Persiano che fece radere a zero uno dei suoi schiavi per tatuargli un messaggio sulla nuca (che quindi neanche lo schiavo poteva leggere), per poi inviarlo alla sua destinazione una volta che i capelli gli furono ricresciuti, con l'ordine di raderseli di nuovo. La crittografia fallisce nel suo scopo se il nemico legge il messaggio. La steganografia invece fallisce se il nemico anche solo scopre l'esistenza del messaggio. La domanda e': come si fa a implementare la steganografia nel mondo digitale? Risposta: sfruttando il noise presente in alcuni file, di solito multimediali. Facciamo un esempio. Prendiamo un'immagine bitmap con una profondita' di colore di 24 bit. Tipicamente essa e' salvata in formato RGB, cioe' ogni pixel e' rappresentato da un valore a 24 bit che ne identifica il colore, valore composto da 3 byte, ognuno dei quali rappresenta l'intensita' di uno dei tre colori primari (che nel caso della cinematografia sono rosso, verde e blu, Red-Green-Blue appunto, e non rosso giallo e blu come accade di solito). Esempio: Pixel nero: rosso: 0 parti su 255 verde: 0 parti su 255 blu: 0 parti su 255 allora il suo RGB sara' (0, 0, 0) ovvero (00000000 00000000 00000000) = 0. Pixel rosso acceso: rosso: 255 parti su 255 verde: 0 parti su 255 blu: 0 parti su 255 allora il suo RGB sara' (255, 0, 0) ovvero (11111111 00000000 00000000) = 16711680. Pixel bianco: rosso: 255 parti su 255 verde: 255 parti su 255 blu: 255 parti su 255 allora il suo RGB sara' (255, 255, 255) ovvero (11111111 11111111 11111111) = 16777215. etc... Allora posso fare la seguente considerazione: se io ho un pixel del tipo (200, 23, 51) (cioe' molto vicino al rosso) e lo cambio con un pixel del tipo (206, 21, 55), voi pensate che ad occhio nudo la differenza si notera' nell'immagine? Potete fare una prova, ma la risposta naturalmente e' NO. Infatti gli ultimi bit di ognuno dei 3 byte dei canali del colore influiscono molto poco sulla visione d'insieme: essi sono infatti RUMORE DI FONDO, e come tali possono essere eliminati compromettendo pochissimo la qualita' dell'immagine (come accade in alcuni algoritmi di compressione). Oppure possono essere... sfruttati. Tipo "iniettando" dei bit scelti da noi al posto di quelli di fondo. Ad esempio: c'ho un pixel di colore (200, 23, 51). La sua rappresentazione in bit sara': Byte 1: colore R: 200 = 11001000 Byte 2: colore G: 23 = 00010111 Byte 3: colore B: 51 = 00110010 Ora voglio "iniettare" in questo pixel i seguenti 9 bit d'informazione: "110 101 111". Detto fatto: cambio gli ultimi 3 bit low-end di ogni canale: Byte 1: colore R : 11001 110 = 206 Byte 2: colore G : 00010 101 = 21 Byte 3: colore B : 00110 111 = 55 e il risultato sara' praticamente indistinguibile dall'originale. Cosi' facendo posso iniettare 9 bit d'informazione ogni 24, cioe' il 37.5%. Bisogna naturalmente trovare un compromesso, perche' iniettarne di piu' potrebbe alterare visibilmente l'immagine, attualmente forse 9 bit su 24 sono anche un po' troppi. Se posso sostituire ad esempio, il 25% d'informazione, posso iniettare un file da 200 KB all'interno di uno da 800 KB. Questo descritto e' un metodo generale, si puo' estendere a file audio, video, etc... Naturalmente nella maggior parte dei casi non si potranno iniettare bit nel codice RGB, perche' spesso questo non viene mantenuto (ad esempio nel formato Jpeg), in questi casi bisognera' sfruttare altre informazioni presenti nel formato specifico in cui e' presente noise, come i coefficienti di Fourier o i valori delle matrici per la compressione DCT, i cicli d'onda sonora etc... Ogni volta in cui posso effettuare una scelta che non compromette in maniera apprezzabile il medium, tale scelta "veicola" informazione (come gia' visto al cap.5), informazione che puo' essere sfruttata. *** INDIVIDUARE L'INFORMAZIONE OCCULTA *** Purtroppo e' piu' facile di quel che si crede. Infatti il noise di un dato formato di file spesso assume delle determinate caratteristiche. E' quindi possibile analizzare il rumore di fondo di un'immagine per poter dire se contiene o no informazione occulta, anche se questo metodo e' molto dispendioso in termini di risorse di calcolo e difficilmente potrebbe venire implementato da sistemi di monitoraggio automatico come Carnivores o Echelon. Uno dei concetti base della teoria dell'Informazione e' la Distanza di Unicita'. Senza entrare nel dettaglio, se io analizzo un rumore di fondo e questo ha una lunghezza maggiore di un certo valore e un'entropia inferiore ad una certa soglia, esiste una possibilita' trascurabile che esso non contenga informazione occulta. E questi sono metodi di individuazione generali, altri metodi si basano sul sistema di steganografia scelto e sono molto piu' efficaci. Ad esempio: iniettare dati mediante la scelta "pilotata" dei colori di palette di un'immagine GIF a colori e' molto insicuro, mentre lo e' di meno se l'immagine e' in scala di grigi. Ovviare a questi problemi pero' e' possibile. Se ad esempio aumento la dimensione del file "contenitore" e riduco la percentuale di bit occulti, l'informazione segreta viene letteralmente "esplosa" all'interno del contenitore, ed e' molto piu' difficile rintracciarla. Il metodo migliore pero' resta quello di usare la steganografia in congiunzione con la crittografia. Basta scegliere un medium in cui l'entropia del rumore di fondo e' molto alta (a livelli dell'entropia di un cyphertext). In questo modo tale noise ha le stesse caratteristiche di un cyphertext, quindi e' possibile sostituire il rumore con il testo crittato e nessuno si accorgera' della differenza. Cosi' facendo l'esistenza dell'informazione occulta e' tecnicamente dimostrabile SOLO conoscendo la chiave del cyphertext. Questo metodo, se usato con criterio, e' uno dei piu' inattaccabili. ============================ *** FILE SYSTEM CRITTATI *** ============================ Una cosa molto interessante e' la possibilita' di crittare non un singolo file, ma intere aree dell'hard-disk. Infatti il file system e' comunque rappresentabile come informazione binaria, e quindi potrebbe essere in teoria soggetto a crittazione. Esistono programmi appositi con i quali si puo' ad esempio creare una partizione crittata del disco fisso, alla quale si puo' accedere solo inserendo una passphrase. I vantaggi sono notevoli. Il primo riguarda la comodita': invece che decrittare volta per volta (inserendo ogni volta la passphrase) ogni singolo file su cui lavorare, potete salvare i vostri dati riservati sull'apposita partizione crittata. All'inizio della sessione di lavoro inserite la passphrase per "sbloccare" la protezione, e quando spegnete il pc la partizione si riblocca automaticamente. Il secondo riguarda la sicurezza. Con questo sistema e' possibile stabilire una combinazione di tasti per lo "smontaggio" rapido della partizione ad esempio, in modo che appena sentite bussare alla porta o fare irruzione, se siete svelti avete quei 0.3 secondi sufficienti a mettervi al sicuro. Inoltre se il nemico vi toglie di botto la corrente, alla riaccensione del pc i dati non saranno piu' in chiaro, perche' la partizione viene decrittata dinamicamente (trucco: dotatevi di un gruppo di continuita', anche da quattro soldi, poiche' in casi di irruzione delle forze dell'ordine durante indagini in cui si miri anche al contenuto del pc, la prima cosa che fanno e' togliervi la corrente). Questo significa che se avete bisogno di un file all'interno di essa, il sistema non decrittera' l'intera partizione, ma decrittera' SOLO quel particolare file di cui avete richiesto l'apertura, mantenendolo pero' soltanto in RAM (che e' volatile), per poi ricrittarlo alla chiusura del documento nel caso che questo venisse modificato. Ma le applicazioni sono infinite :) Potete ad esempio impostare DUE passphrase: una serve effettivamente a sbloccare la partizione, un'altra serve a formattarla irrimediabilmente. Se il nemico vi costringesse a rivelargliela, beh... potreste fargli uno scherzetto. Egli non avrebbe modo di sapere se la passphrase e' quella giusta se non DOPO che questa ha gia' fatto il suo lavoro. Inoltre potete impostare il sistema affinche' richieda, oltre ad una passphrase, un piccolo file-chiave che salverete su un floppy o su un cd e nasconderete. Una cosa da tener presente durante le perquisizioni e' che una passphrase e' lunga e difficile da tenere a mente e quindi puo' essere facilmente *dimenticata* ;) Stesso discorso per la chiave salvata su supporto esterno ("Accidenti! Non ricordo proprio dove ho messo quel floppy..."). C'e' di piu': il programma che si occupa dello sblocco potrebbe mostrare all'inizio la schermata di richiesta del floppy, permettendo in realta' comunque l'accesso alla digitazione della sola passphrase. Un bluff insomma. Il nemico vedendo la schermata si spacchera' in quattro per cercare un floppy inesistente. Infine l'ultima chicca da tenere a mente: l'uso di un file system crittato e' una cosa veramente efficace se utilizzata in combinazione con la steganografia. Se avete una partizione crittata e steganografata all'interno di un file DivX ad esempio, la sua esistenza non e' nemmeno tecnicamente dimostrabile senza conoscere la chiave giusta. =========== *** PGP *** =========== La storia di Pretty Good Privacy (PGP) e' lunga e travagliata e meriterebbe un articolo a parte. Da quando questa rivoluzionaria applicazione ha visto la luce il mondo e' cambiato a causa sua: tante persone lo hanno utilizzato, nuove leggi sono state emanate per arginarne la diffusione, nuove associazioni sono nate per promuoverla, intere comunita' di programmatori e crittologhi lo hanno sezionato, analizzato e corretto, e chilometri di testo sono stati scritti sull'argomento. Per ragioni di spazio io quindi mi limitero' solo a descriverne il funzionamento tecnico. *** OVERVIEW *** PGP e' un'utility di crittografia per le masse. E' stato pensato per mettere a disposizione di qualunque utente, anche abbastanza "incolto" in materia, la sicurezza della crittografia per la protezione della propria privacy. Per ottenere questo fa uso di tecnologie che fino a pochi anni fa erano in mano solo alle agenzie governative: esso impiega algoritmi "militar-grade" in uno standard pensato per la sicurezza, la portabilita' e la facilita' d'uso del software. E' improbabile che persino organismi militari dispongano di tecnologia sufficente a violare questo standard, il PGP e' stato pensato proprio per non permettere questo. Da quando e' stato diffuso per la prima volta, e' stato passato ai raggi X da praticamente chiunque si occupi di crittografia. Non e' assurdo pensare che nelle ultime release delle versioni "pubbliche" siano stati corretti praticamente TUTTI i bug. Non e' assurdo pensare che i protocolli che usa siano in linea di massima inattaccabili. E pur tuttavia i crittologhi hanno imparato ad essere paranoici: queste considerazioni vanno prese con la dovuta cautela, specialmente per quel che riguarda la dimensione delle chiavi. Io personalmente sono piuttosto scettico sul fatto che una chiave RSA da 1024 bit sia sufficente a garantire un adeguato livello di sicurezza oggigiorno, o che la stessa chiave da 2048 bit sia destinata a rimanere inattaccabile per i prossimi vent'anni, ma queste cose non dipendono dal protocollo bensi' dall'implementazione dell'algoritmo, e comunque sono stime pressoche' impossibili da fare a causa dell'imprevedibile sviluppo delle tecnologie. E comunque attaccare una chiave difficilmente rappresenta il modo piu' facile per compromettere la sicurezza di PGP. PGP supporta crittazione dei file con crittografia simmetrica, crittografia a chiave pubblica, firma digitale e - in molte versioni - funzioni di wipe dei dati e di crittazione del file system. Vediamo ora in dettaglio le operazione che il PGP svolge, io considerero' per semplicita' il funzionamento di una versione con le seguenti specifiche: algoritmo di hashing: MD5 128 bit, algoritmo di crittografia simmetrica: IDEA 128 bit, algoritmo per la firma e per la crittazione asimmetrica: RSA 2048 bit. Si capisce che il metodo e' del tutto generale e non ci sono problemi volendo sostituire l'IDEA con il CAST o il 3DES ad esempio, o l'RSA con l'accoppiata DH/DSS. *** FUNZIONAMENTO DEL PGP *** [ Generazione delle chiavi ] Innanzitutto il PGP chiede all'utente un user ID da associare alla chiave, che puo' essere il vero nome dell'utente, un nickname, perfino un singolo spazio bianco " " (ma non una stringa vuota), e al quale puo' essere associato o meno un indirizzo e-mail. Poi chiede di inserire le specifiche della propria coppia di chiavi, cioe' la dimensione del modulo, l'algoritmo da utilizzare, etc. Quindi chiede di inserire la passphrase che servira' a sbloccare la propria chiave privata. Una passphrase contrariamente ad una singola password contiene piu' parole, magari combinate senza senso e con caratteri di punteggiatura e alfanumerici, lettere maiuscole e minuscole (il PGP e' case sensitive) etc. Questa passphrase dev'essere molto complicata: il PGP effettua su di essa alcuni test preliminari, e se la passphrase risulta troppo corta o troppo semplice vi chiede di sceglierne un'altra; in alcune versioni c'e' una barra che indica, mano a mano che battete, la "qualita'" della passphrase. L'hash della passphrase rappresenta quella chiave simmetrica che il PGP usera' per crittare la vostra chiave privata. (Consiglio: nella passphrase evitate di usare caratteri diversi da lettere maiuscole, minuscole, cifre e caratteri di punteggiatura non stampabili, poiche' se vi doveste trovare a digitare la vostra passphrase su un altro computer ad esempio, o su una tastiera diversa, potreste avere dei problemi, tanto piu' perche' PGP non mostra sullo schermo quello che digitate). A questo punto il PGP chiede di battere a caso dei tasti sulla tastiera, o di muovere a caso il mouse, per raccogliere dati random. Utilizzera' questi dati per inizializzare un motore pseudocasuale crittografico per la generazione delle chiavi. Appena finito di raccogliere dati vi chiedera' di attendere mentre lui elabora: in questa fase applica tutti gli algoritmi di generazione delle chiavi che abbiamo gia' visto, la cosa puo' impiegare anche molto tempo, a seconda della potenza del computer e della dimensione della chiave scelta. Nel caso dell'RSA, il PGP genera due numeri primi random di dimensione dimezzata rispetto al modulo scelto (es.: se ho scelto una chiave da 2048 bit, generera' due primi da 1024 bit), poi trova un esponente pubblico ed uno privato, fa tutti i controlli del caso e vi comunica che ha generato le vostre chiavi. A questo punto calcola l'hash della coppia di chiavi (o meglio, di un parametro particolare appartenente univocamente a QUELLA coppia di chiavi e non altre, il modulo RSA stesso ad esempio): quello e' il FINGERPRINT della chiave, ovvero una sequenza di caratteri esadecimali che identifica in maniera pressoche' univoca la vostra chiave. Se infatti doveste identificare con precisione un utente tramite la sua chiave non potreste affidarvi al suo username, perche' potrebbe non essere univoco (chissa' quanti "Pippo" ci saranno al mondo...). D'altro canto identificarlo tramite il modulo della chiave potrebbe essere molto scomodo: un modulo da 2048 bit sono 512 caratteri esadecimali! Allora una funzione hash ci viene in aiuto per creare un "distillato" della chiave allo scopo di aiutare l'identificazione - inutile che vi dica che e' pressoche' impossibile che si verifichi una collisione. Poi il PGP piglia il modulo, l'esponente pubblico, lo username e il fingerprint, ci attacca un header con tutti i dati del caso e lo schiaffa nel pubring.pgp. Che cos'e'? Non e' altro che un "keyring", un mazzo di chiavi cioe', ovvero un file in cui il PGP mette tutte le chiavi pubbliche (il corrispettivo per le chiavi private e' il "secring.pgp", ed e' salvato in un'area del disco diversa). Analogamente piglia i due fattori primi che generano il modulo, l'esponente privato etc. e li schiaffa nel secring.pgp, questo pero' solo DOPO averli crittati simmetricamente (tramite IDEA ad esempio) con l'hash della passphrase scelta (in questo modo, anche se il secring cadesse nelle mani del nemico, egli dovrebbe comunque trovare la passphrase giusta per ognuna delle chiavi che intende usare). Alla coppia di chiavi generata, oltre al fingerprint, il PGP associa anche un Key ID, che sarebbe una specie di "hash dell'hash". E' una stringa composta di pochi caratteri esadecimali, normalmente si tratta di un solo doubleword. E' piu' corta e permette una prima identificazione approssimativa della chiave piu' immediata, ma naturalmente e' molto meno accurata. A questo punto il PGP salva lo stato interno del generatore random in un file chiamato RANDSEED (normalmente "randseed.bin" o "randseed.rnd") e lo critta. La prossima volta che userete il PGP esso si limitera' a raccogliere pochi dati random (come l'ora del giorno etc.), ed invece che richiedervi di nuovo di digitare i tasti o muovere il mouse, decrittera' il randseed e lo usera' per reinizializzare il generatore. Le chiavi sono pronte, a questo punto il PGP vi chiede se volete connettervi ad Internet ed inviare la vostra chiave pubblica ad un keyserver, ovvero un servizio pubblico che contiene un database con tutte le chiavi pubbliche degli utenti: se cercate la chiave pubblica di "Pippo", non dovete fare altro che collegarvi ad un keyserver qualsiasi (i keyserver "dialogano" tra di loro i modo che il database sia in comune) e fare una ricerca per nome utente (spesso trovate piu' risultati), oppure per key ID (uno o cmq pochi risultati), oppure per fingerprint (un solo risultato). Spedire la propria chiave pubblica ad un keyserver non e' necessario, ed e' a volte meglio evitarlo in alcune circostanze, ma puo' essere utile se volete permettere che tutti possano accedere alla vostra chiave pubblica. E' possibile anche esportare le singole chiavi dai keyring, cioe' salvarle su un file esterno che puo' essere in formato binario (.bin, .pgp, .pub, .sec o altro) oppure in formato Base64 (.asc, .b64 etc...), in quest'ultimo caso potete incollare la vostra chiave pubblica ad esempio in un'e-mail. [ Firma di un file ] Prima di tutto il PGP calcola l'hash del messaggio, questo principalmente allo scopo di garantirne l'integrita' e la non-alterazione. Poi piglia questo message digest, ci attacca una timestamp e tutte le amenita' del caso, e lo firma con la chiave privata dell'utente (naturalmente questi per farlo dovra' inserire la sua passphrase), infine aggiunge un header con tutte le informazioni necessarie (tipo il fingerpint della chiave necessaria alla verifica etc...). Il blocco di dati costituito da: - header - message digest (contiene anche timestamp + varie ed eventuali) - message digest firmato e' la FIRMA vera e propria del messaggio. Come tale, essa potra' essere concatenata al messaggio (che, ricordate, resta in chiaro, noi ora stiamo analizzando SOLO il procedimento di firma), oppure salvata su un file a parte (.sig: "detached signature file"). Per verificarne la validita' l'utente legge il messaggio, ne calcola l'hash e lo controlla con il message digest nella firma. Se questo non coincide potrebbe significare che il messaggio e' stato alterato. Poi prende l'hash firmato, lo decritta con la chiave pubblica del firmatario e lo confronta con quello da lui calcolato. Se non coincide puo' significare che e' stato qualcun altro a firmare il messaggio, e non il mittente che l'utente si aspettava. Se invece tutti e tre gli hash coincidono, allora la firma e' valida. [ Crittazione simmetrica di un file ] Innanzitutto il PGP vi chiede di inserire la passphrase che sara' necessaria per la decrittazione del file. Questa NON e' la passphrase della vostra chiave pubblica (non e' nemmeno necessario possedere una propria coppia di chiavi per utilizzare la crittazione simmetrica), bensi' una frase o password scelta appositamente per proteggere QUEL messaggio. A questo punto calcola l'hash di quella passphrase: quello sara' la chiave simmetrica da usare in crittazione. Poi piglia il messaggio (che puo' essere gia' firmato oppure no) e ne calcola il message digest, allo scopo di garantirne l'integrita' in fase di decompressione. Infatti la cosa che fa subito dopo e' di prendere il message digest, concatenarlo al messaggio stesso e quindi applicare un algoritmo ZIP al tutto. Questo ha due scopi: innanzitutto ridurre le dimensioni del messaggio (che, come avevamo gia' visto, una volta crittato non sara' piu' comprimibile), inoltre inevitabilmente ne aumenta l'entropia, cosa che sembra conferire maggiore protezione da un certo tipo di attacchi crittanalitici. Fatto cio', il PGP usa la chiave generata in precedenza per crittare simmetricamente, tipo con l'IDEA, il file compresso ottenuto. Come ultima cosa concatena l'hash della chiave di decrittazione (che in pratica sarebbe l'hash dell'hash della passphrase, servira' per capire al volo se la passphrase inserita per la decodifica e' giusta o no) al messaggio crittato, insieme con un header. Eventualmente, se richiesto, codifica tutto in Base64. In fase di decrittazione, il PGP chiede all'utente di inserire la frase chiave per lo sblocco. Ne calcola l'hash, quindi ne calcola l'hash dell'hash e lo confronta con quello allegato al cyphertext. Se non coincidono dice qualcosa del tipo "You have typed an incorrect passphrase. Ritenta and you will be piu' fortunato", altrimenti usa l'hash come chiave per decrittare il messaggio. Poi decomprime cio' che ottiene, ne estrae il message digest originale e calcola il message digest del plaintext decompresso. Se i due message digest non coincidono e' probabile che ci sia stato un errore di trasmissione, o comunque il messaggio non e' integro (warning: "PGP bad packet!", evenienza abbastanza rara anche se a me e' capitato), altrimenti, una volta decompresso, avete finalmente ottenuto il vostro plaintext (se il messaggio era anche firmato, ora potete procedere con la verifica della firma come visto sopra). [ Crittazione asimmetrica di un file ] Per prima cosa il PGP vi chiede di selezionare dal vostro pubring la chiave pubblica, o le chiavi pubbliche (il PGP supporta destinatari multipli) degli utenti a cui dovra' essere spedito il messaggio. Poi critta il messaggio in maniera simmetrica come appena visto, solo che invece che chiedervi una passphrase per generare una chiave IDEA da 128 bit, questa chiave la genera da solo usando il messaggio ZIPpato come seed per inizializzare il motore random. A questo punto ottiene un messaggio che assomiglia in tutto e per tutto ad un file crittato simmetricamente, solo che ora piglia la chiave random usata per la crittazione, la critta con la chiave pubblica di uno dei destinatari ed allega quel che ottiene al messaggio. Ripete il procedimento per ognuno dei destinatari, cioe' allega un blocco di decrittazione per ogni chiave pubblica coinvolta, quindi aggiunge un header contenente le informazioni sulle chiavi dei destinatari. Di nuovo, se richiesto codifica il tutto in Base64. L'utente che riceve il messaggio legge l'header e va a vedere se qualcuna delle chiavi private autorizzate a decrittare il messaggio e' in suo possesso. In caso affermativo sblocca la chiave privata con la sua passphrase, estrae il blocco di decrittazione corrispondente e lo decritta con tale chiave privata. Quindi controlla l'hash della chiave simmetrica ottenuta con quello allegato al cyphertext, se e' corretto procede come nel caso simmetrico alla decodifica. [ Altre funzioni PGP ] La quasi totalita' delle versioni di PGP supporta una quantita' di altre funzioni basate principalmente sull'infrastruttura di chiavi pubbliche creata. Il PGP infatti, contrariamente ad altre applicazioni, non fa affidamento su una qualche autorita' esterna per garantire l'autenticita' delle chiavi, bensi' su una "rete di fiducia". Ogni utente, ad esempio, puo' impostare un "livello di fiducia" per una data chiave, a seconda se e' stato lui stesso a generarla, oppure se l'ha ricevuta di persona da un amico, o se l'ha ricevuta via internet da una terza parte di cui puo' o meno fidarsi etc... Se siamo completamente sicuri della provenienza di una chiave, possiamo firmarla (aggiungere cioe' un "certificato di fiducia") con la nostra chiave. Se qualcuno che si fida di noi riceve questa chiave firmata, allora sa che probabilmente puo' fidarsi anche di quella chiave, etc... Inoltre possiamo impostare una "data di scadenza" per la nostra chiave, cosi' che non sara' possibile riutilizzarla dopo quella data, oppure emanare un "certificato di revoca" di una delle nostre chiavi, e moltre altre cose. Per la descrizione dettagliata di queste operazioni vi rimando alla guida utente del PGP. Da notare la simpatica cosa della "sicurezza a strati" offerta dal PGP: l'unica informazione che potreste ricavare da un file crittato e' l'identificativo della chiave che serve a sbloccarlo: non saprete nemmeno leggerne l'eventuale firma all'interno, o solo il TIPO di file o la data e l'ora di crittazione senza avere la chiave adeguata. Anche la dimensione del file originale e' difficile da stimare a causa della compressione. *** PGP: LE VERSIONI *** Le varie versioni di PGP possono differire da molto a moltissimo l'una dall'altra. In questo articolo non si pretende di dare una panoramica completa della storia e delle varie versioni di PGP (la release 1.0 risale al 1991), che tralaltro e' incredibilmente complicata e frammentaria, anzi, non e' affatto escluso che in questa sede ci possano essere delle imprecisioni quindi utilizzate queste info con la dovuta cautela. PGP e' nato come applicazione MS-DOS e si e' evoluto poi all'ambiente Windows, ma ovviamente ne esistono versioni anche per Linux e altre piattaforme. La scelta della versione e' di importanza cruciale ed e' legata direttamente alla storia del PGP. Il brevetto di questo programma infatti e' passato di mano in mano diverse volte. Abbiamo gia' detto che l'esportazione del PGP dagli USA era considerato in origine alla stregua del contrabbando d'armi, il sito "commerciale" della Pretty Good Privacy Inc. e' www.pgp.com e si riferisce appunto alle versioni pensate per gli USA. Ma parallelamente e' nato un altro sito: www.pgpi.com (la "i" sta per "international"), che e' dedicato alle versioni "modificate" del PGP, cosa che e' stato possibile fare grazie a delle "falle" nella legislazione americana e alla disponibilita' del codice sorgente. Le versioni international sono identificate da una "i" finale (es.: 6.0.1i), di tutte e' disponibile il codice sorgente, sono legali sia in USA che in Europa e sono non-commercial free. Per ragioni di copyright in alcuni casi non sono compatibili con le chiavi pubbliche generate da vecchissime versioni di PGP, ma questo non e' un problema primo perche' quelle versioni erano usate solo in america e sono molto vecchie, ormai non le usa piu' quasi nessuno, secondo perche' in genere esiste sempre un'opzione (attivabile dall'utente, magari dal sorgente) che rende una versione international compatibile con una USA. La cosa naturalmente non e' legalissima, ma gli sviluppatori non la inseriscono di default e mettono un bel disclaimer sull'uso - insomma, loro se ne lavano le mani ;) Non tutto e' rose e fiori pero', e questo perche' ad un certo punto la NAI (network Associates Inc., l'azienda proprietaria della Pretty Good Privacy Inc. dal dicembre 1997) cambia la visione delle cose. Nel febbraio 1991 Phil R. Zimmermann (PRZ), "padre" di PGP che era rimasto a lavorare alla NAI come socio, lascia l'azienda emettendo tra l'altro un comunicato sui newsgroup che spiega le sue ragioni. Questo divorzio era annunciato: negli ultimi mesi del 2000 nuovi membri del consiglio di management hanno assunto il controllo della PGP Inc., e la loro politica era radicalmente diversa da quella di PRZ: si prende la decisione di limitare il piu' possibile la pubblicazione di codice sorgente di tutte le versioni future di PGP (all'epoca era stato pubblicato da poco il sorgente della 6.5.8), inclusa la 7.0.3 allora in lavorazione (da notare che meno di un anno dopo accade il piu' grave evento terroristico della storia: forse le cose stavano per cambiare e qualcuno sapeva?...). Dopo l'11 settembre 2001 la NAI abbandona la PGP Inc. (che torna alla McAfee, proprietaria della NAI) e il progetto PGP viene definitivamente chiuso, mentre continuano ad uscire le release e il codice sorgente di GnuPG, che in questo periodo acquista popolarita'. Nell'agosto 2002 pero' si forma una nuova societa', la PGP Corporation, la quale riprende il progetto PGP. Il 3 dicembre 2002 viene rilasciata la nuova versione freeware di PGP, la 8.0 (la prima perfettamente compatibile con Windows XP e Mac OS X), completa di sorgenti: PGP rinasce. E qui finisce la parte storica, ora arriva la parte delle preferenze personali, da prendere rigorosamente con le molle proprio in quanto personali. Scegliere una versione di PGP non e' affatto cosa semplice e dipende dalle necessita' dell'utente. Se da un lato la corsa all'ultima release e' pericolosa (perche' non sempre sono presenti i sorgenti e perche' e' capitato un sacco di volte di trovare dei bug che sono stati poi corretti), d'altro canto la mania di molti cypherpunk "old school" di continuare ad usare le vecchissime versioni per DOS tipo la 2.0 puo' essere parimenti pericolosa. Di sicuro e' scomoda, perche' ci sono diversi casi di incompatibilita' tra le chiavi. Ma puo' anche essere pericolosa nel vero senso della parola, perche' queste versioni usano algoritmi vecchi (come l'MD5 che non e' da considerarsi una gran sicurezza rispetto allo SHA-1 o al RipeMD-160), e inoltre sono affette da bug che sono stati corretti solo riprogettando l'intero software nelle versioni successive (installare patch a ripetizione in questo campo non e' una gran bella cosa...). Sicuramente non dovete usare versioni successive alla 6.5.8 (riguardo alla 8.0, essendo un prodotto post-11 settembre la vedo con sospetto, consiglio diffidenza e aspettare quantomeno che i sorgenti vengano analizzati a fondo alla ricerca di bug), quali la 7.x (PRZ affermo' che a suo avviso la 7.0.3 e' una delle piu' sicure versioni mai uscite, ma non ha nuove features interessanti rispetto alla 6.5.8 e non ne e' disponibile il sorgente). Per quanto riguarda le vecchie versioni per DOS, forse la migliore e' la 2.6.3i, che tra l'altro e' totalmente compatibile con le chiavi RSA fino a 2048 bit. Tra le versioni per Windows e Linux, la prima veramente degna di nota e' la 5.5.x, che tralaltro corregge alcuni bug della 5.0, credo sia un buon compromesso perche' tralaltro e' abbastanza "leggera". Forse la release migliore e' la 6.5.8, ma per usarla con un po' di criterio reputo quasi indispensabile andare ad analizzare di persona il sorgente (o la firma PGP del pacchetto) e quindi ricompilarsela da soli, cosa non alla portata di tutti. *** CONSIGLI PER L'USO *** Se la parte precedente era da prendere con le molle questa lo e' ancora di piu'. Io non sono ne' un crittologo professionista, ne' un "esperto" di PGP nel vero senso della parola, ma voglio illustrare qui alcune considerazioni che spero servano almeno come spunto di riflessione. Innanzitutto il primo consiglio e': quando installate il PGP, prima ancora di generare una propria coppia di chiavi, andate alla voce "opzioni" per cambiare alcune impostazioni - e non abbiate paura di andare alla voce "advanced". Le opzioni tipiche che potete trovare sono: - Cache decryption/signing passphrase for: