<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    Android漏洞之戰——整體加殼原理和脫殼技巧詳解

    VSole2022-07-09 16:48:55

    前言

    為了幫助更加方便的進行漏洞挖掘工作,前面我們通過了幾篇文章詳解的給大家介紹了動態調試技術、過反調試技術、Hook技術、過反Hook技術、抓包技術等,掌握了這些可以很方便的開展App漏洞挖掘工作,而最后我們還需要掌握一定的脫殼技巧,進行進一步助力我們漏洞挖掘的效率。

    本文主要介紹Android App加殼中的整體dex加殼,幫助大家掌握加殼的原理和脫殼的各種技能。

    本文第二節主要講述Android啟動流程和加殼原理。

    本文第三節主要介紹整體加殼的實現。

    本文第四節主要講當下脫殼點的概念。

    本文第五節講述現有的脫殼技巧。

    相關介紹

    1.Android App啟動流程

    (1)Android系統啟動流程

    我們要徹底的了解App加殼原理,首先我們從了解App的啟動流程出發,先于App啟動之前,Android系統是啟動最早,下面我們來詳細查看一下Android系統的啟動過程:

     

    我在Xposed源碼定制(https://blog.csdn.net/hzwailll/article/details/85339714)一文中詳細的講解了Android的啟動流程,簡單來說就是:

    加載BootLoader --> 初始化內核 --> 啟動init進程 --> init進程fork出Zygote進程 --> Zygote進程fork出SystemServer進程
    

    我們就了解了最后Zygote進程fork出第一個進程:SystemServer進程,SystemServer主要完成了以下工作:

    android app安裝

    首先這里我們先介紹一下PackageManagerService,其主要是完成Android中應用程序安裝的服務,我們了解的Android應用程序安裝的方式:

    · 系統啟動時安裝,沒有安裝界面· 第三方應用安裝,有安裝界面,也是我們最熟悉的方式· ADB命令安裝,沒有安裝界面· 通過各類應用市場安裝,沒有安裝界面
    

    雖然安裝方式不同,但是最后四種方式都是通過PackageManagerService服務來完成應用程序的安裝。而PackageManagerService服務則通過與Installd服務通信,發送具體的指令來執行應用程序的安裝、卸載等工作。

    public static final IPackageManager main(Context context, Installer installer,    boolean factoryTest, boolean onlyCore) {        PackageManagerService m = new PackageManagerService(context, installer, factoryTest, onlyCore);        ServiceManager.addService("package", m);    return m;}
    

    應用程序在安裝時涉及到如下幾個重要目錄:

    我們了解完App的安裝流程是由PackageManagerService,同理SystemServer啟動了一個更加重要的服務ActivityManagerService, 而AMS其中很重要的一個作用就是啟動Launcher進程,具體是怎么啟動的,大家可以參考文章:Android系統啟動流程(四)Launcher啟動過程與系統啟動流程(https://blog.csdn.net/itachi85/article/details/56669808),這里就不再詳細講解,而進入Launcher進程,我們就進入了App啟動的流程。

    (2)App啟動流程

    Android系統啟動的最后一步是啟動一個Home應用程序,這個應用程序用來顯示系統中已經安裝的應用程序,這個Home應用程序就叫做Launcher。應用程序Launcher在啟動過程中會請求PackageManagerService返回系統中已經安裝的應用程序的信息,并將這些信息封裝成一個快捷圖標列表顯示在系統屏幕上,這樣用戶可以通過點擊這些快捷圖標來啟動相應的應用程序。

    前面我們描述了AMS將Launcher啟動,然后進入App啟動流程,這里參考文章:ActivityThread的理解和APP的啟動過程(https://blog.csdn.net/hzwailll/article/details/85339714

    ① 點擊桌面APP圖標時,Launcher的startActivity()方法,通過Binder通信,調用system_server進程中AMS服務的startActivity方法,發起啟動請求。

    ② system_server進程接收到請求后,向Zygote進程發送創建進程的請求。

    ③ Zygote進程fork出App進程,并執行ActivityThread的main方法,創建ActivityThread線程,初始化MainLooper,主線程Handler,同時初始化ApplicationThread用于和AMS通信交互。

    ④ App進程,通過Binder向sytem_server進程發起attachApplication請求,這里實際上就是APP進程通過Binder調用sytem_server進程中AMS的attachApplication方法,AMS的attachApplication方法的作用是將ApplicationThread對象與AMS綁定。

    ⑤ system_server進程在收到attachApplication的請求,進行一些準備工作后,再通過binder IPC向App進程發送handleBindApplication請求(初始化Application并調用onCreate方法)和scheduleLaunchActivity請求(創建啟動Activity)。

    ⑥ App進程的binder線程(ApplicationThread)在收到請求后,通過handler向主線程發送BIND_APPLICATION和LAUNCH_ACTIVITY消息,這里注意的是AMS和主線程并不直接通信,而是AMS和主線程的內部類ApplicationThread通過Binder通信,ApplicationThread再和主線程通過Handler消息交互。

    ⑦ 主線程在收到Message后,創建Application并調用onCreate方法,再通過反射機制創建目標Activity,并回調Activity.onCreate()等方法。

    ⑧ 到此,App便正式啟動,開始進入Activity生命周期,執行完onCreate/onStart/onResume方法,UI渲染后顯示APP主界面。

    到這里,我們的大致弄清了APP的啟動流程,而這里我們就進入了加殼中十分重要的地方ActivityTread。

    (3)ActivityThread啟動流程

    寒冰大佬在FART:ART環境下基于主動調用的自動化脫殼方案(https://bbs.pediy.com/thread-252630.htm) 一文中講述了ActivityThread.main()是進入App世界的大門,并由此展開了對加殼原理的講述。

    同理接下來,我們開始進行源碼分析,了解ActivityThread的具體操作:

    xref/frameworks/base/core/java/android/app/ActivityThread.java

    根據寒冰大佬描述,在ActivityThread完成實例化操作,調用thread.attach(false)完成一系列初始化準備工作,最后主線程進入消息循環,等待接收來自系統的消息。當收到系統發送來的bindapplication的進程間調用時,調用函數handlebindapplication來處理該請求。

    public void handleMessage(Message msg) {****    case BIND_APPLICATION:        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");        AppBindData data = (AppBindData)msg.obj;        handleBindApplication(data);        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);        break;****}
    

    在處理消息過程,很很明顯進入了handlebindapplication函數。

    這里我再用寒冰大佬文章的內容:

    我們定位第四步,Application進行實例化,然后進入makeApplication。

    然后我們進入newApplication。

    這里我們可以看見完成了兩件事:

    ① 完成了Application的實例化;

    ② 并調用Application.attach()函數。

    然后我們繼續進入Application.attach()函數。

    這里我們就進一步調用了attachBaseContext()方法。

    最后回到handlebindapplication中執行第6步,進入callApplicationOnCreate()函數。

     

    就執行了Application.onCreate()方法。

     

    總結:

    從上可知, App的運行流程是

    初始化————>Application的構造函數————>Application.attachBaseContext()————>Application.onCreate()函數

    最后才會進入MainActivity中的attachBaseContext函數、onCreate函數

    所以加殼廠商要在程序正式執行前,也就是上面的流程中進行動態加載和類加載器的修正,這樣才能對加密的dex進行釋放,而一般的1廠商往往選擇在Application中的attachBaseContext或onCreate函數進行。

    這里我附上網上一個大佬的詳細執行流程圖:

    2.整體加殼原理詳解

    (1)整體加殼原理

    Dex整體加殼可以理解為在加密的源Apk程序外面有套上了一層外殼,簡單過程為:

     

    如何對App進行加一層外殼呢,這里就需要應用動態加載的原理,關于動態加載和類加載器,我在上篇文章中有詳細講解:Android加殼脫殼學習(1)——動態加載和類加載機制詳解(https://bbs.pediy.com/thread-271538.htm)。

    這里我們可以用一個案例來進一步講述,我們打開一個整體加殼的樣本。

    我們很明顯看見,除了一個代理類Application,其他相關的代碼信息都無法發現。

    在代理類中反射調用了一些方法,很顯然我們解析出的結果都無法查找,很明顯就說明在Application.attchBaseContext()和Application.onCreate()中必須要完成對源加密的dex的動態加載和解密。

    結合上面的描述,App加載應用解析時就是這個流程:

    ① BootClassLoader加載系統核心庫。

    ② PathClassLoader加載APP自身dex。

    ③ 進入APP自身組件,解析AndroidManifest.xml,然后查找Application代理。

    ④ 調用聲明Application的attachBaseContext()對源程序進行動態加載或解密。

    ⑤ 調用聲明Application的onCreate()對源程序進行動態加載或解密。

    ⑥ 進入MainActivity中的attachBaseContext(),然后進入onCreate()函數,執行源程序代碼。

    (2)類加載器的修正

    上面我們已經很清晰的了解了殼加載的流程,我們很明顯的意識到一個問題,我們從頭到尾都是用PathClassLoader來加載dex,而上篇文章我在講類加載器的過程中說過。

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

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

    ② BaseDexClassLoader:實際應用層類文件的加載,而真正的加載委托給pathList來完成。

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

    ④ PathClassLoader:可以加載系統類和應用程序的類,通常用來加載已安裝的apk的dex文件。

    補充:

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

    我們要想動態加載dex文件必須使用自定義的DexClassLoader,那我們直接使用DexClassLoader進行加載就可以么,很顯然不行,還是會報異常。

    DexClassLoader加載的類是沒有組件生命周期的,即DexClassLoader即使通過對APK的動態加載完成了對組件類的加載,當系統啟動該組件時,依然會出現加載類失敗的異常。

    所以我們要想使用DexClassLoader進行動態加載dex,我們需要進行類加載器的修正。

    當前實現類加載器的修正,主要有兩種方案:

    ① 替換系統組件類加載器為我們的DexClassLoader,同時設置DexClassLoader的parent為系統組件加載器;

    ② 打破原有的雙親委派關系,在系統組件類加載器PathClassLoader和BootClassLoader的中間插入我們自己的DexClassLoader。

    <1>類加載器替換

    怎么去替換系統的類加載器了,這就和我們上面分析的ActivityThread中LoadedApk有關了,LoadedApk主要負責加載一個Apk程序,我們進一步分析源碼。

    很明顯,我們可以想到我們通過反射獲取mclassLoader,然后使用我們的DexClassLoader進行替換,不就可以成功的讓DexClassLoader擁有生命周期了么。

    源碼實現:

    總結:

    ① 獲取ActivityThread實例;

    ② 通過反射獲取類加載器;

    ③ 獲取LoadedApk;

    ④ 獲取mClassLoader系統類加載器;

    ⑤ 替換自定義類加載器為系統類加載器。

    public static void replaceClassLoader(Context context,ClassLoader dexClassLoader){       ClassLoader pathClassLoader = MainActivity.class.getClassLoader();       try {           //1.獲取ActivityThread實例           Class ActivityThread = pathClassLoader.loadClass("android.app.ActivityThread");           Method currentActivityThread = ActivityThread.getDeclaredMethod("currentActivityThread");           Object activityThreadObj = currentActivityThread.invoke(null);           //2.通過反射獲得類加載器           //final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();           Field mPackagesField = ActivityThread.getDeclaredField("mPackages");           mPackagesField.setAccessible(true);           //3.拿到LoadedApk           ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj);           String packagename = context.getPackageName();           WeakReference wr = (WeakReference) mPackagesObj.get(packagename);           Object LoadApkObj = wr.get();           //4.拿到mclassLoader           Class LoadedApkClass = pathClassLoader.loadClass("android.app.LoadedApk");           Field mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");           mClassLoaderField.setAccessible(true);           Object mClassLoader =mClassLoaderField.get(LoadApkObj);           Log.e("mClassLoader",mClassLoader.toString());           //5.將系統組件ClassLoader給替換           mClassLoaderField.set(LoadApkObj,dexClassLoader);       }       catch (ClassNotFoundException e) {           e.printStackTrace();       } catch (NoSuchMethodException e) {           e.printStackTrace();       } catch (IllegalAccessException e) {           e.printStackTrace();       } catch (InvocationTargetException e) {           e.printStackTrace();       } catch (NoSuchFieldException e) {           e.printStackTrace();       }   }
    

    <2>類加載器插入

    還有一種方案,動態加載中我們講述了類加載器的雙親委派機制,就是說我們的類加載器剛拿到類,并不會直接進行加載,而是先判斷自己是否加載,如果沒有加載則給自己的父類,父類再給父類,所以我們讓DexClassLoader成為PathClassLoader的父類,這樣就可以解決DexClassLoader生命周期的問題。

    總結:

    ① 將DexClassloader父節點設置為BootClassLoader;

    ② 將PathClassLoader父節點設置為DexClassloader。

    代碼實現:

    public static void replaceClassLoader(Context context, ClassLoader dexClassLoader){        //將pathClassLoader父節點設置為DexClassLoader        ClassLoader pathClassLoaderobj = context.getClassLoader();        Class<ClassLoader> ClassLoaderClass = ClassLoader.class;        try {            Field parent = ClassLoaderClass.getDeclaredField("parent");            parent.setAccessible(true);            parent.set(pathClassLoaderobj,dexClassLoader);        } catch (NoSuchFieldException e) {            e.printStackTrace();        } catch (IllegalAccessException e) {            e.printStackTrace();        }
        }
    

    完成殼加載器的修正后,我們就可以正常的加載dex了。

    整體加殼案例實現

    前面我們詳細講述了App運行機制和整體加殼的實現機制,下面我們就按照前面的講述,來實現一個簡單的整體加殼案例。

    實驗準備:

    源程序加殼程序
    

    1.編寫源程序

    這就是我們的源程序,源程序運行,我們會在日志中看見我們打印的信息,然后我們生成dex文件。

    2.編寫殼程序

    (1)準備工作

    將dex文件上傳sdcard,并給應用設置存儲權限。

    (2)編寫代理類

    我們首先編寫代理類,模仿上面的加殼應用。

    然后我們設置AndroidManifest.xml中的代理類別。

    然后我們選擇在attachBaseContext或onCreate中對我們的dex進行動態加載和類加載器修正即可,因為這里我們源dex并未進行加密,所以也無需解密的過程。

    然后加入導入類的Activity。

    (3)動態加載

    我們進行動態加載classes.dex。

    然后使用上面的一種方法進行類加載器修正。

    然后運行:

    運行成功,說明我們的整體加殼成功。

    脫殼點相關概念詳解

    上面我們已經理解了APP加殼的基本原理,下面我們進一步來學習如何進行脫殼,Android APP脫殼繞不開DexFile、ArtMethod兩個概念,這兩個在脫殼中扮演的至關重要的地位,無數的脫殼點都是從其演變而來。

    1.Dex加載流程

    我們在分析脫殼點過程中,首先就需要明白Dex加載的基本流程。

     

    DexPathList:該類主要用來查找Dex、SO庫的路徑,并這些路徑整體呈一個數組;

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

    DexFile:用來描述Dex文件,Dex的加載以及Class的查找都是由該類調用它的native方法完成的。

    我們依次來分析這個過程中的源碼。

    DexPathList

    /libcore/dalvik/src/main/java/dalvik/system/DexPathList.javapublic DexPathList(ClassLoader definingContext, String dexPath,            String librarySearchPath, File optimizedDirectory) {**********************        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,                                         suppressedExceptions, definingContext);   **********************             }
    

    makeDexElements

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,          List<IOException> suppressedExceptions, ClassLoader loader) {**********************                  DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);   **********************                  }
    

    loadDexFile

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

    loadDex

    static DexFile loadDex(String sourcePathName, String outputPathName,      int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {      return new DexFile(sourcePathName, outputPathName, flags, loader, elements);  }
    

    DexFile

    /libcore/dalvik/src/main/java/dalvik/system/DexFile.javaDexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {        mCookie = openDexFile(fileName, null, 0, loader, elements);        mInternalCookie = mCookie;        mFileName = fileName;        //System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);    }
    

    這里出現的mCookie,mCookie在C/C++層中是DexFile的指針,我們在下面詳細講解。

    openDexFile

    private static Object openDexFile(String sourceName, String outputName, int flags,        ClassLoader loader, DexPathList.Element[] elements) throws IOException {       // Use absolute paths to enable the use of relative paths when testing on host.        return openDexFileNative(new File(sourceName).getAbsolutePath(),                                 (outputName == null)                                    ? null                                   : new File(outputName).getAbsolutePath(),                                      flags,                                   loader,                                   elements);    }
    

    這里就進入了C/C++層。

    openDexFileNative

    為了節約篇幅,我們快速分析,中間再經過一些函數。

    OpenDexFilesFromOat()MakeUpToDate()GenerateOatFileNoChecks()Dex2Oat()
    

    最后進進入了Dex2Oat,這就進入了Dex2Oat的編譯流程。

    反之如果我們在下面Dex2Oat的流程中通過Hook相關方法或execv或execve導致dex2oat失敗,我們就會返回到OpenDexFilesFromOat。

    OpenDexFilesFromOat

    會先在HasOriginalDexFiles里嘗試加載我們的Dex,也就是說,倘若我們的殼阻斷了dex2oat的編譯流程,然后又調用了DexFile的Open函數。

    DexFile::Open

    校驗dex的魔術字字段,然后調用DexFile::OpenFile。 

    DexFile::OpenFile

    /art/runtime/dex_file.ccstd::unique_ptr<const DexFile> DexFile::OpenFile(int fd,                                                const std::string& location,                                                bool verify,                                                bool verify_checksum,                                                std::string* error_msg) { ************************************** std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),                                                map->Size(),                                                location,                                                dex_header->checksum_,                                                kNoOatDexFile,                                                verify,                                                verify_checksum,                                                error_msg);    **************************************
                                                    }
    

    OpenCommon

    最后又再次回到DexFile類,這里我們的dex文件加載基本流程分析完畢。

    2.Dex2Oat編譯流程

    Dex2oat是google公司為了提高編譯效率的一種機制,從Android8.0開始實施,一些加殼廠商實現抽取殼往往會禁用Dex2oat,而針對整體加殼沒有禁用的Dex2Oat也成為了脫殼點。

    Exec

    /art/runtime/exec_utils.ccbool Exec(std::vector<std::string>& arg_vector, std::string* error_msg) {  int status = ExecAndReturnCode(arg_vector, error_msg);  if (status != 0) {    const std::string command_line(android::base::Join(arg_vector, ' '));    *error_msg = StringPrintf("Failed execv(%s) because non-0 exit status",                              command_line.c_str());    return false;  }  return true;}
    

    ExecAndReturnCode

    而我們就可以通過Hook execv或execve來禁用Dex2Oat,而如果我們不禁用dex2oat,execve函數是用來調用dex2oat的二進制程序實現對dex文件的加載,我們這時候找到dex2oat.cc這個文件,找到main函數。

    /art/dex2oat/dex2oat.cc int main(int argc, char** argv) {  int result = static_cast<int>(art::Dex2oat(argc, argv));  if (!art::kIsDebugBuild && (RUNNING_ON_MEMORY_TOOL == 0)) {    _exit(result);  }  return result;
    

    這里我們調用了Dex2oat。

    Dex2Oat

    /art/dex2oat/dex2oat.ccstatic dex2oat::ReturnCode Dex2oat(int argc, char** argv) {   **************************************   dex2oat::ReturnCode setup_code = dex2oat->Setup();    dex2oat::ReturnCode result;  if (dex2oat->IsImage()) {    result = CompileImage(*dex2oat);  } else {    result = CompileApp(*dex2oat); }   **************************************}
    

    Dex2oat中會對dex文件進行逐個類逐個函數的編譯,setup()函數完成對dex的加載。

    然后順序執行,就會進入CompileApp。

    編譯過程中會按照逐個函數進行編譯,就會進入CompileMethod。

    到這里Dex2oat的基本流程就分析完畢。

    3.類加載流程

    要理解DexFile為什么如此重要,首先我們要清除Android APP的類加載流程。Android的類加載一般分為兩類隱式加載和顯式加載。

    1.隱式加載:    (1)創建類的實例,也就是new一個對象    (2)訪問某個類或接口的靜態變量,或者對該靜態變量賦值    (3)調用類的靜態方法    (4)反射Class.forName("android.app.ActivityThread")    (5)初始化一個類的子類(會首先初始化子類的父類)2.顯示加載:    (1)使用LoadClass()加載    (2)使用forName()加載
    

    我們詳細看一下顯示加載:

    Class.forName 和 ClassLoader.loadClass加載有何不同:(1)ClassLoader.loadClass也能加載一個類,但是不會觸發類的初始化(也就是說不會對類的靜態變量,靜態代碼塊進行初始化操作)(2)Class.forName這種方式,不但會加載一個類,還會觸發類的初始化階段,也能夠為這個類的靜態變量,靜態代碼塊進行初始化操作
    

    我們在詳細來看一下在類加載過程中的流程:

    java層

    我們可以發現類加載中關鍵的DexFile,該類用來描述Dex文件,所以我們的脫殼對象就是DexFile。

    這里從DexFile進入Native層中,還有一個關鍵的字段就是mCookie。

    后面我們詳細的介紹mCookie的作用。

    我們進一步分析,進入Native層。

    Native層

    /art/runtime/native/[dalvik_system_DexFile.cc

    ConvertJavaArrayToDexFiles對cookie進行了處理
    

    通過這里的分析,我們可以知道mCooike轉換為C/C++層指針后,就是dexfile的索引。

    我們繼續分析DefineClass。

    art/runtime/class_linker.ccmirror::Class* ClassLinker::DefineClass(Thread* self,                                      const char* descriptor,                                        size_t hash,                                       Handle<mirror::ClassLoader> class_loader,                                        const DexFile& dex_file,                                        const DexFile::ClassDef& dex_class_def) {***************LoadClass(self, *new_dex_file, *new_class_def, klass);***************}
    

    LoadClass

    art/runtime/class_linker.ccvoid ClassLinker::LoadClass(Thread* self,3120                            const DexFile& dex_file,3121                            const DexFile::ClassDef& dex_class_def,3122                            Handle<mirror::Class> klass) {3123  const uint8_t* class_data = dex_file.GetClassData(dex_class_def);3124  if (class_data == nullptr) {3125    return;  // no fields or methods - for example a marker interface3126  }3127  LoadClassMembers(self, dex_file, class_data, klass);3128}
    

    LoadClassMembers

    art/runtime/class_linker.ccvoid ClassLinker::LoadClassMembers(Thread* self,                                   const DexFile& dex_file,                                   const uint8_t* class_data,                                   Handle<mirror::Class> klass) {***************      LoadMethod(dex_file, it, klass, method);      LinkCode(this, method, oat_class_ptr, class_def_method_index);***************}
    

    LoadMethod

    art/runtime/class_linker.ccvoid ClassLinker::LoadMethod(const DexFile& dex_file,                           const ClassDataItemIterator& it,                            Handle<mirror::Class> klass,                             ArtMethod* dst) {}
    

    LinkCode

    我們可以發現這里就進入了從linkcode后就進入了解釋器中,并對是否進行dex2oat進行了判斷,我們直接進入解釋器中繼續分析。

    我們知道Art解釋器分為兩種:解釋模式下和quick模式下,而我們又知道Android8.0開始進行dex2oat。

    如果殼沒有禁用dex2oat,那類中的初始化函數運行在解釋器模式下;

    如果殼禁用dex2oat,dex文件中的所有函數都運行在解釋器模式下

    則類的初始化函數運行在解釋器模式下。

    所以一般的加殼廠商會禁用掉dex2oat,這樣可以是所有的函數都運行在解釋模式下,所以一些脫殼點選在dex2oat流程中,可能針對禁用dex2oat的情況并不使用,我們這里主要針對整體加殼,就不展開講述,最后我們得知解釋器中會運行在Execute下。

    Execute

    art/runtime/interpreter/interpreter.ccstatic inline JValue Execute(    Thread* self,    const DexFile::CodeItem* code_item,    ShadowFrame& shadow_frame,    JValue result_register,    bool stay_in_interpreter = false) REQUIRES_SHARED(Locks::mutator_lock_){
    ***************      ArtMethod *method = shadow_frame.GetMethod();***************
        }
    

    這里我們大致分析完成了類加載的思路。

    4.DexFile詳解

    前面我們分析了很多,對dex加載、類加載等都已經有了一個很詳細的了解,而最終一切的核心就是DexFile,DexFile就是我們脫殼所關注的重點,寒冰大佬在撥云見日:安卓APP脫殼的本質以及如何快速發現ART下的脫殼點中提到,在ART下只要獲得了DexFile對象,那么我們就可以得到該dex文件在內存中的起始地址和大小,進而完成脫殼。

    我們先查看一些DexFile的結構體。

    只要我們能獲得起始地址begin和大小size,就可以成功的將dex文件脫取下來,這里我們記得DexFile含有虛函數表,所以根據C++布局,要偏移一個指針。

    而DexFile類還給我們提供了方便的API。

    這樣只要我們找到函數中有DexFile對象,就可以通過調用API來進一步dump dex文件,由此按照寒冰大佬的思想,大量的脫殼點由此產生。

    (1)直接查找法

    我們通過直接在Android源碼中搜索DexFile,就可以獲得海量的脫殼點。

    我們通過在IDA中搜索libart.so導出的DexFile,同樣可以獲得大量的脫殼點。

    (2)間接查找法

    這里就是寒冰大佬在文章中提到的通過ArtMethod對象的getDexFile()獲取到ArtMethod所屬的DexFile對象的這種一級間接法,通過Thread的getCurrentMethod()函數首先獲取到ArtMethod或者通過ShadowFrame的getMethod獲取到ArtMethod對象,然后再通過getDexFile獲取到ArtMethod對象所屬的DexFile的二級間接法。

    getDexFile()getMethod()
    

    5.ArtMethod詳解

    上面我們已經詳細分析了DexFile的文件結構,我們知道通過ArtMethod可以獲得DexFile,那么為啥又要單獨提ArtMethod呢,因為ArtMethod在抽取殼和VMP等殼中扮演了重要的角色。

    ArtMethod結構體

    我們通過ArtMethod可以獲得codeitem的偏移和方法索引,熟悉dex結構的朋友知道codeitem就是代碼實際的值,而codeitem則再后續加殼技術扮演了至關重要的地址,而且ArtMethod還有非常豐富的方法,可以幫助大家實現很多功能,所以在脫殼工作中也是十分重要的。

    脫殼技術歸納

    前面分析了很多,最后無非整體加殼的脫殼方案落腳在DexFile的關鍵對象上,由此產生了一些常用的方法。

    1.現有工具脫殼法

    工欲善其事必先利其器,整體加殼已經很多年,不少的大佬們都開發了很多非常好用的工具,我們在自己掌握原理過程時,平時工作中也可以使用很多大佬的開發工具,這里隨便舉幾個自己經常用的工具,這里我對各個大佬的脫殼工具進行了一個梳理。

    (1)FRIDA-DEXDump

    這是葫蘆娃大佬開發的針對整體加殼的工具,主要通過frida技術,文章參考:深入 FRIDA-DEXDump 中的矛與盾(https://www.anquanke.com/post/id/221905),該工具的特點是一般的hook方案通過直接搜索DEX的頭文件dex.035來定位dex的起始地址,但是后來不少公司對頭文件的魔術字段進行了抹除,這樣針對沒有文件頭的 DEX 文件,該工具通過map_off 找到 DEX 的 map_list, 通過解析它,并得到類型為 TYPE_MAP_LIST 的條目計算出文件的大小和起始地址,也很好的提供了一種解決思路。

    使用方法:

    FRIDA-DEXDump使用十分的簡單,詳細參考github:FRIDA-DEXDump

    https://github.com/hluwa/frida-dexdump

    這里引用一張大佬星球的使用流程圖,非常詳細,快速進行脫殼。

    我們簡單演示一下,這里結合objection一起使用。

    然后再次打開脫下來的dex,即可。

    (2)FDex2

    Fdex2主要是利用Android7.0及版本以下的特殊API getDex()來進行脫殼,原本是基于Xposed的模塊,不過掌握原理后,大家可以使用各種Hook框架去實現,參考鏈接:安卓xposed脫殼工具FDex2。

    (3)其他工具

    針對整體殼的脫殼工具有很多,無非是針對各種脫殼點再采用不同的方法,其原理是殊途同歸,而基于源碼定制的Fart、youpk等等針對整體加殼殼都可以基本實現完全的脫殼,而且抽取殼也有著很好的效果,下面我們就依次來講述具體的脫殼方法原理,各種脫殼工具如下圖所示:

    2.Hook脫殼法

    我們前面知道了,只要函數中包含DexFile對象,我們就可以通過Hook技術拿到對象,然后取到begin和size,從而進行脫殼,市面上使用較多的無非是Xposed和frida,我平時使用frida較為方便,這里也用frida和大家演示:

    首先我們使用GDA識別加殼程序。

    很明顯是進行了整體加殼,有沒其他加殼暫時不知道,我們先進行脫殼。

    找到脫殼點

    通過IDA打開libart.so,搜索DexFile,我們可以找到海量的脫殼點。

    我們就隨便找一個包含DexFile的脫殼函數,然后記錄符號值。

    然后我們編寫hook腳本。

    這里之所以獲取begin加上一個指針,是因為我們前面講了dexfile含有一個虛函數地址,所以加上一個指針偏移。

    然后啟動frida_server。

    附加進程進行dump,這里我們存在sdcard下面,所以需要提前賦予sdcard權限。

    這里就脫殼成功。

    然后我們打開相應的dex。

     

    此時說明我們整體脫殼成功,不過應用還有抽取殼,這個不是本文解決的內容。

    3.插樁脫殼法

    插樁脫殼法,就是在Android源碼里面定位到相應的脫殼點,然后插入相應的代碼,重新編譯源碼生成系統鏡像,最后就可以使用定制的系統進行脫殼。

    我們在源碼編譯(1)——Android6.0源碼編譯詳解(https://bbs.pediy.com/thread-269575.htm)中已經講述了如何編譯源碼,接下來我們進行插樁脫殼。

    同理、還是定位脫殼點,我們還是隨便定位一個脫殼點LoadMethod 然后進行插樁。

    //addchar dexfilepath[100]=0;memset(dexfilepath,0,100);sprintf(dexfilepath,"%d_%zu_LoadMethod.dex",getpid(),dex_file.Size());int dexfd = open(dexfilepathm,O_CREAT|O_RDWR,666);if(dexfd>0){    int result = write(dexfd,dex_file.Begin(),dex_file.Size());    if(result>0){        close(dexfd);        LOG(WARNING)<<"LoadMethod"<<dexfilepath;    }
    }//add
    

    同理我們在execute同樣插樁此段代碼,最后進行編譯,編譯成功。

    然后給程序授權sdcard權限,再次啟動應用,就可以看見脫取的dex文件就保存在sdcard目錄下。

    再次將sdcard下dex文件打開,這里我們已經看見了8732435這個文件,再次打開脫取成功。

    4.反射脫殼法

    反射脫殼法的核心思想就是利用前面我們提到的mCooike值。

    核心思路:反射 + mCookie步驟:1、找到加固apk的任一class,一般選擇主Application或Activity2、通過該類找到對應的Classloader3、通過該Classloader找到BaseDexClassLoader4、通過BaseDexClassLoader找到其字段DexPathList5、通過DexPathList找到其變量Element數組dexElements6、迭代該數組,該數組內部包含DexFile結構7、通過DexFile獲取其變量mCookie和mFileName
    至此我們已經獲取了mCookie
    對該mCookie的解釋:#1、4.4以下好像,mCookie對應的是一個int值,該值是指向native層內存中的dexfile的指針#2、5.0是一個long值,該值指向native層std::vector<const DexFile*>* 指針,注意這里有多個dex,你需要找到你要的#3、8.0,該值也是一個long型的值,指向底層vector,但是vector下標0是oat文件,從1開始是dex文件// 至于你手機是那個版本,如果沒有落入我上面描述的,你需要自己看看代碼
    8、根據mCookie對應的值做轉換,最終你能找到dexfile內存指針9、把該指針轉換為dexfile結構,通過findClassDef來匹配你所尋找的dex是你要的dex10、dump寫文件
    

    綜述mCookie是在native層就是dexfile的指針,我們利用反射原理來獲取mCookie,從而就可以進行脫殼了,這里我們同樣使用frida演示:

    編寫hook代碼

    我們看見了和上面同樣大小的8841876_mCookie.dex。

    使用工具打開,發現同樣脫殼成功。

    5.動態調試脫殼法

    所謂動態調試法,核心原理和上面一樣,就是我們在動態調試的過程中找到DexFile的起始地址和大小,然后執行腳本進行dump。

    首先選取脫殼點,我們還是選擇DexFile::DexFile。

    動態調試的步驟我在前面的文章中已經做了詳細的講解,不會的朋友去看前面的文章。

    首先我們啟動android_server。

    然后我們附加上進程。

     

    然后我們打開libart.so,并定位到DexFile::DexFile。

    然后在該函數下斷點,然后F9過來。

    此處我們就可以很明顯看到X1就是我們的起始地址,X4是我們的偏移值。

    編寫腳本進行hook。

    static main(void){       auto fp, begin, end, dexbyte;         fp = fopen("d:\\dump.dex", "wb+");         begin =  0x76FCD93020;       end = begin + 0x7EEC5600;    for ( dexbyte = begin; dexbyte<end;dexbyte++)    {    fputc(Byte(dexbyte), fp);           }  
    }
    

    直接運行run。

    然后我們查看dump.dex文件。

     

    我們可以發現這里是代理類,還沒有到我們想要的dex,我們再次F9,再次到這里,地址再次改變,再次結合長度來計算,我們每次計算可以取小點值,先試一下。

    發現還是不是,我們需要不停測試直到dump出dex為此。

    這里大家可以下去按照此方法嘗試,或者換一個脫殼點來嘗試。

    6.特殊API脫殼法

    所謂特殊的API脫殼法就是通過Android自身提供的API來獲得Dex,這主要是參考Fdex2,前面我們講了Fdex2主要是利用Android7.0及以下提供了getDex()和getBytes()兩個API,我們可以直接可以獲得class對象,然后直接調用這兩個API。

    編寫hook代碼:

    ① 使用frida枚舉所有Classloader。

    ② 確定正確的ClassLoader并獲取目標類的Class對象。

    ③ 通過Class對象獲取得到dex對象。

    ④ 通過dex對象獲取內存字節流并保存。

    然后我們查看程序的類對象,隨便dump一個類對象。

    然后我們再次用工具打開。

     

    發現就可以成功的dump。

    通過這種方式,我們發現神奇的事我們還可以抽取殼的情況,比如我們之前為空類

    我們明顯可以發現這里是采用了函數抽取的技術,一般的一代殼dump方案是無法解決抽取殼的,我們使用特殊API方法。

    再次打開,成功dump。

    這其實主要是抽取殼的一個回填時機的問題,這個詳細放在以后抽取殼中講解。

    實驗總結

    本文總結了當下dex整體加殼的基本原理,和常用的一些脫殼方案,并一一進行復現,還有一些文件監控法等,由于我平時用的很少就沒列舉了,復現實驗過程中由于涉及到不同的實驗,所以我用了Android 6.0 Android 7.0 Android 8.0三臺機器進行實驗,所以大家可以注意下對應的方法和其Android版本,這里徹底解決了整體加殼的脫殼方案,到這里可以掌握脫殼、抓包、Hook、反Hook、反調、反簽等基本手段,這樣在進行Android App漏洞挖掘過程中將事半功倍。后面我將繼續講解Android App漏洞中的XSS漏洞、Sql注入漏洞、文件上傳漏洞、端口掃描漏洞、WebView漏洞等。

    脫殼腳本相關樣本會放在github,所有的脫殼腳本和工具和上傳知識星球。

    函數調用android開發
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    文中使用的示例代碼可以從 這里 獲取。的功能是在終端打印出hello這6個字符(包括結尾的?編譯它們分別生成libtest.so和?存在嚴重的內存泄露問題,每調用一次say_hello函數,就會泄露1024字節的內存。
    Android 平臺的普及這導致開發商投入資源以維持需求的上升。較早版本的App可能會受到逆向工程和其他攻擊。在從現實世界收集的1500個主流應用中,DroidSkynet顯示攻擊使用早期版本的應用的成功率為34%。
    APP開發的背景知識的介紹APP開發遵循邏輯和視圖分離的思想:我們創建一個activity,android studio會自動生成其對應的xml文件。視圖視圖在xml中定義:可以直接可視化移動一個按鈕進視圖,也可以用代碼編寫。
    C:\Users\bk\Desktop\天府科技云APP\天府科技云服務平臺\天府科技云服務平臺.apkC:\Program Files\Java\jdk1.8.0_111\bin\jarsigner.exe?文件將解壓出來的classes.dex文件拷貝到dex2jar工具文件夾中執行命令:d2j-dex2jar classes.dex執行完畢后,得到反編譯而來的classes-dex2jar.jar文件使用jd-gui.exe或者luyten-0.5.4打開 classes-dex2jar.jar文件,得到360安全加固混淆加密的源代碼。應同時使用V1+V2簽名)6.應用完整性校檢將反編譯出來源碼中修改圖片文件名為test.png進行重新生成apk包,命令如下:java -jar apktool.jar b -f?
    前言最近一段時間在研究Android加殼和脫殼技術,其中涉及到了一些hook技術,于是將自己學習的一些hook技術進行了一下梳理,以便后面回顧和大家學習。主要是進行文本替換、宏展開、刪除注釋這類簡單工作。所以動態鏈接是將鏈接過程推遲到了運行時才進行。
    由于該研究是在一年前完成的,所以截止本文發布時,華為不僅解決了本文提到的漏洞(CVE-2021-22337),而且現在已經將他們的許多設備從 Android 升級到了鴻蒙操作系統。因此,不要期望在你最新的華為設備上找到完全相同的漏洞利用細節。
    大廠基本為了程序的安全,會使用大量內聯SVC去調用系統函數,以此來保護程序的安全。如何實現SVC指令的IO重定向,成為最大的問題。內核態是當Linux需要處理文件,或者進行中斷IO等操作的時候就會進入內核態。當arm系列cpu發現svc指令的時候,就會陷入中斷,簡稱0x80中斷。
    而iOS呢肯定是iPhone了,但是如何選系統如何自己越獄呢?比如手機越獄后,發現開不開機無法進入主界面,有可能是注入的插件有問題。然后進入frida-ios-dump腳本的目錄直接執行./dump 包名。
    企業安全規劃建設過程中,往往會涉及到開發的代碼安全,而更多可以實現落地的是源代碼安全審計中,使用自動化工具代替人工漏洞挖掘,并且可以交付給研發人員直接進行安全自查,同時也更符合SDL的原則,此外可以顯著提高審計工作的效率。
    FartExt是我之前學習脫殼實踐時做的一個自動脫殼機,是基于FART的主動調用思想實現對特定的抽取殼進行優化處理的工具。由于原本的FART沒有配置相關的,所以我增加了配置對指定app脫殼。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类