Java Language
Modificatie bytecode
Zoeken…
Wat is Bytecode?
Bytecode is de verzameling instructies die door de JVM wordt gebruikt. Om dit te illustreren, nemen we dit Hello World-programma.
public static void main(String[] args){
System.out.println("Hello World");
}
Dit wordt het als het wordt gecompileerd in 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
Wat is de logica hierachter?
getstatic - Hiermee wordt de waarde van een statisch veld van een klasse opgehaald. In dit geval is de PrintStream "Out" van het systeem .
ldc - Duw een constante op de stapel. In dit geval wordt de tekenreeks 'Hallo wereld'
invokevirtual - Roept een methode op voor een geladen referentie op de stapel en plaatst het resultaat op de stapel. Parameters van de methode worden ook uit de stapel gehaald.
Nou, er moet toch meer zijn?
Er zijn 255 opcodes, maar deze zijn nog niet allemaal geïmplementeerd. Een tabel met alle huidige opcodes vindt u hier: Java bytecode instructielijsten .
Hoe kan ik bytecode schrijven / bewerken?
Er zijn meerdere manieren om bytecode te schrijven en te bewerken. U kunt een compiler gebruiken, een bibliotheek gebruiken of een programma gebruiken.
Om te schrijven:
Om te bewerken:
- bibliotheken
- Gereedschap
- Bytecode-Viewer
- JBytedit
- reJ - Ondersteunt geen Java 8+
- JBE - Ondersteunt geen Java 8+
Ik wil graag meer weten over bytecode!
Er is waarschijnlijk een specifieke documentatiepagina specifiek voor bytecode. Deze pagina richt zich op de wijziging van bytecode met behulp van verschillende bibliotheken en hulpmiddelen.
Hoe jar-bestanden te bewerken met ASM
Eerst moeten de klassen uit de pot worden geladen. We gebruiken drie methoden voor dit proces:
- loadClasses (File)
- 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;
}
Met deze methoden wordt het laden en wijzigen van een jar-bestand eenvoudig een kwestie van ClassNodes in een kaart wijzigen. In dit voorbeeld zullen we alle strings in de pot vervangen door hoofdletters met behulp van de Tree API.
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();
}
}
}
}
}
Nu alle tekenreeksen van ClassNode zijn gewijzigd, moeten we de wijzigingen opslaan. Om de wijzigingen op te slaan en een werkende output te hebben, moeten een paar dingen worden gedaan:
- Export ClassNodes naar bytes
- Laad niet-klasse jar-ingangen (bijv. Manifest.mf / andere binaire bronnen in jar) als bytes
- Bewaar alle bytes in een nieuwe pot
Uit het laatste gedeelte hierboven maken we drie methoden.
- processNodes (Map <String, ClassNode> knooppunten)
- loadNonClasses (File jarFile)
- saveAsJar (Map <String, byte []> outBytes, String fileName)
Gebruik:
Map<String, byte[]> out = process(nodes, new HashMap<String, MappedClass>());
out.putAll(loadNonClassEntries(jarFile));
saveAsJar(out, "sample-edit.jar");
De gebruikte 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();
}
}
Dat is het. Alle wijzigingen worden opgeslagen in "sample-edit.jar".
Hoe een ClassNode als een Class te laden
/**
* 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;
}
}
Hoe klassen in een jar-bestand te hernoemen
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 is een bestaande klasse in de ASM-bibliotheek. Het staat echter alleen toe dat klassennamen worden gewijzigd. Als u velden en methoden wilt hernoemen, moet u uw eigen implementatie van de klasse Remapper maken.
Javassist Basic
Javassist is een bytecode-instrumentatiebibliotheek waarmee u bytecode-injecterende Java-code kunt wijzigen die door Javassist naar bytecode wordt geconverteerd en tijdens runtime aan de geïnstrumenteerde klasse / methode wordt toegevoegd.
Laten we de eerste transformator schrijven die in feite een hypothetische klasse "com.my.to.be.instrumented.MyClass" heeft en een logaanroep toevoegen aan de instructies van elke methode.
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;
}
}
Om deze transformator te gebruiken (zodat onze JVM de methode transform op elke klasse aanroept tijdens het laden) moeten we dit instrument toevoegen of dit met een agent:
import java.lang.instrument.Instrumentation;
public class EasyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// registers the transformer
inst.addTransformer(new DynamicTransformer());
}
}
De laatste stap om ons eerste instrumentor-experiment te starten, is deze agentklasse daadwerkelijk te registreren voor de JVM-machine-uitvoering. De eenvoudigste manier om dit daadwerkelijk te doen, is om het te registreren met een optie in de shell-opdracht:
java -javaagent:myAgent.jar MyJavaApplication
Zoals we kunnen zien, is het agent / transformator-project toegevoegd als een pot aan de uitvoering van elke toepassing met de naam MyJavaApplication die een klasse met de naam "com.my.to.be.instrumented.MyClass" moet bevatten om onze geïnjecteerde code daadwerkelijk uit te voeren.