FastHook——一種高效穩定、簡潔易用的Android Hook框架

1、概述

在使用YAHFA框架的過程當中,遇到了些問題,爲了解決這些問題在YAHFA的基礎上寫了FastHook框架。本文分析內容基於Android 8.1。git

項目地址:FastHookgithub.com/turing-tech…github

2、YAHFA

2.1 YAHFA原理

首先咱們來看看YAHFA框架基本流程,再分析其實現原理。 數組

YAHFA原理.png

  1. Target方法EntryPoint替換爲HookTrampoline。當執行Target方法時,實際執行HookTrampoline,從而實現方法Hook。
  2. HookTrampoline先將r0設置爲Hook方法,再跳轉到Hook方法EntryPoint執行Hook方法
  3. Hook方法參數與Target方法參數須一致,在Hook方法裏調用Backup方法達到間接調用Target方法的目的。
  4. Backup方法必須是static方法(若是Target方法不是static方法,Backup方法第一個參數必須爲this,Hook方法不須要必定是static方法,只要保證參數一致便可)。static方法是靜態分派的,這能夠保證調用的是Backup方法自己
  5. Backup方法必需要徹底備份Target方法,ART須要知道native code與dex code的映射關係,例如一條native指令對應哪條dex指令,這個映射關係須要EntryPoint來計算,而爲了實現Hook,咱們替換了Target方法的EntryPoint。因此咱們必須徹底複製Backup方法,此時咱們執行的仍是Backup方法,只是這個Backup方法的內容跟Target徹底同樣,這樣間接達到調用Target方法的目的

2.2 YAHFA缺陷

  1. 方法執行效率低。YAHFA經過禁止JIT和AOT編譯來規避一些問題,可是同時也極大下降了方法執行效率。
  2. Backup方法不能被再次解析。因爲Backup方法已備份爲Target方法,於是再此被解析將引起NoSuchMethod異常。
  3. Moving GC引起的空指針異常。當Target方法的某些成員(例如Class)被移動時,因爲Backup方法是備份獲得的,於是不會更新到新地址,致使空指針異常。
  4. 方法內聯致使Hook失效。Hook是經過替換方法EntryPoint實現的,所以當方法被內聯時就不會用到EntryPoint,這是Hook將失效。

3、FastHook

FastHook提供了兩種方案,一種相似Native Inline Hook,另外一個依舊是Entrypoint替換安全

3.1 Inline模式

Inline模式.png
Inline模式由5部分組成:

  1. JumpTrampoline:Hook鏈表的頭節點,一段跳轉指令,覆蓋原方法前幾字節,將會跳轉到HookTrampoline。
  2. HookTrampoline:判斷是否須要Hook,若是是,設置r0爲Hook方法並跳轉到Hook方法,不然跳轉到下一個HookTrampoline(多個類似的不一樣方法可能會共用相同的指令,所以多個方法Hook將造成一個鏈表結構)。
  3. OriginalTrampoline:Hook鏈表的尾節點,用於恢復原方法執行流。
  4. Hook方法:執行想要的邏輯,修改原方法參數、屏蔽原方法調用(Hook方法經過調用Forward方法來實現原方法調用)。
  5. Forward方法:一個靜態的native方法。沒有方法體、也不會被實際調用,如其名僅僅起到Forward的做用,方法EntryPoint將會被TargetTrampoline替換
  6. TargetTrampoline:用於執行原方法,設置r0爲原方法並恢復原方法執行流。

綜上可知,原方法沒有任何修改、而Forward方法僅僅修改了EntryPoint,從理論上解決了方法解析和Moving GC所帶來的問題bash

3.1.1 方法編譯

Inline模式要求方法必須有編譯後的機器代碼,而7.0以後默認不會進行AOT編譯,於是必須找到一個能編譯方法的方案。幸運的是Android默認的JIT便提供了這樣的方法:「jit_compile_method」。該方法由libart-compile.so導出,能夠利用dlsym獲取(7.0以後限制了dlsym,改用enhanced_dlsym代替,不只支持.dynsym(動態符號表)查詢,還支持.symtab(符號表)查詢)。值得注意的是,JIT編譯會改變線程狀態,爲了線程保持正確的狀態,編譯完成後須要恢復線程狀態微信

3.1.2 指令對齊

對於Thumb2指令集, JumpTrampoline是8字節 ,但Thumb有16位和32位兩種模式,也就是說JumpTrampoline覆蓋掉的指令有多是不完整的,所以須要作指令判斷,複製完整的指令,多是8字節,也多是10字節架構

3.1.3 PC相關指令

覆蓋的指令若包含PC相關指令,須要進行指令恢復,否則計算出來的地址將是錯誤的。FastHook並不作實際修復,僅判斷覆蓋的指令是否包含有PC相關指令,若是包含就使用EntryPoint模式框架

3.1.4 Hook限制

下列幾種狀況下將Hook失敗:post

  1. JIT編譯失敗
  2. 編譯後的指令長度小於JumpTrampoline的長度
  3. Native方法(沒有實際方法體所以也不能Hook)

當Inline模式Hook失敗將自動轉換爲EntryPoint模式。性能

3.2 EntryPoint替換模式

EntryPoint替換模式.png
EntryPoint模式由4個部分組成:

  1. HookTrampoline:設置r0爲Hook方法並跳轉到Hook方法。
  2. Hook方法:與Inline模式一致。
  3. Forward方法:與Inline模式一致。
  4. TargetTrampoline:用於執行原方法,與Inline模式不一樣的是,原方法將固定以解釋模式執行

綜上可知,雖然原方法EntryPoint被修改了,但其將固定以解釋模式執行,雖然犧牲了性能,可是也完全解決了方法解析與Moving GC所帶來的問題

3.2.1 InterpreterToInterpreter

在8.0以後,若是在Debug編譯版本,使用EntrypPoint替換模式會出現Hook失效的狀況,方法調用進入InterpreterTointerpreter,不會用到EntryPoint,這裏採用YAHFA的方案,Target方法設置kAccNative來規避,只在Debug版本下修改,Release版本不受影響,不修改

3.3 Hook安全

不管Inline模式仍是EntryPoint模式,都要求EntryPoint不能改變。下列幾種狀況會改變方法EntryPoint:

  1. dex文件加載
  2. 類初始化
  3. JIT編譯
  4. JIT垃圾回收(相似Mark-Sweep,設置爲QuickToInterpreterBridge)。
  5. 解釋執行(若是存在JIT入口則設置爲JIT入口 )。

當進行Hook時,方法所在類必定是初始化了的。因此只須要處理JIT,要準確的判斷出當前方法的JIT狀態。若是其等待JIT編譯或者正在JIT編譯,則需待其編譯完成再Hook,其餘狀況可安全Hook

3.4 方法內聯

不管Inline模式仍是EntryPoint模式,方法內聯都會致使Hook失效,所以須要想方法禁止方法內聯。先看看什麼狀況下會進行內聯。

//代理方法不內聯
  if (method->IsProxyMethod()) {
    return false;
  }
  //遞歸超過限制不內聯
  if (CountRecursiveCallsOf(method) > kMaximumNumberOfRecursiveCalls) {
    return false;
  }
const DexFile::CodeItem* code_item = method->GetCodeItem();
  //native方法不內聯
  if (code_item == nullptr) {
    return false;
  }
  //方法指令大小超過nline_max_code_units不內聯
  size_t inline_max_code_units = compiler_driver_->GetCompilerOptions().GetInlineMaxCodeUnits();
  if (code_item->insns_size_in_code_units_ > inline_max_code_units) {
    return false;
  }
  //有異常捕獲不內聯
  if (code_item->tries_size_ != 0) {
    return false;
  }
  //設置了kAccCompileDontBother,這裏沒有返回false,因此並不能阻止內聯
  if (!method->IsCompilable()) {
  }
  //Verifiy失敗不內聯
  if (!method->GetDeclaringClass()->IsVerified()) {
    uint16_t class_def_idx = method->GetDeclaringClass()->GetDexClassDefIndex();
    if (Runtime::Current()->UseJitCompilation() ||
        !compiler_driver_->IsMethodVerifiedWithoutFailures(
            method->GetDexMethodIndex(), class_def_idx, *method->GetDexFile())) {
      return false;
    }
  }
  //靜態方法或私有方法關聯<clinit>不內聯
  if (invoke_instruction->IsInvokeStaticOrDirect() &&
      invoke_instruction->AsInvokeStaticOrDirect()->IsStaticWithImplicitClinitCheck()) {
    return false;
  }
複製代碼

考慮到修改方法屬性可能會其餘未知的風險,所以選擇修改inline_max_code_units。inline_max_code_units是CompilerOptions的成員,CompilerOptions是jit_compile_handle的成員,jit_compile_handle是一個全局靜態變量,所以能夠經過dlsym獲取。經過修改其爲0來禁止JIT編譯。這種方式只能阻止JIT內聯,對AOT無效。AOT編譯的時候會新創建Runtime環境,而咱們只能修改當前Runtime環境。對OSR也無能爲力

3.5 小結

簡而言之,FastHook方案就是:Hook方法Hook原方法,原方法Hook Forward方法,Hook方法調用Forward方法來實現調用原方法

4、使用FastHook

4.1 提供HookInfo

private static String[] mHookItem = {
            "mode",
            "targetClassName","targetMethodName","targetParamSig",
            "hookClassName","hookMethodName","hookParamSig",
            "forwardClassName","forwardMethodName","forwardParamSig"
};
public static String[][] HOOK_ITEMS = {
             mHookItem
};
複製代碼
  1. HookInfo類能夠是任意類,可是必須存在一個名爲HOOK_ITEMS的靜態二維數組成員變量
  2. HookItem的格式是固定的,如上圖所示,mode有兩個取值:"1":Inline模式;"2":EntryPoint替換模式,特別注意,sig要求的是參數簽名而不是完整的方法簽名

4.2 Hook接口

/**
 *
 *@param hookInfoClassName HookInfo類名
 *@param hookInfoClassLoader HookInfo類所在的ClassLoader,若是爲null,表明當前ClassLoader
 *@param targetClassLoader Target方法所在的ClassLoader,若是爲null,表明當前ClassLoader
 *@param hookClassLoader Hook方法所在的ClassLoader,若是爲null,表明當前ClassLoader
 *@param forwardClassLoader Forward方法所在的ClassLoader,若是爲null,表明當前ClassLoader
 *@param jitInline 是否內聯,false,禁止內聯;true,容許內聯
 *
 */
public static void doHook(String hookInfoClassName, ClassLoader hookInfoClassLoader, ClassLoader targetClassLoader, ClassLoader hookClassLoader, ClassLoader forwardClassLoader, boolean jitInline)
複製代碼

1. 插件式Hook:建議在attachBaseContext方法裏調用

//插件式Hook,須要提供插件的ClassLoader
FastHookManger.doHook("hookInfoClassName",pluginsClassloader,null,pluginsClassloader,pluginsClassloader,false);
複製代碼

2. 內置Hook,建議在attachBaseContext方法裏調用

//內置Hook,都位於當前ClassLoader
FastHookManger.doHook("hookInfoClassName",null,null,null,null,false);
複製代碼

3. Root Hook,建議在handleBindApplication方法裏合適的地方調用,通常在加載apk後,調用attachBaseContext前

//Root Hook,須要體供插件的ClassLoader和apk的ClassLoader
FastHookManger.doHook("hookInfoClassName",pluginsClassloader,apkClassLoader,pluginsClassloader,pluginsClassloader,false);
複製代碼

4.3 支持的Android版本

5.0 ~ 9.0

4.4 支持的架構

Thumb2 Arm64

參考

  1. YAHFA:github.com/rk700/YAHFA
  2. Enhanced_dlfunctions:github.com/turing-tech…

FastHook系列

  1. FastHook——巧妙利用動態代理實現非侵入式AOP
  2. FastHook——遠超YAHFA的優異穩定性
  3. FastHook——如何使用FastHook免root hook微信
  4. FastHook——實現.dynsym段和.symtab段符號查詢
相關文章
相關標籤/搜索