I have to retrieve the value of all fields in a class loaded from a jar.
So I need an instance to do that :
field.get(gameClassInstance);
for each field.
Here the code that load the class and try to create an instance :
private Loader() throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
File jarFile = new File(System.getProperty("user.dir")+File.separator+"games"+File.separator+gameName+".jar");
// Create the URLClassLoader
URL url = jarFile.toURI().toURL();
URL[] urls = new URL[]{url};
URLClassLoader cl = new URLClassLoader(urls);
// Search the class
JarFile jar = new JarFile(jarFile.toString());
Enumeration<JarEntry> e = jar.entries();
while (e.hasMoreElements()) {
JarEntry je = (JarEntry) e.nextElement();
if(je.isDirectory() || !je.getName().endsWith(".class")){
continue;
}
if (je.getName().contains(gameName)){
String className = je.getName().substring(0,je.getName().length()-6); // Remove ".class"
className = className.replace('/', '.');
gameClass = cl.loadClass(className);
gameClassInstance = gameClass.newInstance(); // Create an instance of the class
}
}
jar.close();
cl.close();
}
Here the loaded class :
public class Solitaire {
public Board board = new Board("Board1", "");
public Layout layout = new Layout();
public Player player = new Player();
public Solitaire() {
}
}
There is a StackOverflowError at the line where I create an instance.
I found the solution to my problem, I just make the fields static and retrieve them with field.get(null)
How do I prevent proguard from modifying the field names in my method?
I know we can use -keep, -keepclassmembers..etc but these seems to only preserve the member names and NOT the variables that are used inside.
Here's what happened:
Before:
private static URL getPluginImageURL(Object plugin, String name) throws Exception {
// try to work with 'plugin' as with OSGI BundleContext
try {
Class<?> BundleClass = Class.forName("org.osgi.framework.Bundle"); //$NON-NLS-1$
Class<?> BundleContextClass = Class.forName("org.osgi.framework.BundleContext"); //$NON-NLS-1$
if (BundleContextClass.isAssignableFrom(plugin.getClass())) {
Method getBundleMethod = BundleContextClass.getMethod("getBundle", new Class[0]); //$NON-NLS-1$
Object bundle = getBundleMethod.invoke(plugin, new Object[0]);
//
Class<?> PathClass = Class.forName("org.eclipse.core.runtime.Path"); //$NON-NLS-1$
Constructor<?> pathConstructor = PathClass.getConstructor(new Class[]{String.class});
Object path = pathConstructor.newInstance(new Object[]{name});
//
Class<?> IPathClass = Class.forName("org.eclipse.core.runtime.IPath"); //$NON-NLS-1$
Class<?> PlatformClass = Class.forName("org.eclipse.core.runtime.Platform"); //$NON-NLS-1$
Method findMethod = PlatformClass.getMethod("find", new Class[]{BundleClass, IPathClass}); //$NON-NLS-1$
return (URL) findMethod.invoke(null, new Object[]{bundle, path});
}
} catch (Throwable e) {
// Ignore any exceptions
}
// else work with 'plugin' as with usual Eclipse plugin
{
Class<?> PluginClass = Class.forName("org.eclipse.core.runtime.Plugin"); //$NON-NLS-1$
if (PluginClass.isAssignableFrom(plugin.getClass())) {
//
Class<?> PathClass = Class.forName("org.eclipse.core.runtime.Path"); //$NON-NLS-1$
Constructor<?> pathConstructor = PathClass.getConstructor(new Class[]{String.class});
Object path = pathConstructor.newInstance(new Object[]{name});
//
Class<?> IPathClass = Class.forName("org.eclipse.core.runtime.IPath"); //$NON-NLS-1$
Method findMethod = PluginClass.getMethod("find", new Class[]{IPathClass}); //$NON-NLS-1$
return (URL) findMethod.invoke(plugin, new Object[]{path});
}
}
return null;
}
After proguard:
private static URL getPluginImageURL(Object plugin, String name)
throws Exception
{
try
{
localClass1 = Class.forName("org.osgi.framework.Bundle");
localClass2 = Class.forName("org.osgi.framework.BundleContext");
if (localClass2.isAssignableFrom(plugin.getClass())) {
localObject1 = localClass2.getMethod("getBundle", new Class[0]);
localObject2 = ((Method)localObject1).invoke(plugin, new Object[0]);
localClass3 = Class.forName("org.eclipse.core.runtime.Path");
localObject3 = localClass3.getConstructor(new Class[] { String.class });
Object localObject4 = ((Constructor)localObject3).newInstance(new Object[] { name });
Class localClass4 = Class.forName("org.eclipse.core.runtime.IPath");
Class localClass5 = Class.forName("org.eclipse.core.runtime.Platform");
Method localMethod = localClass5.getMethod("find", new Class[] { localClass1, localClass4 });
return (URL)localMethod.invoke(null, new Object[] { localObject2, localObject4 });
}
}
catch (Throwable localThrowable)
{
Class localClass2;
Object localObject1;
Object localObject2;
Class localClass3;
Object localObject3;
Class localClass1 = Class.forName("org.eclipse.core.runtime.Plugin");
if (localClass1.isAssignableFrom(plugin.getClass()))
{
localClass2 = Class.forName("org.eclipse.core.runtime.Path");
localObject1 = localClass2.getConstructor(new Class[] { String.class });
localObject2 = ((Constructor)localObject1).newInstance(new Object[] { name });
localClass3 = Class.forName("org.eclipse.core.runtime.IPath");
localObject3 = localClass1.getMethod("find", new Class[] { localClass3 });
return (URL)((Method)localObject3).invoke(plugin, new Object[] { localObject2 });
}
}
return null;
}
Notice how localObject1 and localObject2 no longer have Class declaration? Doesn't this make the code syntactically incorrect?
Help...
It's your decompiler that produces the syntactically incorrect code. You could try a different decompiler.
ProGuard indeed removes the local variable names, since they are just optional debugging information. They are not required by the virtual machine.
This is the common approach with working with loading class dynamically:
try {
File file = new File(JAR_FILE);
String classToLoad = "com.mycompany.MyClass";
URL jarUrl = new URL("jar", "","file:" + file.getAbsolutePath()+"!/");
URLClassLoader loader = new URLClassLoader(new URL[] {jarUrl}, Thread.currentThread().getContextClassLoader());
Class c = loader.loadClass(classToLoad);
} catch (Exception e) {
e.printStackTrace();
}
However, I need an approach where:
We don't need to create a File (as the jar I am trying to process is a byte array[] when fetched)
Or we won't need to create a temporary file from byte[] array (as AppEngine, the platform I work with does not allow to create temporary files)
You will have to create your own class loader.
Something like this, basic idea in pseudo code:
class MyClassLoader extends ClassLoader {
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
JarInputStream jis = new JarInputStream(new ByteArrayInputStream(bytearrayJarData));
JarEntry entry = jis.getNextJarEntry();
while (entry != null) {
//compare entry to requested class
// if match, return Byte data
// else entry = jis.getNextJarEntry();
}
return null; // nothing found
}
}
Write your own ClassLoader and override findClass(). There you can use defineClass() to load your byte[].
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
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 });
}
}
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.