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
输出的目录
sd-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();
}
}
通过以上代码可以dump出com.example.filter.Myfilter的类文件,针对类文件,可以使用ASM框架进行分析是否为恶意类。
3.选择dump的类
一个JVM中包含了很多的类,但我们并不需要全部dump下来,copagent项目给出了一些内存马的特征点,可以按这些特征点有选择性的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;
}
}
通过以上代码,我们对dump的类进行了筛选,针对不同的内存马类型,可以更改检测特征点来提高检测效率。
4.类的筛选优化
之前类的筛选是针对内存马类的一些特征点(类名
、父类名
、实现的接口类名
)创建对应的黑名单,将符合要求的类dump下来,但有些内存马是不具备这些特点的。例如spring的controller内存马
,controller内存马
的实现实际上只需要向RequestMappingHandlerMapping类对象
中注册对应的恶意类和恶意方法,对应恶意类不需要继承具体的其他类或者实现其他类的接口,这就使得controller内存马没有了对应的特征。
针对这一问题,我们将使用javaagent技术来获取具体的类名,由于controller内存马对于类没有具体的特征,但是要想实现恶意交互功能,势必要将对应的类注册到RequestMappingHandlerMapping类对象
中,所以我们可以通过RequestMappingHandlerMapping类对象
获取到要dump的类名。
SpringMemShell这个项目中给出了一个mapping接口用于查看url到具体类名方法的映射关系
代码如下(原本只会输出patternsCondition路径,做了些改良加上了pathPatternsCondition相关路径):
@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
要从一个jvm中dump一个类下来有两种的方法,一种是通过上述所提到的通过jdi工具dump,另一种则是常用的通过Javaagent进行dump,两种方式体验下来主要有以下不同点:
1.兼容性,测试发现使用jdk1.8.65
的sa-jdi是无法从jdk1.8.71
环境的tomcat项目中dump类的,只能在同jdk版本项目中获取类的字节码,而javaagent则可以在不同的jdk版本下使用,也就意味着如果在其他jdk环境使用sa-jdi,则需要将检测工具进行对应jdk版本的重新编译。
2.类的类型,Javaagent获取类是通过Instrumentation
的getAllLoadedClasses()
方法获取到一个Class
数组,而jdi获取到的是InstanceKlass
类(InstanceKlass
是JVM中的概念,用来表示普通 Java 类的实例的元数据),两种类的类型不同意味着在对类进行筛选的时候方法和角度也会不一样(例如Class类可以通过getInterfaces()
获取到接口类,而InstanceKlass
中没有对应的方法获取)。
3.dump下来的字节码,在Agent 内存马的攻防之道中提到对于已有字节码缓存(attach过且transformer返回值不为null)的情况,攻击者通过redefineClasses
进行攻击以后,通过javaagent检测时无法检测到关键类被修改,同时会进行关键类的回滚,而jdi是直接从JVM中获取类字节码,所得到的就是当前类真实的字节码,不会被攻击者影响。
两种方式各有优劣,就准确度而言jdi明显好于javaagent,但兼容来看javaagent更胜一筹。
基于ASM进行分析
ASM 是一种通用 Java 字节码操作和分析框架。它可以用于修改现有的 class 文件或动态生成 class 文件。
在pom.xml中导入最新的ASM
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.7</version>
</dependency>
ASM的基本使用:
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);
}
}
上述代码会获取类名以及父类信息,以及类中含有的属性和方法名
输出:
被分析类的原始代码:
Runtime.getRuntime().exec()检测
首先通过MethodVisitor
类的visitMethodInsn
方法可以获取分析的类方法中所调用的类方法
在visitMethod
方法中先调用父类的visitMethod
方法,然后返回一个MethodVisitor
类,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); //先调用父类的visitMethod方法
return new ExecMethodVisitor(this.api); //返回一个MethodVisitor类,会触发这个类中的visitMethodInsn方法
}
}
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);
}
}
输出:
可以看到在dofilter方法中使用了java.io.PrintStream类的println方法和javax.servlet.FilterChain类的doFilter方法。
通过这种方法我们就可以来判断class文件中是否有使用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;"); //判断是否调用了Runtime.getRuntime().exec()
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);
}
}
反编译分析
除了通过ASM直接分析class文件以外,还可以通过CFR将class反编译以后进行分析
在pom.xml中添加依赖
<!-- https://mvnrepository.com/artifact/org.benf/cfr -->
<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进程里dump出类的class文件的小工具--dumpclass