Java ASM字节码操作无法成功注入代码到方法中

huangapple go评论71阅读模式
英文:

Java ASM byte code manipulation to inject code into a method not working

问题

我想将一些代码注入到现有的类/方法中,但我无法获取类加载器以使其"找到"这个类,以便使用修改后的字节码。

MyClassInjector.java

import org.objectweb.asm.*;

public class MyClassInjector {
    public static void main(String[] args) throws Exception {
        // Load the MyClass class
        ClassReader cr = new ClassReader("MyClass");
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        MyClassVisitor cv = new MyClassVisitor(cw);
        cr.accept(cv, 0);

        // Inject code into the myMethod method
        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "myMethod", "()V", null, null);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "MyClassInjector", "newMethod", "()V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();

        // Define the newMethod method
        MethodVisitor mv2 = cv.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "newMethod", "()V", null, null);
        mv2.visitCode();
        mv2.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv2.visitLdcInsn("Injected code");
        mv2.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv2.visitInsn(Opcodes.RETURN);
        mv2.visitMaxs(0, 0);
        mv2.visitEnd();

        // Define the new byte array with the modified class bytecode
        byte[] modifiedClass = cw.toByteArray();

        // Define a new class loader to load the modified class
        ClassLoader cl = new ClassLoader() {
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                if (name.equals("MyClass")) {
                    return defineClass(name, modifiedClass, 0, modifiedClass.length);
                } else {
                    return super.findClass(name);
                }
            }
        };

        // Load the modified class and call myMethod
        Class<?> myClass = cl.loadClass("MyClass"); //<----------------------- HERE
        Object myObject = myClass.newInstance();
        myClass.getMethod("myMethod").invoke(myObject);
    }
}

class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }
}

MyClass.java

public class MyClass {
    public void myMethod() {
        System.out.println("Hello, world!");
    }
}

当我调用loadClass(上面标有HERE的地方),它没有调用"findClass",所以方法没有被修改。根据我所了解,loadClass() 应该调用 findClass()。有什么想法吗?

英文:

I want to inject some code into an existing class/method. But I am unable to get the classloader to "find" the class in order to use the modified byte code.

MyClassInjector.java

import org.objectweb.asm.*;
public class MyClassInjector {
public static void main(String[] args) throws Exception {
// Load the MyClass class
ClassReader cr = new ClassReader(&quot;MyClass&quot;);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
MyClassVisitor cv = new MyClassVisitor(cw);
cr.accept(cv, 0);
// Inject code into the myMethod method
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, &quot;myMethod&quot;, &quot;()V&quot;, null, null);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, &quot;MyClassInjector&quot;, &quot;newMethod&quot;, &quot;()V&quot;, false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
// Define the newMethod method
MethodVisitor mv2 = cv.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, &quot;newMethod&quot;, &quot;()V&quot;, null, null);
mv2.visitCode();
mv2.visitFieldInsn(Opcodes.GETSTATIC, &quot;java/lang/System&quot;, &quot;out&quot;, &quot;Ljava/io/PrintStream;&quot;);
mv2.visitLdcInsn(&quot;Injected code&quot;);
mv2.visitMethodInsn(Opcodes.INVOKEVIRTUAL, &quot;java/io/PrintStream&quot;, &quot;println&quot;, &quot;(Ljava/lang/String;)V&quot;, false);
mv2.visitInsn(Opcodes.RETURN);
mv2.visitMaxs(0, 0);
mv2.visitEnd();
// Define the new byte array with the modified class bytecode
byte[] modifiedClass = cw.toByteArray();
// Define a new class loader to load the modified class
ClassLoader cl = new ClassLoader() {
@Override
protected Class&lt;?&gt; findClass(String name) throws ClassNotFoundException {
if (name.equals(&quot;MyClass&quot;)) {
return defineClass(name, modifiedClass, 0, modifiedClass.length);
} else {
return super.findClass(name);
}
}
};
// Load the modified class and call myMethod
Class&lt;?&gt; myClass = cl.loadClass(&quot;MyClass&quot;); &lt;----------------------- HERE
Object myObject = myClass.newInstance();
myClass.getMethod(&quot;myMethod&quot;).invoke(myObject);
}
}
class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
}

MyClass.java

public class MyClass {
public void myMethod() {
System.out.println(&quot;Hello, world!&quot;);
}
}

When I call loadClass (marked by HERE above), it is not invoking "findClass" so method is not modified. From what I read, loadClass() is supposed to class findClass(). Any idea?

答案1

得分: 2

已经有用户16320675在评论中提到,loadClass会首先尝试从父类加载器加载类。因此,您可以将new ClassLoader()更改为new ClassLoader(null),将引导类加载器设置为其父类加载器,这样它将不会看到原始的定义。

然而,这只对非常简单的情况有效,因为修改后的类无法访问应用程序类加载器定义的其他类。

如果尚未加载该类,您可以像这样实例化新定义:

Class<?> myClass = MethodHandles.lookup().defineClass(modifiedClass);

这将在当前上下文中创建类,因此即使不使用反射的代码也将使用修改后的类。

如果在此点之前无法阻止环境加载该类,则没有办法绕过使用Instrumentation API编写真正的Java代理或创建一个包含可能与修改后的类合作的所有类的自定义类加载器的新环境。换句话说,类文件变换器的环境和要进行变换的代码的环境必须不同。


无论哪种情况,在修复此问题后,您将会得到一个java.lang.ClassFormatError: Duplicate method name "myMethod" with signature "()V" in class file MyClass,因为您的转换代码复制了原始类文件的所有构件,并添加了另一个myMethod。要替换myMethod,必须在类访问者在遇到原始myMethod时拦截类访问者。

import java.lang.invoke.MethodHandles;

import org.objectweb.asm.*;
    
public class MyClassInjector {
    public static void main(String[] args) throws Exception {
        // Load the MyClass class
        ClassReader cr = new ClassReader("MyClass");
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        MyClassVisitor cv = new MyClassVisitor(cw);
        cr.accept(cv, 0);

        // Define the new byte array with the modified class bytecode
        byte[] modifiedClass = cw.toByteArray();

        Class<?> myClass = MethodHandles.lookup().defineClass(modifiedClass);
        Object myObject = myClass.getConstructor().newInstance(); // Class.newInstance() is deprecated
        myClass.getMethod("myMethod").invoke(myObject);
    }
}

class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if(name.equals("myMethod") && descriptor.equals("()V")) {
            instrument(mv);
            return null;
        }
        return mv;
    }

    private void instrument(MethodVisitor mv) {
      // 修改myMethod
      mv.visitCode();
      mv.visitMethodInsn(Opcodes.INVOKESTATIC, "MyClass", "newMethod", "()V", false);
      mv.visitInsn(Opcodes.RETURN);
      mv.visitMaxs(0, 0);
      mv.visitEnd();
    }

    @Override
    public void visitEnd() {
        // 定义newMethod方法
        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "newMethod", "()V", null, null);
        mv.visitCode();
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("注入的代码");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
        super.visitEnd();
    }
}

请注意,还有另一个问题,您尝试在修改后的代码中调用MyClassInjector.newMethod()而不是MyClass.newMethod()

英文:

As already said by user16320675 in a comment, loadClass will attempt to load the class from the parent class loader first. So you can change new ClassLoader() to new ClassLoader(null) to set the bootstrap loader as its parent and it will not see the original definition.

However, that will only work for very simple cases as then, the modified class can’t access other classes defined by the application class loader then.

If the class has not been loaded yet, you can materialize the new definition like

Class&lt;?&gt; myClass = MethodHandles.lookup().defineClass(modifiedClass);

This will create the class in your current context, so even code using MyClass without Reflection will use the modified class.

If you can’t preclude the environment from loading the class before this point, there is no way around writing a real Java Agent using the Instrumentation API or create a new environment with the custom class loader containing all classes the modified class might collaborate with. In other words, the environment of the class file transformator and the environment of the code to transform must be different then.


In either case, after fixing this issue, you’ll get a java.lang.ClassFormatError: Duplicate method name &quot;myMethod&quot; with signature &quot;()V&quot; in class file MyClass, because your transformation code copies all artifacts of the original class file and adds another myMethod. To replace myMethod, you must intercept the class visitor when it encounters the original myMethod.

import java.lang.invoke.MethodHandles;

import org.objectweb.asm.*;
    
public class MyClassInjector {
    public static void main(String[] args) throws Exception {
        // Load the MyClass class
        ClassReader cr = new ClassReader(&quot;MyClass&quot;);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        MyClassVisitor cv = new MyClassVisitor(cw);
        cr.accept(cv, 0);

        // Define the new byte array with the modified class bytecode
        byte[] modifiedClass = cw.toByteArray();

        Class&lt;?&gt; myClass = MethodHandles.lookup().defineClass(modifiedClass);
        Object myObject = myClass.getConstructor().newInstance(); // Class.newInstance() is deprecated
        myClass.getMethod(&quot;myMethod&quot;).invoke(myObject);
    }
}

class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if(name.equals(&quot;myMethod&quot;) &amp;&amp; descriptor.equals(&quot;()V&quot;)) {
            instrument(mv);
            return null;
        }
        return mv;
    }

    private void instrument(MethodVisitor mv) {
      // change myMethod
      mv.visitCode();
      mv.visitMethodInsn(Opcodes.INVOKESTATIC, &quot;MyClass&quot;, &quot;newMethod&quot;, &quot;()V&quot;, false);
      mv.visitInsn(Opcodes.RETURN);
      mv.visitMaxs(0, 0);
      mv.visitEnd();
    }

    @Override
    public void visitEnd() {
        // Define the newMethod method
        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, &quot;newMethod&quot;, &quot;()V&quot;, null, null);
        mv.visitCode();
        mv.visitFieldInsn(Opcodes.GETSTATIC, &quot;java/lang/System&quot;, &quot;out&quot;, &quot;Ljava/io/PrintStream;&quot;);
        mv.visitLdcInsn(&quot;Injected code&quot;);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, &quot;java/io/PrintStream&quot;, &quot;println&quot;, &quot;(Ljava/lang/String;)V&quot;, false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
        super.visitEnd();
    }
}

Note that there was another issue that you tried to invoke MyClassInjector.newMethod() in the modified code instead of MyClass.newMethod().

huangapple
  • 本文由 发表于 2023年3月9日 15:07:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/75681407.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定