1. 程式人生 > >Android Hook技術防範漫談

Android Hook技術防範漫談

技術專欄 美團技術團隊技術專欄

背景 

當下,資料就像水、電、空氣一樣無處不在,說它是“21世紀的生產資料”一點都不誇張,由此帶來的是,各行業對於資料的爭奪熱火朝天。隨著網際網路和資料的思維深入人心,一些灰色產業悄然興起,資料販子、爬蟲、外掛軟體等等也接踵而來,網際網路行業中各公司競爭對手之間不僅業務競爭十分激烈,黑科技的比拼也越發重要。隨著移動網際網路的興起,爬蟲和外掛也從單一的網頁轉向了App,其中利用Android平臺下Dalvik模式中的Xposed Installer和Cydia Substrate框架對App的函式進行Hook這一招,堪稱老牌經典。

接下來,本文將分別介紹針對這兩種框架的防護技術。

Xposed Installer

原理

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;
       }
   }
    ...
}

Cydia Substrate

原理

Cydia Substrate注入Hook的一個典型流程如下圖所示,在Java層配置注入的關鍵So庫libsubstrate.so和libsubstratedvm.so。考慮到Java層檢測強度太低,Substrate的檢測主要在Native層來實現。

檢測

動態載入式檢測

讀取/proc/self/maps,列出了App中所有載入的檔案。

上圖為Cydia Substrate在Android 4.4上注入後的程序maps表,其中libsubstrate.so和libsubstrate-dvm.so兩個檔案為Substrate必載入檔案。通過IDA Pro分析對其分析。

先來看libsubstrate-dvm.so的匯出表,共有9個函式匯出。

當程序maps表中出現libsubstrate-dvm.so,可以嘗試去load該so檔案並呼叫MSJavaHookMethod方法,它會返回該方法的地址即判定為惡意模組(第三方程式)。

void* lookup_symbol(char* libraryname,char* symbolname)  
{
    void *imagehandle = dlopen(libraryname, RTLD_GLOBAL | RTLD_NOW);
    if (imagehandle != NULL){
        void * sym = dlsym(imagehandle, symbolname);
        if (sym != NULL){
            return sym; //發現Cydia Substrate相關模組
            }
      ...
}

該方式基於載入庫檔案的檔名或檔案路徑和匯出函式來判斷是否為惡意模組,如果完全依賴此方式來判斷可能會誤判,但也不失為檢測方式的一個點。

基於方法特徵碼檢測

特徵碼即用來判斷某段資料屬於哪個計算機欄位。在非Root環境下一般一個正常App在啟動時候,系統會排程相關大小的記憶體、空間給App使用,此時App的執行環境內產生的資料、記憶體、儲存等是獨立於其它App的(即獨立執行在沙箱中)。因為處於執行沙箱環境中的程序對沙箱的記憶體有最高讀寫許可權,當我們的App程序被惡意模組附加或注入時,就可以通過對當前程序的PID所對應的maps中載入的模組進行合法校驗。這裡的模組校驗我們可以採取對單個模組內容取樣來判斷是否為惡意模組,這種方式被定義為“基於方法的特徵碼檢測”。

下面對一段程式段中OpcodeSample方法來提取特徵碼。

方法原型:

#define  LOGD(fmt, args...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG, fmt, ##args)    
void OpcodeSample(int a ,int b){  
    int c,d,e;
    c = a + b;
    d = a * b;
    e = a / b;
    LOGD("Hello It's c !%s\n", c);  
    LOGD("Hello It's d !%s\n", d);  
    LOGD("Hello It's e !%s\n", e);  
    return;
}

通過IDA Pro對其分析。

左側紅色方框代表為OpcodeSample方法的操作碼,右邊為操作碼對應ARM平臺的指令集。我們要在左側的操作碼中取出一段作為OpcodeSample的定位特徵碼,選用__android_log_print方法呼叫指令集上下文,來確定特徵碼。

第一次取樣:"03 20 31 46 42 46 FF F7 ?? EA"

通過第一次取樣,查詢結果有三處相似,再進一步分析。這次我們加入一個常量取樣:

第二次取樣:"7E 44 ?? ?? F8 44 03 20 31 46 42 46 FF F7 ?? EA"

繼而得出唯一特徵碼,到此,我們對特徵碼方法取樣有了初步的瞭解。下面來把它轉為實用的技能——動態載入式檢測+特徵碼結合。

我們對libsubstrate-dvm.so中匯出函式MSJavaHookMethod來精準定位。

IDA PRO匯出函式表如圖:

第三次取樣:"55 57 56 53 E8 CC 14 ?? ?? 81 C3 DB ?? ?? ?? 8D 64 ?? ?? 8B 83 F4 ?? ?? ??"

以上即為對Cydia Substrate的注入檢測識別,通過檢測/proc/self/maps下的載入so庫列表得到各個庫檔案絕度路徑,通過fopen函式將so庫的內容以16進位制讀進來放在記憶體裡面進行規則比對,採用字串模糊查詢來檢測是否命中黑名單中的方法特徵碼。

總結

在安全對抗領域,相比攻擊方,防守方歷來處於弱勢的一方。上文所提到的Xposed Installer和Cydia Substrate的檢測也僅僅是保障App安全的手段之一。App安全的防禦不應僅僅依賴於此,應該構建起整體的安全防禦閉環,儘可能在所有已知的可能攻擊點都追加檢測,再配合程式碼加固,將防禦程式碼隱藏。遺憾的是App防禦程式碼隱藏再深也終究會被破解,僅僅依賴於客戶端的防禦顯然是不足的。移動網際網路領域的整體安全防禦應該是走端雲結合協作之道,共同防禦,方能在攻防對抗中佔據優勢地位。

作者簡介

禮讚,美團安全工程師,2016年11月加入美團。專注於二進位制、移動端攻防相關工作,現負責美團Android移動安全元件的建設工作。

毅然,美團技術專家,2016年初加入美團。致力於美團配送App組的Android App crash解決工作、Android App效能優化、Android App反外掛、反爬蟲。目前主導負責美團配送Android App移動安全相關建設。