為了幫助更加方便的進行漏洞挖掘工作,前面我們通過了幾篇文章詳解的給大家介紹了動態調試技術、過反調試技術、Hook技術、過反Hook技術、抓包技術等,掌握了這些可以很方便的開展App漏洞挖掘工作,而最后我們還需要掌握一定的脫殼技巧,進行進一步助力我們漏洞挖掘的效率,本文主要介紹Android App加殼中的整體dex加殼,幫助大家掌握加殼的原理和脫殼的各種技能。
本文第二節主要講述Android啟動流程和加殼原理
本文第三節主要介紹整體加殼的實現
本文第四節主要講當下脫殼點的概念
本文第五節講述現有的脫殼技巧
(1)Android系統啟動流程
我們要徹底的了解App加殼原理,首先我們從了解App的啟動流程出發,先于App啟動之前,Android系統是啟動最早,下面我們來詳細查看一下Android系統的啟動過程:

我在Xposed源碼定制一文中詳細的講解了Android的啟動流程,簡單來說就是:

1 | 加載BootLoader - - > 初始化內核 - - > 啟動init進程 - - > init進程fork出Zygote進程 - - > Zygote進程fork出SystemServer進程 |
我們就了解了最后Zygote進程fork出第一個進程:SystemServer
進程,SystemServer主要完成了以下工作:

?
android app安裝
首先這里我們先介紹一下PackageManagerService
,其主要是完成Android中應用程序安裝的服務,我們了解的Android應用程序安裝的方式:
1 2 3 4 | · 系統啟動時安裝,沒有安裝界面 · 第三方應用安裝,有安裝界面,也是我們最熟悉的方式 · ADB命令安裝,沒有安裝界面 · 通過各類應用市場安裝,沒有安裝界面 |

雖然安裝方式不同,但是最后四種方式都是通過PackageManagerService服務來完成應用程序的安裝。而PackageManagerService服務則通過與Installd服務通信,發送具體的指令來執行應用程序的安裝、卸載等工作
1 2 3 4 5 6 | 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啟動過程與系統啟動流程,這里就不再詳細講解,而進入Launcher
進程,我們就進入了App啟動的流程。
(2)App啟動流程
Android系統啟動的最后一步是啟動一個Home應用程序,這個應用程序用來顯示系統中已經安裝的應用程序,這個Home應用程序就叫做Launcher。應用程序Launcher在啟動過程中會請求PackageManagerService返回系統中已經安裝的應用程序的信息,并將這些信息封裝成一個快捷圖標列表顯示在系統屏幕上,這樣用戶可以通過點擊這些快捷圖標來啟動相應的應用程序
前面我們描述了AMS將Launcher啟動,然后進入App啟動流程,這里參考文章:ActivityThread的理解和APP的啟動過程

1 2 3 4 5 6 7 8 | ( 1 )點擊桌面APP圖標時,Launcher的startActivity()方法,通過Binder通信,調用system_server進程中AMS服務的startActivity方法,發起啟動請求 ( 2 )system_server進程接收到請求后,向Zygote進程發送創建進程的請求 ( 3 )Zygote進程fork出App進程,并執行ActivityThread的main方法,創建ActivityThread線程,初始化MainLooper,主線程Handler,同時初始化ApplicationThread用于和AMS通信交互 ( 4 )App進程,通過Binder向sytem_server進程發起attachApplication請求,這里實際上就是APP進程通過Binder調用sytem_server進程中AMS的attachApplication方法,AMS的attachApplication方法的作用是將ApplicationThread對象與AMS綁定 ( 5 )system_server進程在收到attachApplication的請求,進行一些準備工作后,再通過binder IPC向App進程發送handleBindApplication請求(初始化Application并調用onCreate方法)和scheduleLaunchActivity請求(創建啟動Activity) ( 6 )App進程的binder線程(ApplicationThread)在收到請求后,通過handler向主線程發送BIND_APPLICATION和LAUNCH_ACTIVITY消息,這里注意的是AMS和主線程并不直接通信,而是AMS和主線程的內部類ApplicationThread通過Binder通信,ApplicationThread再和主線程通過Handler消息交互。 ( 7 )主線程在收到Message后,創建Application并調用onCreate方法,再通過反射機制創建目標Activity,并回調Activity.onCreate()等方法 ( 8 )到此,App便正式啟動,開始進入Activity生命周期,執行完onCreate / onStart / onResume方法,UI渲染后顯示APP主界面 |
到這里,我們的大致弄清了APP的啟動流程,而這里我們就進入了加殼中十分重要的地方ActivityTread
(3)ActivityThread啟動流程
寒冰大佬在FART:ART環境下基于主動調用的自動化脫殼方案?一文中講述了ActivityThread.main()是進入App世界的大門,并由此展開了對加殼原理的講述
同理接下來,我們開始進行源碼分析,了解ActivityThread的具體操作:
xref/frameworks/base/core/java/android/app/ActivityThread.java

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

這里我們可以看見完成了兩件事:
1 2 | ( 1 )完成了Application的實例化 ( 2 )并調用Application.attach()函數 |
然后我們繼續進入Application.attach()
函數

這里我們就進一步調用了attachBaseContext()
方法
最后回到handlebindapplication
中執行第6步,進入callApplicationOnCreate()函數

就執行了Application.onCreate()方法
總結:
1 2 3 4 | 從上可知, App的運行流程是 ???? 初始化————>Application的構造函數————>Application.attachBaseContext()————>Application.onCreate()函數 最后才會進入MainActivity中的attachBaseContext函數、onCreate函數 所以加殼廠商要在程序正式執行前,也就是上面的流程中進行動態加載和類加載器的修正,這樣才能對加密的dex進行釋放,而一般的 1 廠商往往選擇在Application中的attachBaseContext或onCreate函數進行 |
這里我附上網上一個大佬的詳細執行流程圖:

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


如何對App進行加一層外殼呢,這里就需要應用動態加載的原理,關于動態加載和類加載器,我在上篇文章中有詳細講解:Android加殼脫殼學習(1)——動態加載和類加載機制詳解
這里我們可以用一個案例來進一步講述,我們打開一個整體加殼的樣本

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

在代理類中反射調用了一些方法,很顯然我們解析出的結果都無法查找,很明顯就說明在Application.attchBaseContext()和Application.onCreate()中必須要完成對源加密的dex的動態加載和解密
結合上面的描述,App加載應用解析時就是這個流程:
1 2 3 4 5 6 | ( 1 )BootClassLoader加載系統核心庫 ( 2 )PathClassLoader加載APP自身dex ( 3 )進入APP自身組件,解析AndroidManifest.xml,然后查找Application代理 ( 4 )調用聲明Application的attachBaseContext()對源程序進行動態加載或解密 ( 5 )調用聲明Application的onCreate()對源程序進行動態加載或解密 ( 6 )進入MainActivity中的attachBaseContext(),然后進入onCreate()函數,執行源程序代碼 |
(2)類加載器的修正
上面我們已經很清晰的了解了殼加載的流程,我們很明顯的意識到一個問題,我們從頭到尾都是用PathClassLoader
來加載dex,而上篇文章我在講類加載器的過程中說過

1 2 3 4 5 6 7 8 | Android中的ClassLoader類型分為系統ClassLoader和自定義ClassLoader。其中系統ClassLoader包括 3 種是BootClassLoader、DexClassLoader、PathClassLoader ( 1 )BootClassLoader:Android平臺上所有Android系統啟動時會使用BootClassLoader來預加載常用的類 ( 2 )BaseDexClassLoader:實際應用層類文件的加載,而真正的加載委托給pathList來完成 ( 3 )DexClassLoader:可以加載dex文件以及包含dex的壓縮文件(apk,dex,jar, zip ),可以安裝一個未安裝的apk文件,一般為自定義類加載器 ( 4 )PathClassLoader:可以加載系統類和應用程序的類,通常用來加載已安裝的apk的dex文件 補充: Android 提供的原生加載器叫做基礎類加載器,包括:BootClassLoader,PathClassLoader,DexClassLoader,InMemoryDexClassLoader(Android 8.0 引入),DelegateLastClassLoader(Android 8.1 引入) |
我們要想動態加載dex文件必須使用自定義的DexClassLoader
,那我們直接使用DexClassLoader
進行加載就可以么,很顯然不行,還是會報異常
1 | DexClassLoader加載的類是沒有組件生命周期的,即DexClassLoader即使通過對APK的動態加載完成了對組件類的加載,當系統啟動該組件時,依然會出現加載類失敗的異常 |
所以我們要想使用DexClassLoader進行動態加載dex,我們需要進行類加載器的修正
當前實現類加載器的修正,主要有兩種方案:
1 2 | ( 1 )替換系統組件類加載器為我們的DexClassLoader,同時設置DexClassLoader的parent為系統組件加載器 ( 2 )打破原有的雙親委派關系,在系統組件類加載器PathClassLoader和BootClassLoader的中間插入我們自己的DexClassLoader |
<1>類加載器替換
怎么去替換系統的類加載器了,這就和我們上面分析的ActivityThread中LoadedApk
有關了,LoadedApk
主要負責加載一個Apk程序,我們進一步分析源碼

很明顯,我們可以想到我們通過反射獲取mclassLoader,然后使用我們的DexClassLoader進行替換,不就可以成功的讓DexClassLoader擁有生命周期了么
源碼實現:
1 2 3 4 5 6 | 總結: ???? ( 1 )獲取ActivityThread實例 ???? ( 2 )通過反射獲取類加載器 ???? ( 3 )獲取LoadedApk ???? ( 4 )獲取mClassLoader系統類加載器 ???? ( 5 )替換自定義類加載器為系統類加載器 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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生命周期的問題
1 2 3 | 總結: ???? ( 1 )將DexClassloader父節點設置為BootClassLoader ???? ( 2 )將PathClassLoader父節點設置為DexClassloader |
代碼實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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運行機制和整體加殼的實現機制,下面我們就按照前面的講述,來實現一個簡單的整體加殼案例
實驗準備:

這就是我們的源程序,源程序運行,我們會在日志中看見我們打印的信息,然后我們生成dex文件
(1)準備工作
將dex文件上傳sdcard,并給應用設置存儲權限


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

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

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

(3)動態加載
我們進行動態加載classes.dex

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

然后運行

運行成功,說明我們的整體加殼成功
上面我們已經理解了APP加殼的基本原理,下面我們進一步來學習如何進行脫殼,Android APP脫殼繞不開DexFile
、ArtMethod
兩個概念,這兩個在脫殼中扮演的至關重要的地位,無數的脫殼點都是從其演變而來。
我們在分析脫殼點過程中,首先就需要明白Dex加載的基本流程

1 2 3 | DexPathList:該類主要用來查找Dex、SO庫的路徑,并這些路徑整體呈一個數組 Element:根據多路徑的分隔符“;”將dexPath轉換成 File 列表,記錄所有的dexFile DexFile:用來描述Dex文件,Dex的加載以及Class的查找都是由該類調用它的native方法完成的 |
我們依次來分析這個過程中的源碼
DexPathList
1 2 3 4 5 6 7 8 | / libcore / dalvik / src / main / java / dalvik / system / DexPathList.java public DexPathList(ClassLoader definingContext, String dexPath, ???????????? String librarySearchPath, File optimizedDirectory) { * * * * * * * * * * * * * * * * * * * * * * ????? ??? this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, ????????????????????????????????????????? suppressedExceptions, definingContext);??? * * * * * * * * * * * * * * * * * * * * * * ? ???????????? } |
makeDexElements
1 2 3 4 5 6 | private static Element[] makeDexElements( List < File > files, File optimizedDirectory, ?????????? List <IOException> suppressedExceptions, ClassLoader loader) { * * * * * * * * * * * * * * * * * * * * * * ??????????? ??????? DexFile dex = loadDexFile( file , optimizedDirectory, loader, elements);??? * * * * * * * * * * * * * * * * * * * * * * ???????? ?????????? } |
loadDexFile
1 2 3 4 5 6 7 8 9 10 | 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
1 2 3 4 | 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
1 2 3 4 5 6 7 | / libcore / dalvik / src / main / java / dalvik / system / DexFile.java DexFile(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
1 2 3 4 5 6 7 8 9 10 11 | 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

為了節約篇幅,我們快速分析,中間再經過一些函數
1 2 3 4 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | / art / runtime / dex_file.cc std::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文件加載基本流程分析完畢
Dex2oat是google公司為了提高編譯效率的一種機制,從Android8.0開始實施,一些加殼廠商實現抽取殼往往會禁用Dex2oat,而針對整體加殼沒有禁用的Dex2Oat也成為了脫殼點

Exec
1 2 3 4 5 6 7 8 9 10 11 | / art / runtime / exec_utils.cc bool 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函數
1 2 3 4 5 6 7 | / 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
1 2 3 4 5 6 7 8 9 10 11 12 | / art / dex2oat / dex2oat.cc static 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的基本流程就分析完畢
要理解DexFile為什么如此重要,首先我們要清除Android APP的類加載流程。Android的類加載一般分為兩類隱式加載
和顯式加載
1 2 3 4 5 6 7 8 9 | 1. 隱式加載: ???? ( 1 )創建類的實例,也就是new一個對象 ???? ( 2 )訪問某個類或接口的靜態變量,或者對該靜態變量賦值 ???? ( 3 )調用類的靜態方法 ???? ( 4 )反射Class.forName( "android.app.ActivityThread" ) ???? ( 5 )初始化一個類的子類(會首先初始化子類的父類) 2. 顯示加載: ???? ( 1 )使用LoadClass()加載 ???? ( 2 )使用forName()加載 |
我們詳細看一下顯示加載:
1 2 3 | 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

1 | ConvertJavaArrayToDexFiles對cookie進行了處理 |

通過這里的分析,我們可以知道mCooike轉換為C/C++層指針后,就是dexfile的索引
我們繼續分析DefineClass
1 2 3 4 5 6 7 8 9 10 11 | art / runtime / class_linker.cc mirror::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
1 2 3 4 5 6 7 8 9 10 11 | art / runtime / class_linker.cc void 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 interface 3126 ? } 3127 ? LoadClassMembers( self , dex_file, class_data, klass); 3128 } |
LoadClassMembers
1 2 3 4 5 6 7 8 9 10 | art / runtime / class_linker.cc void 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
1 2 3 4 5 6 | art / runtime / class_linker.cc void ClassLinker::LoadMethod(const DexFile& dex_file, ??????????????????????????? const ClassDataItemIterator& it, ???????????????????????????? Handle<mirror::Class> klass, ????????????????????????????? ArtMethod * dst) { } |
LinkCode

我們可以發現這里就進入了從linkcode后就進入了解釋器中,并對是否進行dex2oat進行了判斷,我們直接進入解釋器中繼續分析
我們知道Art解釋器分為兩種:解釋模式下
和quick模式下
,而我們又知道Android8.0開始進行dex2oat
1 2 3 | 如果殼沒有禁用dex2oat,那類中的初始化函數運行在解釋器模式下 如果殼禁用dex2oat,dex文件中的所有函數都運行在解釋器模式下 則類的初始化函數運行在解釋器模式下 |
所以一般的加殼廠商會禁用掉dex2oat,這樣可以是所有的函數都運行在解釋模式下,所以一些脫殼點選在dex2oat流程中,可能針對禁用dex2oat的情況并不使用,我們這里主要針對整體加殼,就不展開講述,最后我們得知解釋器中會運行在Execute
下
Execute
1 2 3 4 5 6 7 8 9 10 11 12 13 | art / runtime / interpreter / interpreter.cc static 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(); * * * * * * * * * * * * * * * ???? } |
這里我們大致分析完成了類加載的思路
前面我們分析了很多,對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的二級間接法。
1 2 | getDexFile() getMethod() |
上面我們已經詳細分析了DexFile的文件結構,我們知道通過ArtMethod可以獲得DexFile,那么為啥又要單獨提ArtMethod呢,因為ArtMethod在抽取殼和VMP等殼中扮演了重要的角色
ArtMethod結構體

我們通過ArtMethod可以獲得codeitem的偏移和方法索引,熟悉dex結構的朋友知道codeitem就是代碼實際的值,而codeitem則再后續加殼技術扮演了至關重要的地址,而且ArtMethod還有非常豐富的方法,可以幫助大家實現很多功能,所以在脫殼工作中也是十分重要的
前面分析了很多,最后無非整體加殼的脫殼方案落腳在DexFile的關鍵對象上,由此產生了一些常用的方法

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

(1)FRIDA-DEXDump
這是葫蘆娃大佬開發的針對整體加殼的工具,主要通過frida技術,文章參考:深入 FRIDA-DEXDump 中的矛與盾,該工具的特點是一般的hook方案通過直接搜索DEX的頭文件dex.035來定位dex的起始地址,但是后來不少公司對頭文件的魔術字段進行了抹除,這樣針對沒有文件頭的 DEX
文件,該工具通過map_off 找到 DEX 的 map_list, 通過解析它,并得到類型為 TYPE_MAP_LIST 的條目計算出文件的大小和起始地址,也很好的提供了一種解決思路。
使用方法:
FRIDA-DEXDump使用十分的簡單,詳細參考github:FRIDA-DEXDump
這里引用一張大佬星球的使用流程圖,非常詳細,快速進行脫殼

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

然后再次打開脫下來的dex,即可
(2)FDex2
Fdex2主要是利用Android7.0及版本以下的特殊API getDex()
來進行脫殼,原本是基于Xposed的模塊,不過掌握原理后,大家可以使用各種Hook框架去實現,參考鏈接:安卓xposed脫殼工具FDex2?
(3)其他工具
針對整體殼的脫殼工具有很多,無非是針對各種脫殼點再采用不同的方法,其原理是殊途同歸,而基于源碼定制的Fart、youpk等等針對整體加殼殼都可以基本實現完全的脫殼,而且抽取殼也有著很好的效果,下面我們就依次來講述具體的脫殼方法原理,各種脫殼工具如下圖所示:

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

很明顯是進行了整體加殼,有沒其他加殼暫時不知道,我們先進行脫殼
找到脫殼點
通過IDA打開libart.so
,搜索DexFile,我們可以找到海量的脫殼點

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

然后我們編寫hook腳本

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

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

這里就脫殼成功

然后我們打開相應的dex

此時說明我們整體脫殼成功,不過應用還有抽取殼,這個不是本文解決的內容
插樁脫殼法,就是在Android源碼里面定位到相應的脫殼點,然后插入相應的代碼,重新編譯源碼生成系統鏡像,最后就可以使用定制的系統進行脫殼
我們在源碼編譯(1)——Android6.0源碼編譯詳解中已經講述了如何編譯源碼,接下來我們進行插樁脫殼
同理、還是定位脫殼點,我們還是隨便定位一個脫殼點LoadMethod 然后進行插樁

1 2 3 4 5 6 7 8 9 10 11 12 13 14 | / / add char 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這個文件,再次打開脫取成功

反射脫殼法的核心思想就是利用前面我們提到的mCooike值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 核心思路:反射 + mCookie 步驟: 1 、找到加固apk的任一 class ,一般選擇主Application或Activity 2 、通過該類找到對應的Classloader 3 、通過該Classloader找到BaseDexClassLoader 4 、通過BaseDexClassLoader找到其字段DexPathList 5 、通過DexPathList找到其變量Element數組dexElements 6 、迭代該數組,該數組內部包含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是你要的dex 10 、dump寫文件 |
綜述mCookie是在native層就是dexfile的指針,我們利用反射原理來獲取mCookie,從而就可以進行脫殼了,這里我們同樣使用frida演示:
編寫hook代碼


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

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

所謂動態調試法,核心原理和上面一樣,就是我們在動態調試的過程中找到DexFile的起始地址和大小,然后執行腳本進行dump
首先選取脫殼點,我們還是選擇DexFile::DexFile

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

然后我們附加上進程



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

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

此處我們就可以很明顯看到X1就是我們的起始地址,X4是我們的偏移值
編寫腳本進行hook
1 2 3 4 5 6 7 8 9 10 11 | 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為此
這里大家可以下去按照此方法嘗試,或者換一個脫殼點來嘗試
所謂特殊的API脫殼法就是通過Android自身提供的API來獲得Dex,這主要是參考Fdex2,前面我們講了Fdex2主要是利用Android7.0及以下提供了getDex()和getBytes()兩個API,我們可以直接可以獲得class對象,然后直接調用這兩個API


編寫hook代碼:

1 2 3 4 | 1. 使用frida枚舉所有Classloader 2. 確定正確的ClassLoader并獲取目標類的Class對象 3. 通過Class對象獲取得到dex對象 4. 通過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,所有的脫殼腳本和工具和上傳知識星球
github:github
1 2 3 4 | https: / / bbs.pediy.com / thread - 252630.htm #msg_header_h2_4 https: / / bbs.pediy.com / thread - 254555.htm #msg_header_h2_4 https: / / www.anquanke.com / post / id / 221905 ?display = mobile https: / / www.qj301.com / news / 317.html |