Java Language
Bytecode Modification
Recherche…
Qu'est-ce que Bytecode?
Bytecode est l'ensemble des instructions utilisées par la JVM. Pour illustrer cela, prenons ce programme Hello World.
public static void main(String[] args){
System.out.println("Hello World");
}
C'est ce qu'il devient en compilant 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
Quelle est la logique derrière cela?
getstatic - Récupère la valeur d'un champ statique d'une classe. Dans ce cas, le PrintStream "Out" du système .
ldc - Poussez une constante sur la pile. Dans ce cas, la chaîne "Hello World"
invokevirtual - Invoque une méthode sur une référence chargée sur la pile et place le résultat sur la pile. Les paramètres de la méthode sont également extraits de la pile.
Eh bien, il doit y avoir plus de droit?
Il y a 255 opcodes, mais tous ne sont pas encore implémentés. Un tableau avec tous les opcodes actuels peut être trouvé ici: listes d'instructions de bytecode Java .
Comment puis-je écrire / modifier le bytecode?
Il existe plusieurs façons d’écrire et de modifier le bytecode. Vous pouvez utiliser un compilateur, utiliser une bibliothèque ou utiliser un programme.
Pour écrire:
Pour l'édition:
- Bibliothèques
- ASM
- Javassiste
- BCEL - Ne prend pas en charge Java 8+
- Outils
- Bytecode-Viewer
- JBytedit
- reJ - Ne supporte pas Java 8+
- JBE - Ne supporte pas Java 8+
J'aimerais en savoir plus sur le bytecode!
Il y a probablement une page de documentation spécifique pour le bytecode. Cette page se concentre sur la modification de bytecode en utilisant différentes bibliothèques et outils.
Comment éditer des fichiers jar avec ASM
Tout d'abord, les classes du pot doivent être chargées. Nous utiliserons trois méthodes pour ce processus:
- loadClasses (Fichier)
- readJar (JarFile, JarEntry, Map)
- 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;
}
Avec ces méthodes, charger et modifier un fichier JAR devient une simple question de changement de ClassNodes dans une carte. Dans cet exemple, nous remplacerons toutes les chaînes du jar par des majuscules à l'aide de l'API Tree.
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();
}
}
}
}
}
Maintenant que toutes les chaînes de ClassNode ont été modifiées, nous devons enregistrer les modifications. Pour enregistrer les modifications et avoir une sortie de travail, il faut faire quelques choses:
- Exporter les ClassNodes en octets
- Charger les entrées jar non class (Ex: Manifest.mf / autres ressources binaires dans jar) en octets
- Enregistrer tous les octets dans un nouveau fichier jar
À partir de la dernière partie ci-dessus, nous allons créer trois méthodes.
- processNodes (Map <String, ClassNode> noeuds)
- loadNonClasses (File jarFile)
- saveAsJar (Map <String, byte []> outBytes, String nomFichier)
Usage:
Map<String, byte[]> out = process(nodes, new HashMap<String, MappedClass>());
out.putAll(loadNonClassEntries(jarFile));
saveAsJar(out, "sample-edit.jar");
Les méthodes utilisées:
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();
}
}
C'est tout. Toutes les modifications seront enregistrées dans "sample-edit.jar".
Comment charger un ClassNode en tant que classe
/**
* 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;
}
}
Comment renommer les classes dans un fichier 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 est une classe existante dans la bibliothèque ASM. Cependant, cela ne permet que de modifier les noms de classe. Si vous souhaitez renommer les champs et les méthodes, vous devez créer votre propre implémentation de la classe Remapper.
Javassist Basique
Javassist est une bibliothèque d'instrumentation de bytecode qui vous permet de modifier le code Java d'injection de bytecode qui sera converti en bytecode par Javassist et ajouté à la classe / méthode instrumentée lors de l'exécution.
Écrivons le premier transformateur qui prend réellement une classe hypothétique "com.my.to.be.instrumented.MyClass" et ajoute aux instructions de chaque méthode un appel de journal.
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;
}
}
Maintenant, pour utiliser ce transformateur (afin que notre JVM appelle la méthode transform sur chaque classe au moment du chargement), nous devons ajouter cet instrument avec un agent:
import java.lang.instrument.Instrumentation;
public class EasyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// registers the transformer
inst.addTransformer(new DynamicTransformer());
}
}
La dernière étape pour démarrer notre première expérience avec un instrument consiste à enregistrer cette classe d'agent dans l'exécution de la machine JVM. Le moyen le plus simple est de l’enregistrer avec une option dans la commande shell:
java -javaagent:myAgent.jar MyJavaApplication
Comme nous pouvons le voir, le projet agent / transformer est ajouté en tant que fichier jar à l'exécution de toute application appelée MyJavaApplication qui est supposée contenir une classe nommée "com.my.to.be.instrumented.MyClass" pour exécuter réellement notre code injecté.