出发点是Java Agent
内存马的自动分析与查杀,实际上其他内存马都可以通过这种方式查杀
本文主要的难点主要是以下三个,我会在文中逐个解答
- 如何
dump
出JVM
中真正的当前的字节码 - 如何解决由于
LAMBDA
表达式导致非法字节码无法分析的问题 - 如何对字节码进行分析以确定某个类是内存马
基于静态分析动态,打破规则之道 -- Java King 对本文的评价
背景
对于Java内存马的攻防一直没有停止,是Java安全领域的重点
回顾Tomcat
或Spring
内存马:Filter
和Controller
等都需要注册新的组件
针对于需要注册新组件的内存马查杀起来比较容易:
例如c0ny1
师傅的java-memshell-scanner
项目,利用了Tomcat API
删除添加的组件。优点在于一个简单的JSP
文件即可查看所有的组件信息,结合人工审查(类名和ClassLoader
等信息)对内存马进行查杀,也可以对有风险的Class进行dump
后反编译分析
或者LandGrey
师傅基于Alibaba Arthas编写的copagent
项目,分析JVM
中所有的Class,根据危险注解和类名等信息dump
可疑的组件,结合人工反编译后进行分析
但实战中,可能并不是以上这种注册新组件的内存马
例如师傅们常用的冰蝎内存马,是Java Agent
内存马。以下这段是冰蝎内存马一段代码,简单分析后可以发现冰蝎内存马是利用Java Agent
注入到javax.servlet.http.HttpServlet
的service
方法中,这是JavaEE
的规范,理论上部署在Tomcat
的都要符合这个规范,简单来理解这是Tomcat
处理请求最先且总是经过的地方,在该类加入内存马的逻辑,可以保证稳定触发
类似的逻辑,可以使用Java Agent
将内存马注入org.apache.catalina.core.ApplicationFilterChain
类中,该类位于Filter
链头部,也就是说经过Tomcat
的请求都会交经过该类的doFilter
方法处理,所以在该方法中加入内存马逻辑,也是一种稳定触发的方式(据说这是老版本冰蝎内存马的方式)
还可以对类似的类进行注入,例如org.springframework.web.servlet.DispatcherServlet
类,针对于Spring
框架的底层进行注入。或者一些巧妙的思路,比如注入Tomcat
自带的Filter
之一org.apache.tomcat.websocket.server.WsFilter
类,这也是Java Agent
内存马可以做到的
上文简单地介绍了各种内存马的利用方式与普通内存马的查杀,之所以最后介绍Java Agent
内存马的查杀,是因为比较困难。宽字节安全的师傅提出查杀思路:基于javaAgent内存马检测查杀指南
引用文章讲到Java Agent
内存马检测的难点:
调用retransformClass
方法的时候参数中的字节码并不是调用redefineClass
后被修改的类的字节码。对于冰蝎来讲,根本无法获取被冰蝎修改后的字节码。我们自己写Java Agent
清除内存马的时候,同样也是无法获取到被redefineClass
修改后的字节码,只能获取到被retransformClass
修改后的字节码。通过Javaassist
等ASM
工具获取到类的字节码,也只是读取磁盘上响应类的字节码,而不是JVM
中的字节码
宽字节安全的师傅找到了一种检测手段:sa-jdi.jar
借用公众号师傅的图片,这是一个GUI
工具,可以查看JVM
中所有已加载的类。区别在于这里获取到的是真正的当前的字节码,而不是获取到原始的,本地的字节码,所以是可以查看被Java Agent
调用redefineClass
后被修改的类的字节码。进一步可以dump
下来认为存在风险的类然后反编译人工审核
介绍
以上是背景,接下来介绍我做了些什么,能够实现怎样的效果
不难看出,以上内存马查杀手段都是半自动结合人工审核的方式,当检测出内存马后
是否可以找到一种方式,做到一条龙式服务:
- 检测(同时支持普通内存马和
Java Agent
内存马的检测) - 分析(如何确定该类是内存马,仅根据恶意类名和注解等信息不完善)
- 查杀(当确定内存马存在,如何自动地删除内存马并恢复正常业务逻辑)
大致看来,实现起来似乎不难,然而实际中遇到了很多坑,接下来我会逐个介绍
SA-JDI分析
我尝试通过Java Agent
技术来获取当前的字节码,发现如师傅所说拿不到被修改的字节码
所以为了可以检测Agent
马需要从sa-jdi.jar
本身入手,想办法dump
得到当前字节码(这样不止可以分析被修改了字节码的Agent
马也可以分析普通类型的内存马)
注意到其中一个类:sun.jvm.hotspot.tools.jcore.ClassDump
并通过查资料发现该类功能正是dump
当前的Class(根据类名也可猜测出)其中的main
方法提供一个dump class
的命令行工具
于是我想了一些办法,用代码实现了命令行工具的功能,并可以设置一个Filter
ClassDump classDump = new ClassDump(); // my filter classDump.setClassFilter(filter); classDump.setOutputDirectory("out"); // protected start method Class<?> toolClass = Class.forName("sun.jvm.hotspot.tools.Tool"); Method method = toolClass.getDeclaredMethod("start", String[].class); method.setAccessible(true); // jvm pid String[] params = new String[]{String.valueOf(pid)}; try { method.invoke(classDump, (Object) params); } catch (Exception ignored) { logger.error("unknown error"); return; } logger.info("dump class finish"); // detach Field field = toolClass.getDeclaredField("agent"); field.setAccessible(true); HotSpotAgent agent = (HotSpotAgent) field.get(classDump); agent.detach();
上文提到设置一个Filter
是用于确定需要对哪些类进行dump
操作(dump过多会导致性能等问题)
public class NameFilter implements ClassFilter { @Override public boolean canInclude(InstanceKlass instanceKlass) { String klassName = instanceKlass.getName().asString(); // 在黑名单中的类需要dump if (blackList.contains(klassName)) { return true; } // 包含了关键字的类也需要dump for (String k : Constant.keyword) { if (klassName.contains(k)) { return true; } } return false; } }
以上包含了类的黑名单和关键字:
- 黑名单:Java Agent内存马通常会Hook的地方,需要
dump
下来进行分析 - 关键字:类名如果出现
memshell
和shell
等关键字认为可能是普通内存马,需要分析
public class Constant { // BLACKLIST (Analysis Target) // CLASS_NAME#METHOD_NAME public static List<String> blackList = new ArrayList<>(); // SHELL KEYWORD public static List<String> keyword = new ArrayList<>(); static { blackList.add("javax/servlet/http/HttpServlet#service"); blackList.add("org/apache/catalina/core/ApplicationFilterChain#doFilter"); blackList.add("org/springframework/web/servlet/DispatcherServlet#doService"); blackList.add("org/apache/tomcat/websocket/server/WsFilter#doFilter"); keyword.add("shell"); keyword.add("memshell"); keyword.add("agentshell"); keyword.add("exploit"); keyword.add("payload"); keyword.add("rebeyond"); keyword.add("metasploit"); } }
另外如果想在Maven
项目中加入JDK/lib
下的依赖,需要特殊配置
<dependency> <groupId>sun.jvm.hotspot</groupId> <artifactId>sa-jdi</artifactId> <version>jdk-8</version> <scope>system</scope> <systemPath>${env.JAVA_HOME}/lib/sa-jdi.jar</systemPath> </dependency>
在打包成工具Jar
包时默认情况下不会加入system scope
的依赖,所以需要特殊处理
<artifactId>maven-assembly-plugin</artifactId> <configuration> <appendAssemblyId>false</appendAssemblyId> <descriptors> <descriptor>assembly.xml</descriptor> </descriptors> <archive> <manifest> <mainClass>org.sec.Main</mainClass> </manifest> </archive> </configuration>
编写assembly.xml
文件
<!-- 省略部分 --> <dependencySets> <dependencySet> <outputDirectory>/</outputDirectory> <unpack>true</unpack> <scope>system</scope> </dependencySet> </dependencySets>
接着就可以通过代码的方式,根据黑名单和关键字来确定需要dump
哪些类然后进行dump
操作了
我在测试中遇到一个小问题,值得分享:HttpServlet
是正常可以dump
的但是ApplicationFilterChain
类没有找到。这是因为SpringBoot
的懒加载问题,需要手动请求下某个接口就可以了