序言
空山新雨后,天色晚来秋。
整理Java Agent相关知识。
概念 Java Agent Java Agent是一个运行在目标JVM的特定程序,它的职责是负责从目标JVM中获取数据,然后将数据传递给外部进程。加载Agent的时机可以是目标JVM启动之时,也可以是在目标JVM运行时加载。
JVMTI ==万物起源==
JVMTI(JVM Tool Interface)是Java虚拟机对外提供的Native编程接口,通过JVMTI,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。
Instrumentation 在Java SE 5之前,要实现一个Agent只能通过编写Native代码来实现。
从Java SE 5开始,可以使用Java的Instrumentation接口(java.lang.instrument 包)来编写Agent。
从而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到Instrumentation的目的。
并且从Java SE 6开始,可以向native method插桩。
==无论是通过Native的方式还是通过Java Instrumentation接口的方式来编写Agent,它们的工作都是借助JVMTI来进行完成。==
Instrumentation 这里把Instrumentation单独拿出来细说java.lang.instrument.Instrumentation.
Instrumentation接口设计初衷是为了收集Java程序运行时的数据,用于监控运行程序状态,记录日志,分析代码用的。
目前可以实现(对应JVM方法):
动态添加或移除自定义的ClassFileTransformer(addTransformer/removeTransformer)
JVM会在类加载时调用Agent中注册的ClassFileTransformer;
动态修改classpath(appendToBootstrapClassLoaderSearch、appendToSystemClassLoaderSearch)
将Agent程序添加到BootstrapClassLoader和SystemClassLoaderSearch(对应的是ClassLoader类的getSystemClassLoader方法,默认是sun.misc.Launcher$AppClassLoader)中搜索;
动态获取所有JVM已加载的类(getAllLoadedClasses);
动态获取某个类加载器已实例化的所有类(getInitiatedClasses)。
重定义某个已加载的类的字节码(redefineClasses)。
动态设置JNI前缀(setNativeMethodPrefix),可以实现Hook native方法。
重新加载某个已经被JVM加载过的类字节码(retransformClasses)。
两种部署模式:
Java Agent支持目标JVM启动时加载,也支持在目标JVM运行时加载,这两种不同的加载模式会使用不同的入口函数。
Java Agent和普通的Java类并没有任何区别,普通的Java程序中规定了main方法为程序入口,而Java Agent则将premain(Agent模式)和agentmain(Attach模式)作为了Agent程序的入口,两者所接受的参数是完全一致的。
Instrumentation类方法如下:
学习API最好的方法就是阅读JavaDoc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 package java.lang.instrument; 该类提供了工具化Java编程语言代码所需的服务。 仪表化是指在方法中添加字节码,以收集数据,供工具使用。 由于这些变化是纯粹的加法,所以这些工具不会修改应用程序的状态或行为。 这种良性工具的例子包括监控代理、剖析器、覆盖率分析器和事件记录器。 下面有两种方式来实例化Instrumentation接口: 1 .当JVM启动时,-javaagent:Agent.jar 指示代理类。在这种情况下,premain方法会接收一个Instrumentation实例作为入参。 2 .当JVM启动之后,可以Attach目标进程上。在这种情况下,agentmain方法会接收一个Instrumentation实例作为入参。 一旦Agent获得一个Instrumentation实例,代理可以在任何时候调用实例上的方法。 void addTransformer (ClassFileTransformer transformer,boolean canRetransform) 注册所提供的Tranformer。从此以后所有类定义都会被Tranformer看到,但不包括任何已注册的Transformer所依赖的类的定义。 当类被加载时,当类被重新定义时,Tranformer被调用; 如果 canRetransform 为真,当类被重新转换时,变压器被调用。 变换调用的顺序请参见 ClassFileTransformer.transformer。 如果一个Transformer在执行过程中抛出了异常,JVM仍然会依次调用其他注册的Transformer。 同一个Transformer可以被添加一次以上,但强烈不鼓励这样做,可以通过创建一个新的Transformer实例来避免这种情况。 boolean removeTransformer (ClassFileTransformer transformer) 解除所提供的Transformer注册。今后的类定义将不会显示给Transformer。 删除Transformer最近添加的匹配实例。由于类加载的多线程特性,Transformer在被删除后可能会收到调用。 Transformer的编写应考虑到这种情况。 boolean isRetransformClassesSupported () 返回当前JVM配置是否支持类的重新转换。 重构已经加载的类的能力是JVM的一个可选能力,只有当代理JAR中的Can-Retransform-Classes manifest属性被设置为true 时,才支持重构。 只有在代理 JAR 文件中 Can-Retransform-Classes manifest 属性被设置为 true (如包规范中所述)且 JVM 支持此能力时,才会支持重构。 在单个JVM的单个实例化过程中,对该方法的多次调用将始终返回相同的答案。 void retransformClasses (Class<?>... classes) throws UnmodifiableClassException 给Class<?> classes 中的若干类进行转换。 该方法便于对已经加载的类进行转换。 当类被初始加载或重新定义时,可以使用ClassFileTransformer对初始类文件字节进行转换。 这个函数会重新运行转换过程(无论之前是否发生过转换)。 重新转换遵循以下步骤: 1. 从初始的类文件字节开始 2. 对于每个添加了 canRetransform false 的Transformer,由 transform 返回的字节将被重新使用,作为变换的输出,不做任何修改; 3. 对于每个添加了 canRetransform true 的Transformer,这些Transformer都会调用 transform 方法。 4.转化后的类文件字节被安装为类的新定义。 变换的顺序在 transform 方法中描述。这个相同的顺序被用于自动重新应用无法变换的变换。 初始类文件的字节代表传递给ClassLoader.defineClass或redefineClasses的字节(在任何转换行为之前),然而它们可能不完全匹配。常量池可能没有相同的布局或内容。常量池可能有更多或更少的条目。常量池条目的顺序可能不同,但是,方法字节码中的常量池索引会对应。一些属性可能不存在。在顺序没有意义的地方,例如方法的顺序,顺序可能不会被保留。 该方法会在一个集合上操作(看入参的形式就是一个不定数组),以允许同时对多个类进行相互依赖的改变(类 A 的重构可能需要类 B 的重构)。 如果一个重构的方法有高频活动的栈帧,这些活动帧将继续运行原方法的字节码。重构后的方法将在新的调用.invoke () 中使用。 这个方法不会引起任何初始化.换句话说,重新定义一个类不会导致其初始化。静态变量的值将保持在调用之前的状态。 重构后的类的实例不会受到影响。 重构可以改变方法体、常量池和类属性。重构不能增加、删除或重命名字段或方法,不能改变方法的签名,也不能改变继承。这些限制也许会在未来的版本中被取消。类文件字节的检查、验证和安装直到应用了转换之后才会进行,如果结果字节有错误,这个方法将抛出一个异常。 如果本方法抛出异常,说明没有类被重新转换。 boolean isRedefineClassesSupported () 返回当前JVM配置是否支持类的重新定义。 重新定义已经加载的类的能力是JVM的一个可选能力,只有当代理JAR文件中的Can-Redefine-Classes manifest属性被设置为true 时,才会支持重新定义。 只有当代理JAR文件中的Can-Redefine-Classes manifest属性被设置为true 时,才会支持重新定义(如包规范中所述),并且JVM支持该能力。在单个JVM的单次实例化过程中,对该方法的多次调用将始终返回相同的答案。 void redefineClasses (ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException 使用提供的类文件重新定义提供的类集。 这个方法用于替换类的定义,而不引用现有的类文件字节。 就像从源码重新编译以进行修复和继续调试时一样。当现有的类文件字节要被转换时(例如在字节码instrumentation中),应该使用retransformClasses。 这个方法在一个集合上操作,以便允许同时对多个类进行相互依赖的改变(对类A的重新定义可能需要对类B的重新定义)。 boolean isModifiableClass (Class<?> theClass) 确定一个类是否可以通过retransformClasses或redefineClasses进行修改。如果一个类是可修改的,那么本方法返回true 。如果一个类是不可修改的,那么这个方法返回false 。 要想对一个类进行重构,isRetransformClassesSupported () 也必须为真。但是is的值不会影响这个函数返回的值。要想重新定义一个类,isRedefineClassesSupported () 也必须为真,但isRedefineClassesSupported () 的值不会影响这个函数返回的值。 基元类(例如java.lang.Integer.TYPE)和数组类是永远不能修改的。 Class[] getAllLoadedClasses () 返回JVM当前加载的所有类的数组。 Class[] getInitiatedClasses (ClassLoader loader) 返回一个数组,该数组包含了所有由loader加载的类。如果提供的Loader为空,则返回由bootstrap类Loader加载的类。 long getObjectSize (Object objectToSize) 返回指定对象消耗的存储量的近似值。这个结果可能包括对象的部分或全部开销,因此对于在单独一个实现内进行比较是有用的,但对于实现之间的比较是没有用的。在JVM的单次调用中,估计值可能会改变。 void appendToBootstrapClassLoaderSearch (JarFile jarfile) 指定一个JAR文件(包含插桩类),让它由Bootstrap加载器加载。 当虚拟机内置的类加载器(称为 "bootstrap class loader")搜索某个类不成功时,JAR文件中的条目也会被搜索。 本方法可以多次使用,按照本方法被调用的顺序添加多个JAR文件进行搜索。 Agent应该注意确保JAR文件中除了那些将由引导类加载器定义的类或资源,不包含任何其他类或资源。如果不遵守这个警告,可能会导致难以诊断的意外行为。例如,假设有一个加载器L,L的用于授权的父类是bootstrap类加载器。此外,L定义的类C中的一个方法引用了一个非公共的访问者类C$1,如果JAR文件中包含一个类C$1,那么授权给bootstrap类加载器将导致C$1被bootstrap类加载器定义。在这个例子中,一个IllegalAccessError将被抛出,可能会导致应用程序失败。避免这类问题的一个方法是为插桩类使用一个独特的包名。 void appendToSystemClassLoaderSearch (JarFile jarfile) 指定一个JAR文件(包含插桩类),让它由SystemClassLoader。 这里面的SystemClassLoader其实就是ApplicationClassLoader。 当委托的系统类加载器(参见getSystemClassLoader () )搜索一个类不成功时,JarFile中的条目将被搜索。 本方法可以多次使用,按照本方法被调用的顺序添加多个JAR文件进行搜索。 Agent应注意确保JAR中不包含除SystemClassLoader以外的任何类或资源。如果不遵守这个警告,可能会导致难以诊断的意外行为(参见appendToBootstrapClassLoaderSearch)。 如果SystemClassLoader实现了一个名为appendToClassPathForInstrumentation的方法,那么它就支持添加一个要搜索的JAR文件,该方法接收一个类型为java.lang.String的单一参数。该方法不需要公开访问。JAR文件的名称是通过调用jarfile上的getName () 方法获得的,并作为参数提供给appendToClassPathForInstrumentation方法。 此方法不会改变java.class.path系统属性的值。 boolean isNativeMethodPrefixSupported () 返回当前JVM配置是否支持设置本地方法前缀。设置本地方法前缀的能力是JVM的一个可选能力。 只有在代理 JAR 文件中 Can-Set-Native-Method-Prefix manifest 属性被设置为 true (如包规范中所述)且 JVM 支持该能力时,才会支持设置本地方法前缀。 在单个JVM的单个实例化过程中,对该方法的多次调用将始终返回相同的答案。 void setNativeMethodPrefix (ClassFileTransformer transformer,String prefix) 设置native method 前缀
java.lang.instrument.ClassFileTransformer是一个转换类文件的代理接口,我们可以在获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器。
可以使用addTransformer注册一个我们自定义的Transformer到Java Agent,当有新的类被JVM加载时JVM会自动回调用我们自定义的Transformer类的transform方法,传入该类的transform信息(类名、类加载器、类字节码等),==可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后将新的类字节码返回给JVM==,JVM会验证类和相应的修改是否合法,如果符合类加载要求JVM会加载我们修改后的类字节码。
继续读JavaDoc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 通常,一个Agent提供了这个接口的实现,以便转换类文件。转换发生在JVM定义类之前。 请注意,类文件这个术语在Java™虚拟机规范第3.1 节中定义,指的是类文件格式的字节序列,无论它们是否驻留在文件中。 byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException 该方法的实现可以对提供的类文件进行转换,并返回一个新的替换类文件。 一旦用addTransformer注册了一个Transformer,那么每一个新的类定义和每一个类的重新定义都会调用这个变换器。能够重构的变换器也会在每次类重构时被调用。 新类定义的请求是通过ClassLoader.defineClass或其原生等价物来实现的。 对类的redefinition的请求是通过Instrumentation.redefineClasses或它的本机等价物提出的。 类的retransformation请求由 Instrumentation.retransformClasses 或其本机等价物发出。 在请求的处理过程中,在类文件字节被验证或应用之前,Transformer被调用。 当有多个Transformer时,Transformer是通过链式变换调用组成的。也就是说,一次变换调用返回的字节数组成为下一次调用的输入(通过classfileBuffer参数)。 对于retransformation,不能重构的变换器不被调用,而是重用之前变换的结果。在所有其他情况下,这个方法都会被调用。在这些分组中,transformers按照注册的顺序被调用。原生变换器由Java虚拟机工具接口中的ClassFileLoadHook事件提供。 参数: loader 定义要转换的类加载器;如果是引导加载器,则为 null className 类名,如:java/lang/Runtime classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null protectionDomain 要定义或重定义的类的保护域 classfileBuffer 类文件格式的输入字节缓冲区(不得修改) return 字节码byte 数组
重写transform方法需要注意以下事项:
ClassLoader如果是被Bootstrap ClassLoader(引导类加载器)所加载那么loader参数的值是空。
修改类字节码时需要特别注意插入的代码在对应的ClassLoader中可以正确的获取到,否则会报ClassNotFoundException,比如修改java.io.FileInputStream(该类由Bootstrap ClassLoader加载)时插入了我们检测代码,那么我们将必须保证FileInputStream能够获取到我们的检测代码类。
JVM类名的书写方式路径方式:java/lang/String 而不是我们常用的类名方式:java.lang.String。
类字节必须符合JVM校验要求,如果无法验证类字节码会导致JVM崩溃或者VerifyError(类验证错误)。
如果修改的是retransform类(修改已被JVM加载的类),修改后的类字节码不得新增方法、修改方法参数、类成员变量。
addTransformer时如果没有传入retransform参数(默认是false)就算MANIFEST.MF中配置了Can-Redefine-Classes: true而且手动调用了retransformClasses方法也一样无法retransform。一定要增加true参数!!!
卸载transform时需要使用创建时的Instrumentation实例。
Java Agent还限制了我们:
必须以jar包的形式运行或加载;
必须包含/META-INF/MANIFEST.MF文件,且该文件中必须定义好Premain-Class(启动前Agent模式)或Agent-Class:(运行中Agent模式)配置;
如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在MANIFEST.MF中添加Can-Retransform-Classes: true或Can-Redefine-Classes: true。
实验部分 接下来进行实验,启动前premain+-javaagent以及启动中Attach 两种模式。
启动前指定Agent位置 如果需要在目标JVM启动的同时加载Agent,实现:
1 2 [1 ] public static void premain (String agentArgs, Instrumentation inst) ; [2 ] public static void premain (String agentArgs) ;
JVM将首先寻找[1],如果没有发现[1],再寻找[2]。
实战一:premain方法测试 写一个demo做个实验:
创建Agent类,声明premain方法:
使用maven-jar-plugin,创建MANIFEST.MF:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-jar-plugin</artifactId > <version > 3.2.0</version > <configuration > <archive > <manifest > <addClasspath > true</addClasspath > </manifest > <manifestEntries > <Premain-Class > com.0range.Agent</Premain-Class > <Can-Redefine-Classes > true</Can-Redefine-Classes > <Can-Retransform-Classes > true</Can-Retransform-Classes > </manifestEntries > </archive > </configuration > </plugin >
MANIFEST.MF:
mvn clean install生成jar包:java_agent_01-1.0-SNAPSHOT
待插桩类:
同样打包,example01-1.0-SNAPSHOT:
终端执行,成功插桩:
实战二:打印加载的类 之前也提到,一旦你addTransformer之后,需要加载的每一个类都会经过transform方法。
一旦用addTransformer注册了一个Transformer,那么每一个新的类定义和每一个类的重新定义都会调用这个变换器。
首先addTranformer:
这里ClassFileTransformerDemo()继承了ClassFileTransformer类,记得最后加上ture:
这里就是简单的执行了打印在Tranformer之后加载的类:
实战三:代码插桩 这里复现一下yz 的实验。这个例子写得很好,类似于破解官方软件的一种绕过过程,毕竟白嫖才是最香的。
首先这里有一个校验函数用来判断用户是否已经过了有效期,这个截止日期是硬编码在代码中的,由于是写死的那么就会一直提示已经过期:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class CheckLicense { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); private static boolean checkExpiry (String expireDate) throws ParseException { try { Date date = DATE_FORMAT.parse(expireDate); if (new Date().before(date)) { return false ; } } catch (ParseException e) { e.printStackTrace(); } return true ; } public static void main (String[] args) { final String expireDate = "2020-10-01 00:00:00" ; new Thread(new Runnable() { @Override public void run () { while (true ) { try { String time = "[" + DATE_FORMAT.format(new Date()) + "] " ; try { if (checkExpiry(expireDate)) { System.err.println(time + "您的授权已过期,请重新购买授权!" ); } else { System.out.println(time + "您的授权正常,截止时间为:" + expireDate); } } catch (ParseException e) { e.printStackTrace(); } TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
硬编码有效期是一个已经过期的License时间final String expireDate = "2020-10-01 00:00:00";
已经过期:
这里开始编写Agent:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class ClassFileTransformerDemo implements ClassFileTransformer { private static final String HOOK_CLASS = "com.sec.CheckLicense" ; @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { String cn = className.replace("/" , "." ); if (cn.equals(HOOK_CLASS)) { System.out.println(cn); try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod ctMethod = ctClass.getDeclaredMethod( "checkExpiry" , new CtClass[]{classPool.getCtClass("java.lang.String" )} ); ctMethod.insertBefore("System.out.println(\"License到期时间:\" + $1);" ); ctMethod.insertAfter("return false;" ); return ctClass.toBytecode(); } catch (Exception e) { e.printStackTrace(); } } return classfileBuffer; } }
这里相当于将检测日期函数写死return false。
打包,运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-shade-plugin</artifactId > <version > 3.2.4</version > <executions > <execution > <phase > package</phase > <goals > <goal > shade</goal > </goals > <configuration > <filters > <filter > <artifact > *:*</artifact > <excludes > <exclude > MANIFEST.MF</exclude > <exclude > META-INF/DEPENDENCIES</exclude > <exclude > META-INF/LICENSE*</exclude > <exclude > META-INF/NOTICE*</exclude > </excludes > </filter > </filters > <artifactSet > <includes > <include > org.javassist:javassist:jar:*</include > </includes > </artifactSet > <relocations > <relocation > <pattern > javassist</pattern > <shadedPattern > com.fxc.deps.javassist</shadedPattern > </relocation > </relocations > </configuration > </execution > </executions > </plugin >
启动中进行Agent Attach 之前的permain方法只能在java程序启动之前执行,而Java SE 6的新特性改变了这种情况,可以通过Java Tool API中的attach方式来达到这种程序启动之后设置代理的效果。
下面来分析一下动态加载Agent的相关技术细节。
AttachListener Attach机制通过Attach Listener线程来进行相关事务的处理,下面来看一下Attach Listener线程是如何初始化的。
1 2 3 4 5 6 7 8 9 10 11 void AttachListener::init() { const char thread_name[] = "Attach Listener" ; Handle string = java_lang_String::create_from_str(thread_name, THREAD); { MutexLocker mu (Threads_lock) ; JavaThread* listener_thread = new JavaThread(&attach_listener_thread_entry); } }
一个线程启动之后都需要指定一个入口来执行代码,Attach Listener线程的入口是attach_listener_thread_entry,下面看一下这个函数的具体实现:
下面看attach_listener_thread_entry的具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static void attach_listener_thread_entry (JavaThread* thread, TRAPS) { AttachListener::set_initialized(); for (;;) { AttachOperation* op = AttachListener::dequeue(); AttachOperationFunctionInfo* info = NULL ; for (int i=0 ; funcs[i].name != NULL ; i++) { const char * name = funcs[i].name; if (strcmp (op->name(), name) == 0 ) { info = &(funcs[i]); break ; }} res = (info->func)(op, &st); } }
整个函数执行逻辑,大概是这样的:
拉取一个需要执行的任务:AttachListener::dequeue。
查询匹配的命令处理函数。
执行匹配到的命令执行函数。
其中第二步里面存在一个命令函数表,整个表如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 static AttachOperationFunctionInfo funcs[] = { { "agentProperties" , get_agent_properties }, { "datadump" , data_dump }, { "dumpheap" , dump_heap }, { "load" , load_agent }, { "properties" , get_system_properties }, { "threaddump" , thread_dump }, { "inspectheap" , heap_inspection }, { "setflag" , set_flag }, { "printflag" , print_flag }, { "jcmd" , jcmd }, { NULL , NULL } };
对于加载Agent来说,命令就是“load”。
任务从哪来,这个秘密就藏在AttachListener::dequeue这行代码里面,接下来来分析一下dequeue这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 LinuxAttachOperation* LinuxAttachListener::dequeue() { for (;;) { struct sockaddr addr ; socklen_t len = sizeof (addr); RESTARTABLE(::accept(listener(), &addr, &len), s); struct ucred cred_info ; socklen_t optlen = sizeof (cred_info); if (::getsockopt(s, SOL_SOCKET, SO_PEERCRED, (void *)&cred_info, &optlen) == -1 ) { ::close (s); continue ; } LinuxAttachOperation* op = read_request(s); return op; } }
上面的代码表明,Attach Listener在某个端口监听着,通过accept来接收一个连接,然后从这个连接里面将请求读取出来,然后将请求包装成一个AttachOperation类型的对象,之后就会从表里查询对应的处理函数,然后进行处理。
Attach Listener使用一种被称为“懒加载”的策略进行初始化,也就是说,JVM启动的时候Attach Listener并不一定会启动起来。
具体实现 运行时Attach方法里面的关键是调用VirtualMachine的attach方法进行Agent挂载的功能。
下面分析一下VirtualMachine的attach方法具体是怎么实现的。
Attach模式需要知道我们运行的Java程序进程ID,通过Java虚拟机的进程注入方式实现可以将我们的Agent程序动态的注入到一个已在运行中的Java程序中。
还是之前那个例子,可以使用jps -l命令进行查看:
Java代码实现可以使用com.sun.tools.attach.VirtualMachine的list方法即可获取本机所有运行的Java进程,如:
有了进程ID我们就可以使用Attach API注入Agent了,Attach Java进程注入通用示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 String pid = args[0 ]; String agentPath = "/xxx/agent.jar" ; VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agentPath); vm.detach();
使用Attach模式启动Agent程序时需要使用到JDK目录下的lib/tools.jar,如果没有配置CLASS_PATH环境变量的话需要在运行Agent程序时添加-Xbootclasspath/a:$JAVA_HOME/lib/tools.jar参数,否则无法使用Attach API。
1 java -Xbootclasspath/a:$JAVA_HOME/lib/tools.jar -jar AgentAttach-1.0 -SNAPSHOT.jar
首先后台运行监测代码,用我们的jar包获取目标进程:16281
接下来直接注入就完事了:
1 java -Xbootclasspath/a:$JAVA_HOME/lib/tools.jar -classpath $JAVA_HOME/lib/tools.jar:AgentAttach-1.0 -SNAPSHOT.jar -jar AgentAttach-1.0 -SNAPSHOT.jar 16281
成功修改:
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 public class AttachAgent { private static final String HOOK_CLASS = "com.sec.CheckLicense" ; public static void main (String[] args) { if (args.length == 0 ) { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor desc : list) { System.out.println("进程ID:" + desc.id() + ",进程名称:" + desc.displayName()); } } String pid = args[0 ]; try { VirtualMachine vm = VirtualMachine.attach(pid); URL agentURL = AttachAgent.class .getProtectionDomain ().getCodeSource ().getLocation () ; String agentPath = new File(agentURL.toURI()).getAbsolutePath(); vm.loadAgent(agentPath); vm.detach(); } catch (Exception e) { e.printStackTrace(); } } public static void agentmain (String args, final Instrumentation inst) { loadAgent(args, inst); } private static void loadAgent (String arg, final Instrumentation inst) { ClassFileTransformer classFileTransformer = createClassFileTransformer(); inst.addTransformer(classFileTransformer, true ); Class[] loadedClass = inst.getAllLoadedClasses(); for (Class clazz : loadedClass) { String className = clazz.getName(); if (inst.isModifiableClass(clazz)) { if (className.equals(HOOK_CLASS)) { try { inst.retransformClasses(clazz); } catch (UnmodifiableClassException e) { e.printStackTrace(); } } } } } private static ClassFileTransformer createClassFileTransformer () { return new ClassFileTransformer() { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) { className = className.replace("/" , "." ); if (className.equals(HOOK_CLASS)) { try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod ctMethod = ctClass.getDeclaredMethod( "checkExpiry" , new CtClass[]{classPool.getCtClass("java.lang.String" )} ); ctMethod.insertBefore("System.out.println(\"License到期时间:\" + $1);" ); ctMethod.insertAfter("return false;" ); classfileBuffer = ctClass.toBytecode(); return classfileBuffer; } catch (Exception e) { e.printStackTrace(); } } return classfileBuffer; } }; } }
Pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > </properties > <dependencies > <dependency > <groupId > org.javassist</groupId > <artifactId > javassist</artifactId > <version > 3.25.0-GA</version > </dependency > <dependency > <groupId > com.sun</groupId > <artifactId > tools</artifactId > <version > 1.8</version > <scope > system</scope > <systemPath > ${java.home}/../lib/tools.jar</systemPath > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.8.1</version > <configuration > <source > 8</source > <target > 8</target > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-jar-plugin</artifactId > <version > 3.2.0</version > <configuration > <archive > <manifest > <addClasspath > true</addClasspath > <mainClass > com.fxc.AttachAgent</mainClass > </manifest > <manifestEntries > <Agent-Class > com.fxc.AttachAgent</Agent-Class > <Can-Redefine-Classes > true</Can-Redefine-Classes > <Can-Retransform-Classes > true</Can-Retransform-Classes > </manifestEntries > </archive > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-shade-plugin</artifactId > <version > 3.2.4</version > <executions > <execution > <phase > package</phase > <goals > <goal > shade</goal > </goals > <configuration > <filters > <filter > <artifact > *:*</artifact > <excludes > <exclude > MANIFEST.MF</exclude > <exclude > META-INF/DEPENDENCIES</exclude > <exclude > META-INF/LICENSE*</exclude > <exclude > META-INF/NOTICE*</exclude > </excludes > </filter > </filters > <artifactSet > <includes > <include > org.javassist:javassist:jar:*</include > </includes > </artifactSet > <relocations > <relocation > <pattern > javassist</pattern > <shadedPattern > com.fxc.deps.javassist</shadedPattern > </relocation > </relocations > </configuration > </execution > </executions > </plugin > </plugins > </build >
坑点
在打包jar的时候,已经要记得将ASM/javassist打进去,maven-shade-plugin插件。