visita il sito del nostro sponsor





Tecniche di protezione del software




Il presente articolo è stato pubblicato per la prima volta sul numero 117 (Gennaio 2001) della rivista:


Potete trovare gli articoli indediti di Enigma Amiga Life al seguente indirizzo:
http://www.amigalife.info



Dopo una lunga introduzione, più che altro storica, sulle tecniche usate per proteggere il software, è finalmente giunto il momento di parlare proprio di quest'ultimo, ed in particolare di come sia strutturato, in modo da comprendere come proteggerl, soprattutto come non farlo.


Senza voler scomodare principi di ingegneria del software, possiamo subito dire che un programma per elaboratore elettronico è suddivisibile, a livello logico, in una routine principale, detta main, eseguita automaticamente ad ogni avvio, e da una collezione di procedure, che possono anche non risiedere nel corpo principale del programma, il cui compito è quello di elaborare i dati secondo le direttive impartite dall'utente. Questa classificazione è del tutto generale e quindi applicabile ad un serio software gestionale o ad un più frivolo videogioco, infatti qualsiasi software è distribuito sotto forma di file, più o meno camuffato, detto eseguibile per destiguerlo da quelli contenenti i soli dati, ma che in realtà eseguibile proprio non lo è, affinchè divenga tale è necessario che il sistema operativo della macchina ospite lo processi, traducendolo in un'immagine nella memoria, solo questa immagine è realmente comprensibile dalla cpu. La conversione è eseguita al volo da un apposita routine del sistema operativo, in gergo detta loader, traducibile in italiano con il termine caricatore. Facciamo un passo indietro, ed analizziamo il processo che porta alla realizzazione del software. In generale il programmatore redige una lista di comandi, che sono la trasposizione dei passi elementari di un algoritmo in sequenze di istruzioni di un linguaggio ad alto livello come
Basic, Pascal, C, Delphi, AmosPro, Java, ecc... Questa lista viene memorizzata in un file detto source (sorgente), non ancora comprensibile alla cpu, quindi verrà tradotto, in una sequenza di istruzioni appartenenti al linguaggio macchina, da un apposito software detto compiler (compilatore), ottenendo così un file, in gergo, definito object (oggetto), che in generale rappresenta solo un modulo dell'applicativo, e deve essere accorpato con altri moduli prodotti dallo stesso programmatore o da terze parti, fosse solo per interfacciarsi con il sistema operativo. Questo compito spetta ad un particolare software che lega tra loro i vari moduli, e per questo è detto linker. L'uscita del linker è un file volgarmente detto eseguibile, ma che in realtà è una semplice collezione di moduli e di regole su come i vari moduli debbano essere legati tra loro una volta caricati in memoria e formare l'immagine. Ogni modulo è incapsulato in una propria unità detta hunkcode. Nei moderni sistemi operativi è implementata una gestione dinamica della memoria, e gli indirizzi di memoria nei quali finirà il programma, dopo il caricamento, variano casualmente, quindi il linker non può legare fisicamente i moduli in un unico blocco, come avveniva nei vecchi calcolatori, proprio perché non conosce l'indirizzo di partenza, e questo compito è demandato al modulo loader dell'OS, che dispone di tutte le informazioni sullo stato della memoria durante l'esecuzione, e conosce le regole redatte dal linker, e riportate nel file eseguibile. Il loader, in sistemi operativi più moderni ed innovativi come AmigaOS, è un vero gioiellino, in quanto può sparpagliare in memoria i singoli hunkcode per ottimizzarne l'uso, ma può fare di più: caricare solo i moduli che verranno effettivamente eseguiti a breve, questa tecnica si chiama overlay, ed una specie di memoria virtuale al rovescio, oggi in oblio dato il costante calo della memoria e l'abbitudine a ricorrere sempre e comunque alla memoria virtuale.


Analizziamo la congiunzione tra la suddivisione logica e quella fisica. Il main ha il compito di prelevare i parametri memorizzati nel file di configurazione o passati dall'utente all'avvio, per esempio se è stato eseguito da shell o dalla sua icona se lanciato attraverso l'ambiente grafico. Il main, inoltre, richiedere al sistema operativo le risorse necessarie per l'esecuzione ed le inizializza correttamente, spesso gestisce anche l'interfaccia utente, anche se questo compito può essere eseguito da una procedura scritta ad hoc, e richiamata dal main. Terminata l'esecuzione dell'applicativo, il main provvede anche a rilasciare tutte le risorse precedentemente allocate. Ecco perché è la routine vitale, e le viene riservato uno speciale hunkcode, di solito il primo presente nell'eseguibile, mentre tutte le altre procedure, vengono raggruppate in altri hunkcode, a seconda dell'oggetto di provenienza. Spesso, è proprio nel main che erroneamente vengono inserite le procedure anticopia, e tra breve vi sarà più chiaro perché questa scelta sia un errore.


Il loader non fa altro che aprire il file in lettura, ed analizzata la sua struttura, riserva una o più aree di memoria, nelle quali ricopia i contenuti dell'hunkcode, operando contestualmente la rilocazione del codice, ovvero legando tra loro i vari moduli aggiungendo un opportuno offset agli indirizzi precalcolati dal linker e segnalati da quest'ultimo al loader, in modo che la
CPU, durante l'elaborazione, veda un'unica entità, anche se sparpagliata in aree diverse. Terminato il suo compito, informa lo scheduler, un apposito modulo del sistema operativo, che è disponibile un nuovo programma pronto per essere eseguito in parallelo con gli altri, quando arriverà il suo turno, lo scheduler non fa altro che cominciare l'esecuzione del main.


Per facilitare la dura arte della programmazione sono stati creati appositi software, detti debugger, il cui compito è quello di agevolare la ricerca degli errori di programmazione. I debugger riproducono il funzionamento del loader e dello scheduler: una volta caricato il programma in memoria lo ripongono in una specie di stato comatoso, e lo risvegliano a comando, e consentono all'utente di analizzarne i segreti più intimi. Per semplificare il compito dei debugger, il compilatore ed il linker inseriscono informazioni extra, che allungano di molto la lunghezza dell'eseguibile, e che a volte consentono di seguire l'esecuzione del programma non solo a livello di linguaggio macchina, ma anche di codice sorgente, un gran bel vantaggio per il programmatore, purtroppo non è il solo a beneficiarne. I debugger sono gli strumenti preferiti dai cracker, e sfortunatamente sono alla portata di tutti, dato che corredano quasi tutti gli ambienti di sviluppo, e per usarli è necessario conoscere un po' di programmazione e magari un po' di assembly, conoscenze minimali per chi vuole sproteggere un software. Non è raro che i cracker scrivano dei debugger personali, dei quali sono ultragelosi. Questi gioiellini sono, spesso, specifici per un particolare ambiente di sviluppo, ma sono ricchi di funzioni utilissime per la sprotezione, non presenti nei debugger ufficiali, come funzioni per cercare stringhe nella memoria anche se crittate con gli algoritmi più comuni.


Per rendere più arduo il compito ai cracker è necessario eliminare le informazioni di debug dagli eseguibili, questa può sembrare un'affermazione lapalissiana, ma non immagitante neanche quanti programmatori dimenticano di farlo, anzi alcuni tool professionali di sviluppo, molto diffusi in ambito Windows, non lo consentono, ed è necessario scrivere utility apposite per eliminarli o per alterarli in modo irreversibile, in fondo il debug è fondamentale per lo sviluppatore e non per l'utilizzatore.


Il cracker quindi deve limitarsi a caricare il programma da sproteggere con un buon debugger e studiare la sua struttura per ritrovare tutte le informazioni necessarie per la sprotezione. Una volta dissamblato il programma, ovvero ricostruito il codice operativo nel formato mnemonico (ma questo viene fatto in automatico dal debugger), più facilmente interpretabile da un essere umano, bisogna seguirne il flusso, ed individuata la porzione di codice responsabile del controllo anticopia è necessario (sufficiente) eliminarla o renderla inoffensiva con varie tecniche, e se le procedure anticopia sono nel main, o richiamate da questo, è un gioco da ragazzi rintracciarle. I programmatori più ingenui confidano nel fatto che per eliminare una protezione sono necessarie conoscenze non superficiali della programmazione Assembly e del Sistema
Operativo sul quale poggia l'applicativo da sproteggere, e che la maggior parte degli utenti non le possiedono, ma dimenticano che basta una piccola crepa per far cedere una diga faticosamente costruita.


Vediamo come chiudere quante più falle possibile. L'interazione uomo macchina, in qualsiasi software, avviene visualizzando sul monitor alcune informazioni, per lo più frasi, ed attendendo che l'utente comunichi le sue scelte, via tastiera, mouse, joystick, ecc... Le frasi sono spesso e volentieri memorizzate come stringhe, ovvero sequenze di codici ASCII, sia all'interno del sorgente e sia all'interno dell'eseguibile, quindi chiunque può facilmente leggerle, ed anche modificarle, con degli strumenti anche rudimentali, per esempio basta battere in una finestra shell il comando dos: type c:type hex, per veder comparire una sfilza di codici senza senso apparente, tra i quali spiccano le frasi. Volendo si può fare di più, basta procurarsi un editor binario di file
, ed usarlo per compiere alcune ricerche all'interno del file, una volta trovata la stringa desiderata, possiamo modificarla a nostro piacere, per esempio sostituendo il nostro nominativo a quello dell'autore, o anche per compiere azioni più lecite, come tradurre nella nostra lingua il software. Questa è una tecnica ampiamente usata in passato per rimuovere i copyright, ormai la maggior parte dei programmatori si è fatta furba, e critta tali informazioni o per lo meno le codifica in modo non standard, ecco perché alcuni tool di crack offrono la possibilità di ricercare stringhe crittate. La tecnica crittografica che istintivamente viene usata da qualsiasi novello programmatore, con un minimo di conoscenze assembly, è basata sulla invertibilità della funzione logica XOR o più nota come OR Esclusivo, ma in pratica non fanno altro che utilizzare la versione povera (ed inefficace) dell'algoritmo ideato da Gilbert Vernam.


Lo scopo principale dei messaggi di copyrights è quello di rivendicare la paternità del prodotto, i programmatori più fantasiosi come ulteriore prova, inseriscono all'interno del codice alcune chicche, più note con il termine easter eggs (uova di Pasqua), che quindi non hanno solo la funzione di semplice burla, ma sono un vero e proprio marchio di fabbrica, in quanto solo chi le ha inserite nel codice può conoscerle tutte, e possono tornare utili per dimostrtare la paternità in sede legale nel caso i messaggi di copyrights siano stati alterati. Nel settore videoludico, si usa un'altra tecnica per impedire la modifica dei copyrights, sicuramente più coreografica. Tutte le scritte sono in realtà dei disegni che vengono composti opportunamente durante l'esecuzione, e sono memorizzati in formato proprietario rendendo quasi impossibile la loro ricerca e sostituzione.

Quanto segue può sembrare banale, ma è bene tenerlo sempre a mente: "rimossa la protezione, il supporto originale anche se di tipo induplicabile con i normali strumenti, diviene copiabile perché non è più necessario riprodurre, sul nuovo anche gli errori, le firme o i marcatori, poiché essi non verranno più ricercati".


Ancora una volta lo spazio a disposizione è terminato, alla prossima puntata.

Francesco De Napoli




visita il sito del nostro sponsor