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的入口点有两个

  1. preMain:在启动时进行加载 ( jdk 1.5 之后)
  2. agentMain:在启动后进行加载 (jdk 1.6 之后)

    环境搭建

    起两个项目,一个是agent项目(用来打包jar,一个是测试项目

    preMain

    ```java 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=======================”); } }

  1. `src/main/resources`下添加文件:`META-INF/MANIFEST.MF`,内容为
  2. ```java
  3. Manifest-Version: 1.0
  4. Premain-Class: com.yq1ng.demo.preMain

接着来到项目结构
image.png
默认即可
image.pngimage.pngimage.png
然后是测试项目

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参数为
image.png
-javaagent:F:\\study\\JavaProject\\agent\\out\\artifacts\\agent_jar\\agent.jar,然后运行
image.png
但是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导入库即可 image.png

run
image.png
介绍一下上面使用到的一些东西

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~");
    }
}

image.png

使用Agent实现内存马

从上面的几个demo可以看到可以使用agent修改方法体,所以我们在实现agent内存马的时候需要考虑两点

  1. 此方法一定会被执行
  2. 修改方法不会对业务造成影响

Filter内存马一文中可以看到,请求发送到servlet之前会经过Filter,而Filter.doFilter()是过滤器链子必须经过的地方,那Filter.doFilter()作为注入方法再好不过了。

搭建环境

用idea就可以创建springboot项目啦!

简单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
image.png
image.png

失败记录

这里本来想试试springboot的Filter内存马的,但是我这里环境注入完毕以后就不能正常访问了,报错如下
image.png
经过排查,发现是 agent 注入到 doFillter()里面的request.getParameter("cmd");不能正确获取值导致的,原因未知,我也暂时放弃了,搞了快一周,先放放吧,如果有知道原因的师傅请一定要告诉我,非常感谢!