MokaByte Numero 07 - Aprile 1997

 
La decompilazione in Java
 
di
Piero Campanelli 
 

 


 


E' Java un maturo linguaggio di programmazione? E' possibile sviluppare progetti complessi? Quali problemi sorgono nella creazione di applicazioni? Queste sono domande che vengono frequentemente poste tra gli sviluppatori e nelle grandi aziende di software. Da quanto visto alla "Italian Java Conference" e da quanto riportato dalle riviste del settore, sembra che i programmatori stiano convergendo verso questa nuova tecnologia, non solo per semplici e banali applets, ma anche per medio-grandi applicazioni.
Tuttavia esiste un problema che grava su chi sviluppa in Java, a cui poco spazio é stato riservato dai media e che, per il momento, é lontano da una soluzione efficace: il problema della decompilazione.


La decompilazione
E' possibile ottenere il file sorgente di una classe java, dal rispettivo formato bytecode di quella classe, o in parole piú semplici, dato un file .class é possibile derivare, con l'ausilio di particolari programmi, il rispettivo .java. Piú precisamente non si ottiene l'esatta copia dell'originale, bensí una ricostruzione il piú possibile fedele del file sorgente originario.
Questo articolo tenterá di evidenziare le cause di questo problema, cercando di non trattare la questione da un punto di vista formale, bensí con un linguaggio piú semplice e talvolta impreciso, con l'unico scopo di focalizzare al meglio il caso trattato.

Il problema del reverse-engineering in campo informatico é in un certo senso limitato, o piú precisamente il rischio arriva da persone abili, dotate di una grande pazienza e, soprattutto, di molto tempo. (persone di non facile reperibilitá)
Generalmente, indipendentemente dal tipo di piattaforma su cui si opera, da un file eseguibile, generato con uno dei linguaggi di programmazione diffusi al momento, possiamo ottenere, tramite un programma chiamato disassemblatore, le istruzioni assembler che compongono il file in questione. E' evidente che l'interpretazione di un listato assembler (in linea di massima esiste una corrispondeza uno a uno tra istruzioni assembler ed istruzioni del microprocessore) non puó esser fatta a cuor leggero e richiede ore se non settimane di duro lavoro, che magari non sempre portano al risultato atteso. Per Java, attualmente, la vicenda é contraria: da un decompilatore non si ottiene il listato del programma in assembler, bensí si puó ottenere il codice ad alto livello, appunto in linguaggio Java. E' immediato rendersi conto di quanto sia piú semplice e fattibile interpretare un programma scritto in un linguaggio ad alto livello nei confronti di uno scritto a basso, come appunto in assembler.

Si vuole far presente che il reverse-engineering é vietato dalle leggi internazionali sulla tutela del copyright del software. Sebbene la maggior parte dei programmatori del mondo sia onesta e non si "degraderebbe" a lavori di questo genere, risulta comunque pericoloso lasciare a persone disoneste la chance di vanificare o sfruttare il frutto di mesi o anni di sodo lavoro.
 

Perché é possibile la decompilazione?
I class files di Java contengono una sequenza di byte, chiamata bytecode, ovvero il set di istruzioni della Java Virtual Machine. Questi bytecodes viaggiano grazie alla rete Internet fino ad arrivare alle JVM di tutto il mondo. Un class file contiene le informazioni necessarie ad una macchina virtuale Java per eseguire la classe contenuta nel file. Purtroppo sfruttando queste informazioni non solo si é in grado di eseguire applicazioni Java, ma anche di decompilare in codice sorgente i programmi in questione.

Senza specificare i dettagli di questo file, per il cui approfondimento é consigliabile la lettura di [1], viene sotto riportata la struttura, evidenziando i campi critici, ovvero i campi portatori di informazioni utili alla decompilazione.
I byte del class file, memorizzati in big-endian, sono organizzati secondo un particolare formato, in modo tale che la JVM sia in grado di riconoscere ed interpretare in modo corretto i vari campi che formano la struttura.
Per alcuni di questi campi la lunghezza non é costante, bensí variabile (varia a seconda del contenuto della classe): di conseguenza, prima di questi campi sono presenti dei bytes che specificano la lunghezza dei campi successivi.

        Classfile {

                4 byte magic;

                2 byte minor_version;

                2 byte major_version;

        }
 
 

I primi 4 byte contengono il seguente numero 0xCAFEBABE. Grazie a questo codice, la JVM é in grado di capire che la sequenza di bit, data in input, é un class file. Quindi major e minor version identificano a quale versione il class file aderisce.

        2 byte constant_pool_count;

        cp_info constant_pool[constant_pool_count - 1];
 
 

Constant pool é un array avente constant_pool_count elementi che contiene le costanti associate ad una classe, ovvero: strighe, nome della classe, nomi e tipi delle variabili (variabili statiche e variabili istanziate, ma non variabili locali), nomi e tipi dei metodi definiti nella classe.
Per ogni elemento dell'array il primo byte indica il tipo di elemento contenuto in quella posizione. In base a questo byte chiamato "tag", la JVM é in grado di interpretare ció che segue. Ad esempio se il "tag" indica un Integer (codice 0x3) i successivi 4 byte conterranno il valore Integer. Ovviamente esistono tag che identificano una classe, un metodo, una variabile e cosí via….
Costant_pool é di lunghezza variabile (infatti le costanzi associate ad una classe, variano a seconda del contenuto della classe stessa), quindi, come spiegato sopra, si necessita di una serie di byte che identifichino la lunghezza di constant_pool. In questo caso si usano 2 byte e sono chiamati constant_pool_count.

         2 byte access_flags;

        2 byte this_class;

        2 byte super_class;

        2 byte interfaces_count;

        2 byte interfaces[interfaces_count];

        2 byte fields_count;

        field_info fields[fields_count];
 
 
 
 

Tralasciando alcuni campi, si arriva alla tabella "fields" (di lunghezza fields_count) che riporta informazioni sui fields della classe, ovvero delle variabili istanziate o delle variabili di classe. Per ogni variabile viene memorizzato il nome, il tipo, il valore se é una costante. Questi non sono memorizzati in questa tabella, ma nel constant_pool. Di conseguenza, la tabella conterrá dei puntatori alle varie locazioni del constant_pool.
Per esempio il valore di una costante di tipo intero sará referenziato con un puntatore ad una particolare locazione del constant_pool, ad esempio alla locazione dove é memorizzato il valore integer sopra descritto.

2 byte methods_count;

method_info methods[methods_count];
 

methods é una tabella simile alla tabella dei fields, ma con riferimento ai metodi della classe.

2 byte attributes_count;

        attribute_info attributes[attribute_count];
 
 

Anche se visto in modo abbastanza approssimativo, (una trattazione esaustiva richiederebbe pagine e pagine di spiegazioni ed esempi…) risulta chiaro che le informazioni trasportate da un class file, sono vitali ad un ambiente java runtime per l'esecuzione della classe e quindi dell'applicazione o dell'applet e sarebbe quindi impensabile eliminare questo tipo di informazioni.
 

Quali sono i software che permettono la decompilazione?
Non esistono molti software che permettono la decompilazione (fortunatamente), tuttavia é stato possibile provare la versione demo di un prodotto della WingSoft (http://www.wingsoft.com): "JavaDis V2.02"
La limitazione di questa versione free riguarda il numero dei metodi di una classe che possono essere decompilati, infatti il programma esegue il reverse-engineering solo dei primi tre metodi. Tuttavia per pochi dollari, direttamente dal sito, é possibile comprare la versione completa.

Il primo sorgente riportato é un esempio scritto dal programmatore, mentre il secondo esempio é l'output ottenuto dalla decompilazione della classe "provadecompilazione". Si puó notare che le variabili locali non sono decompilate in modo corretto, in quanto, come detto precedentemente, il loro nome non viene memorizzato nel classfile. Si puó inoltre constatare che il sorgente ottenuto non é una copia fedele dell'originale, ma una sua ricostruzione che logicamente ne conserva la semantica.

// Classe di esempio

import java.io.*;
 
 

class provadecompilazione {
 
 

   static int variabile_1;

   int variabile_2;

   String str=new String("Prova!!");
 
 

   public static void main (String args[]) {

      int variabile_locale_1=0;

      int variabile_locale_2;
 
 

      variabile_locale_1=variabile_locale_1+10;

      variabile_1=1000;

      variabile_locale_2=variabile_locale_1;

   }
 
 

   int metodo_prova (int parametro_1,String parametro_2) {

      return parametro_1;

   }

}
 

Ecco il risultato della decompilazione
// This program is genearted by Javadis 2.02, a product from WingSoft

// For more information about WingSoft, please visit http://www.wingsoft.com
 
 

synchronized class  provadecompilazione {

    static int variabile_1;

    int variabile_2;

    String str;
 
 

    public static void main(

        String[] L0) {
 
 

        L1= 0;

        L1= ( (L1 + 10);

        variabile_1= 1000;

        L2= L1;

        return;
 
 

    }
 
 

    int metodo_prova(

        int L1,

        String L2) {
 
 

        return L1;
 
 

    }
 
 

    provadecompilazione() {
 
 

        super();

        str= new String("Prova!!");

        return;
 
 

    }

}
 


Javadis non é l'unico decompilatore che si puó trovare nella rete. Ricordo che esiste un altro programma, anzi il primo decompilatore java, chiamato "mocha". Purtroppo non é stata possibile una prova di questo prodotto, in quanto sembra che sia in giro solo una versione beta che si é rivelata abbastanza instabile nelle prove.

Quali sono i possibili rimedi?
Non é facile trovare dei rimedi a questo problema, tantoché nemmeno Sunsoft ha trovato una possibile soluzione nel JDK 1.1.
Tuttavia, come é stato confermato da alcuni membri di Sunsoft, non é un problema dimenticato, ma si sta studiando una possibile soluzione.
Esistono, comunque, dei tools che garantiscono una minima forma di difesa da questo tipo di minaccia il cui funzionamento consiste "nell'offuscare" il class file, ovvero di complicarlo. Sostitutendo, ad esempio, tutti i nomi di variabili e di metodi con nomi complicati e senza senso in modo da rendere un un po' piú ardua la comprensione del sorgente.
 

Bibliografia
[1] - Tim Lindholm, Frank Yellin "THE JAVA VIRTUAL MACHINE SPECIFICATION", Addison-Wesley http://www.aw.com/cp/lindholm-yellin.html
Una versione ridotta si trova si trova a http://java.sun.com/1.0alpha3/doc/vmspec/vmspec_1.html
 
Note sull'autore
Piero Campanelli é studente di informatica presso l'universitá statale di Milano. 
"Vivo a Varese e mi occupo di sistemi operativi, di reti e di algoritmi. Nel tempo libero mi interesso di astronomia e televisione via satellite. Posso essere contattato tramite posta elettronica all'indirizzo campanel@tin.it oppure se siete radioamatori, via packet-radio a iw2lkg@ik1msl.ipie.ita.eu"

  

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it