Buscar..


¿Qué es Bytecode?

Bytecode es el conjunto de instrucciones utilizadas por la JVM. Para ilustrar esto tomemos este programa Hello World.

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

Esto es en lo que se convierte cuando se compila en 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

¿Cuál es la lógica detrás de esto?

getstatic : recupera el valor de un campo estático de una clase. En este caso, el PrintStream "Out" del sistema .

ldc - Empuja una constante en la pila. En este caso, el String "Hello World".

invokevirtual : invoca un método en una referencia cargada en la pila y coloca el resultado en la pila. Los parámetros del método también se toman de la pila.

Bueno, tiene que haber más derecho?

Hay 255 códigos de operación, pero no todos están implementados todavía. Una tabla con todos los códigos de operación actuales se puede encontrar aquí: listas de instrucciones de bytecode de Java .

¿Cómo puedo escribir / editar el bytecode?

Hay varias formas de escribir y editar el código de bytes. Puede utilizar un compilador, una biblioteca o un programa.

Para la escritura:

Para la edición:

¡Me gustaría aprender más sobre el bytecode!

Probablemente hay una página de documentación específica específicamente para el código de bytes. Esta página se enfoca en la modificación del código de bytes usando diferentes bibliotecas y herramientas.

Cómo editar archivos jar con ASM

En primer lugar las clases del tarro deben ser cargadas. Usaremos tres métodos para este proceso:

  • loadClasses (Archivo)
  • readJar (JarFile, JarEntry, Mapa)
  • 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 estos métodos, cargar y cambiar un archivo jar se convierte en una simple cuestión de cambiar ClassNodes en un mapa. En este ejemplo, reemplazaremos todas las cadenas en el contenedor por unas en mayúsculas usando la API del árbol.

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();
                }
            }
        }
    }
}

Ahora que se han modificado todas las cadenas de ClassNode, debemos guardar los cambios. Para guardar los cambios y tener una salida de trabajo, hay que hacer algunas cosas:

  • Exportar ClassNodes a bytes
  • Cargar entradas de jar que no sean de clase (Ej: Manifest.mf / otros recursos binarios en jar) como bytes
  • Guarda todos los bytes en un nuevo jar

De la última parte de arriba, crearemos tres métodos.

  • processNodes (Mapa <String, ClassNode> nodos)
  • loadNonClasses (archivo jarFile)
  • saveAsJar (Map <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");

Los métodos utilizados:

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();
    }
}

Eso es. Todos los cambios se guardarán en "sample-edit.jar".

Cómo cargar un ClassNode como una clase

/**
 * 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;
    }
}

Cómo cambiar el nombre de las clases en un archivo 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 es una clase existente en la biblioteca ASM. Sin embargo, solo permite que se cambien los nombres de clase. Si desea cambiar el nombre de campos y métodos, debe crear su propia implementación de la clase Remapper.

Javassist Basic

Javassist es una biblioteca de instrumentación de bytecode que le permite modificar el código de Java de inyección de bytecode que se convertirá en bytecode por Javassist y se agregará a la clase / método instrumentado en el tiempo de ejecución.

Permite escribir el primer transformador que realmente tome una clase hipotética "com.my.to.be.instrumented.MyClass" y agregar a las instrucciones de cada método una llamada de 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;
    }
}

Ahora, para usar este transformador (para que nuestra JVM invoque la transformación del método en cada clase en el momento de la carga) necesitamos agregar este instrumento o esto 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());
    }
}

El último paso para iniciar nuestro primer experimento de instrumentación es registrar esta clase de agente en la ejecución de la máquina JVM. La forma más fácil de hacerlo es registrarlo con una opción en el comando de shell:

java -javaagent:myAgent.jar MyJavaApplication

Como podemos ver, el proyecto de agente / transformador se agrega como un jar para la ejecución de cualquier aplicación llamada MyJavaApplication que se supone que contiene una clase llamada "com.my.to.be.instrumented.MyClass" para ejecutar nuestro código inyectado.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow