Java类加载机制

类加载机制流程

Java 是⼀个依赖于 JVM(Java虚拟机)实现的跨平台的开发语⾔。Java 程序在运⾏前需要先编译成 class ⽂件 ,Java 类初始化的时候会调⽤java.lang.ClassLoader加载类字节码,ClassLoader 会调⽤ JVM 的 native ⽅法( defineClass0/1/2)来定义⼀个java.lang.Class实例。

JVM 将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize),链接又分为三个步骤,如图所示。

image-20220317204509743

  1. 装载:查找并加载类的二进制数据

  2. 链接:

    • 验证:确保被加载类的正确性
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
    • 解析:把类中的符号引用转换为直接引用
  3. 初始化:为类的静态变量赋予正确的初始值

java 编译器将.java文件编译成扩展名为.class的文件,.class文件中保存着 java 转换后,虚拟机将要执行的指令。当需要某个类的时候,java 虚拟机会加载.class文件,并创建对应的 class 对象,将 class 文件加载到虚拟机的内存,这个过程被称为类的加载。

类初始化时机

  • 隐式加载:new 创建类的实例

  • 显式加载:loaderClass、forName等

  • 访问某个类或接口的静态变量,或者对该静态变量赋值

  • 调用类的静态方法

  • 使用反射方式创建某个类或者接口对象的Class对象

  • 初始化一个类的子类(会首先初始化子类的父类)

  • JVM启动时标明的启动类,即文件名和类名相同的那个类

类加载器

JVM 的类加载是通过 ClassLoader 及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

  1. 引导类加载器(Bootstrap ClassLoader)

    这个类加载器负责将\lib⽬录下的类库加载到虚拟机内存中,⽤来加载 java 的核⼼库,此类加载器并不继承于java.lang.ClassLoader ,不能被java程序直接调⽤,代码是使⽤C++编写的, 是虚拟机⾃⾝的⼀部分

  2. 扩展类加载器(Extendsion ClassLoader)

    这个类加载器负责加载\lib\ext⽬录下的类库,⽤来加载 java 的扩展库,开发者可以直接使⽤这个类加载器

  3. 应⽤程序类加载器(Application ClassLoader)

    这个类加载器负责加载⽤⼾类路径(CLASSPATH)下的类库,⼀般我们编写的java类都是由这个类加载器加载,这个类加载器是 CLassLoader 中的getSystemClassLoader()⽅法的返回值,所以也称为系统类加载器,⼀般情况下这就是系统默认的类加载器

  4. 自定义类加载器(Custom ClassLoader)

    属于应用程序根据自身需要自定义的 ClassLoader,如 tomcat、jboss 都会根据 j2ee 规范自行实现ClassLoader,加载过程中会先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLoader 逐层检查,只要某个 classloader 已加载就视为已加载此类,保证此类在所有 ClassLoader 只加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

ClassLoader 类有如下核心方法:

  1. loadClass(加载指定的Java类)
  2. findClass(查找指定的Java类)
  3. findLoadedClass(查找JVM已经加载过的类)
  4. defineClass(定义一个Java类)
  5. resolveClass(链接指定的Java类)

类加载器使用顺序

在 JVM 虚拟机中,如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

也就是说,对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。这就是双亲委派模型

为什么需要双亲委派模型呢?这可以提高Java的安全性,以及防止程序混乱。

  1. 提高安全性

    假设我们使用一个第三方 Jar 包,该 Jar 包中自定义了一个 String 类,它的功能和系统 String 类的功能相同,但是加入了恶意代码。那么,JVM 会加载这个自定义的 String 类,从而在我们所有用到 String 类的地方都会执行该恶意代码。
    如果有双亲委派模型,自定义的 String 类是不会被加载的,因为最顶层的类加载器会首先加载系统的 java.lang.String 类,而不会加载自定义的 String 类,防止了恶意代码的注入。

  2. 防止程序混乱

    假设用户编写了一个 java.lang.String 的同名类,如果每个类加载器都自己加载的话,那么会出现多个 String 类,导致混乱。如果本加载器加载了,父加载器则不加载,那么以哪个加载的为准又不能确定了,也增加了复杂度。

类加载方式

Java 类加载方式分为显式隐式显式即我们通常使用Java反射或者ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。

常用的类动态加载方式:

// 反射加载HelloWorld示例
Class.forName("com.h4ckfun.HelloWorld");

// ClassLoader加载HelloWorld示例
this.getClass().getClassLoader().loadClass("com.h4ckfun.HelloWorld");
复制代码

Class.forName()是⼀个静态⽅法,最常⽤的是Class.forname(String className);根据传⼊的类的全限定名返回⼀个 Class 对象。该⽅法在将 Class ⽂件加载到内存的同时,会执⾏类的初始化。

注: A a = (A)Class.forName("package.A").newInstance();A a = new A(); 是⼀样的效果,它们的区别在于创建对象的⽅式不⼀样,前者是使⽤类加载机制,后者是创建⼀个新类。

ClassLoader.loadClass默认不会初始化类方法。

自定义类加载器

我们先自己编写一个需要被加载的类:

Test.java

package com.h4ckfun;

public class Test {
    public Test() {
        System.out.println("Test: " + getClass().getClassLoader());
        System.out.println("Test Parent: " + getClass().getClassLoader().getParent());
    }

    public String hello() {
        return "Hello World!";
    }
}
复制代码

使用javac编译为 class 文件:

javac Test.java
复制代码

使用javap查看反汇编:

javap -c -p -l Test.class
复制代码

image-20220318151156297

我们可以自定义类加载器,只需继承 ClassLoader 抽象类,并重写 findClass 方法(如果要打破双亲委派模型,需要重写 loadClass 方法)。

**注意:**一般尽量不要覆写已有的 loadClass 方法中的委派逻辑,一般在 JDK1.2 之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在 JVM 规范和 JDK 文档中(1.2 或者以后版本中),都没有建议用户覆写 loadClass 方法,相反,明确提示开发者在开发自定义的类加载器时覆写 findClass 逻辑。

具体原因可以看 loadClass 的源码:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
复制代码

这个是 ClassLoader 中的 loadClass 方法,大致流程如下:

  1. 检查类是否已加载,如果是则不用再重新加载了
  2. 如果未加载,则通过父类加载(依次递归)或者启动类加载器(bootstrap)加载
  3. 如果还未找到,则调用本加载器的findClass方法

而 ClassLoader 中 findClass 方法源码如下:

		protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
复制代码

直接返回 ClassNotFoundException,并且注释中明确说明自定义类加载器时需要覆写该方法。

我们自己写一个自定义加载器:

package com.h4ckfun;

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

class HelloWorldClassLoader extends ClassLoader {
    // Test类名
    private String testClassName;
    // Test类字节码
    private byte[] testClassBytes;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            return defineClass(name, testClassBytes, 0, testClassBytes.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    private byte[] readByte(String classPath) throws Exception {
        FileInputStream fis = new FileInputStream(classPath);
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

    public static void main(String[] args) {
        // 创建自定义的类加载器
        HelloWorldClassLoader loader = new HelloWorldClassLoader();
        try {
            loader.testClassName = "com.h4ckfun.Test";
            loader.testClassBytes = loader.readByte("/tmp/Test.class");
            System.out.println("class byte: " + Arrays.toString(loader.testClassBytes));
            // 使用自定义的类加载器加载Test类
            Class testClass = loader.loadClass(loader.testClassName);
            // 反射创建Test类,等价于 Test t = new Test();
            Object testInstance = testClass.newInstance();
            // 反射获取hello方法
            Method method = testInstance.getClass().getMethod("hello");
            // 反射调用hello方法,等价于 String str = t.hello();
            String str = (String) method.invoke(testInstance);
            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码

代码中注意重写 findClass 方法,然后进行自定义加载前注意要把之前生成的Test.class移动到 CLASSPATH 之外的目录,这里我移动到了/tmp/Test.class,还有Test.java如果和加载器是同一项目下,记得删除或者重命名为其他文件,比如Test.java.bak,否则 Test 类会被 AppClassLoader 加载(自定义类加载器的 parent 是 AppClassLoader)。

加载结果:

image-20220318151722702

远程类加载

URLClassLoader继承了ClassLoaderURLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。

我们先写一个被远程执行的恶意类:

import java.io.IOException;

public class Cmd {
    public static Process exec(String cmd) throws IOException {
        return Runtime.getRuntime().exec(cmd);
    }
}

复制代码

然后使用javac编译为 class 文件,单独打包为 jar 包:

jar cvf Cmd.jar Cmd.class
复制代码

image-20220318154942195

Cmd.java重命名为非java文件后,简单使用 python 启动 web 服务:

python3 -m http.server 9000
复制代码

编写URLClassLoader的代码:

package com.h4ckfun;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URLClassLoader;
import java.net.URL;

public class RemoteURLClassLoader {

    public static void main(String[] args) {
        try {
            // 定义远程加载的jar路径
            URL url = new URL("http://127.0.0.1:9000/Cmd.jar");
            // 创建URLClassLoader对象,并加载远程jar包
            URLClassLoader ucl = new URLClassLoader(new URL[]{url});
            // 定义需要执行的系统命令
            String cmd = "whoami";
            // 通过URLClassLoader加载远程jar包中的CMD类
            Class cmdClass = ucl.loadClass("Cmd");
            // 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
            Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);

            // 获取命令执行结果的输入流
            InputStream in   = process.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[]                b    = new byte[1024];
            int                   a    = -1;

            // 读取命令执行结果
            while ((a = in.read(b)) != -1) {
                baos.write(b, 0, a);
            }
            // 输出命令执行结果
            System.out.println(baos.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码

然后执行远程类加载:

image-20220318155111267

image-20220318155301984

Reference

javasec.org/javase/Clas…

欢迎关注公众号,获取最新安全文章

Java安全从0到1 – h4ck fun

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享