Agent 简单使用
javaagent 使用指南
官方:https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html
什么是 Agent
在 JDK1.5 以后引入了java/lang/instrument
包,此包用来协助监测、运行甚至替换其他 JVM 上的程序。使用它可以实现虚拟机级别的 AOP 功能,这种方法也称 Java Agent 技术。简单来说就是 Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法
Agent 内存马就是利用这种方法修改原来的字节码,将恶意方法添加进去实现内存马
Agent 的入口点有两个
- preMain:在启动时进行加载 ( jdk 1.5 之后)
- agentMain:在启动后进行加载 (jdk 1.6 之后)
环境搭建
起两个项目,一个是 agent 项目(用来打包 jar,一个是测试项目
preMain
package com.yq1ng.demo;
import java.lang.instrument.Instrumentation;
/**
* @author ying
* @Description
* @create 2021-12-11 10:25 PM
*/
public class preMain {
public static void premain(String args, Instrumentation inst) throws Exception{
System.out.println("=======================PreMain=======================");
}
}
在src/main/resources
下添加文件:META-INF/MANIFEST.MF
,内容为
Manifest-Version: 1.0
Premain-Class: com.yq1ng.demo.preMain
接着来到项目结构
默认即可
然后是测试项目
import com.sun.tools.attach.*;
/**
* @author ying
* @Description
* @create 2021-12-11 22:46
*/
public class helloword {
public static void main(String[] args) {
System.out.println("hello word~");
}
}
我用的是 idea2021.3,添加 vm 参数为-javaagent:F:\\study\\JavaProject\\agent\\out\\artifacts\\agent_jar\\agent.jar
,然后运行
但是 premain 实际上用不上,你又不能操控服务器,更别说在启动项目的时候加载我们的 jar 包了
AgentMain
这个时候来看 AgentMain
package com.yq1ng.demo;
import java.lang.instrument.Instrumentation;
/**
* @author ying
* @Description
* @create 2021-12-11 23:04
*/
public class agentMain {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("=======================AgentMain=======================");
}
}
META-INF/MANIFEST.MF
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.yq1ng.demo.preMain
Agent-Class: com.yq1ng.demo.agentMain
属性 | 作用 |
---|---|
Premain-Class | 指定代理类 |
Agent-Class | 指定代理类 |
Boot-Class-Path | 指定 bootstrap 类加载器的搜索路径,在平台指定的查找路径失败的时候生效, 可选 |
Can-Redefine-Classes | 是否需要重新定义所有类,默认为 false,可选 |
Can-Retransform-Classes | 是否需要 retransform,默认为 false,可选 |
打包,然后看测试
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
/**
* @author ying
* @Description
* @create 2021-12-11 22:46
*/
public class helloword {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
System.out.println("hello word~");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list){
if (vmd.displayName().equals("helloword")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("F:\\study\\JavaProject\\agent\\out\\artifacts\\agent_jar\\agent.jar");
virtualMachine.detach();
}
}
}
}
注意:如果导包提示 VirtualMachine 不存在将 jdk/lib/tools.jar 导入库即可
run
介绍一下上面使用到的一些东西
Agent 使用的一些类
VirtualMachine
代表一个 Java 虚拟机,即程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作等等
官方文档:https://www.apiref.com/java11-zh/jdk.attach/com/sun/tools/attach/VirtualMachine.html
id()
:返回此 Java 虚拟机的标识符。attach(String id)
:传入 jvm 的 pid(即id()
返回的值),然后连接到 jvm 上loadAgent(String agent)
:传入代理 jar 包路径,然后加载此代理对象loadAgent(String agent, String options)
:传入代理 jar 包路径与 instrument 实例,然后按照 instrument 规范启动代理detach()
:从虚拟机分离,即解除代理
VirtualMachineDescriptor
描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能
官方文档:https://www.apiref.com/java11-zh/jdk.attach/com/sun/tools/attach/VirtualMachineDescriptor.html
displayName()
:显示名称组件equals()
:测试此 VirtualMachineDescriptor 是否与另一个对象相等
Instrumentation
提供允许 Java 编程语言代理程序检测在 JVM 上运行的程序的服务。 可以监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义
官方文档:https://www.apiref.com/java11-zh/java.instrument/java/lang/instrument/package-summary.html
好文推荐:浅析 Java Instrument 插桩技术
addTransformer(ClassFileTransformer transformer)
:增加一个 Class 文件的转换器,该转换器用于改变 class 二进制流的数据,参数 canRetransform 设置是否允许重新转换removeTransformer(ClassFileTransformer transformer)
:删除一个类转换器retransformClasses(Class<?>... classes)
:在类加载之后,重新定义 class。事实上,该方法 update 了一个类isModifiableClass(Class<?> theClass)
:判断目标类是否能够修改getAllLoadedClasses()
:获取加载的所有类数组
ClassFileTransformer
此接口用于改变运行时的字节码,这个改变发生在 jvm 加载这个类之前,对所有的类加载器有效
官方文档:https://www.apiref.com/java11-zh/java.instrument/java/lang/instrument/ClassFileTransformer.html
接口中只有一个方法
byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
注意此方法是byte[]
类型,所以并不是真正的修改字节码(class),而是 jvm 读取字节码后的 byte。而 ClassFileTransformer 需要添加到 Instrumentation 实例中才能生效。来个 demo 看看
package com.yq1ng.demo;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
/**
* @author ying
* @Description
* @create 2021-12-11 23:04
*/
public class agentMain {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("=======================AgentMain=======================");
// 获取所以已经加载的类
Class[] classes = inst.getAllLoadedClasses();
for (Class AllClass : classes){
// 只注入特定的类
if (AllClass.getName().equals(TestClassFileTransformer.editClassName)){
inst.addTransformer(new TestClassFileTransformer(), true);
try {
// 这里必须用try,不能在方法后抛异常,否则agent会加载失败
inst.retransformClasses(AllClass);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
}
}
package com.yq1ng.demo;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
/**
* @author ying
* @Description
* @create 2021-12-13 5:39 PM
*/
public class TestClassFileTransformer implements ClassFileTransformer {
// 定义要修改的类的全限定名
public static final String editClassName = "com.yq1ng.sayHello";
// 定义修改的方法名
public static final String editMethod = "say";
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {
ClassPool classPool = new ClassPool().getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
CtClass ctClass = classPool.get(editClassName);
CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
ctMethod.setBody("{System.out.println(\"hack you...\");}");
byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}
Manifest-Version: 1.0
Premain-Class: com.yq1ng.demo.preMain
Agent-Class: com.yq1ng.demo.agentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
打包,然后写测试类
package com.yq1ng;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
import java.util.Scanner;
/**
* @author ying
* @Description
* @create 2021-12-14 5:03 PM
*/
public class helloWord {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
System.out.println("=======================HelloWord=======================");
// 第一次调用是为了加载sayHello,或者直接Class.forName("com.yq1ng.sayHello");
// 这样可以直观看出方法被修改了
sayHello say = new sayHello();
say.say();
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list){
if (vmd.displayName().equals("com.yq1ng.helloWord")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("F:\\study\\JavaProject\\agent\\out\\artifacts\\agent_jar\\agent.jar");
virtualMachine.detach();
}
}
sayHello say1 = new sayHello();
say1.say();
}
}
package com.yq1ng;
/**
* @author ying
* @Description
* @create 2021-12-14 5:04 PM
*/
public class sayHello {
public void say(){
System.out.println("hello yq1ng~");
}
}
使用 Agent 实现内存马
从上面的几个 demo 可以看到可以使用 agent 修改方法体,所以我们在实现 agent 内存马的时候需要考虑两点
- 此方法一定会被执行
- 修改方法不会对业务造成影响
在Filter 内存马一文中可以看到,请求发送到 servlet 之前会经过Filter
,而Filter.doFilter()
是过滤器链子必须经过的地方,那Filter.doFilter()
作为注入方法再好不过了。
搭建环境
简单 demo
agent 和前面的一样,只需更改TestClassFileTransformer
package com.yq1ng.demo;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;
/**
* @author ying
* @Description
* @create 2021-12-13 5:39 PM
*/
public class TestClassFileTransformer implements ClassFileTransformer {
// 定义要修改的类的全限定名
public static String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
// 定义修改的方法名
public static final String editMethod = "doFilter";
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {
editClassName = editClassName.replace("/", ".");
ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
CtClass ctClass = classPool.get(editClassName);
CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
ctMethod.insertBefore("System.out.println(\"I'm hacking in...\");");
byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}
测试环境非常简单,为了简洁只写了一个路由
package com.yq1ng.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author ying
* @Description
* @create 2021-12-15 1:53 PM
*/
@Controller
public class TestController {
@ResponseBody
@RequestMapping(value="/hello", produces="text/plant;charset=utf-8")
public String hello(HttpServletRequest request, HttpServletResponse response){
System.out.println("hello");
return "hello word~";
}
}
接着是将 agent 注入进去
package com.yq1ng.demo;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.net.URL;
import java.util.List;
/**
* @author ying
* @Description
* @create 2021-12-15 5:07 PM
*/
public class test {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
// 第一次访问是为了让服务器加载org.apache.catalina.core.ApplicationFilterChain
URL url = new URL("http://127.0.0.1:8888/hello");
url.openStream();
String agentPath = "F:\\study\\JavaProject\\agent\\out\\artifacts\\agent_jar\\agent.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list){
if (vmd.displayName().equals("com.yq1ng.demo.DemoApplication")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent(agentPath);
virtualMachine.detach();
System.out.println("success~");
}
}
// 这里进行测试
url.openStream();
}
}
启动 spring-boot,接着运行 test
失败记录
这里本来想试试 springboot 的 Filter 内存马的,但是我这里环境注入完毕以后就不能正常访问了,报错如下
经过排查,发现是 agent 注入到 doFillter()
里面的request.getParameter("cmd");
不能正确获取值导致的,原因未知,我也暂时放弃了,搞了快一周,先放放吧,如果有知道原因的师傅请一定要告诉我,非常感谢!