La 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 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.