内存马 - 06 检测与防御

攻防一体。理解检测手段才能写出更隐蔽的内存马,理解内存马才能更好地检测

检测思路总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
检测方法
├── 组件枚举法(最常用)
│ ├── 枚举所有 Filter,检查是否有异常
│ ├── 枚举所有 Servlet,检查是否有异常
│ └── 枚举所有 Listener,检查是否有异常

├── 类分析法
│ ├── 检查类的 ClassLoader(是否从磁盘加载)
│ ├── 检查类是否有对应的 .class 文件
│ └── 检查类的字节码是否被修改(Agent 型)

├── 行为分析法
│ ├── 检查是否调用了 Runtime.exec()
│ ├── 检查是否使用了反射访问敏感字段
│ └── 检查是否存在异常的命令执行模式

└── 流量分析法
├── 检查是否有异常的请求参数(cmd, shell 等)
├── 检查响应中是否包含命令执行结果特征
└── 检查请求频率和模式

组件枚举检测

枚举所有 Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
StandardContext context = ...;
FilterDef[] filterDefs = context.findFilterDefs();
for (FilterDef def : filterDefs) {
System.out.println("Filter: " + def.getFilterName());
System.out.println(" Class: " + def.getFilterClass());

// 检查点1:Filter 类是否有对应的 .class 文件
Class<?> filterClass = def.getFilter().getClass();
URL classFile = filterClass.getResource(
filterClass.getSimpleName() + ".class");
if (classFile == null) {
System.out.println(" [!] 可疑:无对应 .class 文件");
}

// 检查点2:是否是匿名内部类
if (filterClass.isAnonymousClass() || filterClass.isSynthetic()) {
System.out.println(" [!] 可疑:匿名/合成类");
}

// 检查点3:ClassLoader 是否异常
ClassLoader cl = filterClass.getClassLoader();
System.out.println(" ClassLoader: " + cl);
}

枚举所有 Servlet

1
2
3
4
5
6
7
8
Container[] children = context.findChildren();
for (Container child : children) {
if (child instanceof Wrapper) {
Wrapper wrapper = (Wrapper) child;
System.out.println("Servlet: " + wrapper.getName());
System.out.println(" Class: " + wrapper.getServletClass());
}
}

枚举所有 Listener

1
2
3
4
Object[] listeners = context.getApplicationEventListeners();
for (Object listener : listeners) {
System.out.println("Listener: " + listener.getClass().getName());
}

类分析检测

检查类是否有磁盘文件

1
2
3
4
5
6
7
8
9
10
11
12
public static boolean isMemoryClass(Class<?> clazz) {
String classFile = clazz.getName().replace('.', '/') + ".class";
URL url = clazz.getClassLoader().getResource(classFile);
if (url == null) {
return true; // 可能是内存马
}
String protocol = url.getProtocol();
if (!"file".equals(protocol) && !"jar".equals(protocol)) {
return true; // 可疑
}
return false;
}

检查字节码是否被修改(Agent 型检测)

1
2
3
4
5
6
7
public static boolean isClassModified(Class<?> clazz, Instrumentation inst) {
String classFile = clazz.getName().replace('.', '/') + ".class";
InputStream is = clazz.getClassLoader().getResourceAsStream(classFile);
byte[] originalBytes = readAllBytes(is);
byte[] currentBytes = getCurrentClassBytes(clazz, inst);
return !Arrays.equals(originalBytes, currentBytes);
}

常见检测工具

Alibaba Arthas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# attach 到目标 JVM
java -jar arthas-boot.jar <pid>

# 查看所有加载的类
sc *Filter*

# 反编译可疑类
jad com.evil.EvilFilter

# 查看类的 ClassLoader
classloader -t

# 监控方法调用
watch javax.servlet.Filter doFilter '{params, returnObj}' -x 2

自研检测 JSP

部署到目标 Web 应用中,枚举所有 Filter/Servlet/Listener 并检查可疑特征

开源检测工具

工具 特点
java-memshell-scanner 专门的内存马扫描 JSP,部署简单
Alibaba Arthas 通用 Java 诊断,可辅助检测
copagent 基于 Agent 的内存马检测
FindShell 查找 Webshell 和内存马

防御手段

RASP(Runtime Application Self-Protection)

1
2
3
4
5
通过 Java Agent 在运行时 hook 关键方法:
- Runtime.exec() / ProcessBuilder.start() → 命令执行监控
- ClassLoader.defineClass() → 动态类加载监控
- StandardContext.addFilter*() → Filter 注册监控
- 反射调用监控

JVM 参数加固

1
2
3
4
5
# 禁止动态 Attach(防 Agent 型内存马)
-XX:+DisableAttachMechanism

# 限制反射访问(JDK 9+)
--illegal-access=deny

定期巡检

定期检查 Filter/Servlet 数量变化

定期检查已加载类列表

定期检查进程的网络连接

对比类的字节码 hash

内存马的反检测技术(了解)

了解这些技术是为了更好地检测,不是为了绕过

Filter 名称伪装

1
2
filterDef.setFilterName("characterEncodingFilter");
// 使用正常的 Filter 名称

避免 Runtime.exec()

1
2
3
4
// 使用 ProcessBuilder 替代
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", command);
// 使用 ScriptEngine 执行脚本
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");

条件触发

1
2
3
4
5
6
String token = req.getHeader("X-Auth-Token");
if (token != null && token.equals(MD5("secret"))) {
// 执行恶意逻辑
} else {
chain.doFilter(request, response); // 完全正常的行为
}

RASP 实现要点(深入)

RASP 的核心是 Java Agent + 字节码插桩,这里给出关键 Hook 点的实现思路

必须 Hook 的方法列表

优先级 方法 检测目的
P0 StandardContext addFilterDef Filter 注入
P0 StandardContext addApplicationEventListener Listener 注入
P0 StandardContext addChild Servlet 注入
P0 Pipeline addValve Valve 注入
P1 Runtime exec 命令执行
P1 ProcessBuilder start 命令执行
P1 ClassLoader defineClass 动态类加载
P2 RequestMappingHandlerMapping registerMapping Spring Controller 注入
P2 ServerContainer addEndpoint WebSocket 注入
P3 Field setAccessible 敏感反射操作

简单 RASP Agent 示例

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
// 拦截 StandardContext.addFilterDef 的 Agent
public class RaspAgent {
public static void premain(String args, Instrumentation inst) {
inst.addTransformer((loader, className, classBeingRedefined,
protectionDomain, classfileBuffer) -> {
if (!"org/apache/catalina/core/StandardContext".equals(className)) {
return null;
}
try {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new LoaderClassPath(loader));
CtClass ctClass = pool.get(
"org.apache.catalina.core.StandardContext");

// Hook addFilterDef
CtMethod method = ctClass.getDeclaredMethod("addFilterDef");
method.insertBefore(
"System.err.println(\"[RASP] addFilterDef called: \""
+ " + $1.getFilterName() + \" class=\" + $1.getFilterClass());"
+ "new Exception(\"Stack trace\").printStackTrace();"
);

byte[] result = ctClass.toBytecode();
ctClass.detach();
return result;
} catch (Exception e) {
return null;
}
}, true);
}
}

内存马取证与应急响应

应急响应 SOP

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
1. 确认内存马存在
└── 使用 java-memshell-scanner 或自研检测 JSP

2. 保留证据
└── dump JVM 堆内存:jmap -dump:format=b,file=heap.hprof <pid>
└── 导出可疑类字节码:arthas 的 dump 命令
└── 保留访问日志和应用日志

3. 识别注入入口
└── 分析日志中的可疑请求(反序列化/JNDI/上传)
└── 检查反序列化漏洞是否存在

4. 清除内存马
└── 方式1:重启应用(最简单但可能丢失线索)
└── 方式2:通过反射移除恶意组件(不中断服务)
└── 方式3:用 Agent retransform 恢复被修改的类

5. 修复漏洞
└── 修补反序列化/JNDI/上传等入口漏洞
└── 升级依赖版本

6. 加固
└── 部署 RASP
└── 添加 JVM 安全参数
└── 建立定期巡检机制

堆内存分析找内存马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. dump 堆内存
jmap -dump:format=b,file=/tmp/heap.hprof $(pgrep -f tomcat)

# 2. 用 Eclipse MAT 或 jhat 分析
jhat -port 7000 /tmp/heap.hprof
# 浏览器访问 http://localhost:7000

# 3. OQL 查询所有 Filter 实例
# 在 MAT 中执行:
# SELECT * FROM javax.servlet.Filter
# 或
# SELECT * FROM INSTANCEOF javax.servlet.Filter

# 4. 重点检查:
# - Filter 实例的 ClassLoader 是否是 WebappClassLoader
# - Filter 类的 CodeSource 是否指向正常的 JAR/WAR
# - 是否有匿名类实例(类名包含 $1, $2 等)

攻防对抗矩阵(完整版)

Servlet 组件层对抗

攻击手法 检测手段 绕过方式 终极检测
新增 Filter(匿名类) 枚举 FilterDef,检查匿名类 用具名内部类或 defineClass 命名 检查类是否有 .class 文件
新增 Filter(伪装名称) 检查 Filter 名称白名单 使用已有 Filter 的名称 对比 Filter 数量基线
新增 Servlet 枚举 Wrapper,检查 URL 映射 映射到看似正常的 URL 对比 Servlet 数量基线
新增 Listener 枚举 EventListener 伪装为正常 Listener 检查类的 CodeSource
新增 Valve 枚举 Pipeline Valve 伪装为日志 Valve Valve 数量基线 + 类文件检查

执行层对抗

攻击手法 检测手段 绕过方式 终极检测
Runtime.exec() RASP hook exec ProcessBuilder hook ProcessBuilder
ProcessBuilder RASP hook start JNI 调用 进程监控(ps/fork)
JNI 本地调用 检查 JNI 方法注册 内存中加载 .so 系统调用审计(auditd)
ScriptEngine hook ScriptEngine 自实现表达式解析 行为分析
反射调用 hook setAccessible Unsafe hook Unsafe

通信层对抗

攻击手法 检测手段 绕过方式 终极检测
明文 cmd 参数 WAF 规则匹配 AES 加密 流量基线异常
AES 加密(固定密钥) 检测 Base64 + 固定长度 动态密钥协商 统计分析请求熵
Header 触发 检查异常 Header 使用常见 Header 名 对比业务正常 Header
WebSocket 通信 检查 WS 端点列表 劫持已有 WS 端点 WS 流量内容分析

Agent 层对抗

攻击手法 检测手段 绕过方式 终极检测
retransformClasses 字节码 hash 基线对比 修改对比工具本身 独立进程外部对比
Attach API -XX:+DisableAttachMechanism 利用已有 Agent 入口 JVM 启动参数审计
修改 MANIFEST 文件完整性监控 内存中构造 JAR 进程命令行审计

练习

  1. 使用检测 JSP 扫描靶场中注入的内存马
  2. 使用 Arthas 连接靶场 JVM,找出可疑的 Filter
  3. 编写一个简单的 RASP Agent,拦截 StandardContext.addFilterDef() 调用
  4. 思考:如果攻击者使用 Agent 型内存马修改了检测工具本身的代码,如何应对?

上一章 目录 下一章
05-反序列化与内存马 内存马 07-通信加密与隧道