1. 背景
1.1. Java内存马是什么?
内存马是一种仅在内存中运行、没有文件落地的恶意程序,因此具有较强的隐蔽性,能够避开常规的基于文件系统的检测。Java内存马是针对Java语言的内存马,它利用Java语言的动态特性,如类加载机制、动态代理和反射技术等,在Java应用的内存中注入恶意代码,从而实现远程控制。
1.2. Java类加载机制
Java虚拟机(JVM)的类加载机制在程序运行时根据需要动态地加载类,而不是在启动时一次性加载所有类。而类可以在运行时从不同的来源,如本地文件系统、远程网络或者其他自定义位置加载。攻击者可以利用这一特性,在运行时加载恶意类,从而向内存中引入不受信任的代码。
1.3. 内存马检测的必要性
近年来,内存马因其持久性、隐蔽性、兼容性强等特性,已被攻击者广泛应用于各种复杂的攻击场景。内存马通过驻留在内存中,规避文件系统的监控,使得传统的杀毒软件和入侵检测系统难以发现其踪迹。因此,需要研究专门的内存马检测技术,以便及时发现并消除内存马带来的安全威胁,从而保障系统的安全性。
2. 检测
Java程序的内存检测一般以Class字节码为维度进行,简单分为两个步骤:
- 读取内存中的Class信息
- 对Class信息进行分析检测
检测大纲:
2.1. Class信息获取
2.1.1. 获取方式
2.1.1.1. Serviceability Agent (SA)
SA是JDK提供的一个强大的调试工具集,适用于语言层和虚拟机层,支持调试运行着的Java进程、core文件和虚拟机crash之后的dump文件。SA运行在单独的进程中,和目标Java进程是隔离的,在使用SA工具时,不会在目标Java进程中执行任何代码,但是,在SA读取数据的过程中,目标Java进程会被挂起。
通过sa-jdi.jar中sun.jvm.hotspot.tools.jcore.ClassDump类的方法接收进程id,能够把对应进程已加载的类全部读取出来。
代码示例:
2.1.1.2. Java Agent
Java Agent 是一种基于 Java Instrumentation API 的机制,可用于在 Java 应用程序运行时动态地修改其字节码。Java Agent 提供了两个主要方法:
- premain:在 Java 进程启动前加载。这个方法可以在实际类加载之前插入自定义的字节码操作逻辑。
- agentmain:在 Java 进程启动后加载。这个方法可以在进程运行时对已加载的类进行操作,如读取、修改类字节码等操作。
通过VirtualMachine.attach(pid)方法,可以将当前进程附加到目标 JVM 实例,实现动态代码注入。
PS:冰蝎工具的防检测功能会删除/tmp/.java_pid文件,导致无法使用attach进行注入,可用SA方式替代。
正常进行attach以后,可使用agentmain方法注入到另一个进程中来实现内存信息获取。代码示例:
2.1.2. 获取内容
获取Java类的详细信息,主要关注以下几个字段:
- 类名(className):获取内存中加载的类的完整名称,有助于识别和追踪潜在的恶意类。
- 类加载器(classLoader):获取负责加载该类的类加载器,以了解其加载来源,并识别异常或不寻常的加载行为。
- 包名(packageName):获取类所属的包,以帮助判断类的组织结构和潜在的风险区域。
- 父类(parentClass):获取该类的父类,分析其继承关系,以发现可能隐藏的恶意代码。
- 类路径(classPath):获取类在文件系统中的位置,用于验证类的合法来源和检查潜在的篡改迹象。
- 接口(interfaces):获取该类实现的所有接口,了解其功能和行为契约,检测异常接口实现或恶意接口。
- 注解(annotations):获取类上的注解,识别可能影响类行为的元数据。
- 类字节码(classBytes):获取类的字节码内容,深入分析其实际实现,识别可能的恶意代码或不符合预期的行为。
2.2. Class信息检测
对获取到的Class信息进行检测,可以分为两个方面:
- 首先对Class进行筛选,识别风险类。
- 根据需求对全部类或者风险类的Class字节码内容进行检测。
2.2.1. Class风险类检测
2.2.1.1. Web风险类检测
首先,根据不同内存马的分类来分析潜在的Web风险类
- 组件型内存马:仅需要针对内存中对应的Servlet、filter、Listener等组件进行分析即可。
- JSP内存马:仅针对内存中实现HttpServlet的类进行分析即可。
- agent型内存马:由于agent机制几乎可以改写内存中的任何类,可以hook的位置非常多,具体特征需跟着技术发展不断迭代更新。hook点一般都在Web调用的关键链路上。
虽然有各种各样的内存马,但他们通常都作用在Web调用的关键链路上,通过Web请求触发内存马,通过请求参数执行逻辑。
1) Web调用链分析:
哪些调用链是Web调用的关键链路?
以springboot 的Web项目为例,处理Web请求的调用堆栈大概如下:
1、tomcat建立HTTP连接并添加读写事件的监听:
2、tomcat 收到请求数据包并放到线程池中处理数据包:
3、tomcat实际处理请求数据包:
实际应用场景中,hook点大概率会发生在tomcat实际处理请求数据包转http请求request之后。
从堆栈信息可以看出来,调用链路会经过tomcat的Valve组件,servlet的FilterChain及Filter组件,Servlet组件,以及springmvc的相关处理,进而执行到Controller的helloworld方法。
那是不是只要关注上述调用链路就可以了?
当然不是,每个方法里都会有其他代码逻辑,实际Web调用过程中会涉及的调用类远比上述调用链更多。
故光靠分析调用链,无法圈定需要内存分析的所有类,只能找出比较高危的类做重点分析。
2) Web容器注册分析
为什么要做Web容器注册分析?
Web框架在进行Web调用处理时,一般分成两步:
- 程序启动时注册组件,即Web程序启动时定义好当收到请求时该交给哪个组件如何进行处理。
- 实际发生Web调用时,Web容器根据调用信息,找到对应的组件进行处理。
所以,我们可以通过读取Web容器的注册组件对应类进行内存分析,目标性更强,更精准。
此处介绍两种最常见的Web容器:tomcat和springmvc。
a) tomcat容器:
在使用tomcat作为Web容器时,可通过其核心对象StandardContext中获得filter、servlet、listener的注册信息。
获取的过程如下:
- StandardContext --> filterConfigs,filterDefs --> filter
- StandardContext --> servletMappings,children --> servlet
- StandardContext --> applicationListeners,listeners --> listener
Valve也是tomcat中非常重要的组件,但valve是tomcat中内置的组件,正常情况下并不会进行自定义,只需要通过判断类实现Valve接口过滤即可。
而filter、servlet、listener是servlet规范中提供给开发者自定义的开放性组件,则更具有进一步通过容器注册分析的意义。
b) springmvc容器:
springmvc作为最常见的Web框架,其注册信息同样具备针对性分析的价值。
需要获取的注册信息:
springmvc中主要获取的是HandlerMethod对象和HandlerInterceptor对象信息。
- 容器中注册的HandlerMethod对象表示请求的实际处理方法,表示springmvc中的controller对象和对应处理方法。
- 容器中的HandlerInterceptor是开放的拦截器,springmvc提供了在请求处理前后进行自定义处理的机制。
如何获取注册信息:
先获取spring的容器组件ApplicationContext,再从中获取springmvc的核心组件DispatcherServlet,进而获取注册信息。获取过程如下:
- ApplicationContext --> DispatcherServlet --> List<HandlerMapping> handlerMappings --> AbstractHandlerMethodMapping.getHandlerMethods()
- ApplicationContext --> DispatcherServlet --> List<HandlerMapping> handlerMappings --> AbstractHandlerMapping.adaptedInterceptors
在实际按照上述方式获取springmvc的注册信息时,可能会用到反射获取信息,且可能会不知道从哪获取最源头的ApplicationContext对象,进而无从下手。
最源头的ApplicationContext对象,一般需要从类的静态变量中获取。可通过内存分析ApplicationContext对象的引用关系,找到获取的链路。
获取方式举例如下:
2.2.1.2. Class基础信息检测
1) classPath为空
通过分析classPath为空的类,排除已知的正常情况,剩下的类大概率为风险类
1、getProtectionDomain().getCodeSource()为空
这意味着该类没有关联的代码源信息。可能的原因包括:
- 该类是 JVM 自带的类(例如,java.* 中的类),因为这些类是由 JVM 自身加载的,它们没有显式的代码源。
- 该类是通过字节码操作动态生成的,或者是以非标准的方式加载的(如通过某些代理类或字节码工具动态生成)。
- 该类是在 Java 运行时生成的,例如使用 Proxy、ClassLoader.defineClass() 等方法生成的类。
2、getClassLoader().getResource(className.replace(".", "/") + ".class")为空
这意味着类加载器无法找到与指定类对应的资源路径。可能的原因包括:
- 类是由一个特定的类加载器加载的,但该类加载器不使用标准的路径查找机制(如 URLClassLoader 或自定义的 ClassLoader)。
- 该类是通过内存中直接加载的方式(如字节码生成工具)加载的,而不是从文件系统或网络资源中获取的,因此找不到相应的类文件资源。
- 类加载器因为安全限制或者其他原因,无法访问该类的实际存储位置。
2) 关键字特征匹配
可以通过检测包名、类名、父类名、类加载器等类基础信息中是否包含诸如 shell、exploit 等可疑关键字,识别可能由内存马生成工具创建的恶意类。
- 冰蝎的包名以net.rebeyond.开头或带有Behinder关键字
- 哥斯拉的包名以core.shell.开头或带有Godzilla关键字
- msf的包名以com.metasploit.开头
- 蚁剑带有AntSword关键字
- ...
2.2.2. Class字节码检测
通过检测Class基础信息方式识别内存马的方式仅对Class的一些表象特征进行分析,其优势在于消耗低,但容易被绕过。而内存马注入的恶意代码肯定是以Class字节码存在在内存之中,通过检查Class字节码内容是一种更直接的方式。
2.2.2.1. 内存与磁盘字节码比对
读取已加载的字节码信息,和磁盘中的字节码信息进行比对。如果不一致,则为可疑类。
如果直接对Class的字节流进行比对,可能经常会误报,可以使用ASM工具将Class内容解析成属性和方法,再对每个属性和方法进行比对。
代码示例:
2.2.2.2. 敏感代码调用检测
使用ASM工具解析Class字节码的方法,与敏感代码特征进行比对
高敏感的代码调用:
1、命令执行:
- java.lang.Runtime#exec
- java.lang.ProcessBuilder#start
- java.lang.UNIXProcess#forkAndExec
- ...
2、JNI加载动态库:
- java.lang.System#load
- java.lang.Runtime#load
- ...
3、类加载:
- java.lang.ClassLoader#defineClass
- java.lang.reflect.Proxy#defineClass0
- sun.misc.Unsafe#defineAnonymousClass
- java.lang.invoke.MethodHandles.Lookup#defineHiddenClass
- ...
其他敏感代码调用:
- 获取系统信息
- 字节码生成类库
- 文件操作
- 编码加密
- ...
2.2.2.3. 反射调用检测
由于Java语言开放和动态的特性,可通过反射等方式动态地调用上述的敏感代码,进而绕过正常的检测规则。
反射调用方式:
- java.lang.reflect.Method#invoke
- sun.reflect.NativeMethodAccessorImpl#invoke
- jdk.internal.reflect.NativeMethodAccessorImpl#invoke0
- java.lang.invoke.LambdaMetafactory#metafactory
- java.lang.invoke.MethodHandle#invoke
- java.lang.invoke.MethodHandle#invokeExact
- java.lang.invoke.MethodHandle#invokeWithArguments
- ...
如何有效检测反射调用:
有师傅提出用模拟栈帧的方式进行检测,但是模拟栈帧的方式开销大,同时只能将检测范围圈定在单个方法内,而反射调用的入参可能在其他方法中定义,甚至在Web请求参数中定义,故有一定的局限性。
由于反射的运行时确定的特性,可以通过运行时检测,修改字节码,在反射代码前插桩,判断反射的参数是否命中敏感代码调用,进而告警。
2.2.2.4. Webshell引擎检测
将Class字节码反编译为Java文件后,交由成熟的Webshell检测引擎进行扫描。将反编译后的Class文件视作普通源代码进行检测,其核心依然是识别潜在的Webshell。通过这种方法,充分利用现有的检测技术,识别出隐藏在内存或代码中的恶意行为,进一步提升检测的准确性与效率。
3. 总结
本文系统性地列举了Java内存马的检测方法,涵盖了从类的基本信息获取到字节码的深入检测。通过这些方法,能够有效识别内存马攻击。可根据不同场景或需求,在检测率与性能开销之间进行平衡,选择适合的检测方式,以实现最佳的检测效果。
然而,攻防对抗本质上是动态的,随着内存马技术的不断发展,新的攻击手段和技术也会不断涌现,现有的检测手段也可能会失效。因此,需要持续更新和调整检测策略,跟踪最新的攻击趋势和技术进展,不断搜集和优化检测特征,以提升系统的防御能力,降低安全风险。