Java Language
Modyfikacja kodu bajtowego
Szukaj…
Co to jest kod bajtowy?
Kod bajtowy to zestaw instrukcji używanych przez JVM. Aby to zilustrować, weźmy ten program Hello World.
public static void main(String[] args){
System.out.println("Hello World");
}
Właśnie w to się zamienia po skompilowaniu do kodu bajtowego.
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
Jaka jest logika tego?
getstatic - Pobiera wartość pola statycznego klasy. W takim przypadku PrintStream „Out” z systemu .
ldc - Wciśnij stałą na stos. W tym przypadku ciąg „Hello World”
invokevirtual - Wywołuje metodę z załadowanego odwołania na stosie i umieszcza wynik na stosie. Parametry metody są również pobierane ze stosu.
Cóż, musi być więcej, prawda?
Jest 255 kodów, ale nie wszystkie są jeszcze zaimplementowane. Tabela ze wszystkimi bieżącymi kodami operacji znajduje się tutaj: Listy instrukcji kodu bajtowego Java .
Jak mogę pisać / edytować kod bajtowy?
Istnieje wiele sposobów pisania i edytowania kodu bajtowego. Możesz użyć kompilatora, biblioteki lub programu.
Do pisania:
Do edycji:
- Biblioteki
- Przybory
- Przeglądarka bajtów
- JBytedit
- reJ - nie obsługuje Java 8+
- JBE - nie obsługuje Java 8+
Chciałbym dowiedzieć się więcej o kodzie bajtowym!
Prawdopodobnie istnieje specjalna strona dokumentacji specjalnie dla kodu bajtowego. Ta strona koncentruje się na modyfikacji kodu bajtowego przy użyciu różnych bibliotek i narzędzi.
Jak edytować pliki jar w ASM
Najpierw należy załadować klasy ze słoika. W tym procesie wykorzystamy trzy metody:
- loadClasses (plik)
- readJar (JarFile, JarEntry, Mapa)
- 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;
}
Dzięki tym metodom ładowanie i zmiana pliku jar staje się prostą sprawą zmiany ClassNodes na mapie. W tym przykładzie zastąpimy wszystkie ciągi w słoiku wielkimi literami za pomocą interfejsu API drzewa.
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();
}
}
}
}
}
Teraz, gdy wszystkie ciągi ClassNode zostały zmodyfikowane, musimy zapisać zmiany. Aby zapisać zmiany i uzyskać działający wynik, należy wykonać kilka czynności:
- Eksportuj ClassNodes do bajtów
- Załaduj nie jaralne wpisy jar (np .: Manifest.mf / inne zasoby binarne do jar) jako bajty
- Zapisz wszystkie bajty do nowego słoika
Z ostatniej części powyżej stworzymy trzy metody.
- processNodes (węzły Map <String, ClassNode>)
- loadNonClasses (plik jarFile)
- saveAsJar (Map <String, byte []> outBytes, String fileName)
Stosowanie:
Map<String, byte[]> out = process(nodes, new HashMap<String, MappedClass>());
out.putAll(loadNonClassEntries(jarFile));
saveAsJar(out, "sample-edit.jar");
Zastosowane metody:
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();
}
}
Otóż to. Wszystkie zmiany zostaną zapisane w pliku „sample-edit.jar”.
Jak załadować ClassNode jako klasę
/**
* 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;
}
}
Jak zmienić nazwę klas w pliku 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 to istniejąca klasa w bibliotece ASM. Pozwala to jednak tylko na zmianę nazw klas. Jeśli chcesz zmienić nazwy pól i metod, powinieneś stworzyć własną implementację klasy Remapper.
Javassist Basic
Javassist to biblioteka instrumentacji bajtekodowej, która pozwala modyfikować kod Java wprowadzający kod bajtowy, który zostanie przekonwertowany przez Javassist na kod bajtowy i dodany do instrumentowanej klasy / metody w czasie wykonywania.
Napiszmy pierwszy transformator, który faktycznie przyjmuje hipotetyczną klasę „com.my.to.be.instrumented.MyClass” i dodajmy do instrukcji każdej metody wywołanie dziennika.
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;
}
}
Teraz, aby użyć tego transformatora (aby nasza JVM wywołała metodę transformacji dla każdej klasy w czasie ładowania), musimy dodać ten instrument lub to z agentem:
import java.lang.instrument.Instrumentation;
public class EasyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// registers the transformer
inst.addTransformer(new DynamicTransformer());
}
}
Ostatnim krokiem do rozpoczęcia naszego pierwszego eksperymentu na instrumencie jest zarejestrowanie tej klasy agenta do wykonania maszyny JVM. Najłatwiej to zrobić, rejestrując go z opcją w poleceniu powłoki:
java -javaagent:myAgent.jar MyJavaApplication
Jak widzimy, projekt agenta / transformatora jest dodawany jako jar do wykonania dowolnej aplikacji o nazwie MyJavaApplication, która powinna zawierać klasę o nazwie „com.my.to.be.instrumented.MyClass”, która faktycznie wykonuje nasz wstrzyknięty kod.