如何使用 javassist 检测从特定 jar 加载的方法?

Posted

技术标签:

【中文标题】如何使用 javassist 检测从特定 jar 加载的方法?【英文标题】:How do I instrument methods that are loaded from a specific jar with javassist? 【发布时间】: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);
    
  

【问题讨论】:

【参考方案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

【讨论】:

感谢您的帮助,这与我最终使其工作的方式接近。抱歉回复晚了,再次感谢您的帮助!

以上是关于如何使用 javassist 检测从特定 jar 加载的方法?的主要内容,如果未能解决你的问题,请参考以下文章

ArtifactTransferException: Failure to transfer javassist:javassist:jar:3.11.0.GA from http://repo.ma

利用javassist破解idea mybatis plugin方法

Javassist使用

Javasist 在检测 org.h2.jdbc.JdbcPreparedStatement 的 setString 方法时抛出 javassist.CannotCompileException

javassist使用全解析

Java逆向基础之导出内存中的类一