【问题标题】:How do I instrument methods that are loaded from a specific jar with javassist?如何使用 javassist 检测从特定 jar 加载的方法?
【发布时间】:2019-11-05 19:11:26
【问题描述】:

我有一个示例 jar,我正在从磁盘加载到类池中。从那里我可以很容易地访问这个类中的方法并使用它们来检测它们,正如你在 JsEval 方法中所做的那样。

但是,在 Helloworld 示例类中,我希望能够检测其他库函数调用。在这个例子中,我试图从 nashorn 脚本引擎中检测 eval 函数。但是,这不起作用。我能够很好地访问类(pool.get),并且能够修补 eval 的方法。但是,当我从 cl.run() 运行 SampleClass 时,方法的执行就像没有插入任何代码一样。我怀疑这与我用来执行 Sampleclass 的类加载器有关,但我被卡住了。关于我在这里做错了什么有什么想法吗?

public class maventest {

  public static void main(String[] args)
    throws NotFoundException, CannotCompileException, Throwable
  {
    ClassPool pool = ClassPool.getDefault();
    Loader cl = new Loader(pool);

    //pool.importPackage(Test.class.getPackage().getName());
    //Get the Jar from disk. This works and the method is instrumented.
    pool.insertClassPath(
      "Z:\\HelloWorld\\target\\HelloWorld-1.0-SNAPSHOT-jar-with-dependencies.jar"
    );  
    pool.importPackage("com.mycompany.helloworld");
    //pool.appendClassPath();

    CtClass helloworld = pool.get("com.mycompany.helloworld.SampleClass");
    helloworld
      .getDeclaredMethod("JsEval")
      .insertBefore(
        "System.out.println(\"Calling JsEval from within helloworld\\n\");"
      );

    //This does not work.
    //Attempt to instrument the eval function that is called from inside of HelloWorld
    String classToLoad = "jdk.nashorn.api.scripting.NashornScriptEngine";
    String constuctor_name = "eval";
    CtClass nash = pool.get(classToLoad);
    //Multiple eval calls.. Just instrument them all.
    CtMethod[] meths = nash.getDeclaredMethods("eval");
    for (CtMethod m : meths) {
      m.insertBefore(
        "System.out.println(\"Nashorn Scripting Engined eval called.\");"
      );
    }

    //Execute the hello world class with null args
    cl.run("com.mycompany.helloworld.SampleClass", null);
  }

}

这是调用我希望检测的 lib 函数的示例代码。

public class SampleClass {
  public static void main(String[] args) throws IOException, NotFoundException {
    JsEval("var greeting='hello world'; print(greeting) + greeting");
  }

  private static void JsEval(String js) {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
    try {
      Object result = engine.eval(js);
    }
    catch (ScriptException ex) {
      Logger.getLogger(SampleClass.class.getName()).log(Level.SEVERE, null, ex);
    }
  }
}

【问题讨论】:

    标签: instrumentation javassist


    【解决方案1】:

    我知道这个问题有点老了,但仍然没有答案,我很好奇。

    这不起作用的原因是 getDeclaredMethods("eval") 不搜索超类中的方法,如 Javadoc 中所述。您正在调用的方法,即采用单个 String 参数的方法是在父类 AbstractScriptEngine 中定义的,但不在 NashornScriptEngine 中。因此,要么您必须将目标类更改为真正定义方法的类,要么通过getMethod(..)getMethods() 搜索方法,这两个方法都返回继承的方法。因为getMethods() 不能采用方法名称参数,而是返回所有方法,并且您必须在检测循环中再次按名称过滤,所以我建议您通过指定其确切签名来挑选出您真正想要检测的一种方法:

    String classToLoad = "jdk.nashorn.api.scripting.NashornScriptEngine";
    CtClass nash = pool.get(classToLoad);
    CtMethod m = nash.getMethod(
      "eval",
      Descriptor.ofMethod(
        pool.get("java.lang.Object"),
        new CtClass[] { pool.get("java.lang.String") }
      )
    );
    m.insertBefore("System.out.println(\"Nashorn Scripting Engined eval called.\");");
    

    或者,如果 Descriptor.ofMethod(..) 对您来说太冗长而您对描述符语法感到满意:

    String classToLoad = "jdk.nashorn.api.scripting.NashornScriptEngine";
    CtClass nash = pool.get(classToLoad);
    CtMethod m = nash.getMethod("eval", "(Ljava/lang/String;)Ljava/lang/Object;");
    m.insertBefore("System.out.println(\"Nashorn Scripting Engined eval called.\");");
    

    现在您的控制台日志输出符合预期:

    Calling JsEval from within helloworld
    
    Warning: Nashorn engine is planned to be removed from a future JDK release
    hello world
    

    更新:糟糕,我错过了您正在尝试修改引导类或更一般地已加载的类的事实。在这种情况下,转换无效,除非您使用 Java 检测 API,即使用 ClassFileTransformer 将其集成到 Java 代理中(如果您不知道 Java 代理是什么以及如何使用,请使用您最喜欢的网络搜索引擎构建一个)或动态附加。在这个例子中,我使用了很小的 byte-buddy-agent 库来将它热附加到正在运行的 JVM 上,这样我就可以向你展示效果了。

    一个不是很通用但设计为只查找eval(String) 方法的超级简单版本,如下所示:

    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    import jdk.nashorn.api.scripting.NashornScriptEngine;
    import net.bytebuddy.agent.ByteBuddyAgent;
    
    import javax.script.ScriptEngine;
    import javax.script.ScriptEngineManager;
    import javax.script.ScriptException;
    import java.io.ByteArrayInputStream;
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.lang.instrument.Instrumentation;
    import java.lang.instrument.UnmodifiableClassException;
    import java.security.ProtectionDomain;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    public class MavenTest {
    
      public static void main(String[] args) throws UnmodifiableClassException {
        Instrumentation instrumentation = ByteBuddyAgent.install();
        instrumentation.addTransformer(new ScriptEngineTransformer());
    
        Class<?> targetClass = NashornScriptEngine.class;
        // Go up the super class hierarchy, pretending we don't know the exact
        // super class class in which the target method is defined
        while (!targetClass.equals(Object.class)) {
          instrumentation.retransformClasses(targetClass);
          targetClass = targetClass.getSuperclass();
        }
    
        jsEval("var greeting='hello world'; print(greeting)");
      }
    
      private static void jsEval(String js) {
        ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
        try {
          engine.eval(js);
        }
        catch (ScriptException ex) {
          Logger.getLogger(MavenTest.class.getName()).log(Level.SEVERE, null, ex);
        }
      }
    
      static class ScriptEngineTransformer implements ClassFileTransformer {
        private static final ClassPool CLASS_POOL = ClassPool.getDefault();
    
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
          CtClass targetClass;
          try {
            // Caveat: Do not just use 'classPool.get(className)' because we would miss previous transformations.
            // It is necessary to really parse 'classfileBuffer'.
            targetClass = CLASS_POOL.makeClass(new ByteArrayInputStream(classfileBuffer));
            CtMethod evalMethod = targetClass.getDeclaredMethod("eval", new CtClass[] { CLASS_POOL.get("java.lang.String") });
            targetClass.defrost();
            evalMethod.insertBefore("System.out.println(\"Scripting engine eval(String) called\");");
          }
          catch (Exception e) {
            return null;
          }
    
          byte[] transformedBytecode;
          try {
            transformedBytecode = targetClass.toBytecode();
          }
          catch (Exception e) {
            e.printStackTrace();
            return null;
          }
    
          return transformedBytecode;
        }
      }
    
    }
    

    您可能已经注意到,我重命名了您的一些类和方法名称,以使它们符合 Java 标准。

    现在控制台日志是:

    Warning: Nashorn engine is planned to be removed from a future JDK release
    Scripting engine eval(String) called
    hello world
    

    【讨论】:

    • 感谢您的帮助,这与我最终使其工作的方式接近。抱歉回复晚了,再次感谢您的帮助!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-04
    • 1970-01-01
    • 1970-01-01
    • 2012-12-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多