Android加殼脫殼學習—動態載入和類載入機制詳解

語言: CN / TW / HK

本文為看雪論壇優秀文章
看雪論壇作者ID:隨風而行aa

前言

最近一直在學習Android 加殼和脫殼,在進行Android加殼和脫殼的學習中,第一步便是深入理解類載入器和動態載入二者之間的關係。

本文詳細的介紹了類載入器和動態載入之間的關係和原理,之所以詳細的講解兩者之間的關係,一是學習脫殼和加殼的需要,二是為後面Android外掛化漏洞挖掘的講解做鋪墊。

類載入器

Android中的類載入器機制與JVM一樣遵循雙親委派模式。

1.雙親委派模式

(1)雙親委派模式定義

(1)載入.class檔案時,以遞迴的形式逐級向上委託給父載入器ParentClassLoader載入,如果載入過了,就不用再載入一遍
(2)如果父載入器沒有載入過,繼續委託給父載入器去載入,一直到這條鏈路的頂級,頂級ClassLoader如果沒有載入過,則嘗試載入,載入失敗,則逐級向下交還呼叫者載入

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//1.先檢查是否已經載入過--findLoaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//2.如果自己沒載入過,存在父類,則委託父類
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}


if (c == null) {
//3.如果父類也沒載入過,則嘗試本級classLoader載入
c = findClass(name);
}
}
return c;
}

程式碼解釋:

① 先檢查自己是否已經載入過class檔案,用findLoadedClass方法,如果已經載入了直接返。

② 如果自己沒有載入過,存在父類,則委派父類去載入,用parent.loadClass(name,false)方法,此時會向上傳遞,然後去父載入器中迴圈第1步,一直到頂級ClassLoader。

③ 如果父類沒有載入,則嘗試本級classLoader載入,如果載入失敗了就會向下傳遞,交給呼叫方式實現.class檔案的載入。

(2)雙親委派模式載入流程

(3)雙親委派的作用

① 防止同一個.class檔案重複載入。

② 對於任意一個類確保在虛擬機器中的唯一性。由載入它的類載入器和這個類的全類名一同確立其在Java虛擬機器中的唯一性。

③ 保證.class檔案不被篡改,通過委派方式可以保證系統類的載入邏輯不被篡改。

2. Android中類載入機制

(1)Android基本類預載入

我們瞭解Android基本類預載入,首先我們回顧上文的Dalvik虛擬機器啟動相關:

我們執行app_process程式,進入main函式裡面,然後進行AndroidRuntime::start:

Zygote native 程序主要工作:

① 建立虛擬機器–startVM

② 註冊JNI函式–startReg

③ 通過JNI知道Java層的com.android.internal.os.ZygoteInit 類,呼叫main 函式,進入java 世界

然後進入Java層:

Zygote總結:

① 解析init.zygote.rc中的引數,建立AppRuntime並呼叫AppRuntime.start()方法。

② 呼叫AndroidRuntime的startVM()方法建立虛擬機器,再呼叫startReg()註冊JNI函式。

③ 通過JNI方式呼叫ZygoteInit.main(),第一次進入Java世界。

④ registerZygoteSocket()建立socket通道,zygote作為通訊的服務端,用於響應客戶端請求。

⑤ preload()預載入通用類、drawable和color資源、openGL以及共享庫以及WebView,用於提高app啟動效率。

⑥ 通過startSystemServer(),fork得力幫手system_server程序,也是Java Framework的執行載體(下面講到system server再詳細講解)。

⑦ 呼叫runSelectLoop(),隨時待命,當接收到請求建立新程序請求時立即喚醒並執行相應工作。

Android的類載入機制和JVM一樣遵循雙親委派模式,在dalvik/art啟動時將所有Java基本類和Android系統框架的基本類載入進來,預載入的類記錄在/frameworks/base/config/preloaded-classes中。

android.R$styleable
android.accessibilityservice.AccessibilityServiceInfo$1
android.accessibilityservice.AccessibilityServiceInfo
android.accessibilityservice.IAccessibilityServiceClient$Stub$Proxy
android.accessibilityservice.IAccessibilityServiceClient$Stub
android.accessibilityservice.IAccessibilityServiceClient
android.accounts.AbstractAccountAuthenticator$Transport
android.accounts.AbstractAccountAuthenticator
android.accounts.Account$1
android.accounts.Account
...


java.lang.Short
java.lang.StackOverflowError
java.lang.StackTraceElement
java.lang.StrictMath
java.lang.String$1
java.lang.String$CaseInsensitiveComparator
java.lang.String
java.lang.StringBuffer
java.lang.StringBuilder
java.lang.StringFactory
java.lang.StringIndexOutOfBoundsException
java.lang.System$PropertiesWithNonOverrideableDefaults
java.lang.System
java.lang.Thread$1
...

這些類只需要在Zygote程序啟動時載入一遍就可以了,後續沒一個APP或Android執行時環境的程序,都是從Zygote中fork出來,天然保留了載入過的類快取。

ZygoteInit.preload()

static void preload(TimingsTraceLog bootTimingsTraceLog) {
// ...省略
preloadClasses();
// ...省略
}


private static void preloadClasses() {
final VMRuntime runtime = VMRuntime.getRuntime();


// 讀取 preloaded_classes 檔案
InputStream is;
try {
is = new FileInputStream(PRELOADED_CLASSES);
} catch (FileNotFoundException e) {
Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
return;
}


// ...省略


try {
BufferedReader br =
new BufferedReader(new InputStreamReader(is), Zygote.SOCKET_BUFFER_SIZE);


int count = 0;
String line;
while ((line = br.readLine()) != null) {
// Skip comments and blank lines.
line = line.trim();
if (line.startsWith("#") || line.equals("")) {
continue;
}


Trace.traceBegin(Trace.TRACE_TAG_DALVIK, line);
try {
// 逐行載入基本類
Class.forName(line, true, null);
count++;
// ...省略
} catch (Throwable t) {
// ...省略
}
}


// ...省略
} catch (IOException e) {
Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
} finally {
// ...省略
}
}

(2)Android類載入器層級關係及分析

Android中的ClassLoader型別分為系統ClassLoader和自定義ClassLoader。其中系統ClassLoader包括3種是BootClassLoader、DexClassLoader、PathClassLoader。

① BootClassLoader:Android平臺上所有Android系統啟動時會使用BootClassLoader來預載入常用的類。

② BaseDexClassLoader:實際應用層類檔案的載入,而真正的載入委託給pathList來完成。

③ DexClassLoader:可以載入dex檔案以及包含dex的壓縮檔案(apk,dex,jar,zip),可以安裝一個未安裝的apk檔案,一般為自定義類載入器。

④ PathClassLoader:可以載入系統類和應用程式的類,通常用來載入已安裝的apk的dex檔案。

補充:

Android 提供的原生載入器叫做基礎類載入器,包括:BootClassLoader,PathClassLoader,DexClassLoader,InMemoryDexClassLoader(Android 8.0 引入),DelegateLastClassLoader(Android 8.1 引入)。

<1> BootClassLoader

啟動類載入器,用於載入 Zygote 程序已經預載入的基本類,可以推測它只需從快取中載入。這是基類 ClassLoader 的一個內部類,是包訪問許可權,所以應用程式無權直接訪問。

public abstract class ClassLoader {
// ...省略


class BootClassLoader extends ClassLoader {
private static BootClassLoader instance;


public static synchronized BootClassLoader getInstance() {
if (instance == null) {
instance = new BootClassLoader();
}


return instance;
}


public BootClassLoader() {
super(null);
}


@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return Class.classForName(name, false, null);
}


// ...省略


@Override
protected Class<?> loadClass(String className, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);


if (clazz == null) {
clazz = findClass(className);
}


return clazz;
}


// ...省略
}
}

原始碼分析:

我們可以看見,BootClassLoader沒有父載入器,在快取取不到類是直接呼叫自己的findClass()方法。

findClass()方法呼叫Class.classForName()方法,而ZygoteInit.preloadClasses()中,載入基本類是Class.forName()。

ublic final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {
// ...省略


public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName(className, true, ClassLoader.getClassLoader(caller));
}


public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
if (loader == null) {
loader = BootClassLoader.getInstance();
}
Class<?> result;
try {
result = classForName(name, initialize, loader);
} catch (ClassNotFoundException e) {
Throwable cause = e.getCause();
if (cause instanceof LinkageError) {
throw (LinkageError) cause;
}
throw e;
}
return result;
}


// 本地方法
static native Class<?> classForName(String className, boolean shouldInitialize,
ClassLoader classLoader) throws ClassNotFoundException;


// ...省略
}

我們可以發現,預載入時,ZygoteInit.preloadClasses()中呼叫Class.forName(),實際是指定BootClassLoader為類載入器,且只需要在預載入的時候進行類初始化,只需要一次。

總之,通過 Class.forName() 或者 Class.classForName() 可以且僅可以直接載入基本類,一旦基本類預載入後,對於應用程式而言,我們雖然不能直接訪問BootClassLoader,但可以通過Class.forName/Class.classForName載入。

無論是系統類載入器(PathClassLoader)還是自定義的類載入器(DexClassLoader),最頂層的祖先載入器預設是 BootClassLoader,與 JVM 一樣,保證了基本類的型別安全。

Class檔案載入:

① 通過Class.forName()方法動態載入

② 通過ClassLoader.loadClass()方法動態載入

類的載入分為3個步驟:1.裝載(Load),2.連結(Link),3.初始化(Intialize)

類載入時機:

1.隱式載入:

① 建立類的例項,也就是new一個物件

② 訪問某個類或介面的靜態變數,或者對該靜態變數賦值

③ 呼叫類的靜態方法

④ 反射Class.forName("android.app.ActivityThread")

⑤ 初始化一個類的子類(會首先初始化子類的父類)

2.顯示載入:

① 使用LoadClass()載入

② 使用forName()載入

<2> PathClassLoader

主要用於系統和app的類載入器,其中optimizedDirectory為null, 採用預設目錄/data/dalvik-cache/。

PathClassLoader 是作為應用程式的系統類載入器,也是在 Zygote 程序啟動的時候初始化的(基本流程為:ZygoteInit.main() -> ZygoteInit.forkSystemServer() -> ZygoteInit.handleSystemServerProcess() -> ZygoteInit.createPathClassLoader()。在預載入基本類之後執行),所以每一個 APP 程序從 Zygote 中 fork 出來之後都自動攜帶了一個 PathClassLoader,它通常用於載入 apk 裡面的 .dex 檔案。

<3> DexClassLoader

可以從包含classes.dex的jar或者apk中,載入類的類載入器, 可用於執行動態載入, 但必須是app私有可寫目錄來快取odex檔案. 能夠載入系統沒有安裝的apk或者jar檔案, 因此很多熱修復和外掛化方案都是採用DexClassLoader。

public class
DexClassLoader extends BaseDexClassLoader {


public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}

總結:

我們可以發現DexClassLoader與PathClassLoader都繼承於BaseDexClassLoader,這兩個類只是提供了自己的建構函式,沒有額外的實現。

區別:

DexClassLoader提供了optimizedDirectory,而PathClassLoader則沒有,optimizedDirectory正是用來存放odex檔案的地方,所以可以利用DexClassLoader實現動態載入。

<4> BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList; //記錄dex檔案路徑資訊


public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
}

dexPath: 包含目標類或資源的apk/jar列表;當有多個路徑則採用:分割;

optimizedDirectory: 優化後dex檔案存在的目錄, 可以為null;

libraryPath: native庫所在路徑列表;當有多個路徑則採用:分割;

ClassLoader:父類的類載入器。

BaseDexClassLoader會初始化dexPathList,收集dex檔案和Native檔案動態庫。

初始化:

DexPathList:

該類主要用來查詢Dex、SO庫的路徑,並這些路徑整體呈一個數組。

final class DexPathList {
private Element[] dexElements;
private final List<File> nativeLibraryDirectories;
private final List<File> systemNativeLibraryDirectories;


final class DexPathList {
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();


//記錄所有的dexFile檔案
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);


//app目錄的native庫
this.nativeLibraryDirectories = splitPaths(libraryPath, false);
//系統目錄的native庫
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
//記錄所有的Native動態庫
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, suppressedExceptions);
...
}
}

DexPathList初始化過程,主要收集以下兩個變數資訊:

(1)dexElements: 根據多路徑的分隔符“;”將dexPath轉換成File列表,記錄所有的dexFile

(2)nativeLibraryPathElements: 記錄所有的Native動態庫, 包括app目錄的native庫和系統目錄的native庫

makePathElements:

private static Element[] makePathElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions) {
return makeDexElements(files, optimizedDirectory, suppressedExceptions, null);
}

makeDexElements:

makeDexElements方法的作用是獲取一個包含dex檔案的元素集合。

private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader) {
return makeDexElements(files, optimizedDirectory, suppressedExceptions, loader, false);
}


private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()]; //獲取檔案個數
int elementsPos = 0;
for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
//匹配以.dex為字尾的檔案
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} else {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}


return elements;
}

該方法的主要功能是建立Element陣列。

loadDexFile:

載入DexFile檔案,而且會把優化後的dex檔案快取到對應目錄。

private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements); //建立DexFile物件
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}

DexFile:

用來描述Dex檔案,Dex的載入以及Class的查詢都是由該類呼叫它的native方法完成的。

DexFile(File file, ClassLoader loader, DexPathList.Element[] elements)
throws IOException {
this(file.getPath(), loader, elements);
}


DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
mCookie = openDexFile(fileName, null, 0, loader, elements);
mInternalCookie = mCookie;
mFileName = fileName;
}

openDexFile:

private static Object openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null) ? null : new File(outputName).getAbsolutePath(),
flags,
loader,
elements);
}
此時引數取值說明:
sourceName為PathClassLoader建構函式傳遞的dexPath中以分隔符劃分之後的檔名;
outputName為null;
flags = 0
loader為null;
elements為makeDexElements()過程生成的Element陣列;

openDexFileNative:

static jobject DexFile_openDexFileNative(JNIEnv* env,
jclass,
jstring javaSourceName,
jstring javaOutputName ATTRIBUTE_UNUSED,
jint flags ATTRIBUTE_UNUSED,
jobject class_loader,
jobjectArray dex_elements) {
ScopedUtfChars sourceName(env, javaSourceName);
if (sourceName.c_str() == nullptr) {
return 0;
}
Runtime* const runtime = Runtime::Current();
ClassLinker* linker = runtime->GetClassLinker();
std::vector<std::unique_ptr<const DexFile>> dex_files;
std::vector<std::string> error_msgs;
const OatFile* oat_file = nullptr;


dex_files = runtime->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),
class_loader,
dex_elements,
/*out*/ &oat_file,
/*out*/ &error_msgs);


if (!dex_files.empty()) {
jlongArray array = ConvertDexFilesToJavaArray(env, oat_file, dex_files);
...
return array;
} else {
...
return nullptr;
}
}

這樣就完成了dex的載入過程,而BaseDexClassLoader派生出兩個子類載入器:PathClassLoader和DexClassLoader。

Android中如果parent類載入器載入不到類,最終還是會呼叫ClassLoader物件自己的findClass()方法。

loadClass()載入:

public abstract class ClassLoader {


public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}


protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//判斷當前類載入器是否已經載入過指定類,若已載入則直接返回
Class<?> clazz = findLoadedClass(className);


if (clazz == null) {
//如果沒有載入過,則呼叫parent的類載入遞迴載入該類,若已載入則直接返回
clazz = parent.loadClass(className, false);


if (clazz == null) {
//還沒載入,則呼叫當前類載入器來載入
clazz = findClass(className);
}
}
return clazz;
}
}

該方法的載入流程如下:

① 判斷當前類載入器是否已經載入過指定類,若已載入則直接返回,否則繼續執行;

② 呼叫parent的類載入遞迴載入該類,檢測是否載入,若已載入則直接返回,否則繼續執行;

③ 呼叫當前類載入器,通過findClass載入。

findLoadedClass:

[-> ClassLoader.java]

protected final Class<?> findLoadedClass(String name) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, name);
}

findClass:

[-> BaseDexClassLoader.java]

public class BaseDexClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class c = pathList.findClass(name, suppressedExceptions);
...
return c;
}
}

DexPathList.findClass:

public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
//找到目標類,則直接返回
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
return null;
}

程式碼解釋:

一個Classloader可以包含多個dex檔案,每個dex檔案被封裝到一個Element物件,這些Element物件排列成有序的陣列 dexElements。當查詢某個類時,會遍歷所有的dex檔案,如果找到則直接返回,不再繼續遍歷dexElements。也就是說當兩個類不同的dex中出現,會優先處理排在前面的dex檔案,這便是熱修復的核心精髓,將需要修復的類所打包的dex檔案插入到dexElements前面。

熱修復原理:

現在很多熱修復技術就是把修復的dex檔案放在DexPathList中Element[]陣列的前面,這樣就實現了修復後的Class搶先載入了,達到了修改bug的目的。

DexFile.loadClassBinaryName:

public final class DexFile {


public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, suppressed);
}


private static Class defineClass(String name, ClassLoader loader, Object cookie, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
}

defineClassNative()這是native方法。

defineClassNative:

[-> dalvik_system_DexFile.cc]

static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName, jobject javaLoader,
jobject cookie) {
std::unique_ptr<std::vector<const DexFile*>> dex_files = ConvertJavaArrayToNative(env, cookie);
if (dex_files.get() == nullptr) {
return nullptr; //dex檔案為空, 則直接返回
}


ScopedUtfChars class_name(env, javaName);
if (class_name.c_str() == nullptr) {
return nullptr; //類名為空, 則直接返回
}


const std::string descriptor(DotToDescriptor(class_name.c_str()));
const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str())); //將類名轉換為hash碼
for (auto& dex_file : *dex_files) {
const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str(), hash);
if (dex_class_def != nullptr) {
ScopedObjectAccess soa(env);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
class_linker->RegisterDexFile(*dex_file);
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> class_loader(
hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
//獲取目標類
mirror::Class* result = class_linker->DefineClass(soa.Self(), descriptor.c_str(), hash,
class_loader, *dex_file, *dex_class_def);
if (result != nullptr) {
// 找到目標物件
return soa.AddLocalReference<jclass>(result);
}
}
}
return nullptr; //沒有找到目標類
}

在native層建立目標類的物件並新增到虛擬機器列表。

我們繼續分析Native層可以發現:

DexFile.defineClassNative() 的實現在 /art/runtime/native/dalvik_system_DexFile.cc,最終由 ClassLinker.DefineClass() 實現
Class.classForName() 的實現在 /art/runtime/native/java_lang_Class.cc,最終由 ClassLinker.FindClass() 實現

ClassLinker核心原理:

先從已載入類的 class_table 中查詢,若找到則直接返回;若找不到則說明該類是第一次載入,則執行載入流程,其中可能需要穿插載入依賴的類,載入完成後將其快取到 class_table 中。

在 ClassLinker 中,會維護兩類 class_table,一類針對基本類,一類針對其它的類。class_table 是作為快取已經載入過的類的緩衝池。不管以什麼樣的方式去載入類,都需要先從 class_table 中先進行查詢以提高載入效能。

ClassLinker 在載入類的時候遇到該類依賴的類,進行穿插載入依賴類:

我們總結BaseDexClassLoader初始化和載入原理:

Android類載入詳細流程:

3.案例

(1)驗證類載入器

我們驗證App中的MainActivity類載入器和系統類String類的類載入器:

ClassLoader thisclassloader = MainActivity.class.getClassLoader();
ClassLoader StringClassloader = String.class.getClassLoader();
Log.e("ClassLoader1","MainActivity is in" + thisclassloader.toString());
Log.e("ClassLoader1","String is in" + StringClassloader.toString());

我們可以明顯發現PathClassLoader載入已安裝的APK類載入器,而BootClassLoader載入系統預安裝的類。

(2)遍歷父類載入器

public static  void printClassLoader(ClassLoader classLoader) {
Log.e("printClassLoader","this->"+ classLoader.toString());
ClassLoader parent = classLoader.getParent();
while (parent!=null){
Log.i("printClassLoader","parent->"+parent.toString());
parent = parent.getParent();
}
}

(3)驗證雙親委派機制

try {
Class StringClass = thisclassloader.loadClass("java.lang.String");
Log.e("ClassLoader1","load StringClass!"+thisclassloader.toString());
} catch (ClassNotFoundException e) {
e.printStackTrace();
Log.e("ClassLoader1","load MainActivity fail!"+thisclassloader.toString());
}

我們使用PathClassLoader去載入 String.class類,還是可以載入成功,因為雙親委派的機制。

(4)動態載入

這裡我借用網上寒冰大佬動態載入的案例,來進一步講述使用DexClassLoader類實現簡單的動態載入外掛dex,並驗證ClassLoader的繼承關係。

我們先編寫一個測試類檔案,然後生成dex檔案。

我們先將dex檔案放到模擬器的sdcard/下。

我們新建一個程式,然後編寫主程式的程式碼,並授權sd讀取許可權。

Context appContext = this.getApplication();
testDexClassLoader(appContext,"/sdcard/classes.dex");
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

然後我們編寫類載入器程式碼。

private void testDexClassLoader(Context context, String dexfilepath) {
//構建檔案路徑:/data/data/com.emaxple.test02/app_opt_dex,存放優化後的dex,lib庫
File optfile = context.getDir("opt_dex",0);
File libfile = context.getDir("lib_dex",0);


ClassLoader parentclassloader = MainActivity.class.getClassLoader();
ClassLoader tmpclassloader = context.getClassLoader();
//可以為DexClassLoader指定父類載入器
DexClassLoader dexClassLoader = new DexClassLoader(dexfilepath,optfile.getAbsolutePath(),libfile.getAbsolutePath(),parentclassloader);


Class clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.test.TestClass");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if(clazz!=null){
try {
Method testFuncMethod = clazz.getDeclaredMethod("test02");
Object obj = clazz.newInstance();
testFuncMethod.invoke(obj);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}


}

(5)獲得類列表

我們通過getClassNameList來獲取類列表

private static native String[] getClassNameList(Object cookie);
public static void getClassListInClassLoader(ClassLoader classLoader){
//先拿到BaseDexClassLoader
try {
//拿到pathList
Class BaseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = BaseDexClassLoader.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathListObj = pathListField.get(classLoader);


//拿到dexElements
Class DexElementClass = Class.forName("dalvik.system.DexPathList");
Field DexElementFiled = DexElementClass.getDeclaredField("dexElements");
DexElementFiled.setAccessible(true);
Object[] dexElementObj = (Object[]) DexElementFiled.get(pathListObj);
//拿到dexFile
Class Element = Class.forName("dalvik.system.DexPathList$Element");
Field dexFileField = Element.getDeclaredField("dexFile");
dexFileField.setAccessible(true);
Class DexFile =Class.forName("dalvik.system.DexFile");
Field mCookieField = DexFile.getDeclaredField("mCookie");
mCookieField.setAccessible(true);
Field mFiledNameField = DexFile.getDeclaredField("mFileName");
mFiledNameField.setAccessible(true);
//拿到getClassNameList
Method getClassNameListMethod = DexFile.getDeclaredMethod("getClassNameList",Object.class);
getClassNameListMethod.setAccessible(true);


for(Object dexElement:dexElementObj){
Object dexfileObj = dexFileField.get(dexElement);
Object mCookiedobj = mCookieField.get(dexfileObj);
String mFileNameobj = (String) mFiledNameField.get(dexfileObj);
String[] classlist = (String[]) getClassNameListMethod.invoke(null,mCookiedobj);
for(String classname:classlist){
Log.e("classlist",classLoader.toString()+"-----"+mFileNameobj+"-----"+classname);
}
}


} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

實驗總結

花了一段時間,斷斷續續總算把這篇類載入器和動態載入的帖子寫完了,從中學習到了很多,這裡如果有什麼錯誤,就請各位大佬指正了。

參考文獻:

http://gityuan.com/2017/03/19/android-classloader/

https://www.jianshu.com/p/7193600024e7

https://www.jianshu.com/p/ff489696ada2

https://www.jianshu.com/p/363a4ad0489d

https://github.com/huanzhiyazi/articles/issues/30

https://juejin.cn/post/6844903940094427150#heading-12

看雪ID:隨風而行aa

https://bbs.pediy.com/user-home-945611.htm

*本文由看雪論壇 隨風而行aa 原創,轉載請註明來自看雪社群

#

往期推薦

1. 某APP sig3 48位演算法逆向分析

2. CVE-2021-26411漏洞分析筆記

3. Windows本地提權漏洞CVE-2014-1767分析及EXP編寫指導

4. Vmprotect3.5.1 壹之型 — 暗月·宵之宮

5. 高階程序注入總結

6. 通過某音Cronet模組學習Quic協議

球分享

球點贊

球在看

點選“閱讀原文”,瞭解更多!