9

I have a method of a class in a jar whose body I want to exchange with my own. In this case I just want to have the method print out "GOT IT" to the console and return true;

I am using the system loader to load the classes of the jar. I am using reflection to make the system classloader be able to load classes by bytecode. This part seems to be working correctly.

I am following the method replacement example found here: asm.ow2.org/current/asm-transformations.pdf.

My code is as follows:

public class Main 
{
    public static void main(String[] args) 
    {
        URL[] url = new URL[1];
        try
        {
            url[0] = new URL("file:////C://Users//emist//workspace//tmloader//bin//runtime//tmgames.jar");
            verifyValidPath(url[0]);
        }
        catch (Exception ex)
        {
            System.out.println("URL error");
        }
        Loader l = new Loader();
        l.loadobjection(url);
    }

    public static void verifyValidPath(URL url) throws FileNotFoundException
    {
        File filePath = new File(url.getFile());
        if (!filePath.exists()) 
        {
          throw new FileNotFoundException(filePath.getPath());
        }
    }
}

class Loader
{
    private static final Class[] parameters = new Class[] {URL.class};

    public static void addURL(URL u) throws IOException
    {
        URLClassLoader sysloader = (URLClassLoader)  ClassLoader.getSystemClassLoader();
        Class sysclass = URLClassLoader.class;

        try 
        {
            Method method = sysclass.getDeclaredMethod("addURL", parameters);
            method.setAccessible(true);
            method.invoke(sysloader, new Object[] {u});
        }
        catch (Throwable t) 
        {
            t.printStackTrace();
            throw new IOException("Error, could not add URL to system classloader");
        }

    }

    private Class loadClass(byte[] b, String name) 
    {
        //override classDefine (as it is protected) and define the class.
        Class clazz = null;
        try 
        {
            ClassLoader loader = ClassLoader.getSystemClassLoader();
            Class cls = Class.forName("java.lang.ClassLoader");
            java.lang.reflect.Method method =
                cls.getDeclaredMethod("defineClass", new Class[] { String.class, byte[].class, int.class, int.class });

            // protected method invocaton
            method.setAccessible(true);
            try 
            {
                Object[] args = new Object[] {name, b, new Integer(0), new Integer(b.length)};
                clazz = (Class) method.invoke(loader, args);
            }
            finally 
            {
                method.setAccessible(false);
            }
        }
        catch (Exception e) 
        {
            e.printStackTrace();
            System.exit(1);
        }
        return clazz;
    }

    public void loadobjection(URL[] myJar)
    {
        try 
        {
            Loader.addURL(myJar[0]);            
            //tmcore.game is the class that holds the main method in the jar
            /*
            Class<?> classToLoad = Class.forName("tmcore.game", true, this.getClass().getClassLoader());
            if(classToLoad == null)
            {
                System.out.println("No tmcore.game");
                return;
            }
            */
            MethodReplacer mr = null;

            ClassReader cr = new ClassReader("tmcore.objwin");
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            MethodVisitor mv = null;
            try
            {
                mr = new MethodReplacer(cw, "Test", "(Ljava/lang/String;ZLjava/lang/String;)Z");
            }
            catch (Exception e)
            {
                System.out.println("Method Replacer Exception");
            }
            cr.accept(mr, ClassReader.EXPAND_FRAMES);

            PrintWriter pw = new PrintWriter(System.out);
            loadClass(cw.toByteArray(), "tmcore.objwin");
            Class<?> classToLoad = Class.forName("tmcore.game", true, this.getClass().getClassLoader());
            if(classToLoad == null)
            {
                System.out.println("No tmcore.game");
                return;
            }

            //game doesn't have a default constructor, so we need to get the reference to public game(String[] args)
            Constructor ctor = classToLoad.getDeclaredConstructor(String[].class);
            if(ctor == null)
            {
                System.out.println("can't find constructor");
                return;
            }

            //Instantiate the class by calling the constructor
            String[] args = {"tmgames.jar"};
            Object instance = ctor.newInstance(new Object[]{args});
            if(instance == null)
            {
                System.out.println("Can't instantiate constructor");
            }

            //get reference to main(String[] args)
            Method method = classToLoad.getDeclaredMethod("main", String[].class);
            //call the main method
            method.invoke(instance);

        }   
        catch (Exception ex)
        {
            System.out.println(ex.getMessage());
            ex.printStackTrace();
        }
    }
}


public class MethodReplacer extends ClassVisitor implements Opcodes
{
    private String mname;
    private String mdesc;
    private String cname;

    public MethodReplacer(ClassVisitor cv, String mname, String mdesc)
    {
        super(Opcodes.ASM4, cv);
        this.mname = mname;
        this.mdesc = mdesc;
    }

    public void visit(int version, int access, String name, String signature, 
                      String superName, String[] interfaces)
    {
        this.cname = name;
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                                     String[] exceptions)
    {
        String newName = name;
        if(name.equals(mname) && desc.equals(mdesc))
        {
            newName = "orig$" + name;
            generateNewBody(access, desc, signature, exceptions, name, newName);
            System.out.println("Replacing");
        }
        return super.visitMethod(access,  newName,  desc,  signature,  exceptions);
    }

    private void generateNewBody(int access, String desc, String signature, String[] exceptions,
                                String name, String newName)
    {
        MethodVisitor mv = cv.visitMethod(access,  name,  desc,  signature,  exceptions);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(access, cname, newName, desc);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("GOTit!");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
        mv.visitInsn(ICONST_0);
        mv.visitInsn(IRETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }
}

The problem seems to be at mv.visitMethodInsn(access, cname, newName, desc); in generateMethodBody inside MethodReplacer.

I get an "Illegal Type in constant pool" error.

I'm not sure what I'm missing...but after reading and testing for about 3 days I'm still not getting anywhere.

[Edit]

In case you were wondering, tmcore is a single player "Objection" game for lawyers. I'm doing this for the fun of it. The program successfully launches the game and everything is fine, removing the modifications from MethodReplacer makes the game behave as designed. So the issue seems to be isolated to bad bytecode/modifications by me inside the method replacer.

[EDIT2]

CheckClassAdapter.verify(cr, true, pw); returns the exact same bytecode that the function is supposed to have before editing. It is as if the changes are not being done.

[EDIT3]

copy of classtoload commented out as per comments

Moshe Katz
  • 15,992
  • 7
  • 69
  • 116
emist
  • 137
  • 1
  • 2
  • 8

2 Answers2

7

If you are using Eclipse, you should install Bytecode Outline - it is indispensible.

I built a small test for what you want to achieve (this should match the signature of your test method, you will have to change package and classname):

package checkASM;

public class MethodCall {

    public boolean Test(String a, boolean b, String c) {
        System.out.println("GOTit");
        return false;
    }
}

requires the following bytecode to build the method:

{
mv = cw.visitMethod(ACC_PUBLIC, "Test",
    "(Ljava/lang/String;ZLjava/lang/String;)Z", null, null);
mv.visitCode();
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System",
   "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("GOTit");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream",
   "println", "(Ljava/lang/String;)V");
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitInsn(ICONST_0);
mv.visitInsn(IRETURN);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLocalVariable("this", "LcheckASM/MethodCall;", null, l1, l3, 0);
mv.visitLocalVariable("a", "Ljava/lang/String;", null, l1, l3, 1);
mv.visitLocalVariable("b", "Z", null, l1, l3, 2);
mv.visitLocalVariable("c", "Ljava/lang/String;", null, l1, l3, 3);
mv.visitMaxs(4, 4);
mv.visitEnd();
}

Calls to visitLineNumber can be omitted. So apparently, you are missing all labels, forgot to load the method parameters, did not ignore the return value, set the wrong values for visitMaxs (this is not necessarily needed, it depends on your ClassWriter flags if I recall correctly) and did not visit local variables (or parameters in this case).

Additionally, your classloading seems to be a little confused / messed up. I don't have the jar (so I can't say if these work), but maybe you could replace Main and Loader:

Main:

import java.io.File;
import java.io.FileNotFoundException;
import java.net.URL;

public class Main {
    public static void main(String[] args) {
        try {
            Loader.instrumentTmcore(args);
        } catch (Exception e) {
            System.err.println("Ooops");
            e.printStackTrace();
        }
    }
}

Loader:

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

public class Loader {

    public static ClassReader fetchReader(String binaryName) throws Exception {
        return new ClassReader(
                Loader.class.getClassLoader().getSystemResourceAsStream(
                    binaryName.replace('.', '/') + ".class"
                )
            )
        ;
    }

    public static synchronized Class<?> loadClass(byte[] bytecode)
                throws Exception {
        ClassLoader scl = ClassLoader.getSystemClassLoader();
        Class<?>[] types = new Class<?>[] {
                String.class, byte[].class, int.class, int.class
        };
        Object[] args = new Object[] {
                null, bytecode, 0, bytecode.length
        };
        Method m = ClassLoader.class.getMethod("defineClass", types);
        m.setAccessible(true);
        return (Class<?>) m.invoke(scl, args);
    }

    public static void instrumentTmcore(String[] args) throws Exception {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        MethodReplacer mr = new MethodReplacer(cw, "Test",
                    "(Ljava/lang/String;ZLjava/lang/String;)Z");
        fetchReader("tmcore.objwin").accept(mr, ClassReader.EXPAND_FRAMES);
        loadClass(cw.toByteArray());
        Class.forName("tmcore.game")
            .getMethod("main", new Class<?>[] {args.getClass()})
            .invoke(null, new Object[] { args });
    }
}
Arne
  • 1,884
  • 1
  • 15
  • 19
  • Hi Ame, I have the plugin installed. You have the right idea about what I'm trying to do except that instead of: public boolean Test(String a, boolean b, String c) { Test(a, b, c); System.out.println("GOTit"); return false; } I'm looking to do just: public boolean Test(String a, boolean b, String c) { System.out.println("GOTit"); return false; } The bytecode that the plugin generates as well as ASMifer matches the bytecode inside my generateBody method. – emist Aug 02 '12 at 16:40
  • If I omit: mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitMethodInsn(access, cname, newName, desc); I get no error but the method also runs with its original body. – emist Aug 02 '12 at 16:42
  • Also, since my ClassWriter is built with COMPUTE_FRAMES, the value of visitMaxx doesn't matter, correct? – emist Aug 02 '12 at 23:10
  • You are right, visitMaxs shouldn't matter. I'll edit the code above. – Arne Aug 02 '12 at 23:37
  • made the changes, no error but no change in the codeflow(it is executing as if the method hasn't changed). Is it possible that I am missing something with the way my classes are being loaded? – emist Aug 03 '12 at 01:54
  • Is that really the code you use? I get a compile error in loadobjection, because `classToLoad` is declared twice. – Arne Aug 03 '12 at 07:13
  • That's my bad, I have a lot of commented out stuff on my end that I didn't copy over to make this question more legible. I have the first declaration of classtoload commented out on my end. I've just commented it out on this side too. It should compile now. Thanks for the tip on the classloader. What is the difference between your method and mine, they seem to be pretty much the same except the AddURL part? – emist Aug 03 '12 at 13:58
  • You load the Class before you instrument it - and so prevent yourself from using the right version. But that's only part of it. When you use my way, you don't have to specify a jar and you can access every class available to your application (thank you, `getSystemRessourceAsStream`). If you do anything with classloading, you must take care to never load anything you want to change before you changed it - unless you really know what's happening under the hood. May I get an upvote or accept btw.? :-) – Arne Aug 03 '12 at 14:35
  • Sorry about no upvotes, I could have sworn I had! I'm at work now but once I go back over it with what you've shown me here I'll be able to tell if the answer works and accept it. I had an idea that loading the class early might be an issue, but my knowledge of classloader is close to 0. Thanks for the help mate! – emist Aug 03 '12 at 19:40
  • I just copied and pasted your code. I'm calling the program as java runtime/Main tmcore/game (and a bunch of different variations of that) and it seems the loader can't find the class to load. I get a classnotfound every time. What am I missing? – emist Aug 04 '12 at 02:13
  • I wasn't able to try it mysqlf, I didn't have your jar lying around. My best guess is you forgot to add it to the classpath? As we don't load it ourselves anymore, it's required. – Arne Aug 05 '12 at 08:41
  • Hi man, tbh I'm not sure if your code works because I wasn't able to work it myself. But I have since re-worked my code and made it work. The guidance and help that you've given me helped a lot, so I will accept your answer as the answer. I'm going to throw up my code in the original question when I get a chance. Thanks for the help mate. – emist Aug 08 '12 at 04:21
0

ASKER'S ANSWER MOVED FROM QUESTION

The java bytecode was never the problem. It is the way I was loading the jar which made it impossible to instrument the code.

Thanks to Ame for helping me tackle it.

The following code works:

MAIN

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.io.FileInputStream;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class Main implements Opcodes
{
    public static void main(String[] args) throws Exception
    {

        byte[] obj = readClass("tmcore/obj.class");
        ClassReader objReader = new ClassReader(obj);
        ClassWriter objWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);

        MethodReplacer demoReplacer = new MethodReplacer(objWriter, "run", "()V");
        demoReplacer.visit(V1_6, ACC_PUBLIC + ACC_SUPER, "tmcore/obj", null, "java/applet/Applet", new String[] { "java/lang/Runnable" });
        objReader.accept(demoReplacer, ClassReader.EXPAND_FRAMES);

        objReader = new ClassReader(objWriter.toByteArray());

        Class objC = Loader.loadClass(objWriter.toByteArray(), "tmcore.obj");
        if(objC == null)
        {
            System.out.println("obj cannot be loaded");
        }

        Class game = ClassLoader.getSystemClassLoader().loadClass("tmcore.game");
        if(game == null)
        {
            System.out.println("Can't load game");
            return;
        }

        Constructor ctor = game.getDeclaredConstructor(String[].class);
        if(ctor == null)
        {
            System.out.println("can't find constructor");
            return;
        }

        //Instantiate the class by calling the constructor
        String[] arg = {"tmgames.jar"};
        Object instance = ctor.newInstance(new Object[]{args});
        if(instance == null)
        {
            System.out.println("Can't instantiate constructor");
        }

        //get reference to main(String[] args)
        Method method = game.getDeclaredMethod("main", String[].class);
        //call the main method
        method.invoke(instance);

    }


    public static void verifyValidPath(String path) throws FileNotFoundException
    {
            File filePath = new File(path);
            if (!filePath.exists()) 
            {
              throw new FileNotFoundException(filePath.getPath());
            }
    }

    public static byte[] readClass(String classpath) throws Exception
    {
        verifyValidPath(classpath);
        File f = new File(classpath);

        FileInputStream file = new FileInputStream(f);
        if(file == null)
            throw new FileNotFoundException();

        byte[] classbyte = new byte[(int)f.length()];

        int offset = 0, numRead = 0;
        while (offset < classbyte.length
                && (numRead=file.read(classbyte, offset, classbyte.length-offset)) >= 0) 
        {
             offset += numRead;
        }

        if (offset < classbyte.length) 
        {
            file.close();
            throw new IOException("Could not completely read file ");
        }

        file.close();
        return classbyte;
    }
}

LOADER:

import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

class Loader
{
    private static final Class[] parameters = new Class[] {URL.class};

     public static void addURL(URL u) throws IOException
     {
            URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
            Class sysclass = URLClassLoader.class;

            try 
            {
                Method method = sysclass.getDeclaredMethod("addURL", parameters);
                method.setAccessible(true);
                method.invoke(sysloader, new Object[] {u});
            } 
            catch (Throwable t) 
            {
                t.printStackTrace();
                throw new IOException("Error, could not add URL to system classloader");
            }

     }

     public static Class loadClass(byte[] b, String name) 
     {
            //override classDefine (as it is protected) and define the class.
         Class clazz = null;
         try 
         {
             ClassLoader loader = ClassLoader.getSystemClassLoader();
             Class cls = Class.forName("java.lang.ClassLoader");
             java.lang.reflect.Method method =
                     cls.getDeclaredMethod("defineClass", new Class[] { String.class, byte[].class, int.class, int.class });

              // protected method invocaton
              method.setAccessible(true);
              try 
              {
                  Object[] args = new Object[] {name, b, new Integer(0), new Integer(b.length)};
                  clazz = (Class) method.invoke(loader, args);
              } 

              finally 
              {
                  method.setAccessible(false);
              }
         } 
         catch (Exception e) 
         {
             e.printStackTrace();
             System.exit(1);
         }
            return clazz;
    }
}

MethodReplacer remains the same.

Moshe Katz
  • 15,992
  • 7
  • 69
  • 116