openrasp中用到了Instrumentation技术,它的最大作用,就是类的动态改变和操作。
使用Instrumentation实际上也可以可以开发一个代理来监视jvm的上运行的程序,可以动态的替换类的定义,就可以达到虚拟机级别的AOP实现,随时可以为应用增加新的功能。
基本功能和用法:
java.lang.instrument包提供的实现依赖于JVMTI,JVMTI(Java Virtual Machine Tool Interface)就是java虚拟机提供的一些本地变成的接口,通过代理的形式来访问JVM。在instrument包当中通过jvmti代理程序来进行类的动态操作,还可以进行虚拟机内存管理、线程控制等
如何启动代理?(有两种方法)
1.启动程序时指定代理(main之前运行)
2.程序启动后用agentmain方法通过attach附加启动代理(main之后运行)
代理何时起作用:
1.addTransformer,当class被装载时(loadclasss),与ClassFileTransformer(我们需要实现其transform方法)结合,转换单个class文件(可以根据拿到的classname进行匹配)
2.redefineClasses,支持多个class文件的转换(自己指定好要转换哪些类,比如类aaa,即aaa.class)
第一种:
在一个含有main函数的java类启动时,通过指定–javaagent
参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
Instrumentation是instrument包中的一个接口,jdk1.5引入
在jdk1.5中,可以通过premain方法来让instrumentation在main函数之前执行
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs)
只需要将需要进行的操作(使用addtransformer或者redefineclasses)定义在premain方法中即可对类进行任意操作,agentArgs是命令行下启动javaagent时传入的参数,inst是insructation的实例(后面都靠它),由jvm传入
之前在学习javacodeview时也做过一个类似的例子 https://www.cnblogs.com/tr1ple/p/12260662.html,这里拿来再分析一下
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import java.util.Arrays; public class testagent { private static byte[] relaceBytes(String classname,byte[] classbuffer) { String bufferStr = Arrays.toString(classbuffer).replace("[","").replace("]",""); System.out.println("classname:"+classname); System.out.println("byes:"+ bufferStr); byte[] findBytes = "hello world".getBytes(); String findStr = Arrays.toString(findBytes).replace("[","").replace("]",""); System.out.println("world"+findStr); byte[] replaceBytes = "hello agent".getBytes(); String replaceStr = Arrays.toString(replaceBytes).replace("[","").replace("]",""); System.out.println("agent"+replaceStr); bufferStr = bufferStr.replace(findStr,replaceStr); System.out.println(bufferStr); String[] bytearr = bufferStr.split("\\s*,\\s*"); byte[] bytes = new byte[bytearr.length]; for(int i=0;i < bytearr.length;i++) { bytes[i] = Byte.parseByte((bytearr[i])); } System.out.println("new byte :"+Arrays.toString(bytes)); return bytes; }
//主要还是premain方法的定义与agent相关,通过instrucmentation的实例inst来添加一个transformer public static void premain(String args,final Instrumentation inst){ inst.addTransformer(new ClassFileTransformer() { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/","."); if(className.equals("helloWorld")){ return relaceBytes(className,classfileBuffer); } return classfileBuffer; } },true); } }
先看一下instrumentation,其中addtransformer添加的transformer在每一次类被加载进jvm时都将被调用,并且转换是可以有多个的(自己添加多个classfiletransformer),当一个类转换报错时,jvm将按顺序调用其它的transformer进行类文件的转换
addTransformer实际上添加的是一个classfileTransfoemer的实例,第二个参数则代表当前转换的结果时候能够被再次转换
也存在addtransformer只传一个transformer进来,则默认只转换一次
在classfiletransformer中可以看到其可以对类文件进行转换并返回一个替换后的文件(我们可以选择只替换我们想要替换的部分)
这个类就只有一个transform方法,loader就是类文件被转换时的类加载器
classname即jvm中定义的完整的类名或接口名,比如java/util/List,那么在每个类都要进行加载时就需要进行一个判断,只对满足我们需求的类进行匹配
classBeingRedefined指transform是类加载时触发还是类被重新转换时触发,感觉像个标志的作用
保护域,这里涉及到了类的加载,所以涉及到了Java Security(主要是一种定义了一些代码执行行为的约束)
java类加载时会形成sandbox,再根据security policy为沙盒生成安全策略,程序执行时再根据安全策略进行程序相应检查,从而保护资源不被恶意操作。
还有最后一个参数即传入的字节码文件所在的字节数组,有了这个字节数组我们就可以对想要转换的class进行操作了
上面的的代码对应的操作实际上就是匹配类名为helloworld的类,然后直接暴力将字节数组转string,然后用等长的字符串替换对应的字节码world为agent,所以原理并不复杂,因为长度未发生变化,若是长度变化,则需要改的就不仅仅是简单的替换,需要结合一些字节码操作类,比如javaassist操作字节码就很方便
第二种:
整个重新替换class的字节码(可以重新用javaassist重新构造一个新的类的各种方法体,成员变量值,然后用其字节码进行替换):
test_agent.java
import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.instrument.ClassDefinition; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; public class testagent { public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException, ClassNotFoundException { File file; file = new File("hello_world"); FileInputStream fi = new FileInputStream(file); byte[] fb = new byte[fi.available()]; fi.read(fb); ClassDefinition cla = new ClassDefinition(hello_world.class, fb); inst.redefineClasses(new ClassDefinition[]{cla}); System.out.println("agent success"); } }
main_pro.java
public class main_pro { public static void main(String[] args) { System.out.println(new hello_world().print()); } }
hello_world.java
public class hello_world { public String print() { return "hello_world"; } }
如上面代码所示,main_pro.java尝试调用hello_world类的print方法输出,此时要用到hello_world这个类,那么要涉及到该类的装载,所以redefinedclass在这里精确拦截的类即为hello_world.class,拦截并重新替换其字节码
更改后的hello_world.java如下所示,因为我们只需要其字节码即可,因此直接编译为class字节码文件,然后该文件名可任意(读取字节码时用)
public class hello_world { public String print() { return "hello_agent"; } }
然后编辑MANIFEST.MF文件,设置premain路径以及能够重定义字节码属性,如果没有该属性将报错如下图所示:
MANIFEST.MF:
然后打包:
不加代理之前:
加上代理之后运行:
可以看到此时已经替换要加载的hello_world类的字节码文件,所以实际上加载进jvm的就是替换后的字节码文件,然后调用print方法时就输入是我们替换之后的了,这里的替换可以看到是整个文件的替换。如果单纯只是插桩在指定的方法内,只要掌握好上面说的第一种addtransformer就好了,针对具体要hook某个类,感觉要针对某种具体的漏洞所经过的最终函数调用栈而言。(具体使用的时候针对我们不同的需求来定义,要是在某个类基础上改,则采用addtransformer,如是新定义类,则用redefineclasses)
上面说的都是在实际的main方法之前加载相应的class字节码文件到jvm中,然后执行代理进行对指定的class进行相应的操作,那么还有另一种就是main之后再用Instrumentation来做代理,openrasp中不仅用到了premain也用到了agentmain,所以两种都得学学(agentmain是jdk1.6中提出来的)
agentmain和premain也有两种定义方法:
public static void agentmain (String agentArgs, Instrumentation inst); //优先级大public static void agentmain (String agentArgs);
第一种优先级高于第二种,agentmain通常和retransformClasses结合在一起用,当然addtransformer的时候要设定标志位为true,允许再次转换jvm已经加载的类
Transformer.java
import java.io.FileInputStream; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class Transformer implements ClassFileTransformer { public byte[] getByteFromFile() throws IOException { FileInputStream fi = new FileInputStream("hello_world"); byte[] fileByte = new byte[fi.available()]; fi.read(fileByte); return fileByte; } public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className.equals("hello_world")) { try { return getByteFromFile(); } catch (IOException e) { e.printStackTrace(); } } return classfileBuffer; } }
main_pro.java
public class main_pro { public static void main(String[] args) throws InterruptedException { System.out.println(new hello_world().print()); int count = 0; while(true){ Thread.sleep(600); count++; System.out.println(new hello_world().print()); if(count==10){ break; } } } }
这里具体的替换方法还是与premain一样,我们替换hello_world为hello_agent,只需要在retransformClasses中指定要重新转换的类hello_world,那么main_pro在实例化hello_world的时候,被agent捕获到,重新返回新的class字节码文件,因为agentmain是启动后附加到应用程序的代理,所以需要定义一个附加过程(代码来自网上):
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; class AttachThread extends Thread { private final List<VirtualMachineDescriptor> listBefore; private final String jar; AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) { listBefore = vms; // 记录程序启动时的 VM 集合 jar = attachJar; } @Override 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)) { //新的jvm虚拟机启动则认为是需要附加的进程(不够精准) vm = VirtualMachine.attach(vmd); break; } } Thread.sleep(500); count++; if (null != vm || count >= 10) { break; } } vm.loadAgent(jar); vm.detach(); } catch (Exception e) { } } public static void main(String[] args) throws InterruptedException { new AttachThread("testagent1.jar", VirtualMachine.list()).start(); } }
这里要用到virtualmachine这个类,默认没在rt.jar里面,所以bootstrap 和extend都没把它加载进来,在idea里只要把jdk下lib下的tools.jar添加进来即可,先学习一下这个类:
virtualmachine这个类就是oracle提供给我们的在目标java虚拟机运行过程中,可以使用该类提供的方法附加到目标java虚拟机中,也就是满足我们在应用运行过程中通过添加代理来探测应用运行的目的。关于什么是java虚拟机,可以把它就当做一个运行的jvm进程。而VirtualMachineDescriptor就是具体去描述这java虚拟机的一个容器类,封装着对目标java虚拟机的描述符和最终提供attach操作的AttachProvider的引用。那么其对应的描述符一般就用操作系统的进程id来唯一标示该虚拟机,可以通过ps拿到进程id信息,或者用jdk提供的工具jps.exe:
那么通过virtualmachine的attach方法就能够通过虚拟机描述符来获得该进程对应的java 虚拟机的引用,关于拿到virtualmachine的实例,官方文档有一句话:
Alternatively, a VirtualMachine instance is obtained by invoking the attach method with a VirtualMachineDescriptor obtained from the list of virtual machine descriptors returned by the list method.
那么首先我们可以通过调用virtualmachine的list方法拿到返回的虚拟接描述符,可以看到其返回一个虚拟机描述符实例的list,那么应该对应着一些进程id
上面的方法体中可以看到虚拟机描述符通过迭代attachProvider拿到的,所以有必要弄清attachProvider是什么,它就是最终为我们提供attach到jvm虚拟机的,可以看到此时通过虚拟机描述符拿到对应的jvm虚拟机进程id作为最终attach的依据,然后到attachVirtualMachine方法进行附加,这个是个抽象方法,oracle中也说了该类是交给具体的平台去实现的(sun的providerAttach只能拿来实现sun平台jvm虚拟机程序的热部署),不同的平台jvm虚拟机的实现和工作模式都是不一样的,所以提供抽象方法,具体实现交给子类实现。
拿到目标要被附加的jvm实例后,此时要做的就是附加代理进程了,virtualmachine提供了三种方法:
第一种loadagent,也就是直接加载我们定义好的jar包中的代理类,比如上面用agentmain编的代理类,里面写好代理逻辑,和MANIFEST.MF一起打包成jar,那么传入该jar包的名字后,目标jvm将把该jar包添加到它的jvm的classpath中,其中options是提供给代理类agentmain方法的入口第一个参数
第二种即为loadAgentLibrary
这种是使用jvmti编写本地的动态链接库,然后将动态链接库附加到目标jvm中,比如有一个libtr1ple.so的代理动态链接库,此时只要传一个tr1ple给该函数,它将到系统环境变量LD_LIBRARY_PATH的路径下去找该so文件,并且会将传入的tr1ple扩展为libtr1ple.so进行查找然后进行附加
第三种是loadAgentpath
这个和第二种很类似,也是加载so文件,不过不需要到环境变量中找so,而是直接提供一个代理so文件的绝对路径来进行加载
其中MANIFEST.MF为:
运行结果如下(本来应该输出hello_world,附加agent以后调用transform转换class 字节码文件):
打包testagent1.jar作为要添加的代理:
然后同时运行附加程序,attack到java虚拟机
上面的监控jvm进程的来附加代理的代码感觉还不够精准,因为只是针对选择新增的jvm进程id来附加,那么对于要被附加代理的java虚拟机,能否准确地获得其虚拟机描述符?如下图的方法是显示虚拟机描述符
看VirtualMachineDescriptor的构造方法,也就是当我们创建虚拟机描述符时如果没有指定displayname的时候,默认将进程id作为虚拟机描述符
那么很容易来看一下现在机器上运行的java虚拟机的虚拟机描述符
virtual.java
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; public class virtual { public static void main(String[] args) { List<VirtualMachineDescriptor> vmList = VirtualMachine.list(); for(VirtualMachineDescriptor vmd:vmList){ System.out.println(vmd.displayName()); } } }
此时运行main_pro,作为要被附加的java虚拟机
并且运行virtual.java,此时由输出可以看到目前有4个java虚拟机
virtual -> 当前运行的主程序
jetbrains -> idea
finalshell
main_pro 要被附加的java虚拟机程序
所以很容易看到此时jvm已经给运行的程序在初始化虚拟机描述符时传入了diplayname,基本就是main方法所在的类名相关了,所有为了精确附加代理,所以可以通过displayname来进行捕获目标java虚拟机:
那么假设提前知道要捕获的目标虚拟机的类名,则用以下代码:
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; class AttachThread extends Thread { private final String jar; AttachThread(String attachJar) { jar = attachJar; } @Override public void run() { VirtualMachine vm = null; try { int count = 0; while (true) { List<VirtualMachineDescriptor>list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { String vmDisplayName = vmd.displayName(); if (vmDisplayName.equals("main_pro")) { vm = VirtualMachine.attach(vmd); break; } } Thread.sleep(500); count++; if (null != vm || count >= 10) { break; } } vm.loadAgent(jar); vm.detach(); } catch (Exception e) { } } public static void main(String[] args) throws InterruptedException { new AttachThread("testagent1.jar").start(); } }
打包为jar
接着运行java -jar attach.jar去附加代理
此时就能看到第一次输出是hello_world,接着新的线程装载类hello_world时将被retransformClasses捕获,此时附加testagent1.jar到main_pro对应的java虚拟机中实现程序运行中附加代理
首先java源文件要编译生成.class文件肯定要经过一些强制性的语法规则检测,那么最常见的情况就是语法不正确将直接编译不通过产生错误。
classloader加载相应的class文件来生成class对象(loadclass->findclass->defineclass->resolveclass),而类的加载也要经过验证,因为并不能保证加载进来的字节码文件是未经过篡改的,比如直接在更改class文件添加相应的后门,或字节码文件更改错误,所以字节码的验证应该是class 字节码被加载到jvm中由jvm进行验证
tips:jvm启动时运行过程中已经加载到jvm的class文件的class对象获取其classloader为null(rt.jar下的class,由bootstrap classloader加载),比如像maven中管理的第三方仓管中class文件,那么此时要在程序中使用,获取到的classloader也为appclassloader,这里加载yyy和instruction属于新的需要加载到jvm的类,因此默认也使用appclassloader。
资源包括文件资源、配置选项,我们可以检查是否可读、可写、远程端口是否可以进行连接等,具体的权限包括以下这些
那么这些检查都是通过java SecurityManager来实现的,该类感觉是jdk为java 应用设计的一种安全检查,其中内置了多种方法在当前应用要进行某种操作时可以调用checkxxx对应的方法来检测是否允许执行
下图中说的方法可以用来检测是否拥有某种操作权限
checkPermission如果只接受一个参数,那么对应的只是当前执行的线程的上下文是否有某种操作权限,那么有时候需要在多线程下检查权限,所以使用getSecurityContext来拿到调用线程的context(默认都是AccessControlContext),然后再调用可以传入context的checkPermission进行相应的权限检查
那么最终的permission对比是由抽象类permission的具体某种操作的子类的implies方法来实现检查,比如URLPermission,通过给定的url、指定的http方法(GET POST等)、http header进行匹配,来检查能否访问某个资源(检查的前提当然要提前设置好相应的权限new URLPermission)
大多数Permission都对应着一个action列表(初始化设置),后面进行check的时候将对应着进行检查
比如通过以下方法就能定义一个URLPermission
这里又学习了一下security manager的相关知识,这里涉及到了jdk本身的安全机制,java sandbox,安全管理器只是其中一个组件,提供了一些jdk的api与操作系统之间联系的一些权限限制,比如最常用的就是获取操作系统的一些相关信息,如下图所示,%java_home%/jre/lib/securicy/java.policy中就包含了一些默认的安全限制选项,或者通常用的默认policy文件即为家目录下的.java.policy,安全选项也就是之前所说的Perminssion的一些子类以及其对应的action操作
关于oracle的相关permission说明见此链接,https://docs.oracle.com/javase/7/docs/technotes/guides/security/permissions.html
其中每一条就是权限(就是相应的类名)+属性+操作
其中security目录下面还有一个java.security文件主要是对沙箱做一些配置的定义,比如如下两个就是其定义的默认policy的寻找地址
因为默认情况下有下面的这个配置,所以是允许自定义策略文件的,还有一些其他配置等以后再单独研究java 沙箱再详细学习
使用policy方式:
java -Djava.security.policy=<URL> (这种一个等号还是会用java.policy)
java -Djava.security.policy==<URL> (这种两个等号只用我们指定的policy来进行检查)
如何使用security manager:
1.使用默认的manager
开启方式:
java -Djava.security.manager
或者在代码中添加:
System.setSecurityManager(new SecurityManager());
使用默认的manager的话,最终调用checkxxx方法时则是如下所示,直接调用securitymanager的checkPermission来进行相应的检查,然后调用AccessController的检查权限的方法来对要进行的操作能否执行进行检查
所以也可以直接实例化某种权限的实例,然后调用AccessController来进行权限检查,比如如下所示,如果对应的操作不允许,则抛出错误
2.自定义securityManager,复写checkxxx,
import java.io.*; import java.lang.instrument.*; import java.security.*; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Set; import java.util.regex.Pattern; public class Instruction { private static class MySecurityManager extends SecurityManager { @Override public void checkRead(String file) { Boolean match = Pattern.matches("/proc.*|/etc.*|/var.*","/proc/self/environ"); if (match) { throw new AccessControlException("cannot read file:" + file); } super.checkRead(file); } } public static void main(String[] args) throws IOException, PrivilegedActionException { final String file = "./com/1.txt"; System.setSecurityManager(new SecurityManager()); SecurityManager sm = System.getSecurityManager(); if(sm!=null){ sm.checkRead(file); } FileInputStream fi = new FileInputStream(file); int a =fi.available(); byte[] aa = new byte[a]; fi.read(aa); String out = new String(aa); System.out.println(out); } }
比如想在读文件前,这里不仅检测你有没有读的权限,还想检测你一下你读的文件是不是在应用想让你读的范围之内,则可以直接定义自己的security manager来重写checkread方法,上面的代码就比如想读取/proc/self/environ文件,但是在正则中匹配到了,因此不允许进行读取,那么我们想要进行的操作进行完之后,只需再次调用父类的checkread方法接着后面的权限检查即可。
以checkread为例,对应其他的一些api我们也可以进行相应的限制,比如这里是读,那么我们可以想到:
1.对应的输出流,checkWrite,那么就可以去检查输出的文件是否满足我们的要求,比如限制写文件的目录,限制写文件的后缀等
2.checkConnect可以检查我们连接的主机和端口,那么在发送连接请求时可以检查是否在允许连接的ip和port范围内等
3.checkExec可以拿到所要执行的命令,对命令来进行权限检查
.....
jdk提供的这一套api,我们都能够在开启security manager的情况下加以利用,使得应用更加安全,比如tomcat中就支持security manager的模式
tomcat cve里面也有几个security bypass的,不过都是低危,并且都是4年前的洞,大多数都是tomcat团队发现的,由下面注释可以看到-security模式将启动security manager,默认情况下webapps下的目录只有读权限,work下的对应的应用目录具有读、写、删除权限
那么如何指定自定义的pollicy来进行安全限制:
import java.io.FileInputStream; import java.io.FilePermission; import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedActionException; public class Instruction { public Instruction() { } public static <PriviliegedAction> void main(String[] args) throws IOException, PrivilegedActionException { String file = "./com/1.txt"; System.setSecurityManager(new SecurityManager()); //开启security manager SecurityManager sm = System.getSecurityManager(); //拿到sm if (sm != null) { FilePermission fp = new FilePermission("./com/1.txt", "read"); //声明对文件的读权限 FilePermission fp1 = new FilePermission("./com/1.txt", "write"); //声明对文件的写权限 AccessController.checkPermission(fp); //直接检查读 AccessController.checkPermission(fp1); //直接检查写 System.out.println("sss"); sm.checkRead("./com/1.txt"); //用sm间接检查读 } FileInputStream fi = new FileInputStream("./com/1.txt"); int a = fi.available(); byte[] aa = new byte[a]; fi.read(aa); String out = new String(aa); System.out.println(out); } }
上面的文件执行编译后运行,然后不制定策略文件的情况下,开启sm,此时默认是对com/1.txt没有读写权限的,所以到read权限检查时直接报错退出,第二次执行指定my.policy为策略文件,此时只指定读不指定写,此时将在写权限检查时退出,即my.policy对当前运行环境生效了
tips:1.这里将class打包成jar然后再执行比较好,如果要进行签名的话也是jar包形式 2.policy中指定文件位置时要相对于执行的class字节码,文件路径我用绝对路径H:\JavaSecStudy\javasec-rasp\target\classes\kk\com\1.txt(单换双杠)竟然不识别,坑了我好长时间,并且只支持下图这种相对路径寻址方法
如下所示直接执行class字节码文件指定policy也可以,只要路径对就行
写这篇总结写看了不少文章,也将相关的链接放到下面,如果想多了解可以点开学习,不过大多数还是得去jdk源码中的注释或者jdk官方文档去查看类的定义去理解某个类的设计意义及用法,文中不免有表述不清,若有不对,还请指出~
https://www.ibm.com/developerworks/cn/java/j-lo-jse61/ Instrumentation 新功能
https://www.jianshu.com/p/9f4e8dcb3e2f
https://www.ibm.com/developerworks/cn/java/j-dyn0203/ 动态类转换
https://www.ibm.com/developerworks/cn/java/j-dyn0414/ 利用bcel涉及字节码
https://www.cnblogs.com/f1194361820/p/4189269.html java security
https://www.cnblogs.com/youxia/p/java004.html security manager
https://www.cnblogs.com/MyStringIsNotNull/p/8268351.html java 沙箱机制
http://www.blogjava.net/china-qd/archive/2006/04/25/42931.html 使用policy设置安全策略
https://blog.spoock.com/2019/12/21/Getting-Started-with-Java-SecurityManager-from-Zero/ security manager
https://www.infoq.cn/article/javaagent-illustrated/ javaagent解读
https://docs.oracle.com/javase/7/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html jvm虚拟机attach
https://blog.csdn.net/qinhaotong/article/details/100693414 java agent 不错的文章