内存马检测与清除实操

内存马检测与清除实操

前面各篇回答了”内存马怎么打“,这一篇回答”内存马怎么查、怎么清“。

内存马最难检测的根本原因,在 《内存马原理总纲》 里已经讲过:它用和正常组件完全相同的注册接口,把恶意代码塞进容器运行时注册表,磁盘上什么都没有。所以检测的思路必须反过来——不再看文件,而是看运行时的注册表和代码行为。

本文的清除方法按”能否热移除”分为两类:Filter / Servlet / Listener / Valve / Spring 型可热移除(无需重启);Agent 型无法热移除,只能重启 JVM。这是应急响应的关键判断。


一、检测的整体思路

检测视角 原理 覆盖类型 缺点
文件侧 扫磁盘 webshell 传统 JSP webshell 对内存马完全失效
注册表基线对比 枚举容器当前注册表,与启动基线 diff Filter / Servlet / Listener / Valve / Spring / WebSocket 需要提前有基线;Agent 型看不到
行为检测(RASP) Hook addFilterDef / addValve / defineClass 等注册调用,判断调用栈是否来自启动阶段 全部类型(含 Agent 的 addTransformer / retransformClasses) 需要部署 RASP,有误报

核心结论:单靠一个视角不够,实战是”注册表枚举 + 内存取证 + 行为监控”三管齐下。


二、按类型的检测特征与命令

以下命令以 Arthas 为主(在线、无需重启),vmtool 用于拿容器实例并访问其实例字段。各类型原理详见对应分篇。

2.1 总表

类型 恶意代码藏在哪 一句话判断特征
Filter filterConfigs / filterDefs / filterMaps 出现非业务类名的 Filter
Servlet children + servletMappings 出现未声明的 Wrapper / 路径
Listener applicationEventListenersList 出现非框架的 *Listener
Valve 容器的 Pipeline 出现非 Standard*Valve 的 Valve
Spring adaptedInterceptors / mappingRegistry 多出非业务拦截器 / 路由
WebSocket WsServerContainer 的 endpointPathMap 出现非业务 Endpoint 路径
Agent 类字节码本身(无注册表) 字节码与磁盘 .class 不一致

2.2 Filter

# 列出 StandardContext 的 filterConfigs(实例字段,先拿实例)
vmtool --action getInstances --className org.apache.catalina.core.StandardContext \
  --express 'instances[0].filterConfigs' --limit 10

# 搜索可疑 Filter 类
sc *.Filter*

判断:filterConfigs 里的 key(filterName)和对应的 filter 实例类名,是否与 web.xml / 注解 / 启动基线一致。类名是否来自非业务 jar、或干脆是个匿名/默认包类,是重要线索。

2.3 Servlet

# 看 children 里有没有未声明的 Wrapper
vmtool --action getInstances --className org.apache.catalina.core.StandardContext \
  --express 'instances[0].children' --limit 10

# 看 servletMappings 有没有可疑路径
vmtool --action getInstances --className org.apache.catalina.core.StandardContext \
  --express 'instances[0].servletMappings' --limit 10

判断:业务里没有的路径(如 /shell、/cmd/*)出现在 servletMappings,或 children 里出现陌生 Wrapper。

2.4 Listener

# 看监听器列表
vmtool --action getInstances --className org.apache.catalina.core.StandardContext \
  --express 'instances[0].applicationEventListenersList' --limit 10

# 搜索可疑 Listener
sc *.*Listener

判断:applicationEventListenersList 里出现非 Spring / 非业务框架的 *Listener(正常只有 Spring 的 ContextLoaderListener 之类的几个)。

2.5 Valve

# 列出管道上的所有 Valve
vmtool --action getInstances --className org.apache.catalina.core.StandardContext \
  --express 'instances[0].getPipeline().getValves()' --limit 10

判断:正常只有 StandardContextValve / StandardHostValve 等内置阀;出现其他 Valve 子类即为可疑。注意也要看 Host / Engine 的 Pipeline(高级 Valve 马会挂在更上层)。

2.6 Spring

// 检测代码:列出所有拦截器与路由
WebApplicationContext ctx =
    WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
RequestMappingHandlerMapping mapping = ctx.getBean(RequestMappingHandlerMapping.class);

for (Object interceptor : mapping.getAdaptedInterceptors()) {
    System.out.println("interceptor: " + interceptor.getClass().getName());
}
for (Object handlerMethod : mapping.getHandlerMethods().keySet()) {
    System.out.println("mapping: " + handlerMethod);
}

判断:出现非业务拦截器类;路由表里出现业务没有的路径(如 /evil)。

2.7 WebSocket

# 看注册的 Endpoint 路径
vmtool --action getInstances --className org.apache.tomcat.websocket.server.WsServerContainer \
  --express 'instances[0].getEndpointPathMap()' --limit 5

判断:出现非业务路径(如 /ws)。

2.8 Agent(最难,注册表看不到)

Agent 型内存马不产生任何容器组件,只能两条路:

# 1. 反编译运行时字节码,与磁盘/源码对比
jad org.apache.catalina.core.ApplicationFilterChain
# 出现 Runtime.getRuntime().exec / 插入的变量名等即为 Agent 内存马

# 2. 枚举已注册的 ClassFileTransformer
# 用 attach 一个自写检测 agent,打印所有 transformer 的类名

三、检测工具链与使用时机

工具 用途 时机
Arthas(vmtool / sc / jad) 在线枚举注册表、反编译字节码,不重启 第一现场,优先
jmap dump + MAT / VisualVM 离线内存取证,能挖到被引用但未注册的对象 现场不便操作 / 复盘
JMX(jconsole / jcmd) 看 MBean(Filter/Valve 会注册 MBean) 已开启 JMX 时
RASP 实时拦截注册调用,判断调用栈 常态化防御,而非事后查
# 快速取证:dump 内存(先确认 PID)
jps -l
jmap -dump:live,format=b,file=heap.hprof <pid>
# MAT 里搜 FilterDef / ApplicationFilterConfig / ValveBase / AbstractTranslet 子类

建议流程:jps 确认进程 → Arthas vmtool 逐个枚举注册表 → 可疑项 sc 看类来源 → 必要时 jad 反编译 / jmap dump 留证。


四、清除实操(按类型)

总原则:内存马 = 注册进容器的一个条目,清除 = 把它从注册表撤销。 类型不同,撤销的 API 不同;多数可热移除,Agent 除外。

4.1 可热移除的类型

Filter——从 filterConfigs / filterDefs / filterMaps 三处撤掉:

StandardContext context = ...;   // 反射链拿 context,见 Filter 篇

// 1. 先移除实例配置
context.removeFilterDef(findFilterDef("shell"));

// 2. 移除映射(filterMaps 在较新版本是包私有 ContextFilterMaps,需反射 remove)
Field mapsField = StandardContext.class.getDeclaredField("filterMaps");
mapsField.setAccessible(true);
Object filterMaps = mapsField.get(context);
Method remove = filterMaps.getClass().getMethod("remove", String.class);
remove.setAccessible(true);
remove.invoke(filterMaps, "shell");

// 3. 移除 filterConfigs 中的实例
Field cfField = StandardContext.class.getDeclaredField("filterConfigs");
cfField.setAccessible(true);
((Map<?, ?>) cfField.get(context)).remove("shell");

Servlet——移除 Wrapper 与映射:

Wrapper wrapper = (Wrapper) context.findChild("shell");
context.removeChild(wrapper);      // 从 children 移除
context.removeServletMapping("/shell");  // 从 servletMappings 移除

Listener——一行:

// 遍历找到恶意 listener 实例后移除
context.removeApplicationEventListener(listener);

Valve——一行:

for (Valve valve : context.getPipeline().getValves()) {
    if (valve.getClass().getName().contains("Evil")) {
        context.getPipeline().removeValve(valve);
    }
}
// 若挂在 Host / Engine,用同样的 getPipeline() 往上取再 removeValve

Spring——移除拦截器 / 反注册路由:

// Interceptor
List<HandlerInterceptor> interceptors = /* 反射拿 adaptedInterceptors */;
interceptors.remove(evilInterceptor);

// Controller 路由
mapping.unregisterMapping(info);   // 反注册 /evil

4.2 无法热移除:Agent 型

Agent 型改的是已加载类的字节码,ClassFileTransformer 一旦生效,JVM 里没有”还原”接口。唯一的彻底清除是重启 JVM。

Agent 内存马
  → 枚举 transformer / 字节码 diff 确认(2.8)
  → 无法热移除
  → 备份取证后重启 JVM
  → 启动参数加 -XX:+DisableAttachMechanism,防止再次 attach

4.3 清除有效性对比

类型 能否热移除 清除后是否立即生效
Filter ✅ 下一次请求构建 Filter 链时(立即)
Servlet ✅ 下一次路由时(立即)
Listener ✅ 下一个请求事件(立即)
Valve ✅ 下一个请求进容器(立即)
Spring ✅ 下一次分发(立即)
WebSocket ✅ 下次连接(立即)
Agent ❌ 必须重启 JVM

五、应急处置流程

确认感染(发现可疑注册表条目 / 字节码 diff)
   ↓
① 隔离:断外网 / 摘负载,避免攻击者继续利用
   ↓
② 取证:Arthas 导出注册表快照 + jmap dump 内存 + 收集日志(留痕,勿先删)
   ↓
③ 清除:按第四节热移除;Agent 型重启 JVM
   ↓
④ 复检:清除后再跑一遍第二节的枚举,确认注册表干净
   ↓
⑤ 加固:修复反序列化 / JNDI 等 RCE 入口(事前补洞)
         + 部署 RASP 监控注册调用(事中)
         + 建立启动基线,周期性对比(事后)

关键提醒:先取证、后清除。热移除会把这个对象从内存里抹掉,若没先 dump 留证,攻击者代码就丢了,不利于溯源和复盘。


总结

类型 检测入口 清除手段
Filter filterConfigs 枚举 removeFilterDef + 反射移除 map
Servlet children / servletMappings removeChild + removeServletMapping
Listener applicationEventListenersList removeApplicationEventListener
Valve getPipeline().getValves() removeValve
Spring adaptedInterceptors / handlerMethods list.remove / unregisterMapping
WebSocket endpointPathMap 反注册 Endpoint
Agent 字节码 diff / transformer 枚举 重启 JVM(唯一)

三句话收尾:

  1. 内存马检测看注册表,不看文件——枚举容器当前注册表,与启动基线对比;
  2. 大部分可热移除,Agent 只能重启——判断类型决定处置方式;
  3. 先取证再清除,防御靠事前——RCE 入口不补,清完还会再进来。
延伸阅读:《内存马原理总纲》 《反序列化与 JNDI 无文件注入内存马》 各类型分篇的”检测方式 / 防御建议”章节:Filter Listener Valve Agent
改变就是好事。