Ricerca…


Cos'è il Bytecode?

Bytecode è l'insieme di istruzioni utilizzate da JVM. Per illustrare questo, prendiamo questo programma Hello World.

public static void main(String[] args){
    System.out.println("Hello World");
}

Questo è ciò che trasforma quando viene compilato in bytecode.

public static main([Ljava/lang/String; args)V    
    getstatic java/lang/System out Ljava/io/PrintStream;
    ldc "Hello World"
    invokevirtual java/io/PrintStream print(Ljava/lang/String;)V

Qual è la logica dietro a questo?

getstatic - Riconosce il valore di un campo statico di una classe. In questo caso, PrintStream "Out" del sistema .

ldc - Spingi una costante nello stack. In questo caso, la stringa "Hello World"

invokevirtual - Invoca un metodo su un riferimento caricato sullo stack e mette il risultato in pila. I parametri del metodo sono anche presi dalla pila.

Bene, ci deve essere più giusto?

Ci sono 255 opcode, ma non tutti sono ancora implementati. Una tabella con tutti gli opcode attuali può essere trovata qui: Elenchi di istruzioni bytecode Java .

Come posso scrivere / modificare bytecode?

Esistono diversi modi per scrivere e modificare bytecode. È possibile utilizzare un compilatore, utilizzare una libreria o utilizzare un programma.

Per scrivere:

Per la modifica:

Mi piacerebbe saperne di più sul bytecode!

Probabilmente esiste una pagina di documentazione specifica per bytecode. Questa pagina si concentra sulla modifica di bytecode utilizzando diverse librerie e strumenti.

Come modificare i file jar con ASM

Innanzitutto le classi del vaso devono essere caricate. Useremo tre metodi per questo processo:

  • loadClasses (File)
  • readJar (JarFile, JarEntry, Mappa)
  • getNode (byte [])
Map<String, ClassNode> loadClasses(File jarFile) throws IOException {
    Map<String, ClassNode> classes = new HashMap<String, ClassNode>();
    JarFile jar = new JarFile(jarFile);
    Stream<JarEntry> str = jar.stream();
    str.forEach(z -> readJar(jar, z, classes));
    jar.close();
    return classes;
}

Map<String, ClassNode> readJar(JarFile jar, JarEntry entry, Map<String, ClassNode> classes) {
    String name = entry.getName();
    try (InputStream jis = jar.getInputStream(entry)){
        if (name.endsWith(".class")) {
            byte[] bytes = IOUtils.toByteArray(jis);
            String cafebabe = String.format("%02X%02X%02X%02X", bytes[0], bytes[1], bytes[2], bytes[3]);
            if (!cafebabe.toLowerCase().equals("cafebabe")) {
                // This class doesn't have a valid magic
                return classes;
            }
            try {
                ClassNode cn = getNode(bytes);
                classes.put(cn.name, cn);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return classes;
}

ClassNode getNode(byte[] bytes) {
    ClassReader cr = new ClassReader(bytes);
    ClassNode cn = new ClassNode();
    try {
        cr.accept(cn, ClassReader.EXPAND_FRAMES);
    } catch (Exception e) {
        e.printStackTrace();
    }
    cr = null;
    return cn;
}

Con questi metodi, caricare e modificare un file jar diventa una semplice questione di modifica dei ClassNode in una mappa. In questo esempio sostituiremo tutte le stringhe nel jar con quelle in maiuscolo utilizzando l'API Tree.

File jarFile = new File("sample.jar");
Map<String, ClassNode> nodes = loadClasses(jarFile);
// Iterate ClassNodes
for (ClassNode cn : nodes.values()){
    // Iterate methods in class
    for (MethodNode mn : cn.methods){
        // Iterate instructions in method
        for (AbstractInsnNode ain : mn.instructions.toArray()){
            // If the instruction is loading a constant value 
            if (ain.getOpcode() == Opcodes.LDC){
                // Cast current instruction to Ldc
                // If the constant is a string then capitalize it.
                LdcInsnNode ldc = (LdcInsnNode) ain;
                if (ldc.cst instanceof String){
                    ldc.cst = ldc.cst.toString().toUpperCase();
                }
            }
        }
    }
}

Ora che tutte le stringhe di ClassNode sono state modificate, è necessario salvare le modifiche. Per salvare le modifiche e avere un output funzionante, alcune cose devono essere fatte:

  • Esporta ClassNodes in byte
  • Caricare voci jar non di classe (ad esempio Manifest.mf / altre risorse binarie in jar) come byte
  • Salva tutti i byte in un nuovo barattolo

Dall'ultima parte sopra, creeremo tre metodi.

  • processNodes (Map <String, ClassNode> nodi)
  • loadNonClasses (File jarFile)
  • saveAsJar (Mappa <String, byte []> outBytes, String fileName)

Uso:

Map<String, byte[]> out = process(nodes, new HashMap<String, MappedClass>());
out.putAll(loadNonClassEntries(jarFile));
saveAsJar(out, "sample-edit.jar");

I metodi usati:

static Map<String, byte[]> processNodes(Map<String, ClassNode> nodes, Map<String, MappedClass> mappings) {
    Map<String, byte[]> out = new HashMap<String, byte[]>();
    // Iterate nodes and add them to the map of <Class names , Class bytes>
    // Using Compute_Frames ensures that stack-frames will be re-calculated automatically
    for (ClassNode cn : nodes.values()) {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        out.put(mappings.containsKey(cn.name) ? mappings.get(cn.name).getNewName() : cn.name, cw.toByteArray());
    }
    return out;
}

static Map<String, byte[]> loadNonClasses(File jarFile) throws IOException {
    Map<String, byte[]> entries = new HashMap<String, byte[]>();
    ZipInputStream jis = new ZipInputStream(new FileInputStream(jarFile));
    ZipEntry entry;
    // Iterate all entries
    while ((entry = jis.getNextEntry()) != null) {
        try {
            String name = entry.getName();
            if (!name.endsWith(".class") && !entry.isDirectory()) {
                // Apache Commons - byte[] toByteArray(InputStream input)
                //
                // Add each entry to the map <Entry name , Entry bytes>
                byte[] bytes = IOUtils.toByteArray(jis);
                entries.put(name, bytes);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jis.closeEntry();
        }
    }
    jis.close();
    return entries;
}

static void saveAsJar(Map<String, byte[]> outBytes, String fileName) {
    try {
        // Create jar output stream
        JarOutputStream out = new JarOutputStream(new FileOutputStream(fileName));
        // For each entry in the map, save the bytes
        for (String entry : outBytes.keySet()) {
            // Appent class names to class entries
            String ext = entry.contains(".") ? "" : ".class";
            out.putNextEntry(new ZipEntry(entry + ext));
            out.write(outBytes.get(entry));
            out.closeEntry();
        }
        out.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Questo è tutto. Tutte le modifiche verranno salvate in "sample-edit.jar".

Come caricare un ClassNode come classe

/**
 * Load a class by from a ClassNode
 * 
 * @param cn
 *            ClassNode to load
 * @return
 */
public static Class<?> load(ClassNode cn) {
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    return new ClassDefiner(ClassLoader.getSystemClassLoader()).get(cn.name.replace("/", "."), cw.toByteArray());
}

/**
 * Classloader that loads a class from bytes.
 */
static class ClassDefiner extends ClassLoader {
    public ClassDefiner(ClassLoader parent) {
        super(parent);
    }

    public Class<?> get(String name, byte[] bytes) {
        Class<?> c = defineClass(name, bytes, 0, bytes.length);
        resolveClass(c);
        return c;
    }
}

Come rinominare le classi in un file jar

public static void main(String[] args) throws Exception {
    File jarFile = new File("Input.jar");
    Map<String, ClassNode> nodes = JarUtils.loadClasses(jarFile);
    
    Map<String, byte[]> out = JarUtils.loadNonClassEntries(jarFile);
    Map<String, String> mappings = new HashMap<String, String>();
    mappings.put("me/example/ExampleClass", "me/example/ExampleRenamed");
    out.putAll(process(nodes, mappings));
    JarUtils.saveAsJar(out, "Input-new.jar");
}

static Map<String, byte[]> process(Map<String, ClassNode> nodes, Map<String, String> mappings) {
    Map<String, byte[]> out = new HashMap<String, byte[]>();
    Remapper mapper = new SimpleRemapper(mappings);
    for (ClassNode cn : nodes.values()) {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassVisitor remapper = new ClassRemapper(cw, mapper);
        cn.accept(remapper);
        out.put(mappings.containsKey(cn.name) ? mappings.get(cn.name) : cn.name, cw.toByteArray());
    }
    return out;
}

SimpleRemapper è una classe esistente nella libreria ASM. Tuttavia, consente solo di modificare i nomi delle classi. Se si desidera rinominare campi e metodi, è necessario creare la propria implementazione della classe Remapper.

Javassist Basic

Javassist è una libreria di strumentazione bytecode che consente di modificare bytecode iniettando codice Java che verrà convertito in bytecode da Javassist e aggiunto alla classe / metodo instrument in fase di esecuzione.

Consente di scrivere il primo trasformatore che effettivamente prende una classe ipotetica "com.my.to.be.instrumented.MyClass" e aggiunge alle istruzioni di ciascun metodo una chiamata di registro.

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
 
public class DynamicTransformer implements ClassFileTransformer {
 
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
        ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
 
        byte[] byteCode = classfileBuffer;
 
        // into the transformer will arrive every class loaded so we filter 
        // to match only what we need
        if (className.equals("com/my/to/be/instrumented/MyClass")) {
 
            try {
                // retrive default Javassist class pool
                ClassPool cp = ClassPool.getDefault();
                // get from the class pool our class with this qualified name
                CtClass cc = cp.get("com.my.to.be.instrumented.MyClass");
                // get all the methods of the retrieved class
                CtMethod[] methods = cc.getDeclaredMethods()
                for(CtMethod meth : methods) {
                    // The instrumentation code to be returned and injected
                    final StringBuffer buffer = new StringBuffer();
                    String name = meth.getName();
                    // just print into the buffer a log for example
                    buffer.append("System.out.println(\"Method " + name + " executed\" );");
                    meth.insertBefore(buffer.toString())
                }
                // create the byteclode of the class
                byteCode = cc.toBytecode();
                // remove the CtClass from the ClassPool
                cc.detach();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
 
        return byteCode;
    }
}

Ora per utilizzare questo trasformatore (in modo che la nostra JVM chiamerà il metodo transform su ogni classe al momento del caricamento) dobbiamo aggiungere questo strumento o questo con un agente:

import java.lang.instrument.Instrumentation;
 
public class EasyAgent {
 
    public static void premain(String agentArgs, Instrumentation inst) {
         
        // registers the transformer
        inst.addTransformer(new DynamicTransformer());
    }
}

L'ultimo passo per iniziare il nostro primo esperimento con lo strumento è registrare effettivamente questa classe di agenti sull'esecuzione della macchina JVM. Il modo più semplice per farlo è registrarlo con un'opzione nel comando shell:

java -javaagent:myAgent.jar MyJavaApplication

Come possiamo vedere, il progetto agent / transformer viene aggiunto come jar all'esecuzione di qualsiasi applicazione denominata MyJavaApplication che deve contenere una classe denominata "com.my.to.be.instrumented.MyClass" per eseguire effettivamente il nostro codice inserito.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow