1. 程式人生 > >Android中免Root實現Hook的Dexposed框架實現原理解析以及如何實現應用的熱修復

Android中免Root實現Hook的Dexposed框架實現原理解析以及如何實現應用的熱修復

一、前言

今天我們來看一下阿里的一個開源框架Dexposed,關於這個框架網上已經有很多解析了,但是都是講解原理,而且講的不是很清楚,這裡因為工作中的需要就研究了一下,所以這裡就先講解一下這個框架的原理,然後在通過一個例子來看看他如何使用,最後在用它來實現應用的熱修復問題。

二、知識點準備

首先在講解這個框架的時候,我們先來了解幾個知識點:

1、關於之前的Xposed框架

我們在很早就知道了這個框架,本來想整理一下順便說一下這個框架的,但是這個框架網上說的很多,而且也很詳細,所以就不做太多的解析了,這裡就大致說一下他的中心思想和核心原理。

通過進行裝置root之後,替換/system/bin/app_process程式控制zygote程序,使得app_process在啟動過程中會載入XposedBridge.jar這個jar包,從而完成對Zygote程序及其建立的Dalvik虛擬機器的劫持。

那麼這裡就引出了一個問題,為什麼要替換app_process程式呢?

關於app_process程式網上也有很多講解了,他其實是一個程式,存放在system/bin目錄下的,他的作用就是啟動一個程式,比如我們熟知的zygote程序,還有所有app啟動,app_process只要找到需要執行程式的main函式也就是入口函式,然後執行,他不僅能執行C/C++程式,也可以執行Java程式,其實在之前我們遇到過一個情況,就是想在Android中執行一個Java程式的jar,其實那個就是用的app_process命令啟動的。執行的命令也很簡單:

app_process [java-options] cmd-dir start-class-name [options]

我們可以檢視他的原始碼app_main.cpp:


從app_process的main函式(在app_main.cpp裡面)可以看出,app_process有兩種啟動方式:一種是init.rc裡面的這種方式,這種方式將會以zygote模式啟動com.android.internal.os.ZygoteInit,並將程序名稱改為zygote;另外一種是以非zygote模擬啟動com.android.internal.os.RuntimeInit,並呼叫它的main方法,main的最後會執行finishInit,finishInit是一個native方法,這個方法會呼叫app_process的onStarted方法,在onStarted裡面將會呼叫真正要執行的class:


在這裡不想介紹的太多,因為後面會說到Android的一個應用的啟動過程的時候,再詳細解析。這裡我們知道一個點就是Android中的所有程式都是通過app_process命令啟動,而且app_process命令還可以啟動任何Java程式,比如我們用到的am命令,這個命令Java原始碼和shell原始碼位於:Android原始碼目錄\frameworks\base\cmds\

我們可以檢視他的java原始碼內部實現:


指定有一個入口函式main

然後再看看他shell原始碼:


看到了這裡首先設定CLASSPATH變數,就是am.jar的路徑,然後開始執行命令,命令中需要指定具體的類名。

這裡我們也學會了如何在Android中執行一個jar檔案啦。

那麼從上面我們分析了app_process命令的作用,那麼我們就知道了為什麼Xposed框架需要替換app_process命令了:

首先在Android系統中,應用程式程序都是由Zygote程序孵化出來的,而Zygote程序是由Init程序啟動的。Zygote程序在啟動時會建立一個Dalvik虛擬機器例項,每當它孵化一個新的應用程式程序時,都會將這個Dalvik虛擬機器例項複製到新的應用程式程序裡面去,從而使得每一個應用程式程序都有一個獨立的Dalvik虛擬機器例項。這也是Xposed選擇替換app_process的原因。
Zygote程序在啟動的過程中,除了會建立一個Dalvik虛擬機器例項之外,還會將Java執行時庫載入到程序中來,以及註冊一些Android核心類的JNI方法來前面建立的Dalvik虛擬機器例項中去。注意,一個應用程式程序被Zygote程序孵化出來的時候,不僅會獲得Zygote程序中的Dalvik虛擬機器例項拷貝,還會與Zygote一起共享Java執行時庫。這也就是可以將XposedBridge這個jar包載入到每一個Android應用程式中的原因。XposedBridge有一個私有的Native(JNI)方法hookMethodNative,這個方法也在app_process中使用。這個函式提供一個方法物件利用Java的Reflection機制來對內建方法覆寫。

看到了吧,這就是Xposed框架選擇app_proess作為入口的一個原因,因為這個入口有兩個好處:

1》、一旦修改了,就可以修改了所有的app

2》、這裡的時機是最早的,可以載入所有的東西

上面就簡單的分析了一下Xposed框架的實現原理,這個是我們需要了解的第一個知識點。

2、修改非native方法為native方法

第二我們需要了解的知識點是,如何將Android中一個非native方法改成native的,並且可以將這個native方法指定成特定執行的函式。

這篇文章就很詳細的講解了我們如何修改系統的獲取Mac地址的方法,原理很簡單:

將系統的獲取Mac地址的方法getMacAddress改成native的,然後指定nativeFunc為dvmResolveNativeMethod這個函式,這個函式是dvm提供的。其中dvmResolveNativeMethod呼叫了dvmLookupInternalNativeMethod和lookupSharedLibMethod來查詢jni中註冊的native函式。 dalvik最後將執行得到的java native函式.

通過上面的程式碼片段,我們瞭解到要對一個java函式進行hook需要步驟有
[1] 把修改method的屬性修改成native
[2] 修改method的registersSize、insSize、nativeFunc、computeJniArgInfo
[3] RegisterNatives註冊目標method的native函式

然後我們將系統獲取Mac地址的方法getMacAddress和我們自己的一個函式做一次註冊:

{"android/net/wifi/WifiInfo","getMacAddress","()Ljava/lang/String;",(void*)test}

那麼這裡的test函式就是我們自己想要做事的函式,可以返回任意值啦~~


這個知識點很重要,也是我們今天說的核心知識點

三、原理分析

分析完了上面的兩個知識點,下面我們就來正式看看Dexposed框架的原理了,首先我們通過一張圖來了解一下:


看到了吧,其實不是很複雜,主要分為Java層和Native層的,

第一、首先我們來分析一下Java層的DexposedBridge.java類

1、hookMethodNative方法:


這個方法是一個native型別的。

引數說明:

1)需要hook的方法物件

2)需要hook的方法所屬的類

3)方法的slot值,關於這個slot的含義就是方法中區域性變數儲存的最小單位,一個Slot可以存放一個32位以內的資料型別.在Java程式編譯為Class檔案時,在方法的Code屬性的max_locals資料項中確定了該方法所需要分配區域性變量表的最大容量.

4)額外附加資訊,比如我們需要hook方法的回撥的物件,hook方法的返回型別,引數型別等資訊,下面看到hookMethodNative方法的呼叫地方。

2、invokeSuperNative和invokeOriginalMethodNative方法


這兩個方法也是native的,而且這兩個方法比較好理解,就是執行需要hook方法的父類方法和原方法,因為我們在hook住一個方法的時候,肯定需要決定是否要呼叫父類方法還是原來的方法。

3、handleHookedMethod方法


這個方法是整個Dexposed框架Java層最核心的一個方法,這個方法就是用來替換我們需要hook的那個方法,具體如何替換的等下面說到native層再說。然後這個方法中做了三件事:

1、執行需要hook之前的所有回撥方法beforeMethod

2、執行被hook的原生方法

3、執行需要hook之後的所有回撥方法afterMethod


這個方法就是我們偷龍轉鳳的上層核心方法,同時在這裡我們可以看到Dexposed框架還有一個好處就是AOP程式設計,關於AOP的概念就是面向切面程式設計,能夠在我們需要攔截或者hook的一個方法之前或者之後能做些什麼,JavaWeb中的Spring框架的一個核心就是AOP程式設計。

第二、上面分析完了DexposedBridge.java核心類,下面來看一下native層的實現 dexposed.cpp

1、initNative函式


這個初始化函式,主要獲取我們在DexposedBridge.java中的一些方法對應的Method物件,後面會用到。

這個方法在JNI_OnLoad這個函式中呼叫的,我們知道這個函式是當so檔案載入的時候就會被呼叫的。所以一般在這裡做初始化操作。在這個方法中還做了哪些事呢?


還有dexposedInfo函式:


這個函式主要獲取裝置的一些資訊,其實他對應的上層Java類就是SystemProperty.java類。

還有就是一個dexposedOnVmCreated函式:


這個函式幹了兩件事,一件事是獲取需要hook的類物件,還有就是執行註冊jni函式:

register_com_taobao_android_dexposed_DexposedBridge


這裡註冊了DexposedBridge.java中的hookMethodNative方法

2、com_taobao_android_dexposed_DexposedBridge_hookMethodNative函式


這個函式做了兩件事:

1)、構造出DexposedHookInfo資訊:


DexposedHookInfo的結構體資訊定義在dexposed.h標頭檔案中。就是需要hook的方法的資訊

2)、將需要hook的方法改成native的,然後指定他的nativeFunc為dexposedCallHandler函式

3、dexposedCallHandler函式


這個方法首先獲取我們剛剛構造的DexposedHookInfo資訊


然後執行dexposedHandleHookedMethod方法


這個方法在initNative中初始化了,就是Java層中DexposedBridge.java中國的handleHookedMethod方法。

好了到這裡我們就分析完了native層的程式碼,下面來總結一下:

1、首先在JNI_OnLoad函式中做了三件事:

1)獲取裝置資訊dexposedInfo

2)註冊JNI方法(com_taobao_android_dexposed_DexposedBridge_hookMethodNative)

3)初始化資訊(獲取Java層DexposedBridge中方法的Method物件)

2、然後在com_taobao_android_dexposed_DexposedBridge_hookMethodNative函式中主要做了兩件事:

1)把Java層傳遞的資訊構造成DexposedInfo資訊

2)設定hook方法為native,並且指定nativeFunc函式

3、最後在執行第二步中的nativeFunc函式dexposedCallHandler函式中主要做了兩件事:

1)獲取剛剛構造的DexposedInfo資訊

2)呼叫Java層DexposedBridge.java中的handleHookedMethod方法

所以我們在整個過程中可以看到,先通過JNI註冊,從Java世界轉到Native世界,然後在native世界中主要修改被hook方法的一些資訊,然後在通過反射呼叫handleHookedMethod回到Java世界。

四、案例分析

1、Hook系統的方法

上面講解完了Dexposed框架的實現原理,下面就通過例子來看看他的功能,這裡我們選擇Hook系統的Log類:


這裡直接呼叫DexposedBridge.findAndHookMethod方法來實現的,注意這裡做了一個裝置的判斷,如果是5.0+的話就hook程式本身的showLog方法,如果不是就hook系統的Log中的d方法。

最後一個引數可以看到,是一個回撥,就是讓我們來操作執行hook方法之前和之後的一些操作,這裡在afterHookedMethod方法中將日誌訊息顯示在螢幕中的TextView了,我們執行看一下效果:



這裡我們可以成功的hook掉系統的方法。

那麼分析到這裡,需要總結一下:

1》、Xposed框架是需要root的,他的功能很全,能夠hook掉系統方法,同時也可以hook掉其他應用的一些方法。

2》、Dexposed框架是不需要root的,但是他只能hook掉在自己的應用程序中的一些方法,其他應用程序是沒辦法hook的

所以我們可以對比一下這兩個框架各有好處。一般做遊戲外掛啥的,Dexposed是做不到的,而Xposed是可以做到的。

2、實現熱修復功能

那麼分析到這裡我們還沒有結束,還有我們今天一個重要的知識點,就是如何使用Dexposed框架實現應用熱修復,所謂熱修復,就是在應用釋出之後,在某個模組出現問題,可以線上修復,不需要從新發版。這個之前其實弄過這快,用動態載入技術既可以做到,可以將每個模組做成動態載入的,然後如果哪個模組有問題的話,就線上更新修復之後的模組(一般是dex或者jar的形式),那麼既然我們介紹了Dexposed框架,而且知道他能hook掉自己應用程序的方法,那麼有一個思路,就是我們可以線上提供一個具備一定協議的模組,然後在這個模組中我們呼叫DexposedBridge的一些方法,然後hook掉我們應用中出現問題的模組方法,從而達到熱修復功能。具體可以看下面一張圖:


下面我們來通過一個例子看看,修復應用中一個展示對話方塊的功能:

首先看一下熱修復工程PatchExample

就一個類,修復對話方塊類:


這個類也夠簡單吧,但是需要注意的是,每個修復類必須遵從IPatch協議介面。

下面我們編譯這個修復包,然後將其copy到需要修復應用的cache目錄下:

adb push PatchDemo.apk /sdcard/Android/data/com.taobao.dexposed/cache/

為什麼是這個目錄呢,下面來分析一下宿主應用中的熱修復程式碼:


看到了吧,這裡就這麼呼叫就可以啦,真正我們在使用的時候,應該是從網上下載apk,然後儲存到本地進行load操作的。

不過這裡我們先不執行,我們先來看看Patch的原始碼:


這裡有一個核心的方法,就是loadAllCallbacks方法:


這裡使用了DexFile類,得到apk中的dex檔案中所有的類,然後判斷哪些類實現了IPatch協議。


最後在呼叫協議遵從的handlePatch方法。實現熱修復功能。

好了分析完了程式碼之後,下面我們來執行程式看看:


不過我們在修復的過程中遇到了這個錯誤,其實這個錯誤我在之前介紹過講解動態載入那篇文章中說到過,就是我們宿主工程中有了IPatch類的定義,動態修復模組中也引用了這個IPatch類,所以導致相同類被載入了兩次的錯誤。這裡我們需要修改的就是修復模組中不要引用宿主模組中已經有的jar,那麼這裡就有一個問題了,怎麼才能編譯出一個不包含指定jar的apk包呢?這個在Eclipse中我是沒找到好的辦法的,最後是ant指令碼進行操作的,就是隻要保證工程編譯通過,同時不要將指定的jar包包含到apk中。具體如何使用ant指令碼打包出一個apk,不瞭解的同學可以參看:如何使用Ant指令碼編譯出apk包 這裡就不在做太多的介紹了。

我們修復了之後的apk,然後執行:


看到了吧,這裡就修復成功啦~~

五、技術概要

好了到這裡我們就講解完了Dexposed這個框架,已經如何使用這個框架來實現應用的熱修復功能。下面來總結一下知識點:

1、Dexposed框架是免root實現hook的,但是他也有侷限性就是隻能hook本應用中的一些方法。

2、Dexposed框架hook方法的原理,就是首先修改需要hook的方法為native方法,然後在設定nativeFunc值為我們需要處理的函式

  然後這個函式在用反射機制呼叫上層的Java程式碼即可

3、Dexposed框架實現熱修復,是基於hook技術,每個需要修復的模組都需要遵循一個協議,然後採用hook技術,去hook掉需要

修復的方法,實現替換功能。

總結Dexposed框架其實就一句話:

找到比較好的時機(JNI_OnLoad),修改hook方法,然後掛鉤上層Java程式碼,實現偷龍轉鳳的功能

六、總結

這裡就介紹完了Dexposed框架的一些知識,在實際專案中我們還會遇到一些問題,但是這篇文章只是講解了這個框架的實現原理,

在實際的使用過程中還是會遇到一些問題的,到時候在進行處理即可。

更多內容請:點選這裡

PS: 關注微信,最新Android技術實時推送