I'm writing a security-boosted Java program that uses ASM tool to add some hooks into JRE classes. The hooks will then call my method to make some rule checks. But the odd thing is, the hooks in JRE classes can never find my classes and always throw NoClassDefFoundError
.
Usually, this error occurs with wrong java.class.path
. I added bytecodes to JRE classes to print java.class.path
and found nothing wrong. java.class.path
doesn't change during method calls.
I try multiple times with different classes in JRE and 3-party library classes. NoClassDefFoundError
always occurs when I add the hook into a JRE class that visits my class or a 3-party class. In other cases, like my class visiting a 3-party class or vice versa, the code works fine. So I'm sure the problem is not with my code.
I guess there's some hidden mechanism that makes my classes invisible to JRE classes, but I can't find clues from Java or ASM documents.
Here is a simple program that can reproduce my problem.
Main.java:
package mypackage;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println(sc.nextLine());
sc.close();
}
public static void insert() {
System.out.println("Main.insert() called.");
}
}
Agent.java
package mypackage;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class Agent {
public static void premain(String ops, Instrumentation inst) {
inst.addTransformer(new myTransformer());
}
}
class myTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("java/util/Scanner".equals(className)) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
myClassVisitor cv = new myClassVisitor(Opcodes.ASM9, cw);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
return null;
}
}
class myClassVisitor extends ClassVisitor implements Opcodes {
protected myClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("nextLine".equals(name)) {
System.out.println("Modify Scanner.nextLine");
return new MethodVisitor(api, methodVisitor) {
@Override
public void visitCode() {
methodVisitor.visitMethodInsn(INVOKESTATIC, "mypackage/Main", "insert", "()V", false);
super.visitCode();
}
};
}
return methodVisitor;
}
}
This program modifies nextLine()
in java.util.Scanner
by adding a call to mypackage.Main.insert()
.
I use this Manifest to pack all classes into a jar file:
Main-Class: mypackage.Main
Premain-Class: mypackage.Agent
Agent-Class: mypackage.Agent
Class-Path: lib/asm-9.4.jar
Can-Set-Native-Method-Prefix: true
Can-Redefine-Classes: true
Can-Retransform-Classes: true
The ASM jar is located in lib/
. Then run the following command:
java -javaagent:mypackage.jar -jar mypackage.jar
Here's the output:
Modify Scanner.nextLine
Exception in thread "main" java.lang.NoClassDefFoundError: mypackage/Main
at java.util.Scanner.nextLine(Scanner.java)
at mypackage.Main.main(Main.java:8)
My Java version is OpenJDK-8. I can reproduce the problem on both Mac and Ubuntu.
You could adjust cmdline as below. The purpose is to add asm and mypackage.jar to bootstrap classloader.
Please change "xxx" of the cmdline to your local full path.
java -javaagent:mypackage.jar -Xbootclasspath/a:xxx/lib/asm-9.4.jar:xxx/mypackage.jar -jar mypackage.jar