攻防一体。理解检测手段才能写出更隐蔽的内存马,理解内存马才能更好地检测
检测思路总览
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());
Class<?> filterClass = def.getFilter().getClass(); URL classFile = filterClass.getResource( filterClass.getSimpleName() + ".class"); if (classFile == null) { System.out.println(" [!] 可疑:无对应 .class 文件"); }
if (filterClass.isAnonymousClass() || filterClass.isSynthetic()) { System.out.println(" [!] 可疑:匿名/合成类"); }
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
| java -jar arthas-boot.jar <pid>
sc *Filter*
jad com.evil.EvilFilter
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
| -XX:+DisableAttachMechanism
--illegal-access=deny
|
定期巡检
定期检查 Filter/Servlet 数量变化
定期检查已加载类列表
定期检查进程的网络连接
对比类的字节码 hash
内存马的反检测技术(了解)
了解这些技术是为了更好地检测,不是为了绕过
Filter 名称伪装
1 2
| filterDef.setFilterName("characterEncodingFilter");
|
避免 Runtime.exec()
1 2 3 4
| ProcessBuilder pb = new ProcessBuilder("cmd", "/c", command);
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
| 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");
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
| jmap -dump:format=b,file=/tmp/heap.hprof $(pgrep -f tomcat)
jhat -port 7000 /tmp/heap.hprof
|
攻防对抗矩阵(完整版)
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 |
进程命令行审计 |
练习
- 使用检测 JSP 扫描靶场中注入的内存马
- 使用 Arthas 连接靶场 JVM,找出可疑的 Filter
- 编写一个简单的 RASP Agent,拦截
StandardContext.addFilterDef() 调用
- 思考:如果攻击者使用 Agent 型内存马修改了检测工具本身的代码,如何应对?