Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to remove method body at runtime with ASM 5.2

I'm trying to remove the method body of test() in the following program so that nothing is printed to the Console. I'm using using ASM 5.2 but everything I've tried doesn't seem to have any effect.

Can someone explain what I'm doing wrong and also point me to some up-to-date tutorials or documentation on ASM? Almost everything Iv'e found on Stackoverflow and the ASM website seems outdated and/or unhelpful.

public class BytecodeMods {

    public static void main(String[] args) throws Exception {
        disableMethod(BytecodeMods.class.getMethod("test"));
        test();
    }

    public static void test() {
        System.out.println("This is a test");
    }

    private static void disableMethod(Method method) {
        new MethodReplacer()
                .visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, method.getName(), Type.getMethodDescriptor(method), null, null);
    }

    public static class MethodReplacer extends ClassVisitor {

        public MethodReplacer() {
            super(Opcodes.ASM5);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            return null;
        }

    }

}
like image 363
kmecpp Avatar asked Dec 11 '22 10:12

kmecpp


1 Answers

You are not supposed to invoke the methods of a visitor directly.

The correct way to use a ClassVisitor, is to create a ClassReader with the class file bytes of the class you’re interested in and pass the class visitor to the its accept method. Then, all the visit methods will be called by the class reader according to the artifacts found in the class file.

In this regard, you should not consider the documentation outdated, just because it refers to an older version number. E.g. this document describes that process correctly and it speaks for the library that no fundamental change was necessary between the versions 2 and 5.

Still, visiting a class does not change it. It helps analyzing it and perform actions when encountering a certain artifact. Note that returning null is not an actual action.

If you want to create a modified class, you need a ClassWriter to produce the class. A ClassWriter implements ClassVisitor, also class visitors can be chained, so you can easily create a custom visitor delegating to a writer, that will produce a class file identical to the original one, unless you override a method to intercept the recreation of a feature.

But note that returning null from visitMethod does more than removing the code, it will remove the method entirely. Instead, you have to return a special visitor for the specific method which will reproduce the method but ignore the old code and create a sole return instruction (you are allowed to omit the last return statement in source code, but not the return instruction in the byte code).

private static byte[] disableMethod(Method method) {
    Class<?> theClass = method.getDeclaringClass();
    ClassReader cr;
    try { // use resource lookup to get the class bytes
        cr = new ClassReader(
            theClass.getResourceAsStream(theClass.getSimpleName()+".class"));
    } catch(IOException ex) {
        throw new IllegalStateException(ex);
    }
    // passing the ClassReader to the writer allows internal optimizations
    ClassWriter cw = new ClassWriter(cr, 0);
    cr.accept(new MethodReplacer(
            cw, method.getName(), Type.getMethodDescriptor(method)), 0);

    byte[] newCode = cw.toByteArray();
    return newCode;
}

static class MethodReplacer extends ClassVisitor {
    private final String hotMethodName, hotMethodDesc;

    MethodReplacer(ClassWriter cw, String name, String methodDescriptor) {
        super(Opcodes.ASM5, cw);
        hotMethodName = name;
        hotMethodDesc = methodDescriptor;
    }

    // invoked for every method
    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {

        if(!name.equals(hotMethodName) || !desc.equals(hotMethodDesc))
            // reproduce the methods we're not interested in, unchanged
            return super.visitMethod(access, name, desc, signature, exceptions);

        // alter the behavior for the specific method
        return new ReplaceWithEmptyBody(
            super.visitMethod(access, name, desc, signature, exceptions),
            (Type.getArgumentsAndReturnSizes(desc)>>2)-1);
    }
}
static class ReplaceWithEmptyBody extends MethodVisitor {
    private final MethodVisitor targetWriter;
    private final int newMaxLocals;

    ReplaceWithEmptyBody(MethodVisitor writer, int newMaxL) {
        // now, we're not passing the writer to the superclass for our radical changes
        super(Opcodes.ASM5);
        targetWriter = writer;
        newMaxLocals = newMaxL;
    }

    // we're only override the minimum to create a code attribute with a sole RETURN

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        targetWriter.visitMaxs(0, newMaxLocals);
    }

    @Override
    public void visitCode() {
        targetWriter.visitCode();
        targetWriter.visitInsn(Opcodes.RETURN);// our new code
    }

    @Override
    public void visitEnd() {
        targetWriter.visitEnd();
    }

    // the remaining methods just reproduce meta information,
    // annotations & parameter names

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return targetWriter.visitAnnotation(desc, visible);
    }

    @Override
    public void visitParameter(String name, int access) {
        targetWriter.visitParameter(name, access);
    }
}

The custom MethodVisitor does not get chained to the method visitor returned by the class writer. Configured this way, it will not replicate the code automatically. Instead, performing no action will be the default and only our explicit invocations on the targetWriter will produce code.

At the end of the process, you have a byte[] array containing the changed code in the class file format. So the question is, what to do with it.

The easiest, most portable thing you can do, is to create a new ClassLoader, which creates a new Class from these bytes, which has the same name (as we didn’t change the name), but is distinct from the already loaded class, because it has a different defining class loader. We can access such dynamically generated class only through Reflection:

public class BytecodeMods {

    public static void main(String[] args) throws Exception {
        byte[] code = disableMethod(BytecodeMods.class.getMethod("test"));
        new ClassLoader() {
            Class<?> get() { return defineClass(null, code, 0, code.length); }
        }   .get()
            .getMethod("test").invoke(null);
    }

    public static void test() {
        System.out.println("This is a test");
    }

    …

In order to make this example do something more notable than doing nothing, you could alter the message instead,

using the following MethodVisitor

static class ReplaceStringConstant extends MethodVisitor {
    private final String matchString, replaceWith;

    ReplaceStringConstant(MethodVisitor writer, String match, String replacement) {
        // now passing the writer to the superclass, as most code stays unchanged
        super(Opcodes.ASM5, writer);
        matchString = match;
        replaceWith = replacement;
    }

    @Override
    public void visitLdcInsn(Object cst) {
        super.visitLdcInsn(matchString.equals(cst)? replaceWith: cst);
    }
}

by changing

        return new ReplaceWithEmptyBody(
            super.visitMethod(access, name, desc, signature, exceptions),
            (Type.getArgumentsAndReturnSizes(desc)>>2)-1);

to

        return new ReplaceStringConstant(
            super.visitMethod(access, name, desc, signature, exceptions),
            "This is a test", "This is a replacement");

If you want to change the code of an already loaded class or intercept it right before being loaded into the JVM, you have to use the Instrumentation API.

The byte code transformation itself doesn’t change, you’ll have to pass the source bytes into the ClassReader and get the modified bytes back from the ClassWriter. Methods like ClassFileTransformer.transform(…) will already receive the bytes representing the current form of the class (there might have been previous transformations) and return the new bytes.

The problem is, this API isn’t generally available to Java applications. It’s available for so-called Java Agents, which must have been either, started together with the JVM via startup options or get loaded dynamically in an implementation-specific way, e.g. via the Attach API.

The package documentation describes the general structure of Java Agents and the related command line options.

At the end of this answer is a program demonstrating how to use the Attach API to attach to your own JVM to load a dummy Java Agent that will give the program access to the Instrumentation API. Considering the complexity, I think, it became apparent, that the actual code transformation and turning the code into a runtime class or using it to replace a class on the fly, are two different tasks that have to collaborate, but whose code you usually want to keep separated.

like image 184
Holger Avatar answered Dec 24 '22 18:12

Holger