本文分享自微信公众号《护网专题第一篇-Java内存马(上)》,作者:零鉴科技
入门
介绍
注意:这里只是简短的介绍一下,想要详细了解,请看参考资料。
在JDK1.5以后,javaagent是一种能够在不影响正常编译的情况下,修改字节码。
java作为一种强类型的语言,不通过编译就不能够进行jar包的生成。而有了javaagent技术,就可以在字节码这个层面对类和方法进行修改。同时,也可以把javaagent理解成一种代码注入的方式。但是这种注入比起spring的aop更加的优美。
Java agent的使用方式有两种:
- 实现
premain
方法,在JVM启动前加载。 - 实现
agentmain
方法,在JVM启动后加载。
premain
和agentmain
函数声明如下,拥有Instrumentation inst
参数的方法优先级更高:
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) {
...
}
第一个参数String agentArgs
就是Java agent的参数。\
第二个参数Instrumentaion inst
相当重要,会在之后的进阶内容中提到。
premain
要做一个简单的premain
需要以下几个步骤:
- 创建新项目,项目结构为:
agent
├── agent.iml
├── pom.xml
└── src
├── main
│ ├── java
│ └── resources
└── test
└── java
- 创建一个类(这里为
com.shiroha.demo.PreDemo
),并且实现premain方法。
package com.shiroha.demo;
import java.lang.instrument.Instrumentation;
public class PreDemo {
public static void premain(String args, Instrumentation inst) throws Exception{
for (int i = 0; i < 10; i++) {
System.out.println("hello I`m premain agent!!!");
}
}
}
- 在
src/main/resources/
目录下创建META-INF/MANIFEST.MF
,需要指定Premain-Class
。
Manifest-Version: 1.0
Premain-Class: com.shiroha.demo.PreDemo
要注意的是,最后必须多一个换行。
- 打包成jar
选择Project Structure
-> Artifacts
-> JAR
-> From modules with dependencies
。
默认的配置就行。
选择Build
-> Build Artifacts
-> Build
。
之后产生out/artifacts/agent_jar/agent.jar
:
└── out
└── artifacts
└── agent_jar
└── agent.jar
- 使用
-javaagent:agent.jar
参数执行hello.jar
,结果如下。
可以发现在hello.jar
输出hello world
之前就执行了com.shiroha.demo.PreDemo$premain
方法。
当使用这种方法的时候,整个流程大致如下图所示:
然而这种方法存在一定的局限性——只能在启动时使用 -javaagent
参数指定。在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。
agentmain
写一个agentmain
和premain
差不多,只需要在META-INF/MANIFEST.MF
中加入Agent-Class:
即可。
Manifest-Version: 1.0
Premain-Class: com.shiroha.demo.PreDemo
Agent-Class: com.shiroha.demo.AgentDemo
不同的是,这种方法不是通过JVM启动前的参数来指定的,官方为了实现启动后加载,提供了Attach API
。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach
包里面。着重关注的是VitualMachine
这个类。\
VirtualMachine
字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息、 loadAgent
,Attach
和 Detach
等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。代理类注入操作只是它众多功能中的一个,通过loadAgent
方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation
实例。
具体的用法看一下官方给的例子大概就理解了:
// com.sun.tools.attach.VirtualMachine
// 下面的示例演示如何使用VirtualMachine:
// attach to target VM
VirtualMachine vm = VirtualMachine.attach("2177");
// start management agent
Properties props = new Properties();
props.put("com.sun.management.jmxremote.port", "5000");
vm.startManagementAgent(props);
// detach
vm.detach();
// 在此示例中,我们附加到由进程标识符2177标识的Java虚拟机。然后,使用提供的参数在目标进程中启动JMX管理代理。
// 最后,客户端从目标VM分离。
下面列几个这个类提供的方法:
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) { ... }
}
根据提供的api,可以写出一个attacher
,代码如下:
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class AgentMain {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String id = args[0];
String jarName = args[1];
System.out.println("id ==> " + id);
System.out.println("jarName ==> " + jarName);
VirtualMachine virtualMachine = VirtualMachine.attach(id);
virtualMachine.loadAgent(jarName);
virtualMachine.detach();
System.out.println("ends");
}
}
过程非常简单:通过pid attach到目标JVM -> 加载agent -> 解除连接。
现在来测试一下agentmain:
package com.shiroha.demo;
import java.lang.instrument.Instrumentation;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) {
for (int i = 0; i < 10; i++) {
System.out.println("hello I`m agentMain!!!");
}
}
}
成功attach并加载了agent。
整个过程的流程图大致如下图所示:
进阶
Instrumentation
Instrumentation
是JVMTIAgent
(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果。
下面列出这个类的一些方法,更加详细的介绍和方法,可以参照官方文档 [1]。也可以看下面的参考资料[2]。
public interface Instrumentation {
// 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
// 在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。
// addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。
// 类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);
// 删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
// 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// 判断目标类是否能够修改。
boolean isModifiableClass(Class<?> theClass);
// 获取目标已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
......
}
由于知识点过多和篇幅限制,只先介绍getAllLoadedClasses
和isModifiableClasses
。
看名字都知道:
getAllLoadedClasses
:获取所有已经加载的类。isModifiableClasses
:判断某个类是否能被修改。
修改之前写的agentmain:
package com.shiroha.demo;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) throws IOException {
Class[] classes = inst.getAllLoadedClasses();
FileOutputStream fileOutputStream = new FileOutputStream(new File("/tmp/classesInfo"));
for (Class aClass : classes) {
String result = "class ==> " + aClass.getName() + "\n\t" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\n";
fileOutputStream.write(result.getBytes());
}
fileOutputStream.close();
}
}
重新attach到某个JVM,在/tmp/classesInfo
文件中有如下信息:
class ==> java.lang.invoke.LambdaForm$MH/0x0000000800f06c40
Modifiable ==> false
class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f06840
Modifiable ==> false
class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07440
Modifiable ==> false
class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07040
Modifiable ==> false
class ==> jdk.internal.reflect.GeneratedConstructorAccessor29
Modifiable ==> true
........
得到了目标JVM上所有已经加载的类,并且知道了这些类能否被修改。
至于如何修改JVM上的字节码,请听下回分解。
参考资料
- [2] javaagent使用指南