<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ART 在 Android 安全攻防中的應(yīng)用

          共 11073字,需瀏覽 23分鐘

           ·

          2021-12-27 13:12


          背景

          在日常的 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)用流程可以簡化如下所示:

          methodfile
          JNIEnv.CallStaticVoidMethoddalvik/libnativehelper/include/nativehelper/jni.h
          JNINativeInterface.CallStaticVoidMethodVdalvik/vm/Jni.c
          Jni.dvmCallMethodVdalvik/vm/interp/Stack.c
          Stack.dvmInterpretdalvik/vm/interp/Interp.c
          dvmInterpretStddalvik/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 有其獨立的文件格式。整體流程如下圖所示:

          Java 代碼處理流程

          值得一提的是,在 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.vdex

          system_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)用鏈路簡化如下表:

          methodfile
          new PathClassLoader...
          new BaseDexClassLoaderlibcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
          new DexPathListlibcore/dalvik/src/main/java/dalvik/system/DexPathList.java
          DexPathList.makeDexElements...
          DexPathList.loadDexFile...
          new DexFilelibcore/dalvik/src/main/java/dalvik/system/DexFile.java
          DexFile.openDexFile...
          DexFile.openDexFileNative...
          DexFile_openDexFileNativeart/runtime/native/dalvik_system_DexFile.cc
          OatFileManager::OpenDexFilesFromOatart/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?data

          VdexFile 結(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)用鏈路:

          methodfile
          Executeart/runtime/interpreter/interpreter.cc
          ExecuteSwitch...
          ExecuteSwitchImplart/runtime/interpreter/interpreter_switch_impl.h
          ExecuteSwitchImplAsm...
          ExecuteSwitchImplAsmart/runtime/arch/arm64/quick_entrypoints_arm64.S
          ExecuteSwitchImplCppart/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
          img-load

          最終調(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


          瀏覽 135
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本a片免费视频 | 西西人体大胆4444w w | 操逼网。| 超碰97人妻 | 国语对白中文字幕第二页视频 |