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 run time.

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í


image
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 tenia que llamar MuchoMasQueVida XD

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";
    } 
}

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.

la siguiente linea es muy importante, ella contendra el método premain, agentmain, o ambos

<Premain-Class>com.prueba.bytebuddyagent.Agent</Premain-Class>

Parte del pom.xml

<archive>
    <manifest>
         <addClasspath>true</addClasspath>
    </manifest>
    <manifestEntries>
        <Premain-Class>com.prueba.bytebuddyagent.Agent</Premain-Class>
         <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>

Este debería ser el valor resultante en el JFrame mirese el comando usado.

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



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.

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

image

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

Pero bua 😕 esa no es la jugada que queremos, deseamos es algo mejor al menos,

El AgentBuilder quedo así donde hace instrumentación al contructor User 🤣, parecido al ejemplo básico del inicio, el Juego tiene una clase User donde modificaremos algunas de sus variables de instancia, Rafael recomendó usar Listener en ves de InstallationListener, nuestro Advice quedará igual al anterior.

new AgentBuilder.Default()
         .with(new AgentBuilder.InitializationStrategy.SelfInjection.Eager())
         .with(AgentBuilder.Listener.StreamWriting.toSystemOut())
         .type((ElementMatchers.nameContains("User")))
         .transform((builder, typeDescription, classLoader, module) ->
                builder.visit(Advice.to(MoreLifeAdvice.class)
                    .on(ElementMatchers.isConstructor()))
         )
         .installOn(inst);            

Ejecutamos

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

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

    image

Se debe seleccionar la jdk8 en el ide de preferencia para dar compatibilidad con el agente y el juego.

bastaria con

mvn clean install

el archivo final estara en la carpeta target llamado

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

que lo renombre a agent por comodidad.

Comments