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