序言
空山新雨后,天色晚来秋。
整理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插件。