Android漏洞之戰——整體加殼原理和脫殼技巧詳解
一
前言
為了幫助更加方便的進行漏洞挖掘工作,前面我們通過了幾篇文章詳解的給大家介紹了動態調試技術、過反調試技術、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,所有的脫殼腳本和工具和上傳知識星球。