2021年2月4日 星期四

Android - DexProtector商用殼破解心得

DexProtector是商用版的加殼(Packer)軟體,它有支援Android和IOS這兩種平台,一般來說只要是商用版的加殼軟體,在解殼的過程都會顯得非常的難,因此有些惡意程式會使用它來加殼,這樣大多數的分析人員可能會選擇放棄,如果本身沒有研究商用殼的原理,可能需要耗費非常多的時間來解殼,不過就算遇到這類的加殼惡意程式,也不需要懼怕它,那麼接下來這篇會介紹它的解殼原理。


由於我們找到的惡意程式是APK檔案,因此只會涉及Android平台上的解殼原理。

一、事前分析工作

大部分的商用加殼軟體在網路上,可能都有熱心的人分享解殼相關資訊,但是DexProtector找不到任何解殼資訊,只知道它的相關特徵,這樣只能先去下載它的試用版來分析,因為這樣能控制加殼的設定,會比較好分析,不過DexProtector的試用版不能直接下載,必須跟他們的業務從中獲取,因此我們就只能直接分析惡意程式了。

在分析之前,DexProtector的官方網站有提供它的防護功能,基本上只要是商用殼,該有的安全防護都會提供,例如Obfuscator-LLVM(程式碼混淆)、Anti-Debugger(gdb、jdwp偵測)、字串加密、主程式加密、完整性驗證(Signed APK)、模擬器偵測、Hook偵測、xposed偵測、frida偵測、VMProtect等等,非常多樣化。



二、DexProtector運行原理

DexProtector運行原理如下說明:

1. 運行APK:在程式運行的開始,會判斷系統的環境,以利要解密哪個so檔(C++ Base),並且解密當前要使用的字串,因為整個Dex檔的字串都會被加密,所以只要有字串的部分都要進行解密。

2. 解密so檔(Native Code、C++ Base):加密的so檔會從assets資料夾獲取(dp.arm-v7.so、dp.arm-v8.so、dp.x86.so、dp.x86_64.so),並且將它解密,然後運行到記憶體,此時的so檔身兼重任,其中包含Anti-Debugger、解密中繼Dex、APK完整性驗證(Signed APK)、解密主程式Dex,並將主程式執行到記憶體。

3. Anti-Debugger:這個部分包含環境模擬器的偵測、gdb和jdwp等等的偵測,只要有相關Debug訊息,就會讓JNI_OnLoad回傳-1,這樣會使Native Code (C++ Base)運行完JNI_OnLoad就會直接Crash,此時很多人第一次分析時,並不知道發生了什麼事,不過要繼續動態分析解殼的部分,就必需繞過Anti-Debugger。

4. 解密中繼Dex檔:加密的中繼Dex檔是存放在Java端的記憶體區塊,在解密完後,並且將它運行到記憶體,接下來主導權就交給中繼Dex檔,那麼它的主要任務是透過so檔(Native Code、C++ Base) 來加載惡意程式的主程式,其中包含完整性驗證(Signed APK)、註冊so檔的Native Method、初始化主程式全域變數(Static Field)。

5. 完整性驗證(Signed APK):中繼Dex檔會調用so檔的函式來運行完整性驗證,並且判斷APK的簽章是否有被替換,如果有,就不會繼續運行,然後讓JNI_OnLoad回傳-1,因此如果有修改程式碼再封裝APK,就必須繞過完整性驗證。

6. 註冊so函式:可能有些同學會好奇為什麼它要註冊so函式(Native Method),由於so檔捨棄Java_class的宣告,這樣可以讓分析人員找不到對應的函式名稱,因此它才需要透過FromReflectedMethod或RegisterNatives來註冊so檔的Native Method。

7. 解密主程式(Dex檔):加密的主程式會放在assets資料夾底下,檔案名稱是classes0.dex,它會先透過簽章和數值的運算後,再與數值運算後的結果進行解密,解密完後,我們就能從陣列上看到完整的Dex檔(Header是dex 035),在運行到記憶體之前,會先初始化主程式的全域變數(Static Field),然後再透過DexFile::OpenCommon或ArtDexFileLoader::Open來載入Dex檔到記憶體上,最後再調用主程式的Main Activity。



為了使Dex檔更難被行為分析,所有的Dex檔的字串都會加密,其中包含一開始的Dex檔、中繼Dex檔和主程式Dex檔,它會利用Package name、Class name、Method name合併成一個字串,然後進行Hash,接下來再比對此Hash是否為有效Hash,最後才會進行解密的運算。



接下來我們開始探討在DexProtector使用的類似VMProtect的加殼技術,它主要是將Java端的程式碼封裝成Native Method(C++ Base、so檔函式),然後透過Native Method來完成Java端的實作,首先Java端會傳遞要運行的Method ID、參數、Method和Field名稱的字串,接下來Native Method會透過Method Reflection來獲取Java端的Method Address,這樣就能從Native Method調用Java端的Method,那麼要處理全域變數(Static Field)就會利用Field Reflection,這樣就能獲取Java端的全域變數,最後運行完後,就會開始處理回傳值,如果接收端是區域變數,就不會在Native Method處理此數值,它會直接return這個區域變數給Java端程式碼。



classes0.dex是被加密的主程式(Dex檔),dp.xxxx.so是被加密的so檔,它會先判斷手機是屬於哪個版本的CPU,然後再進行so檔的解密。



onStart是解密字串的Method,解密完後,會回傳so檔的檔案名稱(libdexprotector.1415.so),其中1415是當前程式的PID,然後將此檔創建後,才會開始對so檔解密,並且將它載入到記憶體,最後還會將so檔從rom移除,主要是防止分析人員Dump。



so檔不但有被加密,而且在載入中繼Dex檔之前,關鍵函式還會先自我解密,這邊主要有運用到加密殼的技術,因此僅僅只有靜態分析是找不到關鍵的程式碼,另外它還有隱藏JNI API的調用,所以透過API分析是沒有用的,因為它在調用JNI API會利用dlsym來抓取函式的起始位址,所以一定要從動態分析來了解它的運行邏輯。

加密的中繼Dex檔,並不是像so檔是在外部檔案,它是存放在一開始運行的Java端程式碼裡(Dex檔),這邊是使用apktool工具來還原的Smali程式碼,我們可以看到x陣列就是存放加密的中繼Dex檔(此檔案非常大,圖中只截取部分的資料)。



DexProtector使用類似VMProtect技術,不外呼就是將主程式的程式碼(Java)交給so檔(C++ Base)實作,這樣就算分析Java程式碼,也並不知道其中的運作,因此我們可以在一開始的Dex檔程式碼(Java)看到它有宣告大量的Native Method,那麼有同學可能覺得,我只要分析so檔就能直接還原Java程式碼了,然而不幸的事,這些Native Method在so檔都是運行時才透過FromReflectedMethod或RegisterNatives來註冊的,因此還需要利用這2個API來找出所有Native Method對應的程式區塊,不過Java端的程式碼非常大,Native Method就會非常的多,那麼要還原整個Java端的程式碼會非常的耗時。




三、突破DexProtector的防護策略

DexProtector在解密主程式的Dex檔,裡面的所有方法(Method)都被它替換成其它的Method(LibMyApp.i是Native Method),在運行時會讓Native Method (C++ Base、so檔函式)來調用這些原來Java端的Method和處理全域變數(Static Field)傳輸的資訊,這樣分析主程式(Dex檔、Java程式碼)的行為就顯得相當困難,此種做法類似於VMProtect。



如果要做到完全解殼(主程式獨立執行)可能真的會耗費很多時間,那麼我們就解一半吧,只要能做到我們的目的就行了(分析主程的行為、修改主程式)。

可能有同學會提出疑問,什麼是將殼解一半 ? 解一半的意思,主要是跟殼(Packer)共存,但在最後運行的主程式我們會將它替換成我們修改好的Dex檔,不過還需要繞過完整性驗證(Signed APK)才不會運行時閃退。

主要用到的技術就是修改初始運行的Dex程式碼,並且我們還會另外撰寫自己的so檔,然後加載到記憶體,其中我們會Hooking它原本so檔(libdexprotector.1415.so)的部分函式。

Hooking的關鍵點有2個,一個就是在驗證完整性的函式,另外一個就是載入主程式(Dex檔)之前的函式,只要在載入主程式之前,替換成我們修改好的Dex檔,就能讓主程式運行如我們預期的行為了。

經過VMProtect加殼的主程式,大部分的邏輯都已被摧毀殆盡,甚至字串也被加密了(Dbiqjw是解密的Method),如過要能正確的判斷它的行為,就必須將Method ID(-27064、30008、27237等等)解密出來。



最後我們將Method ID解出成相對應的Method name字串,並且將它的參數之輸入輸出也都還原成字串名稱,也順便解密全部的加密字串,這樣就能不費吹灰之力就能分析它的行為了。