Tecniche di protezione del software




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


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


Anche se lo scorso mese la parola fine era apparsa in coda all'articolo, siamo di nuovo qui a parlare si protezione del software, anche grazie ad alcune conversazioni private, via e-mail, con alcuni lettori. Questa volta, però, analizzeremo l' aspetto progettuale.


Decidere come realizzare una protezione non è cosa semplice, e bisogna valutare, in modo appraccurato qualsiasi possibile effetto prodotto dalle nostre scelte, ma allo stesso tempo non perdere di vista quali siano le reali esigenze, i vincoli temporali, e le risorse umane ed economiche che si possono dedicare alla sua realizzazione.


Il sistema deve essere il frutto del giusto bilanciamento di efficacia e semplicità. Un sistema di protezione molto efficace, ma che introduca grosse limitazioni o che richieda una gran pazienza da parte dell'utente, legalmente autorizzato all'uso del software, come accade nel caso si debba, ad ogni avvio, inserire una password e magari prelevarla in modo casuale dal manuale cartaceo, non è certo una soluzione ottimale, e potrebbe spingere l'utente a non acquistare il software perché troppo scomodo da usare. Allo stesso tempo, una protezione trasparente o quasi, ovvero che non richieda particolari attenzioni da parte dell'utente, ma di scarsa efficacia è del tutto inutile per operare un buon controllo sulla diffusione delle copie pirata.


Non va neanche trascurato un'altro fattore, che può incidere pesantemente sulla tipologia della protezione, e che è strettamente legato alle caratteristiche tecniche del sistema ospite, ovvero della macchina sulla quale dovrà girare l'applicativo, e non meno importanti sono quelle dell'ambiente di sviluppo adottato per scrivere sia l'applicativo da proteggere e sia il codice della protezione, di solito questi coincidono, data la difficoltà di integrare moduli provenienti da diversi linguaggi. Per esempio se sulla macchina ospite si trova una scheda di rete si potrebbe usare il codice identidicativo di quest'ultima, unico a livello mondiale, o il numero di serie del Pentium III, o la presenza di memorie di massa Content Protecion Removable Media (sui quali forse toneremo a parlare più in là) per rendere la nostra tecnica ancora più efficace.


La diffusione del sistema operativo sul quale poggerà il software introduce diversi aspetti da considerare. Il principale è certamente il tipo di utenza e la sua preparazione tecnica, nonché la propensione a rifornirsi dal mercato delle copie pirata. Facciamo un piccolo esempio: trovare una copia pirata di
PhotoShop per Macintosh è ben più difficile che su Wintel, nonostante il prodotto sia nato e cresciuto su Macintosh, e solo successivamente sia stato portato anche su Windows. Tutto ciò è strettamente legato alla quota di mercato detenuta dal sistema operativo ed alla tipologia dell'utenza, molto ampia e variegata, ma livellata verso il basso, per la piattaforma Wintel, e di tipo professionale e ristretta nell'area Macintosh, quindi più incline all'acquisto del prodotto originale nonostante la disponibilità della copia pirata, ma sempre con scarse conoscenze informatiche. Questo esempio lascia intuire come sia necessario scegliere un livello di sicurezza più alto nei mercati di tipo orizzontale rispetto a quello necessario nei mercati di tipo verticale, ma tale scelta può essere ribaltata in funzione del prezzo dell'applicativo e dalla presenza di concorrenti, infatti non va assolutamente dimenticato di tarare il livello di protezione su quello dei prodotti concorrenti, ovviamente cercando di non commetterne gli stessi errori, questo per non correre il rischio che le copie pirata dei prodotti concorrenti si diffondano più rapidamente di quelle del proprio software, che così rischierebbe di essere buttato fuori dal mercato, di vittime illustri ce ne sono tante.


Le procedure anticopia, come è intuibile, possono essere sviluppate con qualsiasi linguaggio, ma alcuni, per manifeste limitazioni sono sconsigliati, mentre altri, garantendo maggiore libertà di movimento sono più idonei, ma richiedono un livello di preparazione superiore da parte dell'implementatore del framework, e tempi di sviluppo più lunghi, e quindi producono inevitabilmente un'incremento dei costi di produzione della soluzione, e che si rifletterà anche sul costo complessivo dell'applicativo. L'investimento potrebbe essere più facilmente ammortizzato, adottando la soluzione su più applicativi, ma ciò introduce un pericolo: se qualcuno trova una falla nel sistema, tutti i software, protetti in quel modo, sono a rischio; per tanto lo schema deve essere studiato con maggiore cura per reggere agli attacchi o per poterlo alterare, di volta in volta, di quel tanto che serve per spiazzare i cracker. Le protezioni di tipo usa e getta sono facilmente realizzabili, quindi a bassissimo costo, e sono particolarmente indicate se il tempo di vita utile, dal punto di vista commerciale, del software è breve; ma a questo punto bisogna riflettere se sia opportuno introdurre una blanda protezione, o invece sarebbe meglio lasciare completamente indifeso il software e puntare su altri fattori per incentivarne l'acquisto. Inoltre, sviluppare tante tecniche diverse, una per ogni applicativo, può rappresentare uno grosso svantaggio, infatti una tecnica per essere adottata efficacemente, soprattutto se raffinata, deve essere ben assimilata da chi la utilizza, e se per ogni applicativo la si cambia radicalmente, oltre ad una pedita tempo non indifferente, dovuta non solo al ciclo di sviluppo, ma soprattutto a quello di apprendimento, si corre il rischio di confondere il programmatore, che finirà con l'usare lo schema in modo improprio, rendendo del tutto vana la sua presenza.


Qui, il termine linguaggio è stato usato nella sua accezione più ampia, con la quale indichiamo l'intero ambiente di sviluppo, il cui anello debole, dal punto di vista della protezione è il suo backend, ovvero il processo di creazione dell'eseguibile.


Spero che i lettori perdonermanno il seguente esempio basato su di un prodotto
Microsoft, ed in particolare Visual Basic. Per sfortuna di chi lo usa per sviluppare applicazioni commerciali, il Visual Basic è il linguaggio preferitto dai cracker, data la semplicità con cui è possibile rintracciare tutte le informazioni necessarie per sproteggere il programma. Pochi programmatori, ed ancora di più quelli Visual Basic, si prendono la briga di controllare cosa ci sia effettivamente nell'eseguibile, e se lo facessero gli si rizzerebbero istantanemente i capelli! Senza voler scomodare complessi tool di debug o i fantomatici decompilatori, che circolano tra i cracker, basta visualizzare un dump esadecimale del codice generato dal compilatore per veder comparire di tanto in tanto le stringhe contenti i messaggi per interagire con l'utente, ma questo è un difetto di tutti i compilatori dei più disparati linguaggi o quasi, ma la pecca più grande è che compaiono anche i nomi delle funzioni dichiarate del programmatore, cosa che consente subito di verificare se ci sia qualche funzione chiamata, con molta fantasia: IsProtectionOn(), CheckDongle(),
IsAValidKey(), ... In un estremo impeto di generosità il compilatore include tutti i simboli di debug, ciò consente di caricare l'eseguibile in un debugger, per esempio quello allegato al pacchetto Visual Basic, per poter seguire passo passo, e senza sforzo alcuno, l'esecuzione del programma e di compenderne le operazioni. Un'altra debolezza, di cui molti ignorano l'esistenza o fanno finta di nulla, é che le istruzioni del Visual Basic sono raggrupate nella libreria:
VBRUNXXX.dll. Per un cracker non è difficile scrivere una fakelibrary da spacciare per la VBRUNXXX.dll, o alterarne una copia, operazione definita in gergo "effettuare un patch", ed usarla per intercettare le chiamate a funzioni standard, come il confronto delle stringhe, mattoncino fondamentale nel 99% delle routine per la verifica del codice di sblocco o delle password, da qui a rintracciare la routine che la chiama, il passo è molto breve. Ovviamente la toppa ad entrambe le falle è possibile e quasi banale, ma quante volte qualcuno si preoccupato di tapparle?


Per esempio, basterebbe creare una funzione proprietaria per la comparazione delle stringhe, ma per rendere più arduo il compito del cracker deve essere usata anche quando la funzione standard è reputata sicura (perché non manipola dati sensibili), ma non sempre, altrimenti il cracker smaliziato magerebbe, troppo in fretta, la foglia. Oppure si potrebbero eseguire i controlli di checksum sulle funzioni offerte dall'OS per verificare sia siano state alterate in qualche modo, fidarsi è bene non fidarsi è ...


Prima partire in quarta a canzonare il mondo windows anche per questa pecca, è bene guardare i nostri tool di sviluppo, ed infatti almeno uno ha lo stesso tallone d'Achille, si tratta dell'AMOS Pro. Per ridurre la dimensione degli eseguibili c'è la possibilità di non linkare le routine dei comandi, e di accedervi tramite la libreria amos.library. A dire il vero, a causa di un curioso bug del compilatore, l'unico modo di far funzionare i programmi compilati è proprio quello di non linkare la libreria, esponendoli tutti ad attacchi basati sull'uso di fakelibrary. Quindi prima di scrivere una killer application con il primo tool che vi capita per le mani, controllate accuratamente anche gli eseguibili da esso prodotti.


Sul CD Rom allegato alla rivista, troverete un framework per identificare univocamente il sistema Amiga sulla quale gira l'applicazione. Il suddetto framework è stato rilasciato come Amiga Foundation Classes quasi 2 anni fa, i suoi sorgenti sono prelevabili dal sito http://afc.sourceforge.org, e maggiori informazioni sul progetto AFC potrete averle leggendo l'articolo di Fabio Rotondo ed Andrea Galimberti apparso sul numero di 105 di Enigma Amiga Life, o visitando il sito http://www.intercom.it/~fsoft/afc.html.


Sul CD Rom, in particolare, troverete il modulo del framework, la sua documentazione, ed un esempio d'uso con tanto di eseguibile e sorgente. Se eseguiamo un dump esadecimale del modulo, in coda vedremo apparire i nomi delle costanti e delle funzioni definite al suo interno, fortunatamente queste informazioni vengono rimosse dal compilatore E quando il modulo viene linkato al resto dell'applicativo, come è possibile rendersi conto effettuando, questa volta, il dump dell'eseguibile. Però, si c'è un però, compaiono le frasi di dialogo con l'utente e i nomi dei file usati dall'applicativo, comunque abbiamo già discusso, nel corso delle precedenti puntate su come non farle comparire.


Supponiamo di voler distribuire il software come shareware, in questo modo si abbattono i costi derivanti dalla catena distribuzione o dalla realizzazione del package; operazioni che possono essere rese del tutto superflue se si rilascia il software solo via internet, lasciando liberi i probabili acquirenti di prelevare, dal nostro sito, una versione dimostrativa. Per consentire una valutazione completa prima del suo acquisto, introduciamo una limitazione a tempo, per esempio sul modello Cinderella, ovvero allo scoccare di una prefissata data il programma disattiva tutte o alcune sue funzioni se non ancora registrato, ma si potrebbe scegliere anche il modello Quiver, ovvero l'utente ha a disposizione un certo numero di crediti, che consentono una fruizione completa del programma, una volta terminati, anche in questo caso, il programma si disattiva. E' necessario impedire che la banale disinstallazione e successiva reinstallazione della versione dimostrativa riattivi interamente il programma, ma questo è lasciato come esercizio al lettore.


Dato che comunque all'utente viene fornita una versione completa del software, ma limitata nel tempo di utilizzo, possiamo scegliere di disattivare la limitazione attraverso l'uso di una password, che una volta inserita, in un apposito requester, innesca un meccanismo che porta alla creazione, direttamente sulla macchina dell'utente, di un keyfile, versione software della Dongle, evitando all'utente la noia di dover,a inserire ad ogn riavvio. Per impedire che il keyfile possa essere ceduto a terzi, o che possa essere rubato attraverso uno dei tanti trojan che circolano, al suo interno deve essere memorizzata la configurazione hardware e software della macchina ospite, e deve essere ritenuto valido solo se la configurazione della macchina rilevata durante l'esecuzione del programma coincida con quella memorizzata nel keyfile, un sempio di come sia possibile realizzate ciò con poca fatica è il framework allegato, vi consiglio di leggere con attenzione la documentazione, anche se in inglese.


Così facendo abbiamo protetto il keyfile, ma la password? Infatti può essere usata più volte e su macchine diverse sia se l'utente non è onesto e sia se qualcuno intercetta la comunicazione della password, magari avvenuta via e-mail.
Come possiamo introdurre un controllo anche su di essa? Chiunque, con un po' di fantasia e qualche conoscenza matematica, può inventarsi un sistema valido, per esempio si può inseire, al suo interno, un particolare codice che l'abilità prima di una certa data, o solo se una ben determinata accoppiata hardware è presente sulla macchina ospite, non dimentichiamoci che possiamo obbligare l'utente ad indicare, tra le altre cose, anche la configurazione della propria macchina, affinchè la registrazione sia valida, o un codice univoco generato dall'applicazione al suo primo avvio. Una soluzione più funzionale, ma poco pratica per le piccole case di produzione, anche se la recente mania di proporre flaterate per internet da parte dei grossi provider nazionali la rente meno onerosa, è di mantenere online 24 ore su 24, 7 giorni su 7, un apposito server e realizzare una procedura di certificazione online, basata su di un protocollo sicuro come il popolare SSL, o un Zero Knowledge Protocol visto nella scora puntata. In questo modo si può anche gestire il pagamento in tempo reale della quota di registrazione, usando una semplice carta di credito, automatizzando in un sol colpo tutta la fase di vendita, lasciando al produttore solo l'onere di aggiornare, in caso di modifiche, l'archivio online contente il software ed il suo prezzo.


Il sistema, ad un esame superficiale, sembra buono, ma presenta ancora due punti deboli, uno riguardante l'efficacia e la sicurezza del sistema password, tra breve vedremo in dettaglio quale sia, mentre il secondo riguarda la memorizzazione della configurazione della macchina all'interno del keyfile.
Questo artificio impedisce di usare la stessa licenza su macchine diverse, e questo è proprio l'effetto desiderato, ma impedisce anche all'utente di aggiornare il proprio computer, o di sostituirlo. Con qualche espediente dobbiamo rendere ciò possibile. Molti prodotti commerciali, nel periodo in cui il DOS faceva da padrone, consentivano di installare il software un numero limitato di volte, corrispondente al numero di licenze acquistate, dopo di che era necessario disinstallare da qualche macchina per guadagnare un nuovo credito di installazione. Possiamo prendere a prestito l'idea, realizzando un'apposita utility, il cui compito è di verificare la congruenza tra la configurazione della macchina ospite ed il keyfile, e quindi procedere alla sua rimozione, rilasciando all'utente una nuova password, o un Certificato di Ritiro Licenza, attraverso il quale ottenere una nuova licenza, e quindi una nova password, senza alcun esborso, ovviamente anche questa fase potrebbe avvenire online, cosa che consentirebbe alla softwarehouse di avere un controllo più efficace ed in tempo reale.


I più scaltri avranno scovato una piccola falla, attraverso la quale farsi rilasciare una password o un Certificato di Ritiro Licenza, e allo stesso tempo non distruggere il keyfile, in effetti basta effettuarne un backup poco prima del ritiro. Anche questo trucco può essere reso inoffensivo una volta noto, per esempio usando la ridondanza, ovvero creando 2 o più keyfile, di cui uno solo visibile all'utente, ed è quello che viene effettivamente rimosso, negli altri doppioni dovrà essere annotato l'avvenuto ritiro della licenza. In alternativa, si può procedere crittando il keyfile, ad ogni lancio, con una nuova chiave, memorizzata in qualche oscuro recesso dei file di sistema; in questo modo ripristinando il backup del solo keyfile, questo risulta lo stesso inservibile, dato che l'ultima chiave memorizzata è certamente, o quasi, differente da quella usata nella vecchia codifica.

Torniamo al punto lasciato in sospeso, e rigurdante la password da inserire per ottenere lo sblocco del programma. Questa volta il punto debole è l'apertura del requester in se, infatti essendo il frutto di una chiamata standard e ben nota, può essere rintracciato facilmente, e con precisione chirurgica, il punto esatto dal quale viene invocata. Per far ciò non c'è bisogno di disassemblare e studiare il sorgente così ottenuto, ma è sufficiente un po' di inventiva ed una banale utility che consenta di misurare le distanza in pixel sullo schermo. Una volta misurato il requester si procede a cercare, nell'eseguibile una particolare sequenza di byte, che consente di identificare i possibili punti nei quali si visualizza un requester le cui dimensioni siano proprio quelle misurate precedentemente; anche la stessa posizione dei gadget, il titolo, o le frasi visualizzate, possono servire da traccia per inseguire e stanare la procedura di controllo della password. Per confondere il cracker, è bene intorbidire le acque, usando requester non standard, ma in tutto e per tutto simili a quelli di sistema, o per lo meno generare dinamicamente sia le frasi sia le dimensioni del requester, che se fatte variare di pochi pixel, in modo del tutto casuale, rendono la ricerca di dimensioni prestabilite del tutto vana. Volendo essere perfidi si potrebbe realizzare una procedura, richiamata in punti diversi, da usarsi solo come trappola dato che, in realtà, la invochiamo solo in presenza di eventi "impossibili", e quindi non facenti parte del normale flusso del programma. Lo scopo della procedura trappola è quello di visualizzare requester di sistema del tutto simile a quello realmente usato per richiedere la password, ed eventualmente possiamo inserire anche una finta funzione di validazione del codice, magari di tipo frattale, giusto per far perdere altro tempo a decifrare un algoritmo inutile.


Si conclude qui, questa chiacchierata. Non era mia intenzione proporre una soluzione chiavi in mano, cosa che per altro non avrebbe molto senso all'atto pratico, ma fornire alcuni ulteriori consigli e trucchi per progettare, e successivamente realizzare, il proprio schema di protezione che non avevano trovato spazio nei precedenti articoli.

Francesco De Napoli