[备份]Agent内存马的自动分析与查杀

出发点是Java Agent内存马的自动分析与查杀,实际上其他内存马都可以通过这种方式查杀

本文主要的难点主要是以下三个,我会在文中逐个解答

  1. 如何dumpJVM真正的当前的字节码
  2. 如何解决由于LAMBDA表达式导致非法字节码无法分析的问题
  3. 如何对字节码进行分析以确定某个类是内存马

基于静态分析动态,打破规则之道 -- Java King 对本文的评价

背景

对于Java内存马的攻防一直没有停止,是Java安全领域的重点

回顾TomcatSpring内存马:FilterController等都需要注册新的组件

针对于需要注册新组件的内存马查杀起来比较容易:

例如c0ny1师傅的java-memshell-scanner项目,利用了Tomcat API删除添加的组件。优点在于一个简单的JSP文件即可查看所有的组件信息,结合人工审查(类名和ClassLoader等信息)对内存马进行查杀,也可以对有风险的Class进行dump后反编译分析

或者LandGrey师傅基于Alibaba Arthas编写的copagent项目,分析JVM中所有的Class,根据危险注解和类名等信息dump可疑的组件,结合人工反编译后进行分析

但实战中,可能并不是以上这种注册新组件的内存马

例如师傅们常用的冰蝎内存马,是Java Agent内存马。以下这段是冰蝎内存马一段代码,简单分析后可以发现冰蝎内存马是利用Java Agent注入到javax.servlet.http.HttpServletservice方法中,这是JavaEE的规范,理论上部署在Tomcat的都要符合这个规范,简单来理解这是Tomcat处理请求最先且总是经过的地方,在该类加入内存马的逻辑,可以保证稳定触发

img

类似的逻辑,可以使用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修改后的字节码。通过JavaassistASM工具获取到类的字节码,也只是读取磁盘上响应类的字节码,而不是JVM中的字节码

宽字节安全的师傅找到了一种检测手段:sa-jdi.jar

借用公众号师傅的图片,这是一个GUI工具,可以查看JVM中所有已加载的类。区别在于这里获取到的是真正的当前的字节码,而不是获取到原始的,本地的字节码,所以是可以查看被Java Agent调用redefineClass后被修改的类的字节码。进一步可以dump下来认为存在风险的类然后反编译人工审核

img

介绍

以上是背景,接下来介绍我做了些什么,能够实现怎样的效果

不难看出,以上内存马查杀手段都是半自动结合人工审核的方式,当检测出内存马后

是否可以找到一种方式,做到一条龙式服务:

大致看来,实现起来似乎不难,然而实际中遇到了很多坑,接下来我会逐个介绍

SA-JDI分析

我尝试通过Java Agent技术来获取当前的字节码,发现如师傅所说拿不到被修改的字节码

所以为了可以检测Agent马需要从sa-jdi.jar本身入手,想办法dump得到当前字节码(这样不止可以分析被修改了字节码的Agent马也可以分析普通类型的内存马)

注意到其中一个类:sun.jvm.hotspot.tools.jcore.ClassDump并通过查资料发现该类功能正是dump当前的Class(根据类名也可猜测出)其中的main方法提供一个dump class的命令行工具

于是我想了一些办法,用代码实现了命令行工具的功能,并可以设置一个Filter

上文提到设置一个Filter是用于确定需要对哪些类进行dump操作(dump过多会导致性能等问题)

以上包含了类的黑名单和关键字:

另外如果想在Maven项目中加入JDK/lib下的依赖,需要特殊配置

在打包成工具Jar包时默认情况下不会加入system scope的依赖,所以需要特殊处理

编写assembly.xml文件

接着就可以通过代码的方式,根据黑名单和关键字来确定需要dump哪些类然后进行dump操作了

我在测试中遇到一个小问题,值得分享:HttpServlet是正常可以dump的但是ApplicationFilterChain类没有找到。这是因为SpringBoot的懒加载问题,需要手动请求下某个接口就可以了

解决非法字节码

接下来我遇到了一个比较大的坑,通过sa-jdidump下来的字节码是非法的

在对ApplicationFilterChain类分析的时候,会报如下的错

img

起初我怀疑是自己用了最新版ASM框架:9.2

于是逐渐降级,发现降级到7.0后不再报错,但ClassReader不报错,在分析时候会报错

经过对比,发现是以下的情况

img

不报错版本

img

稍微分析了下,发现是ApplicationFilterChain类包含了LAMBDA

不止这个类,不少的类都有可能会包含LAMBDA

img

发现通过sa-jdi获取的字节码在存在LAMBDA的情况下是非法字节码,无法进行分析

这时候如果还想进行分析,只有两个选择:

根据Java基础知识可以得知:LAMBDAINVOKEDYNAMIC指令相关,于是我改了ASM的代码

(这里不解释为什么这么改了,是经过多次调试确定的)

org/objectweb/asm/ClassReader#274

org/objectweb/asm/ClassReader#2456

改了源码后,就可以正常对非法字节码进行分析了。目前来看没有什么大问题,可以正常分析,但不确定这样的修改是否会存在一些隐患和BUG。总之目前能继续了

分析字节码

分析字节码并不需要太深入做,因为大部分可能出现的内存马都是Runtime.exec或冰蝎反射调ClassLoader.defineClass实现的,针对于这两种情况做分析,足以应对绝大多数情况

以下代码是读取dump的字节码并针对两种情况对所有方法分析

对于Runtime.exec型的分析最为简单,仅判断已dump的字节码中所有方法中是否存在该方法的调用即可(理论上会存在误报,但黑名单类不可能存在该方法,关键字类本身就是可疑的,所以这样做并无不妥)

但这种情况不适用于冰蝎反射调ClassLoader.defineClass

代码不长,但对应的字节码较复杂

对应字节码

这种操作需要多个步骤,并不是简单的一个INVOKE那么简单,不特殊处理的话,由于反射和ClassLoader相关操作都算是比较常见的,有一定的误报可能

于是继续掏出栈帧分析大法,具体不再介绍,之前文章 已有详细解释

根据字节码,在defineClassLjava/lang/ClassLoader;通过LDC指令入栈之前,应该认为这是恶意操作,模拟JVM指令执行后应该在栈顶设置污点

后续主要是对于两个INVOKE进行分析

检测效果如下:

先写个内存马注入的Agent注入到HttpServlet中(关于这个不是文章重点)

img

然后跑起来我写的工具

img

自动修复

接下来是内存马的修复,自行写一个Java Agent即可

暂时只处理ApplicationFilterChainHttpServlet的情况(也是最常见的情况)

处理的逻辑并不复杂

当我们写好了Agent后,需要加入自动修复的逻辑

如果分析出了结果,且用户选择了修复功能,才会进入修复逻辑(暂只修复这两个最常见的类)

修复的核心代码:把打包好的Agent拿过来,做一下AtachLoad将字节码替换为正常情况即可

注意使用VirtualMachineAPI需要加入tools.jar,由于上文已经配置了打包插件,所以可以直接打入Jar包,使用时候java -jar xxx.jar --pid 000这样会比较方便

通过以上这些修复手段可以做到的效果:

总结

关于Dump字节码

经过我的一些测试,使用sa-jdi库不能保证dump所有的字节码,会出现莫名其妙的异常,猜测是某些字节码不允许被dump下来。但测试了常见TomcatSpringBoot等程序,发现基本没有问题

关于非法字节码

只要是包含LAMBDA的字节码都是非法字节码,无法正常处理,需要用修改源码后的ASM来做。这种方式终究不是完美的办法,是否存在能够dump下来合法字节码的方式呢(经过一些尝试没有找到办法)

关于检测

可以看到,字节码分析的过程比较简单,尤其是Runtime.exec的普通执行命令内存马,很容易绕过,但个人认为这已足够,因为之前的一些条件已经限制了分析的类是不可能包含Runtime.exec的黑名单类,且大多数用户都是脚本小子,使用免杀型内存马的可能性不大。大多数用户可能直接用了现成的工具,例如冰蝎型内存马的检测方式已完成,暂时来看这样做是足够的,没有必要加入各种免杀检测手段

关于查杀

使用Agent恢复字节码的修复方式理论上没有问题。但其中的ApplicationFilterChain类的doFilter方法中包含了LAMBDA和匿名内部类,这两者都是Javassist框架不支持的内容,可以用ASM来做,但可能难度较高

另外对于普通型内存马的修复,通过Agent技术只能覆盖方法体,不可以增加或删除方法。所以理论上可以根据方法的返回值类型,做返回NULL的处理进行修复

关于拓展

例如代码中我定义的黑名单和关键字,可以根据实战经验自行添加新的类,以实现更完善的效果。在查杀方面我做了最常见的两种,可以根据实际情况自行添加更多的逻辑