Java Language
Bytecode-Änderung
Suche…
Was ist Bytecode?
Bytecode ist der Satz von Anweisungen, der von der JVM verwendet wird. Um dies zu veranschaulichen, nehmen wir dieses Hello World-Programm.
public static void main(String[] args){
System.out.println("Hello World");
}
Das ist, woraus es wird, wenn es in Bytecode kompiliert wird.
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
Was ist die Logik dahinter?
getstatic - Erhält den Wert eines statischen Felds einer Klasse. In diesem Fall ist der PrintStream "Out" des Systems .
ldc - Schiebe eine Konstante auf den Stapel. In diesem Fall ist der String "Hello World"
invokevirtual - Ruft eine Methode für eine geladene Referenz im Stapel auf und legt das Ergebnis auf dem Stapel ab. Die Parameter der Methode werden ebenfalls vom Stack übernommen.
Nun, da muss mehr sein?
Es gibt 255 Opcodes, aber noch nicht alle sind implementiert. Eine Tabelle mit allen aktuellen Opcodes finden Sie hier: Java-Bytecode-Befehlslisten .
Wie kann ich Bytecode schreiben / bearbeiten?
Es gibt mehrere Möglichkeiten, Bytecode zu schreiben und zu bearbeiten. Sie können einen Compiler, eine Bibliothek oder ein Programm verwenden.
Zum Schreiben:
Für die Bearbeitung:
- Bibliotheken
- Werkzeuge
- Bytecode-Viewer
- JBytedit
- reJ - Unterstützt nicht Java 8+
- JBE - Java 8+ wird nicht unterstützt
Ich möchte mehr über Bytecode erfahren!
Es gibt wahrscheinlich eine spezifische Dokumentationsseite speziell für Bytecode. Diese Seite konzentriert sich auf die Änderung von Bytecode mit verschiedenen Bibliotheken und Werkzeugen.
So bearbeiten Sie JAR-Dateien mit ASM
Zunächst müssen die Klassen aus dem Glas geladen werden. Wir werden drei Methoden für diesen Prozess verwenden:
- loadClasses (Datei)
- 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;
}
Mit diesen Methoden wird das Laden und Ändern einer JAR-Datei zum einfachen Ändern von ClassNodes in einer Map. In diesem Beispiel werden wir alle Strings in der Dose mit Hilfe der Tree-API durch großgeschriebene ersetzen.
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();
}
}
}
}
}
Nachdem nun alle Zeichenfolgen des ClassNode geändert wurden, müssen die Änderungen gespeichert werden. Um die Änderungen zu speichern und einen Arbeitsausgang zu haben, müssen einige Dinge getan werden:
- Exportieren Sie ClassNodes in Bytes
- Laden Sie Nicht-Klassen-JAR-Einträge (z. B. Manifest.mf / andere binäre Ressourcen in Jar) als Bytes
- Speichern Sie alle Bytes in einem neuen Glas
Im letzten Abschnitt werden wir drei Methoden erstellen.
- processNodes (Map <String, ClassNode> Knoten)
- loadNonClasses (Datei jarFile)
- saveAsJar (Map <String, Byte []> outBytes, String Dateiname)
Verwendungszweck:
Map<String, byte[]> out = process(nodes, new HashMap<String, MappedClass>());
out.putAll(loadNonClassEntries(jarFile));
saveAsJar(out, "sample-edit.jar");
Die verwendeten Methoden:
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();
}
}
Das ist es. Alle Änderungen werden in "sample-edit.jar" gespeichert.
So laden Sie einen ClassNode als Klasse
/**
* 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;
}
}
So benennen Sie Klassen in einer JAR-Datei um
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 ist eine vorhandene Klasse in der ASM-Bibliothek. Es können jedoch nur Klassennamen geändert werden. Wenn Sie Felder und Methoden umbenennen möchten, sollten Sie Ihre eigene Implementierung der Remapper-Klasse erstellen.
Javassist Basic
Javassist ist eine Bytecode-Instrumentierungsbibliothek, mit der Sie den Bytecode ändern können, der Java-Code einfügt, der von Javassist in Bytecode konvertiert und zur Laufzeit der instrumentierten Klasse / Methode hinzugefügt wird.
Schreiben wir den ersten Transformer, der eine hypothetische Klasse "com.my.to.be.instrument.MyClass" enthält, und fügt den Anweisungen jeder Methode einen Protokollaufruf hinzu.
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;
}
}
Um diesen Transformator zu verwenden (damit unsere JVM die Methodentransformation zum Zeitpunkt des Ladens in jeder Klasse aufruft), müssen Sie dieses Instrument hinzufügen oder dies mit einem Agenten:
import java.lang.instrument.Instrumentation;
public class EasyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// registers the transformer
inst.addTransformer(new DynamicTransformer());
}
}
Der letzte Schritt, um unser erstes Instrumentor-Experiment zu starten, ist das Registrieren dieser Agentenklasse bei der JVM-Maschinenausführung. Der einfachste Weg, dies zu tun, ist die Registrierung mit einer Option im Shell-Befehl:
java -javaagent:myAgent.jar MyJavaApplication
Wie wir sehen, wird das Agent / Transformer-Projekt als Jar zur Ausführung einer Anwendung mit dem Namen MyJavaApplication hinzugefügt, die eine Klasse mit dem Namen "com.my.to.be.instrumented.MyClass" enthalten soll, um den eingefügten Code tatsächlich auszuführen.