1. 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 虛擬機,而是復(fù)用原有的虛擬機執(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 中負責(zé)加載指定類的對象是?ClassLoader[3],Android 中也是類似,BaseDexClassLoader 繼承自 ClassLoader 類,實現(xiàn)了許多 DEX 相關(guān)的加載操作,其子類包括:

        ?DexClassLoader: 負責(zé)從?.jar?或者?.apk?中加載類;?PathClassLoader: 負責(zé)從本地文件中初始化類加載器;?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)用后恢復(fù)寄存器的代碼,其核心是調(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)用前后都保存和恢復(fù)了當前上下文,使得不同調(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 方法和指令的運行過程。正所謂無利不起早,之所以花費這么多時間精力去學(xué)習(xí) ART,是因為其在 Android 運行過程中起著舉足輕重的作用,下面就列舉一些常見的應(yīng)用場景。

        熱修復(fù) & Hook

        所謂熱修復(fù),就是在不修改原有代碼的基礎(chǔ)上修改應(yīng)用功能,比如替換某些類方法的實現(xiàn),達到熱更新的目的。猶記得在幾年前,熱修復(fù)的概念在 Android 生態(tài)中甚囂塵上,隨著 ART 替換 Dalvik,以及碎片化引入的一系列問題導(dǎo)致這種方案逐漸銷聲匿跡。但是熱修復(fù)的使用場景并沒有完全消失,比如在 Android 應(yīng)用安全研究中 Hook 的概念也是熱修復(fù)的一種延續(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ù)恢復(fù)
        ????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é)值得學(xué)習(xí),感興趣的可以自行閱讀代碼。

        安全加固

        了解過 Android 逆向工程的人應(yīng)該都知道,基于 Java 編譯出來的 Dalvik 字節(jié)碼其實很好理解,加上一些開源或者商業(yè)的反編譯工具,甚至可以將字節(jié)碼還原為和源代碼非常接近的 Java 代碼表示。這對于很多想在代碼中隱藏秘密的公司而言是很不愿意看到的。

        因此,安全工程師們就想出了一些保護代碼防止靜態(tài)逆向分析的方案,業(yè)內(nèi)常稱為?加殼,國外叫做?Packer,即在原始字節(jié)碼上套上一層保護殼,并在運行時進行執(zhí)行解密還原。

        回顧我們學(xué)習(xí)的知識可以腦暴出幾種安全加固方案(其實是業(yè)內(nèi)已有方案):

        1.把整個 DEX 文件加密,然后在殼程序啟動時還原解密文件并加載;2.優(yōu)化上述方案,不落地文件,直接在內(nèi)存中解密加載;3.提取出 DEX 文件中的字節(jié)碼,并在運行時還原;4.替換掉 DEX 文件中每個方法的字節(jié)碼為解密代碼,運行時解密執(zhí)行;5.……

        這些加固方案根據(jù)解密粒度不同也常稱為整體殼、抽取殼。對于整體加密的方案不必多說,在 PC 時代也有很多類似的混淆方法;而對于抽取殼,實現(xiàn)就百花齊放了,比如有的加固方案是在類初始化期間進行還原,有的是在方法執(zhí)行前進行還原。

        回顧上面介紹熱修復(fù)的內(nèi)容,殼代碼其實也可以看做是一個熱修復(fù)框架,只不過是對于每個函數(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)儲恢復(fù)。

        如果要恢復(fù)完整的 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 代碼編譯來防止逆向分析?!?/p>

        這時,傳統(tǒng)的脫殼方法就不見得有效了,因為即便還原出字節(jié)碼或者 Java 代碼,其流程也是混亂的,對于 Java2C 則更不用說,只能在二進制中想辦法將 JNI 調(diào)用還原。

        不過我們可以思考一下,逆向工程的目的是什么?如果是為了分析還原程序的執(zhí)行流程,對其行為進行畫像和取證,那么完全可以通過動態(tài)跟蹤的方式實現(xiàn)。上文中已經(jīng)介紹了如果對某個指定方法進行熱修復(fù)或者說 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)是復(fù)雜的,這其中目前可預(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)常遇到的場景,比如熱修復(fù)、動態(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
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
          
          

            1. 欧美A片免费在线观看 | 999av | 国产性xxxx高清 | 给大家科普一下八重神子大战史莱姆 | 成人爱爱视频 |