With instrumentation we can do things with the bytecode before and after it is uploaded to the JVM, here I have 3 simple classes, a pathetic JFrame with an X=10; that can represent from the life of a player or any other type of variable in any java application (desktop, web app etc) with this technique we can modify that value.
The creator of this api, this issue recommended me to simply instrument the constructor of the User class and add an exit prompt aka (Advice line 21 of the Agent class)
and set the desired value.
In this way we execute our agent next to the target or .jar to instrument, in fact the premain method is used, why? because this applies the work before
the bytecode is loaded to the JVM, different thing to dynamic attach, that this uses the agentmain method, to attach itself to the process of the application after
starting, due to the necessity of the pid that the OS assigns to it, and to be able to make instrumentation later in execution time or 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";
}
}
When you create this .jar with any dependency management system, gradle, maven(the best)
, ant etc, and run this simple
.jar it should look like this
Creating the premain
Well this is our class that will contain the premain
method
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);
}
}
The MoreLifeAdvice.class
which is the decorator.
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 | The pom.xml must be configured with the <archive> tag for the generation of a folder in our project called META-INF which will contain a MANIFEST.MF file that will allow to redefine and retransform the classes in the JVM. |
2 | Very important, it will contain the premain, agentmain, or both methods. |
Resulting value in the JFrame
This should be the result in the JFrame once we run our agent.
java -javaagent:agent.jar -jar target.jar
And why so much boredom ?
I have the example of a class that makes reflection to this java game by the popular 3n31ch
but it happens that
the changes are applied after the game ends 🤣, which we do not want.
What it does is to open a JFileChooser
, to search for the game, and add the main class at runtime via the
URLClassLoader
to the classpath, allowing to modify values in memory via reflection
,
we use the java.lang.reflect package with its classes to do some magic.
But bua
😕 that’s not the play we want, nor the kind either, we wish it was something better at least.
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 | Instantiating the AgentBuilder |
2 | The Game has a User class where we will modify some of its instance variables, Rafael recommended to use Listener instead of InstallationListener , also the console output, you can see part of the instrumentation, bytebuddy’s log. |
3 | To the contructor User 🤣, similar to the basic example of the beginning, but it is machined by this object. |
4 | Our Advice will be the same as above. |
5 | Match the constructor. |
6 | The instance of the java.lang.instrument.Instrumentation class is rebuilt. |
Running
The jdk8 must be selected in the ide of preference to provide compatibility. |
To compile the app and create out agent.
mvn clean install
The final file will be in the target folder named
InstrumentacionByteBuddyInstallOn-1.0-SNAPSHOT-jar-with-dependencies.jar |
To run our agent, the game (the .jar to be implemented) must be in the same directory as the agent
java -javaagent:agent.jar -jar juego.jar
In the previous animation I renamed it to agent.jar for convenience.
Our player
should now be with life at 1000
and other things too 🤣 that is:
-
life = 1000;
-
attack = 1000;
-
speed = 100f;
-
shootSpeed = 10f;
-
shootDelay = 10;
-
nombreUsuario = XD