内存马查杀的几种思路和demo

admin 2025-12-28 01:55:43 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文探讨Java内存马查杀思路,对比了JSP、Agent及内存Dump三种方式,重点详解JavaAgent技术:通过比对物理文件与内存文件、识别异常父类与接口、内容黑名单匹配及类篡改哈希比对等方法检测恶意类。此外还提供对抗手段,利用SA-JDI工具在攻击者删除pid文件时按包Dump类文件以辅助分析。 综合评分: 88 文章分类: 应急响应,恶意软件,WEB安全,安全工具,代码审计


cover_image

内存马查杀的几种思路和demo

原创

flowerwind

长个新的脑袋

2025年8月27日 16:21 江苏

前言

前段时间参与了一场内存马的应急,以前打内存马这种攻击操作做的很多,但实际从防守角度去找别人的内存马这种操作基本没做过。试用了网上的很多工具,引发了我对如何能在内存中找到他人内存马的思考。由于网上的实操工具很多,我就不做演示,后面只从纯原理角度探讨Java内存马的查杀理论思路和demo代码

查杀手段

Jsp查杀

https://github.com/c0ny1/java-memshell-scanner/blob/master/tomcat-memshell-scanner.jsp

列出中间件中的所有Filter/Listener/Sevlet,从该类是否实际存在于物理路径上来判断是否为内存马

Java Agent查杀

https://github.com/LandGrey/copagent/tree/master

从文件内容、父类信息、注解信息、包名信息、接口信息等维度通过是否匹配到黑名单来判断目标是否为内存马

进程内存Dump

内存马查杀的暴力解法,直接dump制定进程的内存马,然后通过二进制编辑器查询

gcore&nbsp;<PID>

综合上面三种方式,从通用和可自动化角度考虑,Java Agent查杀是最好的方式。因此后面我们主要从Java Agent角度出发来讨论如何辨别内存马

查杀方式

获取JVM中所有的类

Java Agent方式查杀的基础是首先获取当前JVM中所有的类

Class&nbsp;allClasses=inst.getAllLoadedClasses()

物理文件和内存文件的比对

内存马是一种无文件攻击,根据此概念,内存马在磁盘上不会存在对应文件。因此可以遍历

for&nbsp;(Class&nbsp;class:allClasses){//此方式获取class的物理路径,同时兼容类在.class和.jar中的情况,通用性比较好String path=Thread.currentThread().getContextClassLoader().loadClass(class.getName()).getClassLoader().getResource(clazz.getName().replace('.',&nbsp;'/') +&nbsp;".class");if&nbsp;(path==null){&nbsp;&nbsp;return&nbsp;"该class在磁盘上不存在对应文件,疑似内存马";}}

此方式优点为:如果目标是类似Filter/Listener/Servlet这种添加“路由”的内存马,很轻松的可以被此种方式识别出来

此方式缺点为:1、如果攻击方不惜破坏内存马的无文件特性,直接在对应的classpath位置写一个内存马物理文件,那么这种检测方式就会失效   2、如果目标使用Java Agent技术,篡改了某些中间件的Filter实现内存马,那么此种方式也无法检测到

异常父类/异常接口/异常注解识别

实现Filter/Listener/Servlet接口或通过注解实现该Filter/Lisener/Servlet的类,拉出标记为高风险类。查看高风险类是否继承了ClassLoader,如果继承了CLassLoader可以直接判断为内存马,因为正常代码不会把这两种功能混在一个类中,而内存马为了开发简单可能会把ClassLoader和实现Filter等写在一个类中

&nbsp;while&nbsp;(class&nbsp;!=&nbsp;null&nbsp;&& !class.getName().equals("java.lang.Object")){if(class.getSuperclass() !=&nbsp;null&nbsp;&&class.getSuperclass().equals("java.lang.ClassLoader")){&nbsp;return&nbsp;"该类为内存马";}class=class.getSuperclass();&nbsp;}

内容识别

对上种方式提取的高风险类进行反编译,然后进行黑名单匹配。包含加解密、反射、命令执行等高危包名和类名

javax.crypto.java.lang.reflect.ProcessBuildergetRuntimeProcessBuildergetMethodgetDeclaredMethod.invokesetAccessibleClassLoadernewInstance

类篡改识别

前面提到Java Agent类型的内存马无法通过物理文件和内存文件的比对检测出来,针对这种篡改性质的内存马,我们可以通过技术手段将其识别出来。原理为,我们通过Java Agent技术添加一个ClassFileTransformer,把当前内存中的所有类的字节码保存下来。然后再读区该类对应的物理路径处的文件,再获得一个字节码。这两个字节码中,前者表示当前类在内存中的字节码,后者表示该类原本的字节码。两者比较字节码的hash,如果一致表示类没有被篡改过,如果不一致表示类已被篡改。

import&nbsp;java.io.InputStream;import&nbsp;java.lang.instrument.*;import&nbsp;java.security.ProtectionDomain;import&nbsp;java.security.MessageDigest;import&nbsp;java.util.*;
public&nbsp;class&nbsp;ModifiedClassDetector&nbsp;{
&nbsp; &nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;agentmain(String&nbsp;agentArgs,&nbsp;Instrumentation&nbsp;inst) throws&nbsp;Exception&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;System.out.println("[*] Starting modified class detection...");
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 收集类与当前字节码的映射&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Map<Class<?>, byte[]> currentBytecodes =&nbsp;new&nbsp;HashMap<>();&nbsp; &nbsp; &nbsp; &nbsp; inst.addTransformer(new&nbsp;DumpTransformer(currentBytecodes),&nbsp;true);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 对所有类重新触发 transform,以便获取字节码&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(Class<?> clazz : inst.getAllLoadedClasses()) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(inst.isModifiableClass(clazz)) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; inst.retransformClasses(clazz);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(Throwable&nbsp;ignored) {}&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 对比原始字节码&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(Map.Entry<Class<?>, byte[]> entry : currentBytecodes.entrySet()) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Class<?> clazz = entry.getKey();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; byte[] currentBytes = entry.getValue();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; byte[] originalBytes =&nbsp;getOriginalClassBytes(clazz);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(originalBytes !=&nbsp;null&nbsp;&& !Arrays.equals(hash(currentBytes),&nbsp;hash(originalBytes))) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;System.out.println("[MODIFIED] "&nbsp;+ clazz.getName());&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;System.out.println("[*] Detection complete.");&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// Transformer 用于收集字节码&nbsp; &nbsp;&nbsp;private&nbsp;static&nbsp;class&nbsp;DumpTransformer&nbsp;implements&nbsp;ClassFileTransformer&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;private&nbsp;final&nbsp;Map<Class<?>, byte[]> bytecodeMap;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;DumpTransformer(Map<Class<?>, byte[]> bytecodeMap) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;this.bytecodeMap&nbsp;= bytecodeMap;&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;@Override&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;public&nbsp;byte[]&nbsp;transform(ClassLoader loader,&nbsp;String&nbsp;className, Class<?> classBeingRedefined,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ProtectionDomain protectionDomain, byte[] classfileBuffer) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bytecodeMap.put(classBeingRedefined, classfileBuffer.clone());&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;null;&nbsp;// 不修改字节码,仅收集&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// 读取原始 class 文件的字节码&nbsp; &nbsp;&nbsp;private&nbsp;static&nbsp;byte[]&nbsp;getOriginalClassBytes(Class<?> clazz) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;String&nbsp;resourcePath = clazz.getName().replace('.',&nbsp;'/') +&nbsp;".class";&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;InputStream&nbsp;is = clazz.getClassLoader() !=&nbsp;null&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ? clazz.getClassLoader().getResourceAsStream(resourcePath)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;ClassLoader.getSystemResourceAsStream(resourcePath);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(is ==&nbsp;null)&nbsp;return&nbsp;null;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;is.readAllBytes();&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(Exception&nbsp;e) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;null;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// 计算 MD5 哈希&nbsp; &nbsp;&nbsp;private&nbsp;static&nbsp;byte[]&nbsp;hash(byte[] data) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;MessageDigest&nbsp;md =&nbsp;MessageDigest.getInstance("MD5");&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;md.digest(data);&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(Exception&nbsp;e) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;throw&nbsp;new&nbsp;RuntimeException(e);&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}

对抗

最后分享一种对抗场景。现在有很多的内存马会删除/tmp/.java_pid文件,这样会导致后续的java agent注入不上。前段时间应急就遇到这种情况,最后dump了进程全部内存,而全部内存的无效数据非常多,给分析造成了很大的阻力。最近研究发现,java自带的工具可以破解/tmp/.java_pid的情况,这里分享出来

sudo&nbsp;java -classpath&nbsp;"$JAVA_HOME/lib/sa-jdi.jar"&nbsp;-Dsun.jvm.hotspot.tools.jcore.filter=sun.jvm.hotspot.tools.jcore.PackageNameFilter -Dsun.jvm.hotspot.tools.jcore.PackageNameFilter.pkgList=com &nbsp;sun.jvm.hotspot.tools.jcore.ClassDump&nbsp;414703

该命令会dump所有com包名开头的class

参考

https://github.com/c0ny1/java-memshell-scanner/blob/master/tomcat-memshell-scanner.jsp

https://github.com/LandGrey/copagent/tree/master

https://blog.csdn.net/hengyunabc/article/details/51106980


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:长个新的脑袋 flowerwind《内存马查杀的几种思路和demo》

评论:0   参与:  2