Java安全之ClassLoader

CoLoo發表於2021-09-26

Java安全之ClassLoader

類載入機制

Java中的原始碼.java字尾檔案會在執行前被編譯成.class字尾檔案,檔案內的位元組碼的本質就是一個位元組陣列 ,它有特定的複雜的內部格式,Java類初始化的時候會呼叫java.lang.ClassLoader載入位元組碼,.class檔案中儲存著Java程式碼經轉換後的虛擬機器指令,當需要使用某個類時,虛擬機器將會載入它的.class檔案,並建立對應的class物件,將class檔案載入到虛擬機器的記憶體,而在JVM中類的查詢與裝載就是由ClassLoader完成的,而程式在啟動的時候,並不會一次性載入程式所要用的所有class檔案,而是根據程式的需要,來動態載入某個class檔案到記憶體當中的,從而只有class檔案被載入到了記憶體之後,才能被其它class所引用。所以ClassLoader就是用來動態載入class檔案到記憶體當中用的。

類載入方式

Java類載入方式分為顯式和隱式

顯式:利用反射或ClassLoader來動態載入一個類

隱式:通過new 一個類或者 類名.方法名返回一個類

示例程式碼

@Test
public void loadClassTest() throws Exception {
    //1、反射載入
    Class<?> aClass = Class.forName("java.lang.Runtime");
    System.out.println(aClass.getName());

    //2、ClassLoader載入
    Class<?> aClass1 = ClassLoader.getSystemClassLoader().loadClass("java.lang.ProcessBuilder");
    System.out.println(aClass1.getName());

}

那也就是其實可以通過ClassLoader.loadClass()代替Class.forName()來獲取某個類的class物件。

ClassLoader

ClassLoader(類載入器)主要作用就是將class檔案讀入記憶體,併為之生成對應的java.lang.Class物件

JVM中存在3個內建ClassLoader:

  1. BootstrapClassLoader 啟動類載入器 負責載入 JVM 執行時核心類,這些類位於 JAVA_HOME/lib/rt.jar 檔案中,我們常用內建庫 java.xxx.* 都在裡面,比如 java.util.*java.io.*java.nio.*java.lang.* 等等。
  2. ExtensionClassLoader 擴充套件類載入器 負責載入 JVM 擴充套件類,比如 swing 系列、內建的 js 引擎、xml 解析器 等等,這些庫名通常以 javax 開頭,它們的 jar 包位於 JAVA_HOME/lib/ext/*.jar 中
  3. AppClassLoader 系統類載入器 才是直接面向我們使用者的載入器,它會載入 Classpath 環境變數裡定義的路徑中的 jar 包和目錄。我們自己編寫的程式碼以及使用的第三方 jar 包通常都是由它來載入的。

除了Java自帶的ClassLoader外,還可以自定義ClassLoader,自定義的ClassLoader都必須繼承自java.lang.ClassLoader類,也包括Java提供的另外二個ClassLoader(Extension ClassLoader和App ClassLoader)在內,但是Bootstrap ClassLoader不繼承自ClassLoader,因為它不是一個普通的Java類,底層由C++編寫,已嵌入到了JVM核心當中,當JVM啟動後,Bootstrap ClassLoader也隨著啟動,負責載入完核心類庫後,並構造Extension ClassLoader和App ClassLoader類載入器。

類載入流程

類載入指的是在.java檔案編譯成.class位元組碼檔案後,當需要使用某個類時,虛擬機器將會載入它的.class檔案,將.class檔案讀入記憶體,並在記憶體中為之建立一個java.lang.Class物件。但是實現步驟看起來會比較空洞和概念化,暫時不去深入研究,理解類載入是做什麼的並瞭解載入過程即可。後續有剛需再去深入。

類載入大致分為三個步驟:載入、連線、初始化。

0x01 載入

類載入指的是將class檔案讀入記憶體,併為之建立一個java.lang.Class物件,即程式中使用任何類時,也就是任何類在載入進記憶體時,系統都會為之建立一個java.lang.Class物件,這個Class物件包含了該類的所有資訊,如Filed,Method等,系統中所有的類都是java.lang.Class的例項。
類的載入由類載入器完成,JVM提供的類載入器叫做系統類載入器,此外還可以通過自定義類載入器載入。
通常可以用如下幾種方式載入類的二進位制資料:

從本地檔案系統載入class檔案。

從JAR包中載入class檔案,如JAR包的資料庫啟驅動類。

通過網路載入class檔案。

把一個Java原始檔動態編譯並執行載入。

0x02 連結

連結階段負責把類的二進位制資料合併到JRE中,其又可分為如下三個階段:

  1. 驗證:確保載入的類資訊符合JVM規範,無安全方面的問題。
  2. 準備:為類的靜態Field分配記憶體,並設定初始值。
  3. 解析:將類的二進位制資料中的符號引用替換成直接引用。

0x03 初始化

類載入最後階段,若該類具有超類,則對其進行初始化,執行靜態初始化器和靜態初始化成員變數(如前面只初始化了預設值的static變數將會在這個階段賦值,成員變數也將被初始化

雙親委派機制

基本概念

前面提到了Java自帶3個ClassLoader,包括我們也可以實現自定義ClassLoader完成類載入,但是具體某個類的載入用的是哪個ClassLoader呢。這裡涉及到一個雙親委派機制(PS:這個我看網上有講的是委託也有是委派,個人覺得委派好聽,先這麼叫著:D)

這裡丟個圖,基本概述了雙親委派機制(先走藍色箭頭再走紅色箭頭)

雙親委派簡單理解:向上委派,向下載入

當一個.class檔案要被載入時。不考慮我們自定義類載入器,首先會在AppClassLoader中檢查是否載入過,如果有那就無需再載入了。如果沒有,那麼會拿到父載入器,然後呼叫父載入器的loadClass方法。父類中同理也會先檢查自己是否已經載入過,如果沒有再往上。注意這個類似遞迴的過程,直到到達Bootstrap classLoader之前,都是在檢查是否載入過,並不會選擇自己去載入。直到BootstrapClassLoader,已經沒有父載入器了,這時候開始考慮自己是否能載入了(向上委派); 如果自己無法載入,會下沉到子載入器去載入,一直到最底層(向下載入)。如果沒有任何載入器能載入,就會丟擲ClassNotFoundException異常。

為什麼?

那麼為什麼載入類的時候需要雙親委派機制呢?

採用雙親委派模式的是好處是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,通過這種層級關可以避免類的重複載入,當父親已經載入了該類時,就沒有必要子ClassLoader再載入一次。

其次是,如果有人想替換系統級別的類:String.java。篡改它的實現,在這種機制下這些系統的類已經被Bootstrap classLoader載入過了(為什麼?因為當一個類需要載入的時候,最先去嘗試載入的就是BootstrapClassLoader),所以其他類載入器並沒有機會再去載入,從一定程度上防止了危險程式碼的植入。

自定義ClassLoader

先看下ClassLoader這個類中的核心方法

ClassLoader核心方法

  1. loadClass(載入指定的Java類)

    一般實現這個方法的步驟是:執行findLoadedClass(String)去檢測這個class是不是已經載入過了。
    執行父載入器的loadClass方法。如果父載入器為null則jvm內建的載入器去替代,也就是Bootstrap ClassLoader。這也解釋了ExtClassLoaderparent為null,但仍然說Bootstrap ClassLoader是它的父載入器。如果向上委託父載入器沒有載入成功;則通過findClass(String)查詢。
    如果class在上面的步驟中找到了,引數resolve又是true的話那麼loadClass()又會呼叫resolveClass(Class)這個方法來生成最終的Class物件。

  2. findClass(查詢指定的Java類)

  3. findLoadedClass(查詢JVM已經載入過的類)

  4. defineClass(定義一個Java類)

  5. resolveClass(連結指定的Java類)

編寫自定義ClassLoader步驟

1、編寫一個類繼承ClassLoader抽象類;

2、重寫findClass()方法;

3、在findClass()方法中呼叫defineClass()方法即可實現自定義ClassLoader;

0x01 編寫測試類

package classloader;

public class test {
    public String hello(){
        return "hello, CoLoo!";
    }
}

0x02 編譯為.class檔案

0x03 .class轉換bytes

public class ByteClass {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("test.class");
        byte[] classBytes = IOUtils.readFully(fis, -1, false);
        System.out.println(Arrays.toString(classBytes));
    }
}

Output:

[-54, -2, -70, -66, 0, 0, 0, 52, 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, 9, 116, 101, 115, 116, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 13, 104, 101, 108, 108, 111, 44, 32, 67, 111, 76, 111, 111, 33, 1, 0, 16, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 116, 101, 115, 116, 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, 3, 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, 5, 0, 1, 0, 11, 0, 0, 0, 2, 0, 12]

0x04 自定義ClassLoader

package classloader;

import java.lang.reflect.Method;

public class ClassLoaderTest extends ClassLoader {

    private static String className = "classloader.test";
    //轉換byte後的位元組碼
    private static byte[] classBytes = new byte[]{54, -2, -70, -66, 0, 0, 0, 52, 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, 9, 116, 101, 115, 116, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 13, 104, 101, 108, 108, 111, 44, 32, 67, 111, 76, 111, 111, 33, 1, 0, 16, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 116, 101, 115, 116, 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, 3, 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, 5, 0, 1, 0, 11, 0, 0, 0, 2, 0, 12};


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //只處理classloader.test類
        if (name.equals(className)) {
            //呼叫definClass將一個位元組流定義為一個類。
            return defineClass(className, classBytes, 0, classBytes.length);
        }
        return super.findClass(name);
    }

    public static void main(String[] args) throws Exception {
        //建立載入器
        ClassLoaderTest clt = new ClassLoaderTest();
        //使用我們自定義的類去載入className
        Class clazz = clt.loadClass(className);
        //反射建立test類物件
        Object test = clazz.newInstance();
        //反射獲取方法
        Method method = test.getClass().getMethod("hello");
        //反射去呼叫執行方法
        String str = (String) method.invoke(test);
        System.out.println(str);

    }
}

執行了test類的hello方法

一些思考

上面自定義ClassLoader流程也可以小結一下

  1. 準備自定義類,編譯為.class檔案
  2. .class檔案內容專為bytes陣列
  3. 自定義ClassLoader,繼承ClassLoader類,重寫findClass()方法,在方法中定義對指定類的處理流程
  4. 主函式建立自定義ClassLoader物件並loadClass()指定類,如果自定義的ClassLoader完成了載入則會獲得該類的class物件,後續可通過反射來深入利用(如執行某個方法)。

那ClassLoader對於安全來說能做什麼?

1、個人這裡目前想到的是代替Class.forName(),通過ClassLoader.loadClass()獲取class物件

@Test
public void classLoaderRuntime() throws Exception {
    Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime");
    Runtime runtime = (Runtime) aClass.getMethod("getRuntime").invoke(aClass);
    runtime.exec("open -a Calculator");

}

2、載入惡意類

如webshell中(之前有次攻防捕捉到一個webshell裡面用到了classloader)或者記憶體馬中應該也可以用到,待研究。

3、... 應該還有很多。

Reference

上面很多內容都是參考下面文章學習到的,可能寫的並不是很正確,建議閱讀下面這些文章

javasec.org

https://www.cnblogs.com/nice0e3/p/13719903.html

https://blog.csdn.net/javazejian/article/details/73413292

https://blog.csdn.net/CNAHYZ/article/details/82219210

https://blog.csdn.net/briblue/article/details/54973413

https://blog.csdn.net/codeyanbao/article/details/82875064

相關文章