Home > java > Bytecode injection con javassist

Bytecode injection con javassist

12 novembre 2010
Translate in English with Google Translate

Lo scopo di questo articolo è di utilizzare una libreria opensource per modificare il bytecode in modo tale da deviare il comportamento di una classe. L’obiettivo può essere quello di apportare una patch, oppure per esigenze di logging o debug del codice oppure per hacking! Prima di tutto dobbiamo partire da una libreria che ci consenta di manipolare il bytecode. Per fare questo possiamo utilizzare diversi progetti:

  1. javassist di JBoss Comunity
  2. ASM del Consorzio ObjectWeb
  3. Byte Code Engineering Library (BCEL) della Apache Software Foundation

Scelgo di usare javassist, progetto molto semplice ed essenziale che mi permette di manipolare il bytecode.

Introduzione al Bytecode

Il Bytecode è il linguaggio macchina della Java Virtual Machine ed assomiglia molto al linguaggio Assembly.

Le istruzioni, dette opcode, hanno la dimensione di 1 byte per questo motivo è stato scelto il nome Bytecode. Le istruzioni sono eventualmente seguite da uno o più argomenti, detti operand.

Il risultato dell’elaborazione viene inserito nell’operand stack oppure viene assegnato a delle variabili.

Il Bytecode viene letto ed interpretato dalla JVM che in base alla compilazione Just-In-Time(JIT) compila a Runtime il codice in linguaggio macchina. Il Bytecode è una sequenza di bytes che devono rispettare una specifica struttura definita nella JSR 202 del Java Comunity Process.

Ogni istruzione corrisponde ad un determinato byte, per facilitare la lettura e la comprensione delle istruzioni espresse in bytes, sono stati stabiliti degli mnemonic ossia delle istruzioni facili da ricordare.

Considerando che ogni mnemonic è di 1 byte, ciò vuol dire che possiamo avere massimo 255 mnemonic, per esempio gli opcode della JVM sono:


binario esadecimale stringa
 00    (0x00)      nop
 01    (0x01)      aconst_null
 02    (0x02)      iconst_m1
 03    (0x03)      iconst_0
 04    (0x04)      iconst_1
 05    (0x05)      iconst_2
 06    (0x06)      iconst_3
...
 196    (0xc4)     wide
 197    (0xc5)     multianewarray
 198    (0xc6)     ifnull
 199    (0xc7)     ifnonnull
 200    (0xc8)     goto_w
 201    (0xc9)     jsr_w

Sono stati stabiliti 3 mnemonic riservati che vengono usati per attività di breakpoint e come back-door:


    202    (0xca)     breakpoint
    254    (0xfe)     impdep1
    255    (0xff)     impdep2

Javassist

La libreria è molto snella e consente attraverso poche istruzioni di apportare modifiche a Runtime al bytecode.
Per capire il suo funzionamento vediamo prima il Class Diagram per capire le relazioni strutturali tra le classi di interesse:

Il punto di partenza è ClassPool e come dice anche il suo nome, è una contenitore di classi, in particolare è un contenitore di CtClass memorizzate in una Hashtable ad accesso sincronizzato residente in memoria. La classe ClassPool permette di creare/caricare delle classi tramite i metodi:

  1. makeClass(): consente di creare una nuova classe
  2. makeInterface(): consente di creare una nuova interfaccia
  3. get(): consente di caricare una classe/interfaccia esistente nel classpath

Questi metodi ritornano un tipo CtClass ( cioè compile-time Class ). Si tratta di una classe astratta che espone i metodi per la manipolazione del bytecode. Poichè si tratta di una classe astratta, l’instanza è rappresentata da una classe figlia, che la specializza, dal nome CtClassType.

CtClassType specializza CtClass ed effettua l’override di alcuni importanti metodi come:

  1. toBytecode(): consente di ottenere il bytecode della classe, in particolare un array di bytes della classe ClassFile
  2. toClass(): consente di convertire la classe in un oggetto java.lang.Class
  3. writeFile(): consente di scrivere il bytecode su file system

La struttura della classe Java, prevista dalla JSR202, viene creata nella classe ClassFile dove vengono conservate le istruzioni (opcode) della JVM. Pertanto non c’è nessuna fase di compilazione, il bytecode viene direttamente modificato!

Passiamo ad un esempio

Cambiamo le caratteristiche di una classe Vittima e modifichiamo la sua superclasse.
Immaginiamo di avere una classe Figlio che eredita dalla classe Padre:


public class Padre {}
public class Figlio extends Padre {}

il codice disassemblato della classe Figlio è il seguente:


public class Figlio extends Padre{
public Figlio();
  Code:
   0:	aload_0
   1:	invokespecial	#1; //Method Padre."":()V
   4:	return
}

Adesso decidiamo di cambiare la paternità della classe Figlio e passarla da Padre a Patrigno.


public class Patrigno {}

Abbiamo il seguente Class Diagram:

Per fare questo creiamo la classe Injection che apporta le modifiche:


import javassist.ClassPool;
import javassist.CtClass;

public class Injection {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("Figlio");
        cc.setSuperclass(pool.get("Patrigno"));
        cc.writeFile();
    }
}

Le istruzioni previste nel metodo main() della classe Injection permettono di:

  1. creare il pool di classi ClassPool
  2. caricare lo stream di bytes del file Figlio.class
  3. modificare la superclasse della classe Figlio e passarla da Padre a Patrigno
  4. salvare le modifiche sul file Figlio.class

Adesso eseguiamo la classe Injecton e controlliamo le modifiche apportate al file Figlio.class usando il disassemblatore Sun/Oracle:


javap -c Figlio
public class Figlio extends Patrigno{
public Figlio();
  Code:
   0:	aload_0
   1:	invokespecial	#15; //Method Patrigno."":()V
   4:	return
}

La modifica è evidente: il costruttore è cambiato da Padre a Patrigno.

Dettagli implementativi

Ora entriamo nei dettagli implementativi e guardiamo prima il Class Diagram.

Notiamo che sia il metodo get() che il metodo makeClass() della classe ClassPool invocano direttamente o indirettamente la classe CtClassType di tipo CtClass


public class ClassPool {
...
        CtClass clazz = new CtClassType(classfile, this);
...
}

di seguito la classe CtClassType instanzia la classe ClassFile:


class CtClassType extends CtClass {
...
    CtClassType(InputStream ins, ClassPool cp) throws IOException {
        this((String)null, cp);
        classfile = new ClassFile(new DataInputStream(ins));
        qualifiedName = classfile.getName();
    }
...
}

La classe ClassFile costruisce/carica la classe secondo le regole strutturali del bytecode previste dalla JSR202.
Il bytecode viene letto e scritto tramite i metodi read() e write() di ClassFile, come segue:


public final class ClassFile {
...
    int major, minor; // version number
    ConstPool constPool;
    int thisClass;
    int accessFlags;
    int superClass;
    int[] interfaces;
    ArrayList fields;
    ArrayList methods;
    ArrayList attributes;
    String thisclassname; // not JVM-internal name
    String[] cachedInterfaces;
    String cachedSuperclass;

    private void read(DataInputStream in) throws IOException {
        int i, n;
        int magic = in.readInt();
        if (magic != 0xCAFEBABE)
            throw new IOException("bad magic number: " + Integer.toHexString(magic));

        minor = in.readUnsignedShort();
        major = in.readUnsignedShort();
        constPool = new ConstPool(in);
        accessFlags = in.readUnsignedShort();
        thisClass = in.readUnsignedShort();
        constPool.setThisClassInfo(thisClass);
        superClass = in.readUnsignedShort();
        n = in.readUnsignedShort();
        if (n == 0)
            interfaces = null;
        else {
            interfaces = new int[n];
            for (i = 0; i < n; ++i)
                interfaces[i] = in.readUnsignedShort();
        }

        ConstPool cp = constPool;
        n = in.readUnsignedShort();
        fields = new ArrayList();
        for (i = 0; i < n; ++i)
            addField2(new FieldInfo(cp, in));

        n = in.readUnsignedShort();
        methods = new ArrayList();
        for (i = 0; i < n; ++i)
            addMethod2(new MethodInfo(cp, in));

        attributes = new ArrayList();
        n = in.readUnsignedShort();
        for (i = 0; i < n; ++i)
            addAttribute(AttributeInfo.read(cp, in));

        thisclassname = constPool.getClassInfo(thisClass);
    }

    public void write(DataOutputStream out) throws IOException {
        int i, n;

        out.writeInt(0xCAFEBABE); // magic
        out.writeShort(minor); // minor version
        out.writeShort(major); // major version
        constPool.write(out); // constant pool
        out.writeShort(accessFlags);
        out.writeShort(thisClass);
        out.writeShort(superClass);

        if (interfaces == null)
            n = 0;
        else
            n = interfaces.length;

        out.writeShort(n);
        for (i = 0; i < n; ++i)
            out.writeShort(interfaces[i]);

        ArrayList list = fields;
        n = list.size();
        out.writeShort(n);
        for (i = 0; i < n; ++i) {
            FieldInfo finfo = (FieldInfo)list.get(i);
            finfo.write(out);
        }

        list = methods;
        n = list.size();
        out.writeShort(n);
        for (i = 0; i < n; ++i) {
            MethodInfo minfo = (MethodInfo)list.get(i);
            minfo.write(out);
        }

        out.writeShort(attributes.size());
        AttributeInfo.writeAll(attributes, out);
    }
...
}

In un successivo articolo vediamo altre caratteristiche di questa libreria.

Categorie:java
%d blogger cliccano Mi Piace per questo: