Java Agent到内存马(一)
原文请关注公众号:
https://mp.weixin.qq.com/s/KA1Ip58fpaYEqUV-nRzWXw
关于Java Agent
介绍
在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法。
使用方式
Java agent的使用方式有两种:
-
实现
premain
方法,在JVM启动前加载。流程图(待加)
-
实现
agentmain
方法,在JVM启动后加载。流程图(待加)
premain
和agentmain
函数声明如下,拥有Instrumentation inst
参数的方法优先级更高:
也就是jvm会优先加载带Instrumentation的方法,且加载成功则忽略第二种
public static void agentmain(String agentArgs, Instrumentation inst) {
...
}
public static void agentmain(String agentArgs) {
...
}
public static void premain(String agentArgs, Instrumentation inst) {
...
}
public static void premain(String agentArgs) {
...
}
- agentArgs为
Java agent
参数 - init是
java.lang.instrument.Instrumentation
的实例,可以用来类定义的转换和操作等等。
premain
JVM启动时 会先执行 premain
方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,就可以结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。
-
创建一个目标程序并编译成jar
package com.helloworld.helloworld; public class HelloworldApplication { public static void main(String[] args) { System.out.println("Target main run!"); } }
-
创建premain方法的Agent
import java.lang.instrument.Instrumentation; public class PreDemo { public static void premain(String args, Instrumentation inst) throws Exception{ System.out.println("Premain agent run!"); } }
-
因为java默认为main入口,如果直接打包成jar,会缺少main方法报错
所以需要在
src/main/resources/
加一个META-INF/MANIFEST.MF
文件,指定入口最后一行需要空行,不能省略
Manifest-Version: 1.0 Premain-Class: com.n0r4h.javaagent.PremainDemo
-
只添加MANIFEST.MF文件是不行的,同样会报找不到main方法,pom.xml需要添加:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.0</version> <configuration> <archive> <!--自动添加META-INF/MANIFEST.MF --> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.n0r4h.javaagent.PremainDemo</Premain-Class> <Agent-Class>com.n0r4h.javaagent.AgentmainDemo</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> </plugins> </build>
注意此处version是3.1.0,springboot默认是3.8.x版本,不支持
标签会报: Element archive is not allowed here
-
最后打包成jar, 执行:java -javaagent:javaagent-0.0.1-SNAPSHOT.jar -jar helloworld-0.0.1-SNAPSHOT.jar
-
可以看到在执行第二个jar之前就是执行了 com.n0r4h.javaagent.PremainDemo#premain方法
此处流程图为:(参考引用文章):
我们能联想到这种方式有一些局限性,就是需要在启动之前指定-javaagent,在我们实际环境中,基本都是运行状态,所以无法预先加载premain方法。
agentmain方式
-
JDK1.6后增加了agentmain方式,测试agentmain前面步骤一致,后面META-INF/MANIFEST.MF中需要添加:
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.n0r4h.javaagent.PremainDemo Agent-Class: com.n0r4h.javaagent.AgentmainDemo
这种方式是在JVM运行之后再加载,所以官方提供了对应的
Attach API
来实现这个功能。而Attach API
中有两个重要的类,在com.sun.tools.attach
中,分别是VirtualMachine
和VirtualMachineDescriptor
,我们主要看VirtualMachine
。 -
VirtualMachine类
翻译过来是就是一个java虚拟机,就是程序需要监控的目标虚拟机,主要提供了以下几个方法:
Attach :从 JVM 上面解除一个代理等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上
loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
Detach:从 JVM 上面解除一个代理(agent)
public abstract class VirtualMachine { // 获得当前所有的JVM列表 public static List<VirtualMachineDescriptor> list() { ... } // 根据pid连接到JVM public static VirtualMachine attach(String id) { ... } // 断开连接 public abstract void detach() {} // 加载agent,agentmain方法靠的就是这个方法 public void loadAgent(String agent) { ... } }
-
下面是一个获取java程序进程id的方法:
package com.n0r4h.javaagent; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; public class test { public static void main(String[] args) { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor virtualMachineDescriptor : list) { System.out.println(virtualMachineDescriptor+"\n"+virtualMachineDescriptor.id()); } } }
有了进程id之后就可以使用attach api注入Agent了。
-
添加agentmain.java:
package com.n0r4h.javaagent; import java.lang.instrument.Instrumentation; public class AgentmainDemo { public static void agentmain(String args, Instrumentation inst) throws Exception{ System.out.println("hello I`m agentmain agent!!!"); } }
打包成jar文件
这里打包有一个坑会找不到程序包com.sun.tools.attach,可以直接导入绝对路径
<dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.8.0</version> <scope>system</scope> <systemPath>/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/tools.jar</systemPath> </dependency>
-
测试:
package com.n0r4h.javaagent; import com.sun.tools.attach.*; import java.io.File; import java.io.IOException; import java.util.List; public class test { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { System.out.println("main running"); File directory = new File(""); directory.getAbsolutePath(); //获取绝对路径。 System.out.println(directory.getAbsolutePath()); List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vir : list) { System.out.println(vir.displayName());//打印JVM加载类名 if (vir.displayName().endsWith("com.n0r4h.javaagent.test")) { VirtualMachine attach = VirtualMachine.attach(vir.id()); //attach注入一个jvm id注入进去 attach.loadAgent("javaagent-0.0.1-SNAPSHOT.jar");//加载agent attach.detach(); } } } }
成功在jvm启动后加载Agentmain。
执行流程引用先知师傅: