[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邏輯。

上面提到的hookMethodNativeXposedBridge.jar中的私有的本地方法,它將一個方法對象作為傳入參數並修改Dalvik虛擬機中對於該方法的定義,把該方法的類型改變為Native並將其實現指向另外一個B方法。

換言之,當調用那個被Hook的A方法時,其實調用的是B方法,調用者是不知道的。在hookMethodNative的實現中,會調用XposedBridge.jar中的handleHookedMethod這個方法來傳遞參數。handleHookedMethod這個方法類似於一個統一調度的Dispatch常式,其對應的底層的C++函數是xposedCallHandler。而handleHookedMethod實現裡面會根據一個全局結構hookedMethodCallbacks來選擇相應的Hook函數並調用他們的beforeafter函數,當多模組同時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類中的fieldCachemethodCacheconstructorCache變數,讀取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;         }     }  	...  }