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:
- BootstrapClassLoader 啟動類加載器 負責加載 JVM 運行時核心類,這些類位於 JAVA_HOME/lib/rt.jar 文件中,我們常用內置庫 java.xxx.* 都在里面,比如
java.util.*
、java.io.*
、java.nio.*
、java.lang.*
等等。- ExtensionClassLoader 擴展類加載器 負責加載 JVM 擴展類,比如 swing 系列、內置的 js 引擎、xml 解析器 等等,這些庫名通常以 javax 開頭,它們的 jar 包位於 JAVA_HOME/lib/ext/*.jar 中
- 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中,其又可分為如下三個階段:
- 驗證:確保加載的類信息符合JVM規范,無安全方面的問題。
- 准備:為類的靜態Field分配內存,並設置初始值。
- 解析:將類的二進制數據中的符號引用替換成直接引用。
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核心方法
loadClass
(加載指定的Java類)一般實現這個方法的步驟是:執行
findLoadedClass(String)
去檢測這個class
是不是已經加載過了。
執行父加載器的loadClass
方法。如果父加載器為null則jvm內置的加載器去替代,也就是Bootstrap ClassLoader
。這也解釋了ExtClassLoader
的parent
為null,但仍然說Bootstrap ClassLoader
是它的父加載器。如果向上委托父加載器沒有加載成功;則通過findClass(String)
查找。
如果class在上面的步驟中找到了,參數resolve又是true的話那么loadClass()
又會調用resolveClass(Class)
這個方法來生成最終的Class對象。
findClass
(查找指定的Java類)
findLoadedClass
(查找JVM已經加載過的類)
defineClass
(定義一個Java類)
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流程也可以小結一下
- 准備自定義類,編譯為
.class
文件- 將
.class
文件內容專為bytes數組- 自定義ClassLoader,繼承
ClassLoader
類,重寫findClass()
方法,在方法中定義對指定類的處理流程- 主函數創建自定義
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)或者內存馬中應該也可以用到。
惡意類加載還是有必要深入學習一下,給后續學習內存馬和反序列化payload打個基礎。
首先上面提到了,關於顯隱式加載類是有些區別的,顯示加載時(反射)可以觸發該類的初始化從而調用靜態代碼塊執行,主要是因為使用java.lang.reflect
對類進行反射調用時,如果該類沒有初始化會先進行類的初始化;而隱式加載,如new,ClassLoader.getSystemClassLoader.loadClass()
,不會初始化類也就不執行靜態代碼塊中的內容。
下面簡單測試了幾個可能會用到的觸發類初始化的類加載方式,簡單列舉下。
- 默認
java.lang.ClassLoader
- loadClass() + newInstance()
- Class.forName()
- Class.forName(ClassName, true, ClassLoaderName) + newInstance()
org.apache.bcel.util.ClassLoader
也是fj經常用到的,需要注意的是在JDK8u251之后就沒有ClassLoader這個類了URLClassLoader
new
Classloader
package classloader;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
public class ClassLoaderBaseClass {
public static void main(String[] args) throws Exception {
// 1、ClassLoader.getSystemClassLoader().loadClass() + 反射newInstance() [+]
ClassLoader.getSystemClassLoader().loadClass("classloader.CalcBaseClass2").newInstance();
// 0x02 new [+]
CalcBaseClass2 calcBaseClass2 = new CalcBaseClass2();
// 0x03 Class.forName() [+]
Class.forName("classloader.CalcBaseClass2");
// 0x04 ClassLoader.getSystemClassLoader().loadClass() [-]
ClassLoaderTest.getSystemClassLoader().loadClass("classloader.CalcBaseClass2");
// 0x05 Class.forName(className, true, ClassLoaderName)
Class.forName("classloader.CalcBaseClass", true, java.lang.ClassLoader.getSystemClassLoader());
// 0x06 bcel
JavaClass clazz = Repository.lookupClass(CalcBaseClass2.class);
String code = Utility.encode(clazz.getBytes(), true);
System.out.println(code);
new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
}
}
calc
package classloader;
import java.io.IOException;
public class CalcBaseClass2 {
static {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
}
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