原文始发于安全客(长亭科技):主机安全技术剖析-手把手教会你防御Java内存马
书接上文,在上篇文章中我们论述了java内存马的一些基本注入方式,本篇我们将从防御和绕过的方式继续讨论java内存马的攻防实战。
1 防 Agent技术下的内存马防御
1.1 内存马的防御思路
目前的内存马防御主要有如下思路:
- 识别内存马特征,检出内存马
- 运行时动态防御, hook相关函数在程序运行中禁止动态添加或注册相关组件,在注入的内存马中添加防御字节码进行禁用
为了落地这两种方案,一种思路如c0ny1师傅的java-memshell-scanner
是编写了一个jsp,审计人员将该jsp添加到web目录访问网页后即可看到所有的servlet/filter/listener进行排查,然而由于jsp技术逐渐被抛弃,其适用场景也越来越有限。事实上,我们还可以使用java为我们提供的java agent技术接口来实现。
什么是Java agent?
利用javaagent技术,可以在字节码这个层面对类和方法进行修改。同时,也可以把javaagent理解成一种代码注入的方式。可以在加载java文件之前做拦截把字节码做修改,也可以在运行期将已经加载的类的字节码做变更。
在一般情况下通过-javaagent:xxx.jar=name=lisi&age=30其中xxx.jar指定对应要加载的agent jar包的名字和路径,后面跟踪自己传入的参数即可。
1.2 一个简单的注入示例
复现一下rebeyond大师傅的示例
•正常运行的java应用:
•利用Agent技术进行注入:
这里使用到了Instrumentation
Instrumentation是JVMTIAgent(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果。
这里我们主要用到:
那么我们可以如下编写一个AngentEntry,并实现agentmain方法:
简单解释一下:
先用Instrumentation.addTransformer()来加载一个我们写好的转换器。
然后在再通过inst遍历所有已经加载的class,然后如果匹配到类名为”priv.eki.normal.Bird”(这里要区分类名和类路径)则通过inst.retransformClasses(c);进行类的字节码转换。
我们编写的Transformer如下:
可以看到实际上是实现了ClassFileTransformer这一接口getBytesFromFile是方便我们从准备好的class文件读取字节流所实现的一个函数。
什么是ClassFileTransformer?
这是一个接口,它提供了一个transform方法,该方法的返回值将成为转换后的字节码。
这里我们通过直接javac编译的方式生成Bird的字节码,当然也可以通过javassist直接动态修改字节码。
修改Bird.java为:
然后编译生成对应的Brid.class。
因此我们要修改一下Manifest。
可以直接写Mainfest:
Manifest-Version: 1.0
Agent-Class: priv.eki.agent.AgentEntry
Can-Retransform-Classes: true
也可以通过maven-assembly-plugin在pom.xml里写:
此后直接运行mvn assembly:assembly即可打包agent。
然后就是我们之前提到的如何在应用启动后使用agent注入字节码,这里通过jdk提供的tool工具包来获取VirtualMachine,通过attach给已经启动的java虚拟机注入我们的agent.jar。
什么是VirtualMachine?
字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例。
如下是一个示例:
在这个实例中,我们是通过遍历virtualMaine.list(),根据其systemProperties中的commond属性来判断是否是我们想要注入的jvm,在内存马实战中,可以通过匹配常见的中间件启动命令来找到对应的jvm。
效果如下:
可以看到我们成功注入到了Example.jar中并替换了Bird类。
那么类似的,可以去替换一些addFilter,addServlet的方法,来阻止攻击者借用这些类方法进行注入
1.3 DumpClass
DumpClass就是利用java agent技术提供的ClassFileTransformer transform 方法去拿到对应类的字节码,直接写入文件或是利用其他反编译工具进行反编译获得代码逻辑,然后逐一进行排查。
一个简单的dumpclassTransformer如下:
同上面agent里遍历getAllLoadedClasses的返回值,找到对应的类dump即可。
1.4 内存马特征
为了提高dumpclass的效率,我们需要根据内存马的特征来选择需要导出的风险类字节码。一般来说,内存马会有如下特征:
1. SuperClass/Implement
基于继承的父类或是实现的接口来判断内存马看起来很鸡肋但实际上还是很有帮助的,因为内存马毕竟最后还是要依赖框架的功能来实现的,必然会继承或实现框架的接口,在应用自身提供的接口不多的情况下,通过遍历所有的servlet/filter/listener进行审计也是一种检测的策略,不过对应像spring controller动态注册中可以绑定任意类的任意方法来说,这种方法就失效了。
在LandGrey师傅的查杀内存马的copagent项目中中,给出了一些容易利用注入的类 :
当然针对不同的框架或中间件还可以添加一些规则,比如:
• tomcat 的 “org.apache.catalina.Valve”
• glassfish 的”org.glassfish.grizzly.filterchain.Filter”
• jfinal 的 “com.jfinal.core.Controller”
等等
2. Class File/Location/Loader
内存马的特性就是无文件落地,只存在内存中,这就代表了这个类对应的Resource是不在本地文件系统中的,那么其Class File/Location/Loader相较于应用正常的组件会有一定的区别。
比如注入的Servlet的classloader:
Class.getClass().getClassLoader().getClass().getName()
注入的Controller的FileLocation:
Class.getProtectionDomain().getCodeSource().getLocation()
1.5 Anti Daynamic Modify
在之前的攻击部分我们介绍了一些利用提供的接口动态添加恶意组件的方法,那么很容易想到通过给这些接口加“防御补丁”来阻止攻击者添加恶意组件的防御思路,这其实也就是rasp的思想,利用java agent的技术,我们可以对相关脆弱类进行hook,并拦截恶意的内存马注册请求。这里简单介绍一下openrasp的hook思路。
openrasp的hook规则集是通过HookAnnotation标注的,通过AnntationScanner能找到指定包路径下所有带HookAnnotation标注的类,同时一个hook规则需要继承AbstractClassHook这个抽象类,一个hook类对应着对一个类的所有hook规则,子类需要实现hookMethod方法,以SocketHook为例。
AbstractClassHook的getInvokeStaticSrc会通过javassist来创建对应方法的字节码,insertBefore会将生成的字节码插入到对应hook类方法的开始部分。
初始化CustomClassTransformer时:
会调用addAnnotationHook,将所有钩子添加到类的hook属性中:
之后调用retransform方法,isClassMatched通过是否在hook列表中判断类是否需要hook:
之后instrument api的retransformClasses方法会调用Transformer的transform方法,此时拿之前hook类生成好的字节码替换就完成了hook的整个流程。
2 攻:使用 java agent 技术注入字节码
之前在讨论防御时简单介绍了java agent技术,从攻击者角度来看,既然防守方能通过agent替换脆弱类代码进行防御,那么攻击方也可以通过替换关键类代码进行攻击。
这里编写一个简单的Servlet容器来模拟agent型内存马的注入。
一个Servlet:
和一个Filter:
web.xml配置如下:
打包成war后使用apache tomcat启动。
同之前类似的我们还是写一个agent.jar然后通过virutalmachine attach 注入我们的代码。
如果说之前在代码注入篇我们是通过注入一个新的web服务或是线程来实现webshell,那么可以看出agent注入就是通过替换服务端上现有类的方法来注入我们的webshell逻辑。那么替换什么类的方法呢。最好是能够实现一种效果,访问web服务器上的任意一个url,只要我们的请求传递给tomcat/weblogic,tomcat等中间件就能响应我们的指令。这个类要尽可能在http请求调用栈的上方,又不能与具体的URL有耦合,而且还能接受客户端request中的数据。
在tomcat中常用的方法是:
在weblogic中常用的的方法是:
比较通用的当然还是:
以tomcat为例,注入org.apache.catalina.core.ApplicationFilterChain 的dofilter方法:
我们要在dofilter方法内注入的内存马代码如下,其中$1,$2是javassist传递给目标方法的参数的指代,这里两个参数正好对应着http的request和response。
把他放到resources/source.txt然后通过javassist编译并注入字节码,这里使用insertBefore(),注入到原代码之前并保留原代码,防止原有代码逻辑被破坏而无法运行。
执行Attach后,效果如下:
访问网页输入对应密码即可RCE:
3 防:阻止恶意java agent注入
在threedr3am师傅的ZhouYu内存马项目中通过ProtectTransformer阻止后续javaagent加载的方式,防止webshell被查杀:
不过这里的Transformer不是之前的ClassfileTransformer,这里实现阻止后续agent马加载的思路实际上是将java.lang.instrument.ClassFileTransformer类置空,后续的agent就无法注入了,那么同样的,在我们的rasp防御中,也可以考虑将java.lang.instrument.ClassFileTransformer置空来阻止后续可能的agent注入。
除此之外对于通过调用redefineClass方式修改字节码的Agent型内存马比如冰蝎
我们在第一部分的中提到的ClassFileTransformer的retransforem方法拿到的字节码可能并不真实。
这里可以采用HotSpot jvm提供的sa-jdi.jar工具来dump真正的字节码
与Agent不同技术不同的是Serviceability Agent(SA)是HotSpot自带的底层调试支持。其运行模式类似“snapshot”,需要将目标jvm进程暂停或是core dump来进行分析。
下面的命令可以打开sa-jdi提供的gui交互面板(注意在attach后java进程会暂停)
java -cp .:$JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
class Browser中可以dump真正的字节码。
当然我们也可以利用其提供的类接口来去写一个基于sa的classdumper。
4 攻:java rasp的一些绕过
当防守方针对一些关键方法进行拦截时,攻击方可以绕过这些方法,直接去修改对象存储的数据结构,甚至直接上传JNI模块,在native层进行操作,此时RASP就无能为力了。
当然,借助攻击方线程马的思想,防守方也可以借助Timer定时扫描AbstractHandlerMethodMapping的mappingRegistry,如果存在恶意的Controller则进行卸载,包括其他种类的内存马也是检测方式也是类似的。
5 防:dumpclass 检测维度再升级
之前提到动态注入的SpringController可以没有任何继承类的特征,然而如果要绑定为一个服务,肯定是需要放在Spring的一个全局变量中的。防守方也可以采取攻击方的思路,去拿到所有的绑定关系,比如下面这段代码就是修改自上面添加Spring Controller的代码:
我们也可用这个整一个类似java-memshell-scanner的spring-memshell-scanner,效果如图:
可以检出urlPath对应的方法,通过方法可以拿到相应的类名,可以添加入dumpClass的规则。要使用这个方法我们需要的就是找到一个servletContext,而这个问题的解决思路和攻击者在通过http线程获得servletContext的思路是一致的。
而对于直接修改org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors注入spring interceptor的内存马,也可以通过类似的方式拿到这个示例对象然后dumpclass
6 结语
java内存马的攻防技术仍然在不断升级。总的来说,攻击方主要是通过应用提供的动态注册服务接口进行内存马注册,或是通过agent手段替换关键类逻辑为内存马。而防御者则是通过检测风险类,动态拦截内存马注册点的方式进行防御。在前文的讨论中我们会发现,攻击和防御的手法并不是绝对的,通过学习攻击的策略,能够更好的提升我们的防御手段。
事实上,牧云的内存马检测模块就是在这一系列的攻防较量中不断完善的。通过dumpclass主动扫描和rasp被动扫描相结合的方式,可以有效发现内存马的踪迹,及时拦截内存马的执行。以注入Controller型内存马为例,当攻击者修改相应数据结构进行注册时,内存马检测插件会及时介入并检测请求合法性来判断是否拦截。
借助RASP热更新的优势,牧云内存马检测模块能够快速响应漏洞威胁。比如前不久爆发的log4j2Shell问题,牧云第一时间上线了对应的防御工具log4j2-vaccine,通过对脆弱类的热更新实现在不重启重要业务的情况下快速修复漏洞。而针对传统rasp对业务资源占用大问题,牧云内存马检测模块通过更精准的hook方式减少正常状态下对业务的影响,同时降低攻击者绕过检测函数的可能。
目前长亭牧云(CloudWalker)主机安全管理平台通过dumpclass主动扫描和rasp被动扫描相结合的方式,已在实际应用中被证实能够有效检测并拦截内存马。更多交流或投递简历,欢迎联系邮箱:[email protected]。
参考资料
Java Agent基本简介和使用
https://cxybb.com/article/fuli_mouren/103762921
log4j2-vaccine
https://github.com/chaitin/log4j2-vaccine
[Agent内存马的自动分析与查杀]
https://xz.aliyun.com/t/10910