java内存马检测基础方法
字数 1312 2025-08-24 07:48:22

Java内存马检测基础方法

核心思路

  1. 利用JDK提供的sa-jdi API基于黑名单dump存在于JVM中的真实字节码
  2. 基于ASM框架进行分析
  3. 反编译检测

sa-jdi的基本使用

1. 通过命令行dump JVM中的class类

ClassDump工具可以设置两个System properties:

  • sun.jvm.hotspot.tools.jcore.filter: Filter的类名
  • sun.jvm.hotspot.tools.jcore.outputDir: 输出的目录

sa-jdi.jar中提供了sun.jvm.hotspot.tools.jcore.PackageNameFilter,可以指定dump哪些包里的类。PackageNameFilter中有一个System property可以指定过滤哪些包:
sun.jvm.hotspot.tools.jcore.PackageNameFilter.pkgList

使用示例:

java -classpath "D:\env\Java\jdk1.8.0_65\lib\sa-jdi.jar" 
-Dsun.jvm.hotspot.tools.jcore.filter=sun.jvm.hotspot.tools.jcore.PackageNameFilter 
-Dsun.jvm.hotspot.tools.jcore.PackageNameFilter.pkgList=com.example.filter 
-Dsun.jvm.hotspot.tools.jcore.outputDir=D:\ClassOut 
sun.jvm.hotspot.tools.jcore.ClassDump 16992

其中:

  • com.example.filter表示需要dump的包
  • D:\ClassOut表示输出文件的路径
  • 16992表示需要dump的JVM进程pid

2. 通过API dump JVM中的class类

代码示例:

import sun.jvm.hotspot.HotSpotAgent;
import sun.jvm.hotspot.oops.InstanceKlass;
import sun.jvm.hotspot.tools.jcore.ClassDump;
import sun.jvm.hotspot.tools.jcore.ClassFilter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class JdiDemo {
    static class MyFilter implements ClassFilter {
        @Override
        public boolean canInclude(InstanceKlass kls) {
            String klassName = kls.getName().asString();
            return klassName.equals("com/example/filter/Myfilter"); //指定dump的类
        }
    }

    public static void main(String[] args) throws Exception {
        int pid = 17072; //要dump的JVM的pid
        ClassFilter filter = new MyFilter();
        ClassDump classDump = new ClassDump();
        classDump.setClassFilter(filter);
        classDump.setOutputDirectory("D:\\ClassOut"); //输出位置
        
        Class<?> toolClass = Class.forName("sun.jvm.hotspot.tools.Tool");
        Method method = toolClass.getDeclaredMethod("start", String[].class);
        method.setAccessible(true);
        String[] params = new String[]{String.valueOf(pid)};
        
        try {
            method.invoke(classDump, (Object)params); //通过反射调用start方法
        } catch (Exception ignored) {
            System.out.println(ignored);
            return;
        }
        
        System.out.println("dump class finish");
        Field field = toolClass.getDeclaredField("agent");
        field.setAccessible(true);
        HotSpotAgent agent = (HotSpotAgent)field.get(classDump);
        agent.detach();
    }
}

3. 选择dump的类

一个JVM中包含了很多类,但不需要全部dump下来。可以按照内存马的特征点有选择性地dump类。

示例筛选器:

static class MyFilter implements ClassFilter {
    private static String[] dginterfaces = {
        "javax/servlet/Filter",
        "javax/servlet/Servlet",
        "javax/servlet/ServletRequestListener"
    };
    
    private static String[] riskPackage = {
        "net/rebeyond/",
        "com/metasploit/"
    };
    
    private static String[] riskSuperClassesName = {
        "javax/servlet/http/HttpServlet"
    };

    @Override
    public boolean canInclude(InstanceKlass kls) {
        String klassName = kls.getName().asString();
        
        //类名黑名单判断
        if (klassName.equals("org/springframework/web/servlet/handler/AbstractHandlerMapping")){
            System.out.println(klassName);
            return true;
        }
        
        for (String rp : riskPackage) {
            if (klassName.startsWith(rp)){
                System.out.println(klassName);
                return true;
            }
        }
        
        //接口黑名单
        KlassArray interfaces = kls.getTransitiveInterfaces();
        int len = interfaces.length();
        for (int i = 0; i < len; ++i) {
            for (String df : dginterfaces){
                if (interfaces.getAt(i).getName().asString().equals(df)){
                    System.out.println(klassName);
                    return true;
                }
            }
        }
        
        //父类黑名单
        Klass spuperklass = kls.getSuper();
        if (spuperklass != null){
            for (String rsk : riskSuperClassesName) {
                if (rsk.equals(spuperklass.getName().asString())){
                    System.out.println(klassName);
                    return true;
                }
            }
        }
        return false;
    }
}

4. 类的筛选优化

对于Spring的controller内存马,由于不需要继承特定类或实现特定接口,可以通过javaagent技术获取类名。controller内存马需要将类注册到RequestMappingHandlerMapping类对象中,因此可以通过RequestMappingHandlerMapping获取要dump的类名。

示例代码:

@RequestMapping("/mappings")
@ResponseBody
public String mappings(){
    try {
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes()
            .getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        RequestMappingHandlerMapping rmhMapping = context.getBean(RequestMappingHandlerMapping.class);
        
        Field _mappingRegistry = AbstractHandlerMethodMapping.class.getDeclaredField("mappingRegistry");
        _mappingRegistry.setAccessible(true);
        Object mappingRegistry = _mappingRegistry.get(rmhMapping);
        
        Field _registry = mappingRegistry.getClass().getDeclaredField("registry");
        _registry.setAccessible(true);
        HashMap<Object, Object> registry = (HashMap<Object, Object>)_registry.get(mappingRegistry);
        
        Class<?>[] tempArray = AbstractHandlerMethodMapping.class.getDeclaredClasses();
        Class<?> mappingRegistrationClazz = null;
        for (Class<?> item : tempArray) {
            if (item.getName().equals("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistration")) {
                mappingRegistrationClazz = item;
            }
        }
        
        StringBuilder sb = new StringBuilder();
        sb.append("<pre>");
        sb.append("| path |").append("\t").append("\t").append("| info |").append("\n");
        
        for (Map.Entry<Object, Object> entry : registry.entrySet()){
            sb.append("-");
            sb.append("\n");
            RequestMappingInfo key = (RequestMappingInfo)entry.getKey();
            
            if (key.getPatternsCondition()!=null){
                List<String> tempList = new ArrayList<>(key.getPatternsCondition().getPatterns());
                sb.append(tempList.get(0)).append("\t").append("-->").append("\t");
            }
            
            if (key.getPathPatternsCondition()!=null){
                List<PathPattern> tempList = new ArrayList<>(key.getPathPatternsCondition().getPatterns());
                sb.append(tempList.get(0).toString()).append("\t").append("-->").append("\t");
            }
            
            Field _handlerMethod = mappingRegistrationClazz.getDeclaredField("handlerMethod");
            _handlerMethod.setAccessible(true);
            HandlerMethod handlerMethod = (HandlerMethod)_handlerMethod.get(entry.getValue());
            
            Field _desc = handlerMethod.getClass().getDeclaredField("description");
            _desc.setAccessible(true);
            String desc = (String)_desc.get(handlerMethod);
            sb.append(desc);
            sb.append("\n");
        }
        
        sb.append("</pre>");
        return sb.toString();
    } catch (Exception e){
        e.printStackTrace();
    }
    return "";
}

5. sa-jdi VS javaagent

比较点 sa-jdi javaagent
兼容性 只能在同jdk版本中使用 可以在不同jdk版本下使用
类的类型 获取的是InstanceKlass类 获取的是Class类
dump的字节码 直接从JVM获取真实字节码 可能被攻击者影响
准确度 更高 较低

基于ASM进行分析

ASM是一种通用Java字节码操作和分析框架,可用于修改现有class文件或动态生成class文件。

1. 基本使用

添加依赖:

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.7</version>
</dependency>

示例代码:

import org.objectweb.asm.*;
import java.io.File;
import java.io.FileInputStream;

class ClassPrinter extends ClassVisitor {
    public ClassPrinter() {
        super(Opcodes.ASM7);
    }
    
    //访问类的基本结构,如类名、访问修饰符、父类和接口
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + " {");
    }
    
    //访问类的源文件信息
    public void visitSource(String source, String debug) {}
    
    //访问当前类是另一个类的成员类的情况
    public void visitOuterClass(String owner, String name, String desc) {}
    
    //访问类级别的注解
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return null;
    }
    
    //访问类级别的其他属性
    public void visitAttribute(Attribute attr) {}
    
    //访问内部类信息
    public void visitInnerClass(String name, String outerName, String innerName, int access) {}
    
    //访问类的字段(属性)
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        System.out.println(" " + desc + " " + name);
        return null;
    }
    
    //访问类的方法
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println(" " + name + desc);
        return null;
    }
    
    //表示类的访问结束
    public void visitEnd() {
        System.out.println("}");
    }
}

public class Analysis {
    public static void doAnalysis(String classpath) throws Exception {
        File classFile = new File(classpath);
        ClassReader cr = new ClassReader(new FileInputStream(classFile));
        ClassPrinter cp = new ClassPrinter();
        cr.accept(cp, ClassReader.SKIP_DEBUG);
    }
}

2. Runtime.getRuntime().exec()检测

通过MethodVisitor类的visitMethodInsn方法可以获取分析的类方法中所调用的类方法。

示例代码:

class ExecClassVisitor extends ClassVisitor {
    public ExecClassVisitor(int api) {
        super(api);
    }
    
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println(name + desc);
        super.visitMethod(access, name, desc, signature, exceptions);
        return new ExecMethodVisitor(this.api);
    }
}

class ExecMethodVisitor extends MethodVisitor {
    public ExecMethodVisitor(int api) {
        super(api);
    }
    
    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface){
        System.out.println("owner:" + owner + " name:" + name + " descriptor:" + descriptor);
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }
}

检测Runtime.getRuntime().exec()的代码:

class ExecClassVisitor extends ClassVisitor {
    public ExecClassVisitor(int api) {
        super(api);
    }
    
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        super.visitMethod(access, name, desc, signature, exceptions);
        return new ExecMethodVisitor(this.api);
    }
}

class ExecMethodVisitor extends MethodVisitor {
    public ExecMethodVisitor(int api) {
        super(api);
    }
    
    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface){
        boolean runtimeCondition = owner.equals("java/lang/Runtime") 
            && name.equals("exec") 
            && descriptor.equals("([Ljava/lang/String;)Ljava/lang/Process;");
        
        if (runtimeCondition) {
            System.out.println("owner:" + owner + " name:" + name + " descriptor:" + descriptor);
            System.out.println("The class has used Runtime.getRuntime().exec()");
        }
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }
}

反编译分析

可以使用CFR工具将class文件反编译后进行分析。

添加依赖:

<dependency>
    <groupId>org.benf</groupId>
    <artifactId>cfr</artifactId>
    <version>0.152</version>
</dependency>

反编译代码示例:

import org.benf.cfr.reader.api.CfrDriver;
import org.benf.cfr.reader.util.getopt.OptionsImpl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class CFRer {
    public static Long cfr(String source, String targetPath) throws IOException {
        Long start = System.currentTimeMillis();
        
        // source jar
        List<String> files = new ArrayList<>();
        files.add(source);
        
        // target dir
        HashMap<String, String> outputMap = new HashMap<>();
        outputMap.put("outputdir", targetPath);
        
        OptionsImpl options = new OptionsImpl(outputMap);
        CfrDriver cfrDriver = new CfrDriver.Builder().withBuiltOptions(options).build();
        cfrDriver.analyse(files);
        
        Long end = System.currentTimeMillis();
        return end - start;
    }
}

通过判断反编译后的java文件中的字符串,也可以作为判断内存马的一种方法。

Java内存马检测基础方法 核心思路 利用JDK提供的sa-jdi API基于黑名单dump存在于JVM中的真实字节码 基于ASM框架进行分析 反编译检测 sa-jdi的基本使用 1. 通过命令行dump JVM中的class类 ClassDump工具可以设置两个System properties: sun.jvm.hotspot.tools.jcore.filter : Filter的类名 sun.jvm.hotspot.tools.jcore.outputDir : 输出的目录 sa-jdi.jar中提供了 sun.jvm.hotspot.tools.jcore.PackageNameFilter ,可以指定dump哪些包里的类。PackageNameFilter中有一个System property可以指定过滤哪些包: sun.jvm.hotspot.tools.jcore.PackageNameFilter.pkgList 使用示例: 其中: com.example.filter 表示需要dump的包 D:\ClassOut 表示输出文件的路径 16992 表示需要dump的JVM进程pid 2. 通过API dump JVM中的class类 代码示例: 3. 选择dump的类 一个JVM中包含了很多类,但不需要全部dump下来。可以按照内存马的特征点有选择性地dump类。 示例筛选器: 4. 类的筛选优化 对于Spring的controller内存马,由于不需要继承特定类或实现特定接口,可以通过javaagent技术获取类名。controller内存马需要将类注册到 RequestMappingHandlerMapping 类对象中,因此可以通过 RequestMappingHandlerMapping 获取要dump的类名。 示例代码: 5. sa-jdi VS javaagent | 比较点 | sa-jdi | javaagent | |--------|--------|-----------| | 兼容性 | 只能在同jdk版本中使用 | 可以在不同jdk版本下使用 | | 类的类型 | 获取的是InstanceKlass类 | 获取的是Class类 | | dump的字节码 | 直接从JVM获取真实字节码 | 可能被攻击者影响 | | 准确度 | 更高 | 较低 | 基于ASM进行分析 ASM是一种通用Java字节码操作和分析框架,可用于修改现有class文件或动态生成class文件。 1. 基本使用 添加依赖: 示例代码: 2. Runtime.getRuntime().exec()检测 通过 MethodVisitor 类的 visitMethodInsn 方法可以获取分析的类方法中所调用的类方法。 示例代码: 检测 Runtime.getRuntime().exec() 的代码: 反编译分析 可以使用CFR工具将class文件反编译后进行分析。 添加依赖: 反编译代码示例: 通过判断反编译后的java文件中的字符串,也可以作为判断内存马的一种方法。