Java Language
बाइटकोड मोडिफिकेशन
खोज…
Bytecode क्या है?
बायटेकोड जेवीएम द्वारा उपयोग किए जाने वाले निर्देशों का समूह है। इसे समझने के लिए आइए इस हैलो वर्ल्ड कार्यक्रम को लेते हैं।
public static void main(String[] args){
System.out.println("Hello World");
}
यह तब होता है जब यह बायटेकोड में संकलित हो जाता है।
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
इसके पीछे क्या तर्क है?
गेटस्टिक - एक वर्ग के स्थिर क्षेत्र के मूल्य को पुनः प्राप्त करता है। इस मामले में, सिस्टम के PrintStream "आउट" ।
ldc - स्टैक पर एक स्थिर दबाएं । इस मामले में, स्ट्रिंग "हैलो वर्ल्ड"
invokevirtual - स्टैक पर एक लोड किए गए संदर्भ पर एक विधि को आमंत्रित करता है और स्टैक पर परिणाम डालता है। विधि के पैरामीटर को स्टैक से भी लिया जाता है।
अच्छा, वहाँ और अधिक अधिकार होना चाहिए?
255 ऑपकोड हैं, लेकिन उन सभी को अभी तक लागू नहीं किया गया है। सभी मौजूदा ऑपकोड के साथ एक तालिका यहां पाई जा सकती है: जावा बाइटकोड निर्देश सूची ।
मैं बायटेकोड को कैसे लिख / संपादित कर सकता हूं?
बाईटकोड को लिखने और संपादित करने के कई तरीके हैं। आप एक संकलक का उपयोग कर सकते हैं, एक पुस्तकालय का उपयोग कर सकते हैं, या एक कार्यक्रम का उपयोग कर सकते हैं।
लिखने हेतु:
संपादन के लिए:
- पुस्तकालय
- उपकरण
- बाईटकोड-व्यूअर
- JBytedit
- reJ - जावा 8+ का समर्थन नहीं करता है
- JBE - जावा 8+ का समर्थन नहीं करता है
मैं bytecode के बारे में अधिक जानना चाहते हैं!
संभवत: बायटेकोड के लिए विशिष्ट प्रलेखन पृष्ठ है। यह पृष्ठ अलग-अलग पुस्तकालयों और औजारों का उपयोग करके बाइटकोड के संशोधन पर केंद्रित है।
एएसएम के साथ जार फ़ाइलों को कैसे संपादित करें
सबसे पहले जार से कक्षाएं लोड करने की आवश्यकता होती है। हम इस प्रक्रिया के लिए तीन विधियों का उपयोग करेंगे:
- loadClasses (फाइल)
- readJar (JarFile, JarEntry, मानचित्र)
- getNode (बाइट [])
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;
}
इन विधियों के साथ जार फ़ाइल को लोड करना और बदलना मैप में क्लासनॉड्स को बदलने का एक सरल मामला बन जाता है। इस उदाहरण में हम जार में सभी स्ट्रिंग्स को ट्री एपीआई का उपयोग करके पूंजीकृत लोगों के साथ बदल देंगे।
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();
}
}
}
}
}
अब जब क्लासनोड के सभी तार संशोधित किए गए हैं तो हमें परिवर्तनों को सहेजने की आवश्यकता है। परिवर्तनों को सहेजने और कार्यशील आउटपुट के लिए कुछ चीजें करनी होंगी:
- बाइट्स के लिए निर्यात ClassNodes
- बाइट के रूप में गैर-श्रेणी जार प्रविष्टियाँ (उदा: Manifest.mf / जार में अन्य बाइनरी संसाधन) लोड करें
- सभी बाइट्स को एक नए जार में सहेजें
ऊपर के अंतिम भाग से, हम तीन विधियाँ बनाएँगे।
- प्रक्रिया नोड्स (मानचित्र <स्ट्रिंग, क्लासऑन> नोड्स)
- loadNonClasses (फ़ाइल jarFile)
- saveAsJar (नक्शा <स्ट्रिंग, बाइट []> आउटबाइट्स, स्ट्रिंग फ़ाइलनाम)
उपयोग:
Map<String, byte[]> out = process(nodes, new HashMap<String, MappedClass>());
out.putAll(loadNonClassEntries(jarFile));
saveAsJar(out, "sample-edit.jar");
उपयोग की जाने वाली विधियाँ:
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();
}
}
बस। सभी परिवर्तन "नमूना-edit.jar" में सहेजे जाएंगे।
क्लास के रूप में क्लासऑन लोड कैसे करें
/**
* 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;
}
}
जार फ़ाइल में कक्षाओं का नाम कैसे बदलें
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 ASM लाइब्रेरी में एक मौजूदा क्लास है। हालाँकि यह केवल वर्ग नामों को बदलने की अनुमति देता है। यदि आप फ़ील्ड और विधियों का नाम बदलना चाहते हैं, तो आपको रेम्पैपर वर्ग के अपने स्वयं के कार्यान्वयन का निर्माण करना चाहिए।
जैवासिस्ट बेसिक
Javassist एक bytecode इंस्ट्रूमेंटेशन लाइब्रेरी है जो आपको bytecode इंजेक्शनिंग जावा कोड को संशोधित करने की अनुमति देता है जो Javassist द्वारा bytecode में परिवर्तित हो जाएगा और रनटाइम में इंस्ट्रूमेंटेड क्लास / विधि में जोड़ा जाएगा।
पहले ट्रांसफार्मर को लिखें जो वास्तव में एक काल्पनिक वर्ग "com.my.to.be.instrumented.MyClass" लेते हैं और प्रत्येक विधि के निर्देशों को एक लॉग कॉल कहते हैं।
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;
}
}
अब इस ट्रांसफार्मर का उपयोग करने के लिए (ताकि हमारे जेवीएम लोड समय पर प्रत्येक वर्ग पर विधि को बदलने के लिए कहेंगे) हमें इस एजेंट को एजेंट के साथ जोड़ने की आवश्यकता है:
import java.lang.instrument.Instrumentation;
public class EasyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// registers the transformer
inst.addTransformer(new DynamicTransformer());
}
}
हमारा पहला इंस्ट्रूमेंट प्रयोग शुरू करने का अंतिम चरण वास्तव में इस एजेंट वर्ग को जेवीएम मशीन के निष्पादन के लिए पंजीकृत करना है। वास्तव में यह करने का सबसे आसान तरीका है कि इसे शेल कमांड में एक विकल्प के साथ पंजीकृत किया जाए:
java -javaagent:myAgent.jar MyJavaApplication
जैसा कि हम देख सकते हैं कि एजेंट / ट्रांसफॉर्मर प्रोजेक्ट को MyJavaApplication नाम के किसी भी एप्लिकेशन के निष्पादन के लिए जार के रूप में जोड़ा गया है, जिसमें "com.my.to.be.instrumented.MyClass" नामक एक वर्ग शामिल है, जो वास्तव में हमारे इंजेक्शन कोड को निष्पादित करता है।