Java Instrumentation简介

整理ibm.com,不完善

插装&ASM

介绍

插桩技术是在保证目标程序原有逻辑完整的情况下,在特定的位置插入代码段,从而收集程序运行时的动态上下文信息

目前基于插桩技术实现Java程序的动态交互安全监测已经有一些实现形式,如RASP,IAST。在Java中插桩通过Instrument以及字节码操作工具(如:ASM,Javassist,Byte Buddy等)实现

相关知识

Instrumentation简介

Java SE 5 引入了静态Instument的概念,利用它我们可以构建一个独立于应用程序的代理程序(Agent),用力啊检测和协助运行在JVM上的程序,这样的特性实际上提供了一种虚拟机级别支持的AOP实现方式,如此无需对应用程序做升级改动,就能实现某些AOP的功能

在Java SE 6 ,instrumentation包被赋予更大的功能包括:启动后的instrument,本地代码(native code)instument,以及动态改变classpath等

Instrumentation基本功能和用法

在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作

使用步骤:

  1. 编写premain函数

    • 编写Java类,包含以下两个方法

      public static void premain(String agentArgs, Instrumentation inst);   //[1]
      public static void premain(String agentArgs);  //[2]
      
    • 其中,[1]的优先级比[2]高,将会被优先([1] [2]同时存在时[2]将会被忽略)

    • 在premain函数中,开发者可以对类进行各种操作。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个核心接口,集中了所有的功能方法

    • agentArgs时premain函数得到的程序参数,随同"- javaagent"一起传入

  2. jar文件打包

    • 将这个Java类打包成一个jar文件,同时在manifest属性中加入Primain-Class来指定步骤一中编写的有oremain的Java类
  3. 运行

    java -javaagent:jar 文件位置 [= 传入 primain的参数]
    

    建立一个Transformer类:

import java.io.File; 
import java.io.FileInputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.lang.instrument.ClassFileTransformer; 
import java.lang.instrument.IllegalClassFormatException; 
import java.security.ProtectionDomain; 
 
class Transformer implements ClassFileTransformer { 
 
   public static final String classNumberReturns2 = "TransClass.class.2"; 
 
   public static byte[] getBytesFromFile(String fileName) { 
       try { 
           // precondition 
           File file = new File(fileName); 
           InputStream is = new FileInputStream(file); 
           long length = file.length(); 
           byte[] bytes = new byte[(int) length]; 
 
           // Read in the bytes 
           int offset = 0; 
           int numRead = 0; 
           while (offset <bytes.length
                   && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { 
               offset += numRead; 
           } 
 
           if (offset < bytes.length) { 
               throw new IOException("Could not completely read file "
                       + file.getName()); 
           } 
           is.close(); 
           return bytes; 
       } catch (Exception e) { 
           System.out.println("error occurs in _ClassTransformer!"
                   + e.getClass().getName()); 
           return null; 
       } 
   } 
 
   public byte[] transform(ClassLoader l, String className, Class<?> c, 
           ProtectionDomain pd, byte[] b) throws IllegalClassFormatException { 
       if (!className.equals("TransClass")) { 
           return null; 
       } 
       return getBytesFromFile(classNumberReturns2); 
 
   } 
}

此类实现了ClassFileTransformer接口,其中getBytesFromFile方法根据二进制流字符,此处transform转化方法不进行详细举例

public class Premain { 
       public static void premain(String agentArgs, Instrumentation inst)  
      		throws ClassNotFoundException, UnmodifiableClassException { 
       		inst.addTransformer(new Transformer()); 
       } 
}

可以看到,addTransformer方法并没有指定要转化哪一个类。转换发生在permain函数执行之后,main函数执行之前,这时状态一个类,transform方法就会执行一次

所以在transform方法(Transformer类)中,使用className.equals("TransClass")判断当前的类是否需要转换

Java SE 6 的新特性:虚拟机启动后的动态instrument

在 Java SE 5 当中,开发者只能在 premain 当中进行处理

Java SE 6 针对这种状况做出了改进,开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序。

类似于permain函数,开发者可以编写一个含有agentmain函数的Java类

public static void agentmain (String agentArgs, Instrumentation inst);        //  [1] 
public static void agentmain (String agentArgs);        //    [2]

同样1的优先级高,agentArgs和Inst的用法与permain相同。开发者可以在agentmain中进行类的各种操作。其中agentArgs和Inst的用法跟premain相同

与Premian-不同的是,agentmain需要在main函数运行后才启动

此时,绑定的时机就成了问题。此时可以使用Java SE 6 中提供的Attach API

Attach API

Attach API不是Java的标准API,是Sun公司提供的一套拓展API,用来向目标JVM附着代理工具程序。可以用此方便的监控JVM,运行外加程序

主要包含两部分:

  • VirtualMachine代表一个Java虚拟机,即程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作等等
  • VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能

TransClass类和Transformer类的代码不变。含有 main 函数的 TestMainInJar 代码为:

public class TestMainInJar { 
   public static void main(String[] args) throws InterruptedException { 
       System.out.println(new TransClass().getNumber()); 
       int count = 0; 
       while (true) { 
           Thread.sleep(500); 
           count++; 
           int number = new TransClass().getNumber(); 
           System.out.println(number); 
           if (3 == number || count >= 10) { 
               break; 
           } 
       } 
   } 
}

含有 agentmain 的 AgentMain 类的代码为:

import java.lang.instrument.ClassDefinition; 
import java.lang.instrument.Instrumentation; 
import java.lang.instrument.UnmodifiableClassException; 
 
public class AgentMain { 
   public static void agentmain(String agentArgs, Instrumentation inst) 
           throws ClassNotFoundException, UnmodifiableClassException, 
           InterruptedException { 
       inst.addTransformer(new Transformer (), true); 
       inst.retransformClasses(TransClass.class); 
       System.out.println("Agent Main Done"); 
   } 
}

其中,retransformClasses 是 Java SE 6 里面的新方法,它跟 redefineClasses 一样,可以批量转换类定义,多用于 agentmain 场合

Jar文件跟Permain那个例里面的Jar文件类似,也是把 main 和 agentmain 的类,TransClass,Transformer 等类放在一起,打包为TestInstrument1.jar。而 Jar 文件当中的 Manifest 文件为 :

Manifest-Version: 1.0 
Agent-Class: AgentMain

为运行Attach API,可以写要给控制程序来模拟监控程序

import com.sun.tools.attach.VirtualMachine; 
 import com.sun.tools.attach.VirtualMachineDescriptor; 
……
 // 一个运行 Attach API 的线程子类
 static class AttachThread extends Thread { 
         
 private final List<VirtualMachineDescriptor> listBefore; 
 
        private final String jar; 
 
        AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) { 
            listBefore = vms;  // 记录程序启动时的 VM 集合
            jar = attachJar; 
        } 
 
        public void run() { 
            VirtualMachine vm = null; 
            List<VirtualMachineDescriptor> listAfter = null; 
            try { 
                int count = 0; 
                while (true) { 
                    listAfter = VirtualMachine.list(); 
                    for (VirtualMachineDescriptor vmd : listAfter) { 
                        if (!listBefore.contains(vmd)) { 
 // 如果 VM 有增加,我们就认为是被监控的 VM 启动了
 // 这时,我们开始监控这个 VM 
                            vm = VirtualMachine.attach(vmd); 
                            break; 
                        } 
                    } 
                    Thread.sleep(500); 
                    count++; 
                    if (null != vm || count >= 10) { 
                        break; 
                    } 
                } 
                vm.loadAgent(jar); 
                vm.detach(); 
            } catch (Exception e) { 
                 ignore 
            } 
        } 
    } 
……
 public static void main(String[] args) throws InterruptedException {    
     new AttachThread("TestInstrument1.jar", VirtualMachine.list()).start(); 
 
 }

运行时,可以首先运行上面这个启动新线程的main函数,然后在5s内(仅模拟简单JVM监控过程),启动方式如下

java – javaagent:TestInstrument2.jar – cp TestInstrument2.jar TestMainInJar

程序会首先在屏幕上打印出1,然后打印出2,表示agentmain已经被Attach API 成功附着到JVM上了,代理程序生效。

Java Instrument工作原理

javaagent

00218c2023a2ba140887543f88a4fd99

posted @ 2021-02-24 09:58  NGinko  阅读(1275)  评论(0编辑  收藏  举报