Instrumentación con ByteBuddy ejecutando método premain

Con instrumentación podemos hacer cosas con el bytecode antes y después que este sea subido/cargado a la JVM, aquí tengo 3 simples clases, un patético JFrame con una X=10; que eso puede representar desde la vida de un jugador o cualquier otro tipo de variable en cualquier aplicación java(escritorio, web app etc) con esta técnica podemos modificar ese valor.

El creador de esta api, aquí ver issue me recomendó simplemente hacerle instrumentación al constructor de la clase Usuario y añadir un aviso de salida aka (Advice linea 21 de la clase Agent) y setear el valor deseado.

De esta manera ejecutamos nuestro agente junto al target o .jar a instrumentar, en realidad se usa el método premain, por que ? pues este aplica el trabajo antes de que el bytecode se carge a la JVM, cosa distinta a dynamic attach, que este usa el método agentmain, para adjuntarse al proceso de la aplicación después de iniciarse, debido a la necesidad del pid que el SO le asigna, y poder luego hacerle instrumentación en tiempo de ejecución o runtime.

public class ReflectionOnClass extends JFrame {

    private static MyProgram2 user = new MyProgram2();
    private JLabel jLabel = new JLabel("X: ");

    public ReflectionOnClass() {
        jLabel.setText("X: " + user.getLife()); // return 10
        setLayout(new FlowLayout());
        setPreferredSize(new Dimension(200, 200));
        add(jLabel);
        setDefaultCloseOperation(3);
        setLocationRelativeTo(null);
        pack();
        setVisible(true);
    }

    public static void main(String ...blablabla) {
        new Thread(ReflectionOnClass::new).start();
    }
}
public class MyProgram2 {

    private static Usuario USUARIO = new Usuario();

    public MyProgram2() {}

    public int getLife() {
        return USUARIO.getLife();
    }

}
public class Usuario {

     private int life= 10;

     public Usuario() {

     }

     public int getLife() {
         return life;
     }

     @Override
     public String toString() {
         return "Hi";
     }
}

Al crear este .jar con cualquier sistema de gestión de dependencias, gradle, maven(el mejor), ant etc, y ejecutar este simple .jar debería de salir así

ventana10

Creando el premain

Bien esta es nuestra clase que contendrá el método premain

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;

/**
*
* Agent with bytebuddy
*
*/
public class Agent {

     public static void premain(String args,Instrumentation inst) {
          System.out.print("premain");
          new AgentBuilder.Default()
                  .with(new AgentBuilder.InitializationStrategy.SelfInjection.Eager())
                  .with(AgentBuilder.InstallationListener.StreamWriting.toSystemOut())
                  .type((ElementMatchers.nameContains("Usuario")))
                  .transform((builder, typeDescription, classLoader, module) ->
                         builder.visit(Advice.to(MoreLifeAdvice.class)
                                 .on(ElementMatchers.isConstructor()))
                  )
                 .installOn(inst);
     }
}

La clase MoreLifeAdvice.class que seŕa el decorador.

import net.bytebuddy.asm.Advice;

/**
* Advice
*/
public class MoreLifeAdvice {

    @Advice.OnMethodExit
    static void giveMeMoreLife(@Advice.FieldValue(value = "attack", readOnly = false) int attack,
                                   @Advice.FieldValue(value = "life", readOnly = false) int life,
                                   @Advice.FieldValue(value = "speed",readOnly = false) float speed,
                                   @Advice.FieldValue(value = "shootSpeed",readOnly = false) float shootSpeed,
                                   @Advice.FieldValue(value = "shootDelay",readOnly = false) int shootDelay,
                                   @Advice.FieldValue(value = "name",readOnly = false) String name)

    throws Exception {
        System.out.println("New life set to 1000");
        life = 1000;
        attack = 1000;
        speed = 100f;
        shootSpeed = 10f;
        shootDelay = 10;
        name = "LaViejaRubena";
    }
}

Parte del pom.xml

<archive> (1)
    <manifest>
         <addClasspath>true</addClasspath>
    </manifest>
    <manifestEntries>
        <Premain-Class>com.prueba.bytebuddyagent.Agent</Premain-Class> (2)
        <Can-Redefine-Classes>true</Can-Redefine-Classes>
        <Can-Retransform-Classes>true</Can-Retransform-Classes>
        <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
    </manifestEntries>
</archive>
1 El pom.xml se debera configurar con el tag <archive> para la generación de una carpeta en nuestro proyecto llamada META-INF que esta contendra un fichero MANIFEST.MF lo cual permitirá redefinir y retransformar las clases en la JVM.
2 Muy importante, esta contendra el método premain, agentmain, o ambos

Valor resultante en el JFrame

Este debería ser el resultado en el JFrame una vez que ejecutamos nuestro agente.

java -javaagent:agent.jar -jar target.jar

ejecutantoJframeMasConsola

Y porque tannnto aburrimiento Ahora ?

Tengo el ejemplo de una clase que hace reflection a este juego en java por el popular 3n31ch pero resulta y acontece que los cambios se aplican luego que el juego termina 🤣, cosa que no queremos.

juego1

Lo que hace es abrir un JFileChooser, para buscar el juego, y añadir la clase principal en tiempo de ejecución por medio del URLClassLoader al classpath, permitiendo modificar valores en memoria vía reflection, usamos al paquete java.lang.reflect con sus clases para hacer algo de magia.

javaLanReflect

Pero bua 😕 esa no es la jugada que queremos, ni la clase támpoco, deseamos es algo mejor al menos.

import com.testjava8.ejemplos.util.GetFileChooser;
import elhacker.User;

import javax.swing.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class TestingReflection {

    //no usado
    private static final String YO = "pran-u375177";

    public TestingReflection() {

        try {
            final Path path = Paths.get("src/main/resources/example.jar");
            final URLClassLoader child = new URLClassLoader(new URL[]{path.toUri().toURL()}, this.getClass().getClassLoader());
            final Class<?> classToLoad = Class.forName("mygame.Program", true, child);
            /*
            * Invocando el metodo main para que se instancie.
            */
            classToLoad.getMethod("main",String[].class).invoke(classToLoad,(Object) null);
            setearNuevosValores(classToLoad);

        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }

    private void setearNuevosValores(final Class<?> program) {
        try {
            final Field copyUser = program.getDeclaredField("USER");
            copyUser.setAccessible(Boolean.TRUE);
            final User user = (User) copyUser.get(program);
            final Class<?> userClass = user.getClass();

            final Field attack = userClass.getDeclaredField("attack");
            final Field speed = userClass.getDeclaredField("speed");
            final Field shootDelay = userClass.getDeclaredField("shootDelay");
            final Field shootSpeed = userClass.getDeclaredField("shootSpeed");
            final Field life = userClass.getDeclaredField("life");
            Stream.of(attack, speed, shootDelay, shootSpeed, life).forEach(fields -> fields.setAccessible(true));
            life.set(user, 10000);
            attack.set(user, 10000);
            speed.set(user, 15);
            shootDelay.set(user, 30);
            shootSpeed.set(user, 100);

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public static void main(String... blabla) {
        new TestingReflection();
    }
}

Esto si esta mejor

new AgentBuilder.Default() (1)
         .with(new AgentBuilder.InitializationStrategy.SelfInjection.Eager())
         .with(AgentBuilder.Listener.StreamWriting.toSystemOut()) (2)
         .type((ElementMatchers.nameContains("User"))) (3)
         .transform((builder, typeDescription, classLoader, module) ->
                builder.visit(Advice.to(MoreLifeAdvice.class) (4)
                    .on(ElementMatchers.isConstructor())) (5)
         )
         .installOn(inst); (6)
1 Instanciando el AgentBuilder
2 El Juego tiene una clase User donde modificaremos algunas de sus variables de instancia, Rafael recomendó usar Listener en ves de InstallationListener, también la salida en consola, se pude ver parte de la instrumentación, log’s de bytebuddy.
3 Al contructor User 🤣, parecido al ejemplo básico del inicio, pero se machea por este objeto.
4 Nuestro Advice quedará igual al anterior.
5 Coincidir con el constructor.
6 Se reuza la instancia de la clase java.lang.instrument.Instrumentation

Ejecutando

Se debe seleccionar la jdk8 en el ide de preferencia para dar compatibilidad.

Para compilar la aplicación y crear nuestro agente

mvn clean install

El archivo final estará en la carpeta target llamado

InstrumentacionByteBuddyInstallOn-1.0-SNAPSHOT-jar-with-dependencies.jar

Para ejecutar nuestro agente, el juego (el .jar a instrumentar) debe estar en el mismo directorio del agente

java -javaagent:agent.jar -jar juego.jar

bytebuddyagent

En la animación anterior lo renombre a agent.jar por comodidad.

Nuestro player ahora debería esta con la vida en 1000 y otras cosas también 🤣 o sea:

  • life = 1000;

  • attack = 1000;

  • speed = 100f;

  • shootSpeed = 10f;

  • shootDelay = 10;

  • nombreUsuario = XD

maximoPuntajeLogrado