[Android][Security] Android 逆向之安全防護基本策略
- 2020 年 1 月 20 日
- 筆記
對抗反編譯
混淆
使用混淆主要可以減小包的大小。混淆對於安全保護來說,只是增加了閱讀難度而已。混淆不會把關鍵程式碼混淆掉,比如MainActivity,Application等,可以通過分析smali和閱讀jar包定位程式碼。
資源混淆也是換湯不換藥,針對載入資源程式碼getString(2131230929)進行進位轉換,變成16進位,從public.xml裡面查找對應的資源,就能定位到資源內容。
簽名保護
這個是防止二次打包驗證,但是,對於java程式碼的簽名保護,可以很容易地進行修改smali程式碼繞過驗證。
手動註冊native方法
安全性也不是很高,只是一種會增加破解成本的方式。一般Native方法根據命名規則生成頭文件然後寫cpp程式碼,這種方式屬於靜態註冊。手動動態註冊是複寫JNI_OnLoad方法,在該函數中手動註冊方法名和對應的方法簽名,方法名可以自定義,這樣避免了靜態註冊的命名規則,讓破解者難以根據規律找到要破解的方法。不過破解者可以分析JNI_OnLoad函數的彙編程式碼找到register函數找到註冊的native方法。
反調試檢測
IDA進行so動態調試是基於進程的注入技術,然後使用linux中ptrace機制,進行調試目標進程的附加操作。
ptrace機制有一個特點:如果一個進程被調試了,在它進程status文件中有一個欄位TracerPid會記錄調試者的進程id值
cat /proc/pidxx/status 可以看到TracerPid欄位
方法是檢測該TracerPid值,大於0就退出。但破解者會通過IDA工具給JNI_OnLoad下斷點,檢測輪詢程式碼,使用nop指令跳過檢測指令。
對抗Xposed
原理
Zygote
在Android系統中App進程都是由Zygote進程「孵化」出來的。Zygote進程在啟動時會創建一個虛擬機實例,每當它「孵化」一個新的應用程式進程時,都會將這個Dalvik虛擬機實例複製到新的App進程裡面去,從而使每個App進程都有一個獨立的Dalvik虛擬機實例。
Zygote進程在啟動的過程中,除了會創建一個虛擬機實例之外還會將Java Rumtime
載入到進程中並註冊一些Android核心類的JNI(Java Native Interface,Java本地介面)方法。一個App進程被Zygote進程孵化出來的時候,不僅會獲得Zygote進程中的虛擬機實例拷貝,還會與Zygote進程一起共享Java Rumtime
,也就是可以將XposedBridge.jar
這個Jar包載入到每一個Android App進程中去。安裝Xposed Installer
之後,系統app_process
將被替換,然後利用Java的Reflection
機制覆寫內置方法,實現功能劫持。下面我們來看一下細節。
Hook和Replace
Xposed Installer
框架中真正起作用的是對方法的Hook和Replace。在Android系統啟動的時候,Zygote進程載入XposedBridge.jar
,將所有需要替換的Method通過JNI
方法hookMethodNative
指向Native方法xposedCallHandler
,這個方法再通過調用handleHookedMethod
這個Java方法來調用被劫持的方法轉入Hook邏輯。
上面提到的hookMethodNative
是XposedBridge.jar
中的私有的本地方法,它將一個方法對象作為傳入參數並修改Dalvik虛擬機中對於該方法的定義,把該方法的類型改變為Native並將其實現指向另外一個B方法。
換言之,當調用那個被Hook的A方法時,其實調用的是B方法,調用者是不知道的。在hookMethodNative的實現中,會調用XposedBridge.jar
中的handleHookedMethod
這個方法來傳遞參數。handleHookedMethod
這個方法類似於一個統一調度的Dispatch常式,其對應的底層的C++函數是xposedCallHandler
。而handleHookedMethod
實現裡面會根據一個全局結構hookedMethodCallbacks
來選擇相應的Hook函數並調用他們的before
和after
函數,當多模組同時Hook一個方法的時候Xposed
會自動根據Module
的優先順序來排序。
調用順序如下:A.before -> B.before -> original method -> B.after -> A.after。

檢測
在做Android App的安全防禦中檢測點眾多,Xposed Installer
檢測是必不可少的一環。對於Xposed框架的防禦總體上分為兩層:Java層和Native層。
Java層檢測
需要說明的是,Java層的檢測基本只能檢測出基礎的Xposed Installer
框架,而不能防護其對App內方法的Hook,如果框架中帶有反檢測則Java層檢測大多不起作用。
下面列出Java層的檢測點,僅供參考。
① 通過PackageManager查看安裝列表
最簡單的檢測,我們調用Android提供的PackageManager
的API來遍歷系統中App的安裝情況來辨別是否有安裝Xposed Installer
相關的軟體包。
PackageManager packageManager = context.getPackageManager(); List applicationInfoList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA); for (ApplicationInfo applicationInfo: applicationInfoList) { if (applicationInfo.packageName.equals("de.robv.android.xposed.installer")) { // is Xposed TODO... } }
通常情況下使用Xposed Installer
框架都會屏蔽對其的檢測,即Hook掉PackageManager的getInstalledApplications
方法的返回值,以便過濾掉de.robv.android.xposed.installer
來躲避這種檢測。
② 自造異常讀取棧
Xposed Installer
框架對每個由Zygote孵化的App進程都會介入,因此在程式方法異常棧中就會出現Xposed
相關的「身影」,我們可以通過自造異常Catch
來讀取異常堆棧的形式,用以檢查其中是否存在Xposed
的調用方法。
try { throw new Exception("blah"); } catch(Exception e) { for (StackTraceElement stackTraceElement: e.getStackTrace()) { // stackTraceElement.getClassName() stackTraceElement.getMethodName() 是否存 在Xposed } } E/GEnvironment: no such table: preference (code 1): while compiling: SELECT keyguard_show_livewallpaper FROM preference ... at com.meituan.test.extpackage.ExtPackageManager.checkUpdate(ExtPackageManager.java:127) at com.meituan.test.MiFGService$1.run(MiFGService.java:41) at android.os.Looper.loop(Looper.java:136) at android.app.ActivityThread.main(ActivityThread.java:5072) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:515) ... at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:609) at de.robv.android.xposed.XposedBridge.main(XposedBridge.java:132) //發現Xposed模組 at dalvik.system.NativeStart.main(Native Method)
③ 檢查關鍵Java方法被變為Native JNI方法
當一個Android App中的Java
方法被莫名其妙地變成了Native JNI
方法,則非常有可能被Xposed Hook
了。由此可得,檢查關鍵方法是不是變成Native JNI
方法,也可以檢測是否被Hook。
通過反射調用Modifier.isNative(method.getModifiers())
方法可以校驗方法是不是Native JNI
方法,Xposed同樣可以篡改isNative
這個方法的返回值。
④ 反射讀取XposedHelper類欄位
通過反射遍歷XposedHelper
類中的fieldCache
、methodCache
、constructorCache
變數,讀取HashMap快取欄位,如欄位項的key中包含App中唯一或敏感方法等,即可認為有Xposed
注入。

boolean methodCache = CheckHook(clsXposedHelper, "methodCache", keyWord); private static boolean CheckHook(Object cls, String filedName, String str) { boolean result = false; String interName; Set keySet; try { Field filed = cls.getClass().getDeclaredField(filedName); filed.setAccessible(true); keySet = filed.get(cls)).keySet(); if (!keySet.isEmpty()) { for (Object aKeySet: keySet) { interName = aKeySet.toString().toLowerCase(); if (interName.contains("meituan") || interName.contains("dianping") ) { result = true; break; } } } ... return result; }
Native層檢測
由上文可知,無論在Java層做何種檢測,Xposed都可以通過Hook相關的API並返回指定的結果來繞過檢測,只要有方法就可以被Hook。如果僅在Java層檢測就顯得很徒勞,為了有效提搞檢測準確率,就須做到Java和Native層同時檢測。每個App在系統中都有對應的載入庫列表,這些載入庫列表在/proc/
下對應的pid/maps
文件中描述,在Native層讀取/proc/self/maps
文件不失為檢測Xposed Installer的有效辦法之一。由於Xposed Installer
通常只能Hook Java層,因此在Native層使用C來解析/proc/self/maps
文件,搜檢App自身載入的庫中是否存在XposedBridge.jar
、相關的Dex、Jar和So庫等文件。
bool is_xposed() { bool rel = false; FILE *fp = NULL; char* filepath = "/proc/self/maps"; ... string xp_name = "XposedBridge.jar"; fp = fopen(filepath,"r")) while (!feof(fp)) { fgets(strLine,BUFFER_SIZE,fp); origin_str = strLine; str = trim(origin_str); if (contain(str,xp_name)) { rel = true; //檢測到Xposed模組 break; } } ... }