Black Magic – Come deserializzare una classe Java senza istanza

java-deserializzare-classe-senza-istanza

java-deserializzare-classe-senza-istanzaLa serializzazione, ovvero la traduzione di una classe in uno stream binario, è largamente utilizzata in molti campi dello sviluppo software perchè costituisce un modo semplice di immagazzinare in memoria o scambiare via rete oggetti Java. Un oggetto serializzato può essere deserializzato utilizzando la definizione della classe originale, ricostruendo di fatto l’istanza di partenza.

Ma che succede se abbiamo uno oggetto serializzato e non abbiamo la più pallida idea di quale sia la classe di partenza? Vediamo qualche espediente che possiamo utilizzare per recuperare i dati.

Perché devo deserializzare un’istanza senza avere la classe?

La risposta a questa domanda non è una sola.

Può succedere ad esempio di non aver versionato correttamente il codice e di non avere più il .class (o il .java) originale. Questo di solito causa un’eccezione quando si prova a fare il cast durante l’operazione di deserializzazione a causa del mismatch del UID (ovvero il long che dovrebbe consentire di discernere fra versioni diverse della stessa classe).

Può succedere anche durante il reverse engineering del codice sorgente di una applicazione decompilata, della quale magari (dico magari eh…) avete trovato qualche file salvato via serializzazione che vi piacerebbe leggere.

Altre casistiche sono possibili, ma normalmente lo sviluppatore tipo si trova di fronte all’impossibilità di ricostruire i dati di partenza.

Come si parte? Caricare il file dal FS ad un ObjectInputStream

La prima operazione è quella ovvia di caricare il file da deserializzare in un InputStream, per poter essere processato dal resto del codice.

Il caricamento è banale e lo si può  fare con 3 righe di codice:

FileInputStream fis = new FileInputStream("/path/mio/file");
InputStream is = fis;

Una volta caricato il file in in input è necessario utilizzare la classe HackedObjectInputStream per deserializzare il file utilizzando il tradizionale metodo  readObject(). Il trucco di questa classe sta nell’override del metodo readClassDescriptor() che, banalizzando il concetto, è il metodo che si occupa di validare la coerenza fra il formato dei dati da deserializzare e la classe che stiamo usando come target. In particolare è possibile evitare la generazione di ClassCastException o di eccezioni derivanti da mismatch sul SerialUID.

package it.elbuild.blog.tutorial;

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

/**
 *
 * @author Luca Adamo, luca.adamo@elbuild.it
 */
class HackedObjectInputStream extends ObjectInputStream {

    public HackedObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
        ObjectStreamClass resultClassDescriptor = super.readClassDescriptor(); // initially streams descriptor
        Class localClass; // the class in the local JVM that this descriptor represents.
        try {
            localClass = Class.forName(resultClassDescriptor.getName());
        } catch (ClassNotFoundException e) {
            return resultClassDescriptor;
        }
        ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass);
        if (localClassDescriptor != null) { // only if class implements serializable
            final long localSUID = localClassDescriptor.getSerialVersionUID();
            final long streamSUID = resultClassDescriptor.getSerialVersionUID();
            if (streamSUID != localSUID) { // check for serialVersionUID mismatch.
                final StringBuffer s = new StringBuffer("Overriding serialized class version mismatch: ");
                s.append("local serialVersionUID = ").append(localSUID);
                s.append(" stream serialVersionUID = ").append(streamSUID);
                Exception e = new InvalidClassException(s.toString());
                resultClassDescriptor = localClassDescriptor; // Use local class descriptor for deserialization
            }
        }
        return resultClassDescriptor;
    }
}

Utilizzare questa classe è abbastanza semplice.

Object myObject = null;
FileInputStream fis = new FileInputStream("/path/mio/file");
InputStream is = fis;
myObject = new HackedObjectInputStream().readObject();

Se sapere già che la classe è un ArrayList o una Collection potete utilizzare direttamente queste istanze per deserializzare l’oggetto. Lo stesso vale per array primari (int[], etc) o per tipi primari semplici. Per semplicità è possibile far eseguire il codice sopra in modalità debug, e posizionare un breakpoint nella linea appena sotto la deserializzazione. Sarà quindi possibile osservare l’oggetto appena deserializzato, valutarne la reppresentazione in byte e ricostruire la sua struttura.

Da questo momento in poi si tratta più che altro di effettuare il reverse engineering del tipo di dato originale, operazione resa più semplice dalla conoscenza, almeno approssimativa del codice sorgente chiamante. Per questo viene in aiuto la decompilazione dei sorgenti, e tutte le operazioni di deoffuscamento che rientrano nel bagaglio tecnico degli sviluppatori più esperti.

ELbuild consulenza Java

elbuild-consulenza-java
ELbuild

ELbuild mette a disposizione di aziende e privati la propria esperienza in termini di sviluppo software e reverse engineering di sistemi esistenti.

Contattaci per sapere come possiamo aiutarti nel rinnovare il tuo gestionale o recuperare dati o software ormai obsoleti, sviluppando versioni moderne in grado di incrementare l’efficienza dei tuoi processi aziendali.

Autore: Luca Adamo

Luca Adamo si è laureato con lode in Ingegneria delle Telecomunicazioni all'Università degli studi di Firenze ed è dottorando in Ingegneria Informatica, Multimedialità e Telecomunicazioni, sempre nella stessa facoltà. Le sue competenze tecniche includono lo sviluppo software, sia orientato al web che desktop, in C/C++ e Java (J2EE, J2SE, J2ME), l'amministrazione di macchine Unix-based, la gestione di reti di telecomunicazioni, ed il design di database relazionali.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *