文章总结: 本文探讨Java内存马查杀思路,对比了JSP、Agent及内存Dump三种方式,重点详解JavaAgent技术:通过比对物理文件与内存文件、识别异常父类与接口、内容黑名单匹配及类篡改哈希比对等方法检测恶意类。此外还提供对抗手段,利用SA-JDI工具在攻击者删除pid文件时按包Dump类文件以辅助分析。 综合评分: 88 文章分类: 应急响应,恶意软件,WEB安全,安全工具,代码审计
内存马查杀的几种思路和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 <PID>
综合上面三种方式,从通用和可自动化角度考虑,Java Agent查杀是最好的方式。因此后面我们主要从Java Agent角度出发来讨论如何辨别内存马
查杀方式
获取JVM中所有的类
Java Agent方式查杀的基础是首先获取当前JVM中所有的类
Class allClasses=inst.getAllLoadedClasses()
物理文件和内存文件的比对
内存马是一种无文件攻击,根据此概念,内存马在磁盘上不会存在对应文件。因此可以遍历
for (Class class:allClasses){//此方式获取class的物理路径,同时兼容类在.class和.jar中的情况,通用性比较好String path=Thread.currentThread().getContextClassLoader().loadClass(class.getName()).getClassLoader().getResource(clazz.getName().replace('.', '/') + ".class");if (path==null){ return "该class在磁盘上不存在对应文件,疑似内存马";}}
此方式优点为:如果目标是类似Filter/Listener/Servlet这种添加“路由”的内存马,很轻松的可以被此种方式识别出来
此方式缺点为:1、如果攻击方不惜破坏内存马的无文件特性,直接在对应的classpath位置写一个内存马物理文件,那么这种检测方式就会失效 2、如果目标使用Java Agent技术,篡改了某些中间件的Filter实现内存马,那么此种方式也无法检测到
异常父类/异常接口/异常注解识别
实现Filter/Listener/Servlet接口或通过注解实现该Filter/Lisener/Servlet的类,拉出标记为高风险类。查看高风险类是否继承了ClassLoader,如果继承了CLassLoader可以直接判断为内存马,因为正常代码不会把这两种功能混在一个类中,而内存马为了开发简单可能会把ClassLoader和实现Filter等写在一个类中
while (class != null && !class.getName().equals("java.lang.Object")){if(class.getSuperclass() != null &&class.getSuperclass().equals("java.lang.ClassLoader")){ return "该类为内存马";}class=class.getSuperclass(); }
内容识别
对上种方式提取的高风险类进行反编译,然后进行黑名单匹配。包含加解密、反射、命令执行等高危包名和类名
javax.crypto.java.lang.reflect.ProcessBuildergetRuntimeProcessBuildergetMethodgetDeclaredMethod.invokesetAccessibleClassLoadernewInstance
类篡改识别
前面提到Java Agent类型的内存马无法通过物理文件和内存文件的比对检测出来,针对这种篡改性质的内存马,我们可以通过技术手段将其识别出来。原理为,我们通过Java Agent技术添加一个ClassFileTransformer,把当前内存中的所有类的字节码保存下来。然后再读区该类对应的物理路径处的文件,再获得一个字节码。这两个字节码中,前者表示当前类在内存中的字节码,后者表示该类原本的字节码。两者比较字节码的hash,如果一致表示类没有被篡改过,如果不一致表示类已被篡改。
import java.io.InputStream;import java.lang.instrument.*;import java.security.ProtectionDomain;import java.security.MessageDigest;import java.util.*;
public class ModifiedClassDetector {
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception { System.out.println("[*] Starting modified class detection...");
// 收集类与当前字节码的映射 Map<Class<?>, byte[]> currentBytecodes = new HashMap<>(); inst.addTransformer(new DumpTransformer(currentBytecodes), true);
// 对所有类重新触发 transform,以便获取字节码 for (Class<?> clazz : inst.getAllLoadedClasses()) { if (inst.isModifiableClass(clazz)) { try { inst.retransformClasses(clazz); } catch (Throwable ignored) {} } }
// 对比原始字节码 for (Map.Entry<Class<?>, byte[]> entry : currentBytecodes.entrySet()) { Class<?> clazz = entry.getKey(); byte[] currentBytes = entry.getValue(); byte[] originalBytes = getOriginalClassBytes(clazz);
if (originalBytes != null && !Arrays.equals(hash(currentBytes), hash(originalBytes))) { System.out.println("[MODIFIED] " + clazz.getName()); } }
System.out.println("[*] Detection complete."); }
// Transformer 用于收集字节码 private static class DumpTransformer implements ClassFileTransformer { private final Map<Class<?>, byte[]> bytecodeMap;
DumpTransformer(Map<Class<?>, byte[]> bytecodeMap) { this.bytecodeMap = bytecodeMap; }
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { bytecodeMap.put(classBeingRedefined, classfileBuffer.clone()); return null; // 不修改字节码,仅收集 } }
// 读取原始 class 文件的字节码 private static byte[] getOriginalClassBytes(Class<?> clazz) { String resourcePath = clazz.getName().replace('.', '/') + ".class"; InputStream is = clazz.getClassLoader() != null ? clazz.getClassLoader().getResourceAsStream(resourcePath) : ClassLoader.getSystemResourceAsStream(resourcePath); if (is == null) return null; try { return is.readAllBytes(); } catch (Exception e) { return null; } }
// 计算 MD5 哈希 private static byte[] hash(byte[] data) { try { MessageDigest md = MessageDigest.getInstance("MD5"); return md.digest(data); } catch (Exception e) { throw new RuntimeException(e); } }}
对抗
最后分享一种对抗场景。现在有很多的内存马会删除/tmp/.java_pid文件,这样会导致后续的java agent注入不上。前段时间应急就遇到这种情况,最后dump了进程全部内存,而全部内存的无效数据非常多,给分析造成了很大的阻力。最近研究发现,java自带的工具可以破解/tmp/.java_pid的情况,这里分享出来
sudo java -classpath "$JAVA_HOME/lib/sa-jdi.jar" -Dsun.jvm.hotspot.tools.jcore.filter=sun.jvm.hotspot.tools.jcore.PackageNameFilter -Dsun.jvm.hotspot.tools.jcore.PackageNameFilter.pkgList=com sun.jvm.hotspot.tools.jcore.ClassDump 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》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论