ART 在 Android 安全攻防中的應(yīng)用
背景
在日常的 Android 應(yīng)用安全分析中,經(jīng)常會遇到一些對抗,比如目標應(yīng)用加殼、混淆、加固,需要進行脫殼還原;又或者會有針對常用注入工具的檢測,比如 frida、Xposed 等,這時候也會想知道這些工具的核心原理以及是否自己可以實現(xiàn)。
其實這些問題的答案就在 Android 的 Java 虛擬機實現(xiàn)中??梢允窃缙诘?Dalvik 虛擬機,也可以是最新的 ART 虛擬機。從時代潮流來看,本文主要專注于 ART。不過,為了銘記歷史,也會對 Dalvik 虛擬機做一個簡單的介紹。最后會從?ART 實現(xiàn)出發(fā)對一些實際的應(yīng)用場景進行討論。
注: 本文分析基于 AOSP?
android-12.0.0_r11
Java VM
我們知道,Java 是一門跨平臺的語言,系統(tǒng)實際運行的是 Java 字節(jié)碼,由 Java 虛擬機去解釋執(zhí)行。如果讀者之前看過?如何破解一個Python虛擬機殼并拿走12300元ETH?一文或者對 Python 虛擬機有所了解的話就會知道,解釋執(zhí)行的過程可以看做是一個循環(huán),對每條指令進行解析,并針對指令的名稱通過巨大的 switch-case 分發(fā)到不同的分支中處理。其實 Java 虛擬機也是類似的,但 JVM 對于性能做了很多優(yōu)化,比如 JIT 運行時將字節(jié)碼優(yōu)化成對應(yīng)平臺的二進制代碼,提高后續(xù)運行速度等。
Android 代碼既然是用 Java 代碼編寫的,那么運行時應(yīng)該也會有一個解析字節(jié)碼的虛擬機。和標準的 JVM 不同,Android 中實際會將 Java 代碼編譯為 Dalvik 字節(jié)碼,運行時解析的也是用自研的虛擬機實現(xiàn)。之所以使用自研實現(xiàn),也許一方面有商業(yè)版權(quán)的考慮,另一方面也確實是適應(yīng)了移動端的的運行場景。Dalvik 指令基于寄存器,占 1-2 字節(jié),Java 虛擬機指令基于棧,每條指令只占 1 字節(jié);因此 Dalvik 虛擬機用空間換時間從而獲得比 Oracle JVM 更快的執(zhí)行速度。
啟動
其實 Java 代碼執(zhí)行并不慢,但其啟動時間卻是一大瓶頸。如果每個 APP 運行都要啟動并初始化 Java 虛擬機,那延時將是無法接受的。在?Android 12 應(yīng)用啟動流程分析?一文中我們說到,APP 應(yīng)用進程實際上是通過 zygote 進程 fork 出來的。這樣的好處是子進程繼承了父進程的進程空間,對于只讀部分可以直接使用,而數(shù)據(jù)段也可以通過 COW(Copy On Write) 進行延時映射。查看 zygote 與其子進程的?/proc/self/maps?可以發(fā)現(xiàn)大部分系統(tǒng)庫的映射都是相同的,這就是 fork 所帶來的好處。
在?Android 用戶態(tài)啟動流程分析?中我們分析了 init、zygote 和 system_server 的啟動流程,其中在介紹 zygote 的啟動流程時說到這是個 native 程序,在其中 main 函數(shù)的結(jié)尾有這么一段代碼:
int?main(int?argc,?char*?const?argv[])?{
????//?...
????if?(zygote)?{
????????runtime.start("com.android.internal.os.ZygoteInit",?args,?zygote);
????}?else?if?(className)?{
????????runtime.start("com.android.internal.os.RuntimeInit",?args,?zygote);
????}?else?{
????????fprintf(stderr,?"Error:?no?class?name?or?--zygote?supplied.\n");
????????//?....
????}
}上述代碼在?frameworks/base/cmds/app_process/app_main.cpp?中,runtime.start?的作用就是啟動 Java 虛擬機并將執(zhí)行流轉(zhuǎn)交給對應(yīng)的 Java 函數(shù)。
void?AndroidRuntime::start(const?char*?className,?const?Vector&?options,?bool?zygote)
{
????/*?start?the?virtual?machine?*/
????JniInvocation?jni_invocation;
????jni_invocation.Init(NULL);
????JNIEnv*?env;
????if?(startVm(&mJavaVM,?&env,?zygote)?!=?0)?{
????????return;
????}
????onVmCreated(env);
????/*
?????*?Register?android?functions.
?????*/
????if?(startReg(env)?0)?{
????????ALOGE("Unable?to?register?all?android?natives\n");
????????return;
????}
????????
???????/*
?????*?Start?VM.??This?thread?becomes?the?main?thread?of?the?VM,?and?will
?????*?not?return?until?the?VM?exits.
?????*/
????jclass?startClass?=?env->FindClass(slashClassName);
????????jmethodID?startMeth?=?env->GetStaticMethodID(startClass,?"main",?"([Ljava/lang/String;)V");
????env->CallStaticVoidMethod(startClass,?startMeth,?strArray);
} 詳細介紹可以會看?Android 用戶態(tài)啟動流程分析?一文,這里我們只需要知道 Java 虛擬機是在 Zygote 進程創(chuàng)建的,并由子進程繼承,因此 APP 從 zygote 進程中 fork 啟動后就無需再次啟動 Java 虛擬機,而是復用原有的虛擬機執(zhí)行輕量的初始化即可。
接口
Android Java 虛擬機包括早期的 Dalvik 虛擬機和當前的 ART 虛擬機,我們將其統(tǒng)稱為 Java 虛擬機,因為對于應(yīng)用程序而言應(yīng)該是透明的,也就是說二者應(yīng)該提供了統(tǒng)一的對外接口。
這個接口可以分為兩部分,一部分是提供給 Java 應(yīng)用的接口,即我們常見的 JavaVM、JNIEnv 結(jié)構(gòu)體提供的諸如 FindClass、GetMethodID、CallVoidMethod 等接口;另一部分則是提供給系統(tǒng)開發(fā)者的接口,系統(tǒng)通過這些接口去初始化并創(chuàng)建虛擬機,從而使自身具備執(zhí)行 Java 代碼的功能。
JniInvocation.Init?方法中即進行了第二部分接口的初始化操作,其中主要邏輯是根據(jù)系統(tǒng)屬性 (persist.sys.dalvik.vm.lib.2) 判斷待加載的虛擬機動態(tài)庫,Dalvik 虛擬機對應(yīng)的是 libdvm.so,ART 虛擬機對應(yīng)的是 libart.so;然后通過?dlopen?進行加載,并通過?dlsym?獲取其中三個函數(shù)符號,作為抽象 Java 虛擬機的接口:
?JNI_GetDefaultJavaVMInitArgs: 獲取默認的 JVM 初始化參數(shù);?JNI_CreateJavaVM: 創(chuàng)建 Java 虛擬機;?JNI_GetCreatedJavaVMs: 獲取已經(jīng)創(chuàng)建的 Java 虛擬機實例;
例如,在上述 zygote 的?AndroidRuntime::startVm?方法實現(xiàn)中,就是通過指定參數(shù)最終調(diào)用 JNI_CreateJavaVM 來完成 Java 虛擬機的創(chuàng)建工作。
通過這三個接口實現(xiàn)了對于不同 Java 虛擬機細節(jié)的隱藏,既可以用 ART 無縫替換 Dalvik 虛擬機,也可以在未來用某個新的虛擬機無縫替換掉 ART 虛擬機。
總的來說,Java 虛擬機只在 Zygote 進程中創(chuàng)建一次,子進程通過 fork 獲得虛擬機的一個副本,因此 zygote 才被稱為所有 Java 進程的父進程;同時,也因為每個子進程擁有獨立的虛擬機副本,所以某個進程的虛擬機崩潰后不影響其他進程,從而實現(xiàn)安全的運行時隔離。
Dalvik
Dalvik 是早期 Android 的 Java 虛擬機,伴隨著 Android 5.0 的更新,正式宣告其歷史使命的結(jié)束:
commit?870b4f2d70d67d6dbb7d0881d101c61bed8caad2
Author:?Brian?Carlstrom?
Date:???Tue?Aug?5?12:46:17?2014?-0700
????Dalvik?is?dead,?long?live?Dalvik!雖然現(xiàn)在 Dalvik 已經(jīng)被 ART 虛擬機所取代,但其簡潔的實現(xiàn)有助于我們理解 Java 代碼的運行流程,因此還是先對其進行簡單的介紹。
上節(jié)中我們知道 zygote 進程創(chuàng)建并初始化 Java 虛擬機后執(zhí)行的第一個 Java 函數(shù)是?com.android.internal.os.ZygoteInit?的 main 方法,這是個靜態(tài)方法,因此在 Native 層調(diào)用的是 JNI 接口函數(shù)?CallStaticVoidMethod。其調(diào)用流程可以簡化如下所示:
| method | file |
| JNIEnv.CallStaticVoidMethod | dalvik/libnativehelper/include/nativehelper/jni.h |
| JNINativeInterface.CallStaticVoidMethodV | dalvik/vm/Jni.c |
| Jni.dvmCallMethodV | dalvik/vm/interp/Stack.c |
| Stack.dvmInterpret | dalvik/vm/interp/Interp.c |
| dvmInterpretStd | dalvik/vm/mterp/out/InterpC-portstd.c (動態(tài)生成) |
Dalvik 虛擬機支持三種執(zhí)行模式,分別是:
?kExecutionModeInterpPortable: 可移植模式,能運行在不同的平臺中,對應(yīng)的運行方法是 dvmInterpretStd;?kExecutionModeInterpFast: 快速模式,針對特定平臺優(yōu)化,對應(yīng)的運行方法是 dvmMterpStd;?kExecutionModeJit: JIT 模式,運行時編譯為特定平臺的 native 代碼,對應(yīng)運行方法也是 dvmMterpStd;
以上述調(diào)用流程中的 portable 模式為例,對應(yīng) dvmInterpretStd 實現(xiàn)的核心代碼如下所示:
#define?INTERP_FUNC_NAME?dvmInterpretStd
bool?INTERP_FUNC_NAME(Thread*?self,?InterpState*?interpState)?{
????//?...
????/*?core?state?*/
????const?Method*?curMethod;????//?method?we're?interpreting
????const?u2*?pc;???????????????//?program?counter
????u4*?fp;?????????????????????//?frame?pointer
????u2?inst;????????????????????//?current?instruction
?
????/*?copy?state?in?*/
????curMethod?=?interpState->method;
????pc?=?interpState->pc;
????fp?=?interpState->fp;
????retval?=?interpState->retval;???/*?only?need?for?kInterpEntryReturn??*/
?
????methodClassDex?=?curMethod->clazz->pDvmDex;
????while?(1)?{
????????/*?fetch?the?next?16?bits?from?the?instruction?stream?*/
????????inst?=?FETCH(0);
????????switch?(INST_INST(inst))?{
????????????HANDLE_OPCODE(OP_INVOKE_DIRECT?/*vB,?{vD,?vE,?vF,?vG,?vA},?meth@CCCC*/)
????????????????GOTO_invoke(invokeDirect,?false);
????????????OP_END
????????????HANDLE_OPCODE(OP_RETURN?/*vAA*/)
????????????HANDLE_OPCODE(...)
????????}
????}
????/*?export?state?changes?*/
????interpState->method?=?curMethod;
????interpState->pc?=?pc;
????interpState->fp?=?fp;
????/*?debugTrackedRefStart?doesn't?change?*/
????interpState->retval?=?retval;???/*?need?for?_entryPoint=ret?*/
????interpState->nextMode?=?
????????(INTERP_TYPE?==?INTERP_STD)???INTERP_DBG?:?INTERP_STD;
????return?true;
}可以看到其核心在于一個巨大的 switch/case,以 PC 為起點不斷讀取字節(jié)碼(4字節(jié)對齊),并根據(jù) op_code 去分發(fā)解釋執(zhí)行不同的指令直到 Java 方法運行結(jié)束返回或者拋出異常。之所以稱為可移植模式(portable)正是因為該代碼純粹是解釋執(zhí)行,既沒有提前優(yōu)化也沒有運行時的 JIT 優(yōu)化,也因此具有平臺無關(guān)性,只要 C 編譯器支持對應(yīng)平臺即可運行。
雖然 Dalvik 已經(jīng)被 ART 取代,但其中的 Dalvik 字節(jié)碼格式還是被保留了下來。即便在最新版本的 Android 中,編譯 Java 生成的依舊是 DEX 文件,其格式可以參考?Dalvik Executable format[1],Dalvik 字節(jié)碼的介紹可以參考官方文檔?Dalvik bytecode[2]。
ART
ART 全稱為 Android Runtime,是繼 Dalvik 之后推出的高性能 Android Java 虛擬機。在本文中我們重點關(guān)注 ART 虛擬機執(zhí)行 Java 代碼的流程。在介紹 ART 的代碼執(zhí)行流程之前,我們需要先了解在 ART 中針對 DEX 的一系列提前優(yōu)化方案,以及由此產(chǎn)生的各類中間文件。
提前優(yōu)化
在我們使用 Android-Studio 編譯應(yīng)用時,實際上是通過 Java 編譯器先將?.java?代碼編譯為對應(yīng)的 Java 字節(jié)碼,即?.class?類文件;然后用?dx(在新版本中是d8) 將 Java 字節(jié)碼轉(zhuǎn)換為 Dalvik 字節(jié)碼,并將所有生成的類打包到統(tǒng)一的 DEX 文件中,最終和資源文件一起 zip 壓縮為?.apk?文件。
在安裝用戶的 APK 時,Android 系統(tǒng)主要通過 PacketManager 對應(yīng)用進行解包和安裝。其中在處理 DEX 文件時候,會通過?installd?進程調(diào)用對應(yīng)的二進制程序?qū)ψ止?jié)碼進行優(yōu)化,這對于 Dalvik 虛擬機而言使用的是?dexopt?程序,而 ART 中使用的是?dex2oat?程序。
dexopt 將 dex 文件優(yōu)化為 odex 文件,即 optimized-dex 的縮寫,其中包含的是優(yōu)化后的 Dalvik 字節(jié)碼,稱為 quickend dex;dex2oat 基于 LLVM,優(yōu)化后生成的是對應(yīng)平臺的二進制代碼,以 oat 格式保存,oat 的全稱為 Ahead-Of-Time。oat 文件實際上是以 ELF 格式進行存儲的,并在其中 oatdata 段(section) 包含了原始的 DEX 內(nèi)容。
在 Android 8 之后,將 OAT 文件一分為二,原 oat 仍然是 ELF 格式,但原始 DEX 文件內(nèi)容被保存到了 VDEX 中,VDEX 有其獨立的文件格式。整體流程如下圖所示:

值得一提的是,在 Andorid 系統(tǒng)中 dex2oat 會將優(yōu)化后的代碼保存在?/data/app?對應(yīng)的應(yīng)用路徑下,系統(tǒng)應(yīng)用會保存在?/data/dalvik-cache/?下,對于后者,產(chǎn)生的實際有三個文件,比如:
$?ls?-l?|?grep?Settings.apk
-rw-r-----?1?system?system???????????77824?2021-12-10?10:33?system_ext@priv-app@[email protected]@classes.art
-rw-r-----?1?system?system??????????192280?2021-11-19?12:50?system_ext@priv-app@[email protected]@classes.dex
-rw-r-----?1?system?system???????????59646?2021-12-10?10:33?system_ext@priv-app@[email protected]@classes.vdexsystem_ext@priv-app@[email protected]@classes.dex?實際上是 ELF 格式的 OAT 文件,所以我們不能以貌(后綴)取人;.art?也是一個特殊的文件格式,如前文所言,Android 實現(xiàn)了自己的 Java 虛擬機,這個虛擬機本身是用 C/C++ 實現(xiàn)的,其中的一些 Java 原語有對應(yīng)的 C++ 類,比如:
?java.lang.Class 對應(yīng) art::mirror::Class?java.lang.String 對應(yīng) art::mirror::String?java.lang.reflect.Method 對應(yīng) art::mirror::Method?……
當創(chuàng)建一個 Java 對象時,內(nèi)存中會創(chuàng)建對應(yīng)的 C++ 對象并調(diào)用其構(gòu)造函數(shù),JVM 管理者這些 C++ 對象的引用。為了加速啟動過程,避免對這些常見類的初始化,Android 使用了?.art?格式來保存這些 C++ 對象的實例,簡單來說,art 文件可以看做是一系列常用 C++ 對象的內(nèi)存 dump。
不論是 oat、vdex 還是 art,都是 Android 定義的內(nèi)部文件格式,官方并不保證其兼容性,事實上在 Android 各個版本中這些文件格式都有不同程度的變化,這些變化是不反映在文檔中的,只能通過代碼去一窺究竟。因此對于這些文件格式我們現(xiàn)在只需要知道其大致作用,無需關(guān)心其實現(xiàn)細節(jié)。
文件加載
在前一篇文章 (Android 12 應(yīng)用啟動流程分析) 中我們知道 APP 最終在 ActivityThread 中完成 Application 的創(chuàng)建和初始化,最終調(diào)用 Activity.onCreate 進入視圖組件的生命周期。但這里其實忽略了一個問題: APP 的代碼(DEX/OAT 文件) 是如何加載到進程中的?
在 Java 中負責加載指定類的對象是?ClassLoader[3],Android 中也是類似,BaseDexClassLoader 繼承自 ClassLoader 類,實現(xiàn)了許多 DEX 相關(guān)的加載操作,其子類包括:
?DexClassLoader: 負責從?.jar?或者?.apk?中加載類;?PathClassLoader: 負責從本地文件中初始化類加載器;?InMemoryDexClassLoader: 從內(nèi)存中初始化類加載器;
ClassLoader
以常見的?PathClassLoader?為例,其構(gòu)造函數(shù)會調(diào)用父類的構(gòu)造函數(shù),整體調(diào)用鏈路簡化如下表:
| method | file |
| new PathClassLoader | ... |
| new BaseDexClassLoader | libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java |
| new DexPathList | libcore/dalvik/src/main/java/dalvik/system/DexPathList.java |
| DexPathList.makeDexElements | ... |
| DexPathList.loadDexFile | ... |
| new DexFile | libcore/dalvik/src/main/java/dalvik/system/DexFile.java |
| DexFile.openDexFile | ... |
| DexFile.openDexFileNative | ... |
| DexFile_openDexFileNative | art/runtime/native/dalvik_system_DexFile.cc |
| OatFileManager::OpenDexFilesFromOat | art/runtime/oat_file_manager.cc |
在?OpenDexFilesFromOat?中執(zhí)行了真正的代碼加載工作,偽代碼如下:
std::vectorconst?DexFile>>?OatFileManager::OpenDexFilesFromOat()?{
????std::vectorconst?DexFile>>?dex_files?=?OpenDexFilesFromOat_Impl(...);
????for?(std::unique_ptr<const?DexFile>&?dex_file?:?dex_files)?{
??????if?(!dex_file->DisableWrite())?{
????????error_msgs->push_back("Failed?to?make?dex?file?"?+?dex_file->GetLocation()?+?"?read-only");
??????}
????}
????return?dex_files;
} 通過 OpenDexFilesFromOat_Impl 加載獲取 DexFile 結(jié)構(gòu)體數(shù)組,值得注意的是加載完 DEX 之后會將內(nèi)存中的 dex_file 設(shè)置為不可寫,當然目前還沒有強制,但可見這是未來的趨勢。
繼續(xù)看實現(xiàn)部分是如何加載 Dex 文件的:
std::vectorconst?DexFile>>?OatFileManager::OpenDexFilesFromOat_Impl()?{
????//?Extract?dex?file?headers?from?`dex_mem_maps`.
????const?std::vector<const?DexFile::Header*>?dex_headers?=?GetDexFileHeaders(dex_mem_maps);
????//?Determine?dex/vdex?locations?and?the?combined?location?checksum.
????std::string?dex_location;
????std::string?vdex_path;
????bool?has_vdex?=?OatFileAssistant::AnonymousDexVdexLocation(dex_headers,
?????????????????????????????????????????????????????????????kRuntimeISA,
?????????????????????????????????????????????????????????????&dex_location,
?????????????????????????????????????????????????????????????&vdex_path);
????if?(has_vdex?&&?OS::FileExists(vdex_path.c_str()))?{
????????vdex_file?=?VdexFile::Open(vdex_path,
????????????????????????????????/*?writable=?*/?false,
????????????????????????????????/*?low_4gb=?*/?false,
????????????????????????????????/*?unquicken=?*/?false,
????????????????????????????????&error_msg);
????}
????//?Load?dex?files.?Skip?structural?dex?file?verification?if?vdex?was?found
????//?and?dex?checksums?matched.
????std::vectorconst?DexFile>>?dex_files;
????for?(size_t?i?=?0;?i?size();?++i)?{
????????static?constexpr?bool?kVerifyChecksum?=?true;
????????const?ArtDexFileLoader?dex_file_loader;
????????std::unique_ptr<const?DexFile>?dex_file(dex_file_loader.Open(
????????????DexFileLoader::GetMultiDexLocation(i,?dex_location.c_str()),
????????????dex_headers[i]->checksum_,
????????????std::move(dex_mem_maps[i]),
????????????/*?verify=?*/?(vdex_file?==?nullptr)?&&?Runtime::Current()->IsVerificationEnabled(),
????????????kVerifyChecksum,
????????????&error_msg));
????????if?(dex_file?!=?nullptr)?{
????????????dex::tracking::RegisterDexFile(dex_file.get());??//?Register?for?tracking.
????????????dex_files.push_back(std::move(dex_file));
????????}
????}
????//?Initialize?an?OatFile?instance?backed?by?the?loaded?vdex.
????std::unique_ptr?oat_file(OatFile::OpenFromVdex(
????????MakeNonOwningPointerVector(dex_files),
????????std::move(vdex_file),
????????dex_location));
????if?(oat_file?!=?nullptr)?{
????????VLOG(class_linker)?<"Registering?"?<GetLocation();
????????*out_oat_file?=?RegisterOatFile(std::move(oat_file));
????}
????return?dex_files;
} 加載過程首先將 vdex 映射到內(nèi)存中,然后將已經(jīng)映射到內(nèi)存中的 dex 或者在磁盤中的 dex 轉(zhuǎn)換為 DexFile 結(jié)構(gòu)體,最后再將 vdex 和 oat 文件關(guān)聯(lián)起來。
VdexFile
vdex 是 Android 8.0 加入的新文件格式,主要用于保存優(yōu)化代碼的原始 DEX 信息,而 OAT 中則主要保存 dex2oat 編譯后的 Native 代碼。
VdexFile 的結(jié)構(gòu)大致如下所示,其中?D?代表 VDEX 中包含的 DEX 文件個數(shù):
VdexFileHeader????fixed-length?header
VdexSectionHeader[kNumberOfSections]
Checksum?section
??VdexChecksum[D]
Optionally:
???DexSection
???????DEX[0]????????????????array?of?the?input?DEX?files
???????DEX[1]
???????...
???????DEX[D-1]
VerifierDeps
???4-byte?alignment
???uint32[D]??????????????????DexFileDeps?offsets?for?each?dex?file
???DexFileDeps[D][]???????????verification?dependencies
?????4-byte?alignment
?????uint32[class_def_size]?????TypeAssignability?offsets?(kNotVerifiedMarker?for?a?class?that?isn't?verified)
?????uint32?????????????????????Offset?of?end?of?AssignabilityType?sets
?????uint8[]????????????????????AssignabilityType?sets
?????4-byte?alignment
?????uint32?????????????????????Number?of?strings
?????uint32[]???????????????????String?data?offsets?for?each?string
?????uint8[]????????????????????String?dataVdexFile 結(jié)構(gòu)詳見:?art/runtime/vdex_file.h[4]
DexFile
dex_file_loader.Open?的調(diào)用路徑如下:
?ArtDexFileLoader::Open?ArtDexFileLoader::OpenCommon?DexFileLoader::OpenCommon?magic == "dex\n" -> new StandardDexFile()?magic == "cdex" -> new CompactDexFile()
實際根據(jù)起始 4 字節(jié)判斷是標準 DEX 還是緊湊型 DEX (cdex, compat dex),并使用對應(yīng)的結(jié)構(gòu)體進行初始化。cdex 是當前 ART 內(nèi)部使用的 DEX 文件格式,主要是為了減少磁盤和內(nèi)存的使用。但不論是 StandardDexFile 還是 CompactDexFile 都繼承于 DexFile,二者的構(gòu)造函數(shù)最終還是會調(diào)用 DexFile 的構(gòu)造函數(shù)。
DexFile 結(jié)構(gòu)詳見?art/libdexfile/dex/dex_file.h[5]
OatFile
在完成所有 DexFile 的初始化之后,會繼續(xù)使用?OatFile::OpenFromVdex?創(chuàng)建 oat_file 并進行注冊。該函數(shù)的調(diào)用鏈路如下:
?OatFile::OpenFromVdex?OatFileBackedByVdex::Open?new OatFileBackedByVdex?OatFileBase::OatFileBase?OatFile::OatFile
與早期使用 odex 的區(qū)別是現(xiàn)在在創(chuàng)建完 OatFile 之后,會調(diào)用?oat_file->SetVdex?獲取 vdex 對象的所有權(quán),用以實現(xiàn) OAT 的部分接口,比如獲取內(nèi)存中對應(yīng) DEX 文件的起始地址:
const?uint8_t*?OatFile::DexBegin()?const?{
??return?vdex_->Begin();
}詳見: art/runtime/oat_file.h
方法調(diào)用
本來按照時間線來看的話,這里應(yīng)該先介紹 ART 運行時類和方法的加載過程,但我從實踐出發(fā),先看 Java 方法的調(diào)用過程,并針對其中涉及到的概念在下一節(jié)繼續(xù)介紹。
在 Web 安全中,Java 服務(wù)端通常帶有一個稱為 RASP (Runtime Application Self-Protection) 的動態(tài)防護方案,比如監(jiān)控某些執(zhí)行命令的敏感函數(shù)調(diào)用并進行告警,其實際 hook 點是在 JVM 中,不論是方法直接調(diào)用還是反射調(diào)用都可以檢測到。因此我們有理由猜測在 Android 中也有類似的調(diào)用鏈路,為了方便觀察,這里先看反射調(diào)用的場景,一般反射調(diào)用的示例如下:
import?java.lang.reflect.*;
public?class?Test?{
????public?static?void?main(String?args[])?throws?Exception?{
????????Class?c?=?Class.forName("com.evilpan.DemoClass");
????????Method?m?=?c.getMethod("foo",?null);
????????m.invoke();
????}
}因此一個方法的調(diào)用會進入到?Method.invoke?方法,這是一個?native 方法,實際實現(xiàn)在?art/runtime/native/java_lang_reflect_Method.cc:
static?jobject?Method_invoke(JNIEnv*?env,?jobject?javaMethod,?jobject?javaReceiver,
?????????????????????????????jobjectArray?javaArgs)?{
??ScopedFastNativeObjectAccess?soa(env);
??return?InvokeMethod(soa,?javaMethod,?javaReceiver,?javaArgs);
} InvokeMethod 定義在?art/runtime/reflection.cc,其實現(xiàn)的核心代碼如下:
template?
jobject?InvokeMethod(const?ScopedObjectAccessAlreadyRunnable&?soa,?jobject?javaMethod,
?????????????????????jobject?javaReceiver,?jobject?javaArgs,?size_t?num_frames)?{
????ObjPtr?executable?=?soa.Decode(javaMethod);
????const?bool?accessible?=?executable->IsAccessible();
????ArtMethod*?m?=?executable->GetArtMethod();
????if?(UNLIKELY(!declaring_class->IsVisiblyInitialized()))?{
????????Thread*?self?=?soa.Self();
????????Runtime::Current()->GetClassLinker()->EnsureInitialized(
????????????self,?h_class,
????????????/*can_init_fields=*/?true,
????????????/*can_init_parents=*/?true)
????}
????if?(!m->IsStatic())?{
????????if?(declaring_class->IsStringClass()?&&?m->IsConstructor())?{
????????????m?=?WellKnownClasses::StringInitToStringFactory(m);
????????}?else?{
????????????m?=?receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m,?kPointerSize);
????????}
????}
????if?(!accessible?&&?!VerifyAccess(/*...*/))?{
????????ThrowIllegalAccessException(
????????StringPrintf("Class?%s?cannot?access?%s?method?%s?of?class?%s",?...));
????}
????InvokeMethodImpl(soa,?m,?np_method,?receiver,?objects,?&shorty,?&result);
} 上面省略了許多細節(jié),主要是做了一些調(diào)用前的檢查和預(yù)處理工作,流程可以概況為:
1.判斷方法所屬的類是否已經(jīng)初始化過,如果沒有則進行初始化;2.將?String.?構(gòu)造函數(shù)調(diào)用替換為對應(yīng)的工廠?StringFactory?方法調(diào)用;3.如果是虛函數(shù)調(diào)用,替換為運行時實際的函數(shù);4.判斷方法是否可以訪問,如果不能訪問則拋出異常;5.調(diào)用函數(shù);
值得注意的是,jobject 類型的 javaMethod 可以轉(zhuǎn)換為?ArtMethod?指針,該結(jié)構(gòu)體是 ART 虛擬機中對于具體方法的描述。之后經(jīng)過一系列調(diào)用:
?InvokeMethodImpl?InvokeWithArgArray?method->Invoke()
最終進入?ArtMethod::Invoke?函數(shù),還是只看核心代碼:
void?ArtMethod::Invoke(Thread*?self,?uint32_t*?args,?uint32_t?args_size,?JValue*?result,
???????????????????????const?char*?shorty)?{
????Runtime*?runtime?=?Runtime::Current();
????if?(UNLIKELY(!runtime->IsStarted()?||
???????????????(self->IsForceInterpreter()?&&?!IsNative()?&&?!IsProxyMethod()?&&?IsInvokable())))?{
????????art::interpreter::EnterInterpreterFromInvoke(...);
????}?else?{
????????bool?have_quick_code?=?GetEntryPointFromQuickCompiledCode()?!=?nullptr;
????????if?(LIKELY(have_quick_code))?{
????????????if?(!IsStatic())?{
????????????????(*art_quick_invoke_stub)(this,?args,?args_size,?self,?result,?shorty);
????????????}?else?{
????????????????(*art_quick_invoke_static_stub)(this,?args,?args_size,?self,?result,?shorty);
????????????}
????????}?else?{
????????????LOG(INFO)?<"Not?invoking?'"?<PrettyMethod()?<"'?code=null";
????????}
????}
????self->PopManagedStackFragment(fragment);
}ART 對于 Java 方法實現(xiàn)了兩種執(zhí)行模式,一種是像 Dalvik 虛擬機一樣解釋執(zhí)行字節(jié)碼,姑且稱為解釋模式;另一種是快速模式,即直接調(diào)用通過 OAT 編譯后的本地代碼。
在 ART 早期指定本地代碼還細分為 Portable 和 Quick 兩種模式,但由于對極致速度的追求以及隨著 Quick 模式的不斷優(yōu)化,Portable 也逐漸退出了歷史舞臺。
閱讀上述代碼可以得知,當 ART 運行時尚未啟動或者指定強制使用解釋執(zhí)行時,虛擬機執(zhí)行函數(shù)使用的是解釋模式,ART 可以在啟動時指定?-Xint?參數(shù)強制使用解釋執(zhí)行,但即便指定了使用解釋執(zhí)行模式,還是有一些情況無法使用解釋執(zhí)行,比如:
1.當所執(zhí)行的方法是 Native 方法時,這時只有二進制代碼,不存在字節(jié)碼,自然無法解釋執(zhí)行;2.當所執(zhí)行的方法無法調(diào)用,比如 access_flag 判定無法訪問或者當前方法是抽象方法時;3.當所執(zhí)行的方式是代理方法時,ART 對于代理方法有單獨的本地調(diào)用方式;
解釋執(zhí)行
解釋執(zhí)行的入口是?art::interpreter::EnterInterpreterFromInvoke,該函數(shù)定義在?art/runtime/interpreter/interpreter.cc,關(guān)鍵代碼如下:
void?EnterInterpreterFromInvoke(Thread*?self,
????????????????????????????????ArtMethod*?method,
????????????????????????????????ObjPtr?receiver,
????????????????????????????????uint32_t*?args,
????????????????????????????????JValue*?result,
????????????????????????????????bool?stay_in_interpreter)?{
????CodeItemDataAccessor?accessor(method->DexInstructionData());
????if?(accessor.HasCodeItem())?{
????????num_regs?=??accessor.RegistersSize();
????????num_ins?=?accessor.InsSize();
????}
????//?初始化棧幀?......
????if?(LIKELY(!method->IsNative()))?{
????????JValue?r?=?Execute(self,?accessor,?*shadow_frame,?JValue(),?stay_in_interpreter);
????????if?(result?!=?nullptr)?{
????????*result?=?r;
????????}
??}
} 其中的?CodeItem?就是 DEX 文件中對應(yīng)方法的字節(jié)碼,還是老樣子,直接看簡化的調(diào)用鏈路:
| method | file |
| Execute | art/runtime/interpreter/interpreter.cc |
| ExecuteSwitch | ... |
| ExecuteSwitchImpl | art/runtime/interpreter/interpreter_switch_impl.h |
| ExecuteSwitchImplAsm | ... |
| ExecuteSwitchImplAsm | art/runtime/arch/arm64/quick_entrypoints_arm64.S |
| ExecuteSwitchImplCpp | art/runtime/interpreter/interpreter_switch_impl-inl.h |
ExecuteSwitchImplAsm?為了速度直接使用匯編實現(xiàn),在 ARM64 平臺中的定義如下:
//??Wrap?ExecuteSwitchImpl?in?assembly?method?which?specifies?DEX?PC?for?unwinding.
//??Argument?0:?x0:?The?context?pointer?for?ExecuteSwitchImpl.
//??Argument?1:?x1:?Pointer?to?the?templated?ExecuteSwitchImpl?to?call.
//??Argument?2:?x2:?The?value?of?DEX?PC?(memory?address?of?the?methods?bytecode).
ENTRY?ExecuteSwitchImplAsm
????SAVE_TWO_REGS_INCREASE_FRAME?x19,?xLR,?16
????mov?x19,?x2???????????????????????????????????//?x19?=?DEX?PC
????CFI_DEFINE_DEX_PC_WITH_OFFSET(0?/*?x0?*/,?19?/*?x19?*/,?0)
????blr?x1????????????????????????????????????????//?Call?the?wrapped?method.
????RESTORE_TWO_REGS_DECREASE_FRAME?x19,?xLR,?16
????ret
END?ExecuteSwitchImplAsm本質(zhì)上是調(diào)用保存在 x1 寄存器的第二個參數(shù),調(diào)用處的代碼片段如下:
template<bool?do_access_check,?bool?transaction_active>
ALWAYS_INLINE?JValue?ExecuteSwitchImpl()?{
????//...
????void*?impl?=?reinterpret_cast<void*>(&ExecuteSwitchImplCpp);
????const?uint16_t*?dex_pc?=?ctx.accessor.Insns();
????ExecuteSwitchImplAsm(&ctx,?impl,?dex_pc);
} 即調(diào)用了?ExecuteSwitchImplCpp,在該函數(shù)中,可以看見典型的解釋執(zhí)行代碼:
template<bool?do_access_check,?bool?transaction_active>
void?ExecuteSwitchImplCpp(SwitchImplContext*?ctx)?{
????Thread*?self?=?ctx->self;
????const?CodeItemDataAccessor&?accessor?=?ctx->accessor;
????ShadowFrame&?shadow_frame?=?ctx->shadow_frame;
????self->VerifyStack();
????uint32_t?dex_pc?=?shadow_frame.GetDexPC();
????const?auto*?const?instrumentation?=?Runtime::Current()->GetInstrumentation();
????const?uint16_t*?const?insns?=?accessor.Insns();
????const?Instruction*?next?=?Instruction::At(insns?+?dex_pc);
????while?(true)?{
????????const?Instruction*?const?inst?=?next;
????????dex_pc?=?inst->GetDexPc(insns);
????????shadow_frame.SetDexPC(dex_pc);
????????TraceExecution(shadow_frame,?inst,?dex_pc);
????????uint16_t?inst_data?=?inst->Fetch16(0);?//?一條指令?4?字節(jié)
????????if?(InstructionHandler(...).Preamble())?{
????????????switch?(inst->Opcode(inst_data))?{
????????????????case?xxx:?...;
????????????????case?yyy:?...;
????????????????...
????????????}
????????}
????}
}在當前版本中 (Android 12),實際上是通過宏展開去定義了所有 op_code 的處理分支,不同版本實現(xiàn)都略有不同,但解釋執(zhí)行的核心思路從 Android 2.x 版本到現(xiàn)在都是一致的,因為字節(jié)碼的定義并沒有太多改變。
快速執(zhí)行
再回到 ArtMethod 真正調(diào)用之前,如果不使用解釋模式執(zhí)行,則通過?art_quick_invoke_stub?去調(diào)用。stub 是一小段中間代碼,用于跳轉(zhuǎn)到實際的 native 執(zhí)行,該符號使用匯編實現(xiàn),在 ARM64 中的定義在?art/runtime/arch/arm64/quick_entrypoints_arm64.S,核心代碼如下:
.macro?INVOKE_STUB_CALL_AND_RETURN
????REFRESH_MARKING_REGISTER
????REFRESH_SUSPEND_CHECK_REGISTER
????//?load?method->?METHOD_QUICK_CODE_OFFSET
????ldr?x9,?[x0,?#ART_METHOD_QUICK_CODE_OFFSET_64]
????//?Branch?to?method.
????blr?x9
.endm
/*
?*??extern"C"?void?art_quick_invoke_stub(ArtMethod?*method,???x0
?*???????????????????????????????????????uint32_t??*args,?????x1
?*???????????????????????????????????????uint32_t?argsize,????w2
?*???????????????????????????????????????Thread?*self,????????x3
?*???????????????????????????????????????JValue?*result,??????x4
?*???????????????????????????????????????char???*shorty);?????x5
?*/
ENTRY?art_quick_invoke_stub
????//?...
????INVOKE_STUB_CALL_AND_RETURN
END?art_quick_invoke_static_stub中間省略了一些保存上下文以及調(diào)用后恢復寄存器的代碼,其核心是調(diào)用了?ArtMethod?結(jié)構(gòu)體偏移?ART_METHOD_QUICK_CODE_OFFSET_64?處的指針,該值對應(yīng)的代碼為:
ASM_DEFINE(ART_METHOD_QUICK_CODE_OFFSET_64,
???????????art::ArtMethod::EntryPointFromQuickCompiledCodeOffset(art::PointerSize::k64).Int32Value())即?entry_point_from_quick_compiled_code_?屬性所指向的地址。
//?art/runtime/art_method.h
static?constexpr?MemberOffset?EntryPointFromQuickCompiledCodeOffset(PointerSize?pointer_size)?{
return?MemberOffset(PtrSizedFieldsOffset(pointer_size)?+?OFFSETOF_MEMBER(
????PtrSizedFields,?entry_point_from_quick_compiled_code_)?/?sizeof(void*)
????????*?static_cast<size_t>(pointer_size));
}可以認為這就是所有快速模式執(zhí)行代碼的入口,至于該指針指向什么地方,又是什么時候初始化的,可以參考下一節(jié)代碼加載部分。實際在方法調(diào)用時,快速模式執(zhí)行的方法可能在其中執(zhí)行到了需要以解釋模式執(zhí)行的方法,同樣以解釋模式執(zhí)行的方法也可能在其中調(diào)用到 JNI 方法或者其他以快速模式執(zhí)行的方法,所以在單個函數(shù)執(zhí)行的過程中運行狀態(tài)并不是一成不變的,但由于每次切換調(diào)用前后都保存和恢復了當前上下文,使得不同調(diào)用之間可以保持透明,這也是模塊化設(shè)計的一大優(yōu)勢所在。
代碼加載
在上節(jié)我們知道在 ART 虛擬機中,Java 方法的調(diào)用主要通過?ArtMethod::Invoke?去實現(xiàn),那么 ArtMethod 結(jié)構(gòu)是什么時候創(chuàng)建的呢?為什么 jmethod/jobject 可以轉(zhuǎn)換為?ArtMethod?指針呢?
在 Java 這門語言中,方法是需要依賴類而存在的,因此要分析方法的初始化需要先分析類的初始化。雖然我們前面知道如何從 OAT/VDEX/DEX 文件中構(gòu)造對應(yīng)的 ClassLoader 來進行類查找,但那個時候類并沒有初始化,可以編寫一個簡單的類進行驗證:
public?class?Demo?{
????static?{
????????Log.i("Demo",?"static?block?called");
????}
????{
????????Log.i("Demo",?"IIB?called");
????}
}如果 Demo 類在代碼中沒有使用,那么上述兩個打印都不會觸發(fā);如果使用?Class.forName("Demo")?進行反射引用,則 static block 中的代碼會被調(diào)用。跟蹤 Class.forName 調(diào)用:
@CallerSensitive
public?static?Class>?forName(String?className)
????????????throws?ClassNotFoundException?{
????Class>?caller?=?Reflection.getCallerClass();
????//?筆者注:?initialize?=?true
????return?forName(className,?true,?ClassLoader.getClassLoader(caller));
}最終調(diào)用到名為?classForName?的 native 方法,其定義在?art/runtime/native/java_lang_Class.cc:
//?"name"?is?in?"binary?name"?format,?e.g.?"dalvik.system.Debug$1".
static?jclass?Class_classForName(JNIEnv*?env,?jclass,?jstring?javaName,?jboolean?initialize,
?????????????????????????????????jobject?javaLoader)?{
????ScopedFastNativeObjectAccess?soa(env);
????ScopedUtfChars?name(env,?javaName);
????std::string?descriptor(DotToDescriptor(name.c_str()));
????Handle?class_loader(
??????hs.NewHandle(soa.Decode(javaLoader)));
????ClassLinker*?class_linker?=?Runtime::Current()->GetClassLinker();
????Handle?c(
??????hs.NewHandle(class_linker->FindClass(soa.Self(),?descriptor.c_str(),?class_loader)));
????if?(initialize)?{
????????class_linker->EnsureInitialized(soa.Self(),?c,?true,?true);
????}
????return?soa.AddLocalReference(c.Get());
} 首先將 Java 格式的類表示轉(zhuǎn)換為 smali 格式,然后通過指定的 class_loader 去查找類,查找過程主要通過?class_linker?實現(xiàn)。由于 forName 函數(shù)中指定了?initialize?為?true,因此在找到對應(yīng)類后還會額外執(zhí)行一步?EnsureInitialized,在后文會進行詳細介紹。
FindClass
FindClass 實現(xiàn)了根據(jù)類名查找類的過程,定義在?art/runtime/class_linker.cc?中,關(guān)鍵流程如下:
ObjPtr?ClassLinker::FindClass(Thread*?self,
?????????????????????????????????????????????const?char*?descriptor,
?????????????????????????????????????????????Handle?class_loader)?
????if?(descriptor[1]?==?'\0')?
????????return?FindPrimitiveClass(descriptor[0]);
????const?size_t?hash?=?ComputeModifiedUtf8Hash(descriptor);
????//?在已經(jīng)加載的類中查找
????ObjPtr?klass?=?LookupClass(self,?descriptor,?hash,?class_loader.Get());
????if?(klass?!=?nullptr)?{
????????return?EnsureResolved(self,?descriptor,?klass);
????}
????//?尚未加載
????if?(descriptor[0]?!=?'['?&&?class_loader?==?nullptr)?{
????????//?類加載器為空,且不是數(shù)組類型,在啟動類中進行查找
????????ClassPathEntry?pair?=?FindInClassPath(descriptor,?hash,?boot_class_path_);
????????return?DefineClass(self,?descriptor,?hash,
???????????????????????????ScopedNullHandle(),
???????????????????????????*pair.first,?*pair.second);
????}
????ObjPtr?result_ptr;
????bool?descriptor_equals;
????ScopedObjectAccessUnchecked?soa(self);
????//?先通過?classLoader?的父類查找
????bool?known_hierarchy?=
????????FindClassInBaseDexClassLoader(soa,?self,?descriptor,?hash,?class_loader,?&result_ptr);
????if?(result_ptr?!=?nullptr)?{
????????descriptor_equals?=?true;
????}?else?if?(!self->IsExceptionPending())?{
????????//?如果沒找到,再通過?classLoader?查找
????????std::string?class_name_string(descriptor?+?1,?descriptor_length?-?2);
????????std::replace(class_name_string.begin(),?class_name_string.end(),?'/',?'.');
????????ScopedLocalRef?class_loader_object(
????????????soa.Env(),?soa.AddLocalReference(class_loader.Get()));
????????ScopedLocalRef?result(soa.Env(),?nullptr);
????????result.reset(soa.Env()->CallObjectMethod(class_loader_object.get(),
?????????????????????????????????????????????????WellKnownClasses::java_lang_ClassLoader_loadClass,
?????????????????????????????????????????????????class_name_object.get()));
????}
????//?將找到的類插入到緩存表中
????ClassTable*?const?class_table?=?InsertClassTableForClassLoader(class_loader.Get());
????class_table->InsertWithHash(result_ptr,?hash);
????return?result_ptr;
} 首先會通過?LookupClass?在已經(jīng)加載的類中查找,已經(jīng)加載的類會保存在 ClassTable 中,以 hash 表的方式存儲,該表的鍵就是類對應(yīng)的 hash,通過 descriptor 計算得出。如果之前已經(jīng)加載過,那么這時候就可以直接返回,如果沒有就需要執(zhí)行真正的加載了。從這里我們也可以看出,類的加載過程屬于懶加載 (lazy loading),如果一個類不曾被使用,那么是不會有任何加載開銷的。
然后會判斷指定的類加載器是否為空,為空表示要查找的類實際上是一個系統(tǒng)類。系統(tǒng)類不存在于 APP 的 DEX 文件中,而是 Android 系統(tǒng)的一部分。由于每個 Android (Java) 應(yīng)用都會用到系統(tǒng)類,為了提高啟動速度,實際通過 zygote 去加載,并由所有子進程一起共享。上述?boot_class_path_?數(shù)組在?Runtime::Init?中通過 ART 啟動的參數(shù)進行初始化,感興趣的可以自行研究細節(jié)。
我們關(guān)心的應(yīng)用類查找過程可以分為兩步,首先在父類的 ClassLoader 進行查找,如果沒找到才會通過指定的 classLoader 進行查找,這也是很多類似 Java 文章中提到的 “雙親委派” 機制。保證關(guān)鍵類的查找過程優(yōu)先通過系統(tǒng)類加載器,可以防止關(guān)鍵類實現(xiàn)被應(yīng)用篡改。
FindClassInBaseDexClassLoader?的實現(xiàn)使用偽代碼描述如下所示:
Class?ClassLinker::FindClassInBaseDexClassLoader(ClassLoader?class_loader,?size_t?hash)?{
????if?(class_loader?==?java_lang_BootClassLoader)?{
????????return?FindClassInBootClassLoaderClassPath(class_loader,?hash);
????}
????if?(class_loader?==?dalvik_system_PathClassLoader?||
????????class_loader?==?dalvik_system_DexClassLoader?||
????????class_loader?==?dalvik_system_InMemoryDexClassLoader)?{
????????//?For?regular?path?or?dex?class?loader?the?search?order?is:
????????//????-?parent
????????//????-?shared?libraries
????????//????-?class?loader?dex?files
????????FindClassInBaseDexClassLoader(class_loader->GetParent,?hash)?&&?return?result;
????????FindClassInSharedLibraries(...)?&&?return?result;
????????FindClassInBaseDexClassLoaderClassPath(...)?&&?return?result;
????????FindClassInSharedLibrariesAfter(...)?&&?return?result;
????}
????if?(class_loader?==?dalvik_system_DelegateLastClassLoader)?{
????????//?For?delegate?last,?the?search?order?is:
????????//????-?boot?class?path
????????//????-?shared?libraries
????????//????-?class?loader?dex?files
????????//????-?parent
????????FindClassInBootClassLoaderClassPath(...)?&&?return?result;
????????FindClassInBaseDexClassLoaderClassPath(...)?&&?return?result;
????????FindClassInSharedLibrariesAfter(...)?&&?return?result;
????????FindClassInBaseDexClassLoader(class_loader->GetParent,?hash)?&&?return?result;
????}
????return?null;
}根據(jù)不同的 class_loader 類型使用不同的搜索順序,如果涉及到父 ClassLoader 的搜索,則使用遞歸查找,遞歸的停止條件是當前 class_loader 為java.lang.BootClassLoader。
FindClassInBootClassLoaderClassPath?的關(guān)鍵代碼如下:
using?ClassPathEntry?=?std::pair<const?DexFile*,?const?dex::ClassDef*>;
bool?ClassLinker::FindClassInBootClassLoaderClassPath(Thread*?self,
??????????????????????????????????????????????????????const?char*?descriptor,
??????????????????????????????????????????????????????size_t?hash,
??????????????????????????????????????????????????????/*out*/?ObjPtr*?result)?{
????ClassPathEntry?pair?=?FindInClassPath(descriptor,?hash,?boot_class_path_);
????if?(pair.second?!=?nullptr)?{
????????ObjPtr?klass?=?LookupClass(self,?descriptor,?hash,?nullptr);
????????if?(klass?!=?nullptr)?{
????????????*result?=?EnsureResolved(self,?descriptor,?klass);
????????}?else?{
????????????*result?=?DefineClass(self,?...);
????????}
????}
????return?true; 如果在 BaseClassLoader 中沒有找到對應(yīng)的類,那么最終會通過傳入的 classLoader 查找,即調(diào)用指定類加載器的 loadClass 方法。在這個場景中(Class.forName),實際指定的是 caller 的 classLoader,編寫一個 APK 進行動態(tài)分析,打印出當前的 classLoader 如下:
dalvik.system.PathClassLoader[
DexPathList[[
zip?file?"/data/app/~~0FBqwacokhdG5rhF1RDZGg==/com.evilpan.test-fCJvsE74xP_SdvTlAfJDcA==/base.apk"
],
nativeLibraryDirectories=[/data/app/~~0FBqwacokhdG5rhF1RDZGg==/com.evilpan.test-fCJvsE74xP_SdvTlAfJDcA==/lib/arm64,?/system/lib64,?/system_ext/lib64]]]所以這是一個?PathClassLoader?對象,該類沒有定義 loadClass,因此是調(diào)用了父類的 loadClass 方法,整體調(diào)用路徑如下所示:
sequenceDiagram
%%?loadClass
participant?P?as?PathClassLoader
participant?B?as?BaseDexClassLoader
participant?C?as?ClassLoader
participant?D?as?DexFile
P?->>?C:?loadClass
C?->>?B:?findClass
B?->>?P:?pathList.findClass
P?->>?D:?loadClassBinaryName
Note?right?of?D:?defineClass?
?defineClassNative
最終調(diào)用了 DexFile 的 native 方法 defineClassNative,實現(xiàn)在?art/runtime/native/dalvik_system_DexFile.cc,關(guān)鍵代碼如下:
static?jclass?DexFile_defineClassNative(JNIEnv*?env,
????????????????????????????????????????jclass,
????????????????????????????????????????jstring?javaName,
????????????????????????????????????????jobject?javaLoader,
????????????????????????????????????????jobject?cookie,
????????????????????????????????????????jobject?dexFile)?{
????std::vector<const?DexFile*>?dex_files;
????ConvertJavaArrayToDexFiles(env,?cookie,?/*out*/?dex_files,?/*out*/?oat_file);
????ScopedUtfChars?class_name(env,?javaName);
????const?std::string?descriptor(DotToDescriptor(class_name.c_str()));
????const?size_t?hash(ComputeModifiedUtf8Hash(descriptor.c_str()));
????for?(auto&?dex_file?:?dex_files)?{
????????const?dex::ClassDef*?dex_class_def?=?OatDexFile::FindClassDef(*dex_file,?descriptor.c_str(),?hash);
????????//?dex_class_def?!=?nullptr
????????ClassLinker*?class_linker?=?Runtime::Current()->GetClassLinker();
????????Handle?class_loader(
??????????hs.NewHandle(soa.Decode(javaLoader)));
????????ObjPtr?dex_cache?=
??????????class_linker->RegisterDexFile(*dex_file,?class_loader.Get());
????????//?dex_cache?!=?nullptr
????????ObjPtr?result?=?class_linker->DefineClass(soa.Self(),
???????????????????????????????????????????????????????????????descriptor.c_str(),
???????????????????????????????????????????????????????????????hash,
???????????????????????????????????????????????????????????????class_loader,
???????????????????????????????????????????????????????????????*dex_file,
???????????????????????????????????????????????????????????????*dex_class_def);
????????class_linker->InsertDexFileInToClassLoader(soa.Decode(dexFile),
?????????????????????????????????????????????????class_loader.Get());
????}
} 也就是說,不論是通過?FindClassInBaseDexClassLoader?查找還是通過指定 classLoader 的?loadClass?加載,最終執(zhí)行的流程都是類似的,即在對應(yīng)的 DexFile(OatDexFile) 中根據(jù)類名搜索對應(yīng)類的 ClassDef 字段,了解 Dex 文件結(jié)構(gòu)的對這個字段應(yīng)該不會陌生,后面可能會單獨寫一篇 DexFile 文件格式的介紹,這里限于篇幅先不展開,只需要知道這個字段包含類的定義即可。
在找到類在對應(yīng) Dex 文件中的 ClassDef 內(nèi)容后,會通過 ClassLinker 完成該類的后續(xù)注冊流程,包括:
?對于當前 DexFile,如果是第一次遇到,會創(chuàng)建一個 DexCache 緩存,保存到 ClassLinker 的?dex_caches_?哈希表中;?通過?ClassLinker::DefineClass?完成目標類的定義,詳見后文;?將對應(yīng) DexFile 添加到類加載器對應(yīng)的 ClassTable 中;
其中 DefineClass 是我們比較關(guān)心的,因此下面單獨進行介紹。
DefineClass
先看代碼:
ObjPtr?ClassLinker::DefineClass(Thread*?self,
???????????????????????????????????????????????const?char*?descriptor,
???????????????????????????????????????????????size_t?hash,
???????????????????????????????????????????????Handle?class_loader,
???????????????????????????????????????????????const?DexFile&?dex_file,
???????????????????????????????????????????????const?dex::ClassDef&?dex_class_def)?{
????ScopedDefiningClass?sdc(self);
????StackHandleScope<3>?hs(self);
????auto?klass?=?hs.NewHandle(nullptr);
????//?Load?the?class?from?the?dex?file.
????if?(UNLIKELY(!init_done_))?{
????????//?[1]?finish?up?init?of?hand?crafted?class_roots_
????}
????ObjPtr?dex_cache?=?RegisterDexFile(*new_dex_file,?class_loader.Get());
????klass->SetDexCache(dex_cache);
????ObjPtr?existing?=?InsertClass(descriptor,?klass.Get(),?hash);
????if?(existing?!=?nullptr)?{
????????//?其他線程正在鏈接該類,阻塞等待其完成
????????return?sdc.Finish(EnsureResolved(self,?descriptor,?existing));
????}
????LoadClass(self,?*new_dex_file,?*new_class_def,?klass);
????//?klass->IsLoaded
????LoadSuperAndInterfaces(klass,?*new_dex_file))
????Runtime::Current()->GetRuntimeCallbacks()->ClassLoad(klass);
????//?klass->IsResolved
????LinkClass(self,?descriptor,?klass,?interfaces,?&h_new_class)
????Runtime::Current()->GetRuntimeCallbacks()->ClassPrepare(klass,?h_new_class);
????jit::Jit::NewTypeLoadedIfUsingJit(h_new_class.Get());
????return?sdc.Finish(h_new_class);
} 這里只列出一些關(guān)鍵代碼,init_done_?用于表示當前 ClassLinker 的初始化狀態(tài),初始化過程用于從 Image 空間或者手動創(chuàng)建內(nèi)部類,手動創(chuàng)建的內(nèi)部類包括:
?Ljava/lang/Object;?Ljava/lang/Class;?Ljava/lang/String;?Ljava/lang/ref/Reference;?Ljava/lang/DexCache;?Ldalvik/system/ClassExt;
它們都直接定義在了?art::runtime::mirror?命名空間中,比如 Object 定義為?mirror::Object,所屬文件為?art/runtime/mirror/object.h?;
LoadClass
ClassLinker::LoadClass?用于從指定 DEX 文件中加載目標類的屬性和方法等內(nèi)容,注意這里其實是在對應(yīng)類添加到 ClassTable 之后才加載的,這是出于 ART 的內(nèi)部優(yōu)化考慮,另外一個原因是類的屬性根只能通過 ClassTable 訪問,因此需要在訪問前先在 ClassTable 中占好位置。其實現(xiàn)如下:
void?ClassLinker::LoadClass(Thread*?self,
????????????????????????????const?DexFile&?dex_file,
????????????????????????????const?dex::ClassDef&?dex_class_def,
????????????????????????????Handle?klass)?{
????ClassAccessor?accessor(dex_file,
?????????????????????????dex_class_def,
?????????????????????????/*?parse_hiddenapi_class_data=?*/?klass->IsBootStrapClassLoaded());
????Runtime*?const?runtime?=?Runtime::Current();
????accessor.VisitFieldsAndMethods(
????????[&](const?ClassAccessor::Field&?field)?{
????????????LoadField(field,?klass,?&sfields->At(num_sfields));
????????????++num_sfields;
????????},
????????[&](const?ClassAccessor::Field&?field)?{
????????????LoadField(field,?klass,?&ifields->At(num_ifields));
????????????++num_ifields;
????????},
????????[&](const?ClassAccessor::Method&?method)?{
????????????ArtMethod*?art_method?=?klass->GetDirectMethodUnchecked(
????????????????class_def_method_index,
????????????????image_pointer_size_);
????????????LoadMethod(dex_file,?method,?klass,?art_method);
????????????LinkCode(this,?art_method,?oat_class_ptr,?class_def_method_index);
????????????++class_def_method_index;
????????},
????????[&](const?ClassAccessor::Method&?method)?{
????????????ArtMethod*?art_method?=?klass->GetVirtualMethodUnchecked(
????????????????class_def_method_index?-?accessor.NumDirectMethods(),
????????????????image_pointer_size_);
????????????LoadMethod(dex_file,?method,?klass,?art_method);
????????????LinkCode(this,?art_method,?oat_class_ptr,?class_def_method_index);
????????????++class_def_method_index;
????????}
????);
????klass->SetSFieldsPtr(sfields);
????klass->SetIFieldsPtr(ifields);
} 上面用到了?C++11?的 lambda 函數(shù)來通過迭代器訪問類中的關(guān)聯(lián)元素,分別是:
1.sfields: static fields,靜態(tài)屬性2.ifields: instance fields,對象屬性3.direct method: 對象方法4.virtual method: 抽象方法
對于屬性的加載通過?LoadField?實現(xiàn),主要作用是初始化 ArtField 并與目標類關(guān)聯(lián)起來;LoadMethod?的實現(xiàn)亦是類似,主要是使用 dex 文件中對應(yīng)方法的 CodeItem 對 ArtMethod 進行初始化,并與 klass 關(guān)聯(lián)。但是對于方法而言,還好進行額外的一步,即?LinkCode。
LinkCode
LinkCode 顧名思義是對代碼進行鏈接,關(guān)鍵代碼如下:
static?void?LinkCode(ClassLinker*?class_linker,
?????????????????????ArtMethod*?method,
?????????????????????const?OatFile::OatClass*?oat_class,
?????????????????????uint32_t?class_def_method_index)?{
????Runtime*?const?runtime?=?Runtime::Current();
????const?void*?quick_code?=?nullptr;
????if?(oat_class?!=?nullptr)?{
?????????//?Every?kind?of?method?should?at?least?get?an?invoke?stub?from?the?oat_method.
?????????//?non-abstract?methods?also?get?their?code?pointers.
?????????const?OatFile::OatMethod?oat_method?=?oat_class->GetOatMethod(class_def_method_index);
?????????quick_code?=?oat_method.GetQuickCode();
????}
????runtime->GetInstrumentation()->InitializeMethodsCode(method,?quick_code);
????if?(method->IsNative())?{
????//?Set?up?the?dlsym?lookup?stub.?Do?not?go?through?`UnregisterNative()`
????//?as?the?extra?processing?for?@CriticalNative?is?not?needed?yet.
????????method->SetEntryPointFromJni(
????????????method->IsCriticalNative()???GetJniDlsymLookupCriticalStub()?:?GetJniDlsymLookupStub());
??}
}其中 quick_code 指針指向的是 OatMethod 中的?code_offset_?偏移處的值,該值指向的是 OAT 優(yōu)化后的本地代碼位置。InitializeMethodsCode?是?Instrumentation?類的方法,實現(xiàn)在?art/runtime/instrumentation.cc,如果看過之前分析應(yīng)用啟動流程的文章應(yīng)該對這個類不會陌生,盡管不是同一個類,但它們的功能卻是類似的,即作為某些關(guān)鍵調(diào)用的收口,并在其中實現(xiàn)可插拔的追蹤行為。其內(nèi)部實現(xiàn)如下:
void?Instrumentation::InitializeMethodsCode(ArtMethod*?method,?const?void*?aot_code)?{
????//?Use?instrumentation?entrypoints?if?instrumentation?is?installed.
????if?(UNLIKELY(EntryExitStubsInstalled()))?{
????????if?(!method->IsNative()?&&?InterpretOnly())?{
????????????UpdateEntryPoints(method,?GetQuickToInterpreterBridge());
????????}?else?{
????????????UpdateEntryPoints(method,?GetQuickInstrumentationEntryPoint());
????????}
????????return;
????}
????if?(UNLIKELY(IsForcedInterpretOnly()))?{
????????UpdateEntryPoints(
????????????method,?method->IsNative()???GetQuickGenericJniStub()?:?GetQuickToInterpreterBridge());
????????return;
????}
????//?Use?the?provided?AOT?code?if?possible.
????if?(CanUseAotCode(method,?aot_code))?{
????????UpdateEntryPoints(method,?aot_code);
????????return;
????}
????//?Use?default?entrypoints.
????UpdateEntryPoints(
??????method,?method->IsNative()???GetQuickGenericJniStub()?:?GetQuickToInterpreterBridge());
}第一部分正是用于追蹤的判斷,如果當前已經(jīng)安裝了追蹤監(jiān)控,那么會根據(jù)當前方法的類別分別設(shè)置對應(yīng)的入口點;否則就以常規(guī)方式設(shè)置方法的調(diào)用入口:
?對于強制解釋執(zhí)行的運行時環(huán)境:?如果是 Native 方法則將入口點設(shè)置為?art_quick_generic_jni_trampoline,用于跳轉(zhuǎn)執(zhí)行 JNI 本地代碼;?對于 Java 方法則將入口點設(shè)置為?art_quick_to_interpreter_bridge,使方法調(diào)用過程會跳轉(zhuǎn)到解釋器繼續(xù);?如果 AOT 編譯的本地代碼可用,則直接將方法入口點設(shè)置為 AOT 代碼;?如果 AOT 代碼不可用,那么就回到解釋執(zhí)行場景進行處理;
設(shè)置 ArtMethod 入口地址的方法是 UpdateEntryPoints,其內(nèi)部實現(xiàn)非常簡單:
static?void?UpdateEntryPoints(ArtMethod*?method,?const?void*?quick_code)
????REQUIRES_SHARED(Locks::mutator_lock_)?{
????if?(kIsDebugBuild)?{
????????...
????}
????//?If?the?method?is?from?a?boot?image,?don't?dirty?it?if?the?entrypoint
????//?doesn't?change.
????if?(method->GetEntryPointFromQuickCompiledCode()?!=?quick_code)?{
????????method->SetEntryPointFromQuickCompiledCode(quick_code);
????}
}內(nèi)部實質(zhì)上是調(diào)用了?ArtMethod::SetEntryPointFromQuickCompiledCode:
void?SetEntryPointFromQuickCompiledCode(const?void*?entry_point_from_quick_compiled_code)
??????REQUIRES_SHARED(Locks::mutator_lock_)?{
????SetEntryPointFromQuickCompiledCodePtrSize(entry_point_from_quick_compiled_code,
??????????????????????????????????????????????kRuntimePointerSize);
??}回顧我們前面分析方法調(diào)用的章節(jié),對于快速執(zhí)行的場景,ArtMethod::Invoke?最終是跳轉(zhuǎn)到?entry_point_from_quick_compiled_code?進行執(zhí)行,而這個字段就是在這里進行設(shè)置的。
至此,我們完成了 ART 方法調(diào)用流程分析的最后一塊拼圖。
類初始化
此時我們已經(jīng)完成了類的加載,包括類中的所有方法、屬性的初始化。在前文?classForName?的實現(xiàn)中,完成類加載后還調(diào)用了一次 EnsureInitialized,在其中調(diào)用了?ClassLinker::InitializeClass?對類進行初始化,主要包括靜態(tài)屬性的初始化以及調(diào)用類中的??代碼,這也是為什么本節(jié)開頭 Demo 類的 static block 中代碼會被調(diào)用的原因。
初始化流程嚴格按照 Java 語言標準實現(xiàn),詳見?Java Language Specification 12.4.2 "Detailed Initialization Procedure"[6]
應(yīng)用場景
通過上面的分析,我們大致了解了 ART 虛擬機的文件、代碼加載流程,以及對應(yīng) Java 方法和指令的運行過程。正所謂無利不起早,之所以花費這么多時間精力去學習 ART,是因為其在 Android 運行過程中起著舉足輕重的作用,下面就列舉一些常見的應(yīng)用場景。
熱修復 & Hook
所謂熱修復,就是在不修改原有代碼的基礎(chǔ)上修改應(yīng)用功能,比如替換某些類方法的實現(xiàn),達到熱更新的目的。猶記得在幾年前,熱修復的概念在 Android 生態(tài)中甚囂塵上,隨著 ART 替換 Dalvik,以及碎片化引入的一系列問題導致這種方案逐漸銷聲匿跡。但是熱修復的使用場景并沒有完全消失,比如在 Android 應(yīng)用安全研究中 Hook 的概念也是熱修復的一種延續(xù)。
那么根據(jù)前面總結(jié)的知識可以考慮一個問題,如何在運行時劫持某個 Java 方法的執(zhí)行流程?最好是可以在指定方法調(diào)用前以及返回前分別觸發(fā)我們自己定義的回調(diào),從而實現(xiàn)調(diào)用參數(shù)和返回值的觀察和修改。
根據(jù)前文對方法調(diào)用和代碼加載的分析,Android 中的 Java 方法在 ART 中執(zhí)行都會通過?ArtMethod::Invoke?進行調(diào)用,在其內(nèi)部要么通過解釋器直接解釋執(zhí)行(配合 JIT);要么通過?GetEntryPointFromQuickCompiledCode?獲取本地代碼進行執(zhí)行,當然后者在某些場景下依然會回退到解釋器,但入口都是固定的,即?entry_point_from_quick_compiled_code?所指向的 quick 代碼。因此,要想實現(xiàn) Java 方法調(diào)用的劫持,可以有幾種思路:
1.修改?ArtMethod::Invoke?這個 C++ 函數(shù)為我們自己的實現(xiàn),在其中增加劫持邏輯;2.修改目標 Java 方法屬性,令所有調(diào)用都走 quick 分支,然后將?entry_point_from_quick_compiled_code?修改為指向我們自己的實現(xiàn),從而實現(xiàn)劫持;3.類似于上述方法,不過不修改指針的值,而是修改 stub code;4.……
當然,前途是光明的,道路是曲折的,這些方法看起來都很直觀,但實現(xiàn)起來有很多工程化的難點。比如需要仔細處理調(diào)用前后的堆棧令其保持平衡,這涉及到 inline-hook 框架本身的魯棒性;有比如在新版本中對于系統(tǒng)類方法的調(diào)用,ART 會直接優(yōu)化成匯編跳轉(zhuǎn)而繞過 ArtMethod 方法的查找過程,因此方法 1、2 無法覆蓋到這些場景,……不一而足。
以大家常用的 frida 為例,其對 Java 方法 Hook 的實現(xiàn)在?frida-java-bridge,關(guān)鍵代碼在?lib/android.js?文件中:
class?ArtMethodMangler?{
????replace?(impl,?isInstanceMethod,?argTypes,?vm,?api)?{
????????this.originalMethod?=?fetchArtMethod(this.methodId,?vm);
????????const?originalFlags?=?this.originalMethod.accessFlags;
????????if?((originalFlags?&?kAccXposedHookedMethod)?!==?0?&&?xposedIsSupported())?{
????????????//?檢測?Xposed,如果已經(jīng)被?Xposed?hook?了會從新獲取源函數(shù)?...
????????}
????????const?replacementMethodId?=?cloneArtMethod(hookedMethodId,?vm);
????????patchArtMethod(replacementMethodId,?{
????jniCode:?impl,
????accessFlags:?((originalFlags?&?~(kAccCriticalNative?|?kAccFastNative?|?kAccNterpEntryPointFastPathFlag))?|?kAccNative)?>>>?0,
????quickCode:?api.artClassLinker.quickGenericJniTrampoline,
????interpreterCode:?api.artInterpreterToCompiledCodeBridge
},?vm);
????//?修改?flags?使解釋器執(zhí)行到我們想要的分支
????let?hookedMethodRemovedFlags?=?kAccFastInterpreterToInterpreterInvoke?|?kAccSingleImplementation?|?kAccNterpEntryPointFastPathFlag;
????if?((originalFlags?&?kAccNative)?===?0)?{
??????hookedMethodRemovedFlags?|=?kAccSkipAccessChecks;
????}
????patchArtMethod(hookedMethodId,?{
??????accessFlags:?(originalFlags?&?~(hookedMethodRemovedFlags))?>>>?0
????},?vm);
????//?將?Nterp?解釋器的入口替換為?art_quick_to_interpreter_bridge?從而令代碼跳轉(zhuǎn)到?quick?入口
????const?quickCode?=?this.originalMethod.quickCode;
????const?{?artNterpEntryPoint?}?=?api;
????if?(artNterpEntryPoint?!==?undefined?&&?quickCode.equals(artNterpEntryPoint))?{
??????patchArtMethod(hookedMethodId,?{
????????quickCode:?api.artQuickToInterpreterBridge
??????},?vm);
????}
????//?開啟劫持
????if?(!isArtQuickEntrypoint(quickCode))?{
????????const?interceptor?=?new?ArtQuickCodeInterceptor(quickCode);
????????interceptor.activate(vm);
????????this.interceptor?=?interceptor;
????}
????//?使用?hash?表記錄已經(jīng)替換的方法,方便后續(xù)恢復
????artController.replacedMethods.set(hookedMethodId,?replacementMethodId);
????notifyArtMethodHooked(hookedMethodId,?vm);
????}
}其中 Nterp 是 ART 中一個改良過的解釋器,用于替代早期 Dalvik 的 mterp 解釋器,這里先不展開實現(xiàn)的細節(jié),只需關(guān)注實際執(zhí)行劫持的地方,即?interceptor.activate(vm)。interceptor 在實例化時指定的 quickCode 即為對應(yīng) ArtMethod 的快速執(zhí)行入口,activate 代碼如下:
activate?(vm)?{
????this._createTrampoline();
????const?{?trampoline,?quickCode,?redirectSize?}?=?this;
????const?writeTrampoline?=?artQuickCodeReplacementTrampolineWriters[Process.arch];
????const?prologueLength?=?writeTrampoline(trampoline,?quickCode,?redirectSize,?vm);
????this.overwrittenPrologueLength?=?prologueLength;
????this.overwrittenPrologue?=?Memory.dup(this.quickCodeAddress,?prologueLength);
????const?writePrologue?=?artQuickCodePrologueWriters[Process.arch];
????writePrologue(quickCode,?trampoline,?redirectSize);
}可以看到 frida 實際上是使用了我們上述的第 3 種 Hook 思路,即修改 stub code 為我們的劫持代碼,這種方式一般稱之為?dynamic callee-side rewriting,優(yōu)點是即便對于 OAT 極致優(yōu)化的系統(tǒng)類方法也同樣有效。當然,我們這里只是管中規(guī)豹,實際的實現(xiàn)上還有很多細節(jié)值得學習,感興趣的可以自行閱讀代碼。
安全加固
了解過 Android 逆向工程的人應(yīng)該都知道,基于 Java 編譯出來的 Dalvik 字節(jié)碼其實很好理解,加上一些開源或者商業(yè)的反編譯工具,甚至可以將字節(jié)碼還原為和源代碼非常接近的 Java 代碼表示。這對于很多想在代碼中隱藏秘密的公司而言是很不愿意看到的。
因此,安全工程師們就想出了一些保護代碼防止靜態(tài)逆向分析的方案,業(yè)內(nèi)常稱為?加殼,國外叫做?Packer,即在原始字節(jié)碼上套上一層保護殼,并在運行時進行執(zhí)行解密還原。
回顧我們學習的知識可以腦暴出幾種安全加固方案(其實是業(yè)內(nèi)已有方案):
1.把整個 DEX 文件加密,然后在殼程序啟動時還原解密文件并加載;2.優(yōu)化上述方案,不落地文件,直接在內(nèi)存中解密加載;3.提取出 DEX 文件中的字節(jié)碼,并在運行時還原;4.替換掉 DEX 文件中每個方法的字節(jié)碼為解密代碼,運行時解密執(zhí)行;5.……
這些加固方案根據(jù)解密粒度不同也常稱為整體殼、抽取殼。對于整體加密的方案不必多說,在 PC 時代也有很多類似的混淆方法;而對于抽取殼,實現(xiàn)就百花齊放了,比如有的加固方案是在類初始化期間進行還原,有的是在方法執(zhí)行前進行還原。
回顧上面介紹熱修復的內(nèi)容,殼代碼其實也可以看做是一個熱修復框架,只不過是對于每個函數(shù)都進行了劫持,在目標函數(shù)運行前對實際的字節(jié)碼進行還原;
有些類級別的加固則是基于上文中代碼加載流程,在類的初始化函數(shù)()中執(zhí)行解密操作,因為 Java 標準保證了這是一個類最先執(zhí)行的代碼。
由于抽取殼本身對字節(jié)碼進行了加密,因此在應(yīng)用安裝期間 dex2oat 就無法優(yōu)化這些代碼,以至于在運行時只能通過解釋執(zhí)行,雖然有一部分 JIT 的加持,但還是讓 ART 的大部分優(yōu)化心血付諸東流;另外,加殼本身會使用到 ART 中的一些內(nèi)部符號和偏移,因此需要針對不同版本進行適配,一個不小心就是用戶端的持續(xù)崩潰。
也因為這些原因,很多頭部廠商的 Android 應(yīng)用其實是不加殼的,對于真正需要保護的代碼,可以選擇 JNI 用 C/C++ 實現(xiàn),并配上 LLVM 成熟的混淆方案進行加固。
脫殼
由于很多安全公司把加固做成了商業(yè)服務(wù),因此除了正常應(yīng)用,大部分惡意軟件和非法應(yīng)用也都用上了商業(yè)的加固方案,這對于正義的安全研究員而言是一個確實的阻礙,因此脫殼也就成了常見需求。
一開始我們在遇到加固的應(yīng)用時候會先嘗試進行手動進行分析、調(diào)試、還原,但是后來大家發(fā)現(xiàn)其實基于 ART 的運行模式有更通用的解決方式。
這里以目前相對較新的抽取殼為例,回顧上文代碼方法調(diào)用和代碼加載的章節(jié),不論加固的抽取和還原方法如何,最終還是要回到解釋執(zhí)行的(至少在 JIT 之前),因為加密的代碼在安裝時并沒有被 AOT 優(yōu)化。而且為了保證原始代碼邏輯不變,對應(yīng)加密方法在實際運行之前肯定需要被正確解密還原。
基于這點事實,我們可以在 ArtMethod 調(diào)用前進行斷點,然后通過?method->GetDexFile()?獲得對應(yīng) dex 文件在內(nèi)存中的地址并進行轉(zhuǎn)儲保存。如果當前內(nèi)存中的 dex 部分偏移被惡意修改,那么還可以通過?method->GetCodeItem()?獲取對應(yīng)方法解密后的字節(jié)碼地址進行手動轉(zhuǎn)儲恢復。
如果要恢復完整的 dex 文件,則需要令目標程序在運行時調(diào)用所有類的所有方法,這顯然不太現(xiàn)實;不過網(wǎng)上已經(jīng)有了一些開源的方案基于主動調(diào)用的思路去批量偽造方法調(diào)用,觸發(fā)殼的解密邏輯從而實現(xiàn)全量還原,比如?DexHunter[7]?和?FART[8],都是通過修改 Android 源碼實現(xiàn)的脫殼方案。
正如上節(jié)所說,安全加固方案五花八門,很難有一種絕對通用的方法去還原所有加固,往往還需要針對不同的殼做一些微小的適配工作。但總的來說,脫殼一方比寫殼一方還是占優(yōu)勢的,前者只需要針對一種環(huán)境實現(xiàn),不用考慮性能成本;后者則需要對 ART 有更深的理解來保證加固程序的穩(wěn)定性,同時還要針對不同環(huán)境都進行覆蓋,這也是攻防不對等的一個典型案例吧。
方法跟蹤
對于上述 Android 應(yīng)用加殼的方案,在數(shù)次攻防角斗下已經(jīng)被證明了只能作為輔助防護,因此移動安全廠商又提出了一些新的加固方案,比如直接對字節(jié)碼本身下手,套用 LLVM 控制流和數(shù)據(jù)流混淆的那一套方案,將字節(jié)碼的執(zhí)行順序打亂,插入各種無效指令來阻礙逆向工程;又或者將字節(jié)碼的實現(xiàn)抽批量自動取到 JNI 層,并輔以二進制級別的安全加固,這種方案通常稱為 Java2C,即將 Java 代碼轉(zhuǎn)譯成 C 代碼編譯來防止逆向分析。……
這時,傳統(tǒng)的脫殼方法就不見得有效了,因為即便還原出字節(jié)碼或者 Java 代碼,其流程也是混亂的,對于 Java2C 則更不用說,只能在二進制中想辦法將 JNI 調(diào)用還原。
不過我們可以思考一下,逆向工程的目的是什么?如果是為了分析還原程序的執(zhí)行流程,對其行為進行畫像和取證,那么完全可以通過動態(tài)跟蹤的方式實現(xiàn)。上文中已經(jīng)介紹了如果對某個指定方法進行熱修復或者說 hook,那么這里的思路就是對應(yīng)用中的所有 Java 方法都進行 hook,從而實現(xiàn)我們的運行時方法跟蹤行為。
例如針對每個 Java 方法在進入和退出前都插入我們的 hook 代碼,作用就是發(fā)送函數(shù)進出事件及其相關(guān)信息,如進程、線程 ID、方法名、參數(shù)等,接收端處理數(shù)據(jù)后實現(xiàn)一個樹狀的調(diào)用流圖。
一個簡單的調(diào)用流圖示例如下所示:
com.evilpan.Foo.onCreate
├──?com.evilpan.Foo.getContacts
│???├──?Context.getContentResolver
│???├──?ContentResolver.query
│???├──?Cursor.getColumnIndex
│???├──?Cursor.getString
│???├──?...
│???└──?Cursor.close
└──?com.evilpan.Foo.upload
????├──?URL.
????├──?URL.openConnection
????├──?HttpURLConnection.getOutputStream
????├──?BufferWriter.write
????└──?...前端通過處理和過濾這些數(shù)據(jù),可以在很大程度上還原程序行為。那么要如何實現(xiàn)所有 Java 方法的追蹤呢?entry_point_from_quick_compiled_code_?是一個重點關(guān)注的點,但如果我們想要像 frida 一樣劫持,就需要對每個方法做許多額外的工作,比如修改函數(shù)的 access_flag,修改解釋器執(zhí)行流程等。因此關(guān)鍵點還是在于如何同時處理解釋執(zhí)行和快速執(zhí)行的代碼,并將潛在的 JIT 運行時優(yōu)化考慮進去,自己造一個輪子無可厚非,但其實 ART 中已經(jīng)提供了這么一個“后門”,那就是在上文?LinkCode?代碼中的那句:
runtime->GetInstrumentation()->InitializeMethodsCode(method,?quick_code);在?Instrumentation::InitializeMethodsCode?的實現(xiàn)中,會先判斷當前是否已經(jīng)注冊了追蹤的 stub,如果有的話會直接替換對應(yīng)方法的入口點:
//?art/runtime/instrumentation.cc
void?Instrumentation::InitializeMethodsCode(ArtMethod*?method,?const?void*?aot_code)
????REQUIRES_SHARED(Locks::mutator_lock_)?{
??//?Use?instrumentation?entrypoints?if?instrumentation?is?installed.
??if?(UNLIKELY(EntryExitStubsInstalled()))?{
????if?(!method->IsNative()?&&?InterpretOnly())?{
??????UpdateEntryPoints(method,?GetQuickToInterpreterBridge());
????}?else?{
??????UpdateEntryPoints(method,?GetQuickInstrumentationEntryPoint());
????}
????return;
??}
??//?...對于已經(jīng)初始化過的 ArtMethod,還可以用?Instrumentation::InstallStubsForMethod?去為指定方法安裝跟蹤代碼。關(guān)于 Instrumentation 網(wǎng)上還沒有太多公開資料,需要通過源碼去進一步研究。
當然還是那句老話,想法是簡單的,實現(xiàn)是復雜的,這其中目前可預(yù)計到的問題就有:
1.運行時開銷;2.開啟和停止方式,可以通過中斷去控制;3.發(fā)送事件的方式,使用單獨的線程進行隊列發(fā)送,多進程通信方式;4.動態(tài)跟蹤的過濾,比如進入到系統(tǒng)方法中就不再進行跟蹤;5.循環(huán)調(diào)用的識別,接收端只能看到一系列循環(huán)事件;6.……
因此再展開就說來話長了,目前也只是在探索階段,后續(xù)有機會再單獨分享這部分內(nèi)容吧。
總結(jié)
本文主要目的是分析 Android 12 中 ART 的實現(xiàn),包括 Java 方法初始化和執(zhí)行的過程?;趯?ART 的深入理解,我們也列舉了幾種實踐中經(jīng)常遇到的場景,比如熱修復、動態(tài)注入、安全加固、脫殼等。也許在工作中信奉拿來主義,只需要工具能用就行,但了解工具背后的原理,才能更好適應(yīng)當前不斷激化的攻防對抗環(huán)境,從而更好地迎接未來的挑戰(zhàn)。
參考資料
?羅升陽: Dalvik 系列[9]?羅升陽: ART 系列[10]?Android Packer - facing the challenges, building solutions(slides)[11]?DexDefender: A DEX Protection Scheme to Withstand MemoryDump Attack Based on Android Platform[12]?ArtHook: Callee-side Method Hook Injection on the New Android Runtime ART[13]?我為 Dexposed +1s: 論ART上運行時 Method AOP 實現(xiàn)[14]?epic - Dynamic java method AOP hook for Android[15]?frida-java-bridge[16]
引用鏈接
[1]?Dalvik Executable format:?https://source.android.com/devices/tech/dalvik/dex-format[2]?Dalvik bytecode:?https://source.android.com/devices/tech/dalvik/dalvik-bytecode[3]?ClassLoader:?https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html[4]?art/runtime/vdex_file.h:?https://cs.android.com/android/platform/superproject/+/master:art/runtime/vdex_file.h[5]?art/libdexfile/dex/dex_file.h:?https://cs.android.com/android/platform/superproject/+/master:art/libdexfile/dex/dex_file.h[6]?Java Language Specification 12.4.2 "Detailed Initialization Procedure":?https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4[7]?DexHunter:?https://github.com/zyq8709/DexHunter[8]?FART:?https://github.com/hanbinglengyue/FART[9]?羅升陽: Dalvik 系列:?https://blog.csdn.net/Luoshengyang/article/details/8852432[10]?羅升陽: ART 系列:?https://blog.csdn.net/Luoshengyang/article/details/39256813[11]?Android Packer - facing the challenges, building solutions(slides):?https://www.virusbulletin.com/uploads/pdf/conference_slides/2014/Yu-VB2014.pdf[12]?DexDefender: A DEX Protection Scheme to Withstand MemoryDump Attack Based on Android Platform:?https://res-www.zte.com.cn/mediares/magazine/publication/com_en/article/201803/RONGYu.pdf[13]?ArtHook: Callee-side Method Hook Injection on the New Android Runtime ART:?http://publications.cispa.saarland/143/[14]?我為 Dexposed +1s: 論ART上運行時 Method AOP 實現(xiàn):?https://weishu.me/2017/11/23/dexposed-on-art/[15]?epic - Dynamic java method AOP hook for Android:?https://github.com/tiann/epic[16]?frida-java-bridge:?https://github.com/frida/frida-java-bridge
