外掛化知識梳理(6) Small 原始碼分析之 Hook 原理

澤毛發表於2017-12-21

一、前言

至此,花了四天時間、五篇文章,學習瞭如何使用Small框架來實現外掛化。但是,對於我來說,一開始的目標就不是滿足於僅僅知道如何用,而是希望通過這一框架作為平臺,學習外掛化中所用到的知識。

對於許多外掛化的開源框架而言,一個比較核心的部分就是Hook的實現,所謂Hook,簡單地來說就是在應用側啟動A.Activity,但是在AMS看來卻是啟動的B.Activity,之後AMS通知應用側後,我們再重新替換成A.Activity

在閱讀這篇文章之前,大家可以先看一下之前的這篇文章 Framework 原始碼解析知識梳理(1) - 應用程式與 AMS 的通訊實現Small其實就是通過替換這一雙向通訊過程中的關鍵類,對呼叫方法中傳遞的引數進行替換,來實現Hook機制。

二、原始碼分析

Hook的過程是Small預初始化的第一步,就是我們前面在自定義的Application構造方法中所進行的操作:

public class SmallApp extends Application {

    public SmallApp() {
        Small.preSetUp(this);
    }

}
複製程式碼

SmallpreSetUp(Application context)函式中,做了下面的兩件事:

  • 例項化三個BundleLauncher的實現類,新增到Bundle類中的靜態變數sBundleLaunchers中,這三個類的繼承關係為:

    外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理

  • 依次呼叫這個三個實現類的onCreate()方法。

    public static void preSetUp(Application context) {
        //1.新增關鍵的 BundleLauncher。
        registerLauncher(new ActivityLauncher());
        registerLauncher(new ApkBundleLauncher());
        registerLauncher(new WebBundleLauncher());
        //2.呼叫 BundleLauncher 的 onCreate() 方法。
        Bundle.onCreateLaunchers(context);
    }
複製程式碼

對於之前新增進入的三個實現類,只有ApkBundleLauncher()實現了onCreate()方法,其它兩個都是空實現。

    protected static void onCreateLaunchers(Application app) {
        //呼叫之前新增進入的 BundleLauncher 的 onCreate() 方法。
        for (BundleLauncher launcher : sBundleLaunchers) {
            launcher.onCreate(app);
        }
    }
複製程式碼

我們看一下ApkBundleLauncher的內部實現,這裡就是Hook的實現程式碼:

    @Override
    public void onCreate(Application app) {
        super.onCreate(app);

        Object/*ActivityThread*/ thread;
        List<ProviderInfo> providers;
        Instrumentation base;
        ApkBundleLauncher.InstrumentationWrapper wrapper;
        Field f;

        // Get activity thread
        thread = ReflectAccelerator.getActivityThread(app);

        // Replace instrumentation
        try {
            f = thread.getClass().getDeclaredField("mInstrumentation");
            f.setAccessible(true);
            base = (Instrumentation) f.get(thread);
            wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
            f.set(thread, wrapper);
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
        }

        // Inject message handler
        ensureInjectMessageHandler(thread);

        // Get providers
        try {
            f = thread.getClass().getDeclaredField("mBoundApplication");
            f.setAccessible(true);
            Object/*AppBindData*/ data = f.get(thread);
            f = data.getClass().getDeclaredField("providers");
            f.setAccessible(true);
            providers = (List<ProviderInfo>) f.get(data);
        } catch (Exception e) {
            throw new RuntimeException("Failed to get providers from thread: " + thread);
        }

        sActivityThread = thread;
        sProviders = providers;
        sHostInstrumentation = base;
        sBundleInstrumentation = wrapper;
    }
複製程式碼

(1) 獲得當前應用程式的 ActivityThread 例項

首先,我們通過反射獲得當前應用程式的ActivityThread例項

thread = ReflectAccelerator.getActivityThread(app)
複製程式碼

具體的邏輯為:

    public static Object getActivityThread(Context context) {
        try {
            //1.首先嚐試通過 ActivityThread 內部的靜態變數獲取。
            Class activityThread = Class.forName("android.app.ActivityThread");
            // ActivityThread.currentActivityThread()
            Method m = activityThread.getMethod("currentActivityThread", new Class[0]);
            m.setAccessible(true);
            Object thread = m.invoke(null, new Object[0]);
            if (thread != null) return thread;

            //2.靜態變數獲取失敗,那麼再通過 Application 的 mLoadedApk 中的 mActivityThread 獲取。
            Field mLoadedApk = context.getClass().getField("mLoadedApk");
            mLoadedApk.setAccessible(true);
            Object apk = mLoadedApk.get(context);
            Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
            mActivityThreadField.setAccessible(true);
            return mActivityThreadField.get(apk);
        } catch (Throwable ignore) {
            throw new RuntimeException("Failed to get mActivityThread from context: " + context);
        }
    }
複製程式碼

這裡面的邏輯為:

  • 通過ActivityThread中的靜態方法currentActivityThread來獲取:
    public static ActivityThread currentActivityThread() {
        return sCurrentActivityThread;
    }
複製程式碼

sCurrentActivityThread是在ActivityThread#attach(boolean)方法中被賦值的,而attach方法則是在入口函式main中呼叫的:

   public static void main(String[] args) {
        //建立應用程式的 ActivityThread 例項。
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
    }
複製程式碼
  • 如果上面的方法獲取失敗,那麼我們再嘗試獲取Application中的LoadedApk#mActivityThread

(2) 替換 ActivityThread 中的 mInstrumentation

通過(1)拿到ActivityThread例項之後,接下來就是替換其中mInstrumentation成員變數為Small自己的實現類ApkBundleLauncher.InstrumentationWrapper,並將原始的mInstrumentation傳入作為其成員變數。

正如 Framework 原始碼解析知識梳理(1) - 應用程式與 AMS 的通訊實現 中所介紹的,當我們呼叫startActivity之後,那麼會呼叫到它內部的mInstrumentationexecStartActivity方法,經過替換之後,就會呼叫ApkBundleLauncher.InstrumentationWrapper的對應方法,下面截圖中的mBase就是原始的mInstrumentation

外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理
ReflectAccelerator又通過反射呼叫了mBase的對應方法:
外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理
由此可見,hook的目的就在於替代者的方法被呼叫,到呼叫原始物件的對應方法之間所進行的操作,也就是下面紅色框中的這兩句:
外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理

首先看一下wrap(Intent intent)方法,它的作用為:當我們在應用側啟動一個外掛Activity時,需要將它替換成為AndroidManifest.xml預先註冊好的佔坑Activity

        private void wrapIntent(Intent intent) {
            ComponentName component = intent.getComponent();
            String realClazz;
            //判斷是否顯示地設定了目標元件的類名。
            if (component == null) {
                //如果沒有顯示設定 Component,那麼通過 resolveActivity 來解析出目標元件。
                component = intent.resolveActivity(Small.getContext().getPackageManager());
                if (component != null) {
                    return;
                }

                //獲得目標元件全路徑名。
                realClazz = resolveActivity(intent);
                if (realClazz == null) {
                    return;
                }
            } else {
                //如果設定了類名,那麼直接取出。
                realClazz = component.getClassName();
                if (realClazz.startsWith(STUB_ACTIVITY_PREFIX)) {
                    realClazz = unwrapIntent(intent);
                }
            }
            if (sLoadedActivities == null) return;
            //根據類名,確定它是否是外掛當中的 Activity 
            ActivityInfo ai = sLoadedActivities.get(realClazz);
            if (ai == null) return;
            //將真實的 Activity 儲存在 Category 中,並加上 > 識別符號。
            intent.addCategory(REDIRECT_FLAG + realClazz);
            //選取佔坑的 Activity 
            String stubClazz = dequeueStubActivity(ai, realClazz);
            //重新設定 intent,用佔坑的 Activity 來替代目標 Activity 
            intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
        }
複製程式碼

其中,dequeueStubActivity就是取出佔坑的Activity,它是預先在AndroidManifest.xml中註冊的一些佔坑Activity,同時,我們也會把真實的目標Activity放在Category欄位當中。

接下來,再看一下ensureInjectMessageHandler(Object thread)函式,程式碼的邏輯很簡單,就是替換AcitivtyThreadmH中的mCallback變數為sActivityThreadHandlerCallback,它的型別為ActivityThreadHandlerCallback,是我們自定的一個內部類。

    private static void ensureInjectMessageHandler(Object thread) {
        try {
            Field f = thread.getClass().getDeclaredField("mH");
            f.setAccessible(true);
            Handler ah = (Handler) f.get(thread);
            f = Handler.class.getDeclaredField("mCallback");
            f.setAccessible(true);

            boolean needsInject = false;
            if (sActivityThreadHandlerCallback == null) {
                needsInject = true;
            } else {
                Object callback = f.get(ah);
                if (callback != sActivityThreadHandlerCallback) {
                    needsInject = true;
                }
            }

            if (needsInject) {
                // Inject message handler
                sActivityThreadHandlerCallback = new ActivityThreadHandlerCallback();
                f.set(ah, sActivityThreadHandlerCallback);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace message handler for thread: " + thread);
        }
    }
複製程式碼

Framework 原始碼解析知識梳理(1) - 應用程式與 AMS 的通訊實現 我們分析過,Activity的生命週期是由AMS使用執行在系統程式的代理物件ApplicationThreadProxy,通過Binder通訊傳送訊息,在應用程式中的ActivityThread#ApplicationThreadonTransact()收到訊息後,再通過mH(一個自定的Handler,型別為H),傳送訊息到主執行緒,HhandleMessage中處理訊息,回撥Activity對應的生命週期方法。

ensureInjectMessageHandler所做就是讓HhandleMessage方法被呼叫之前,進行一些額外的操作,例如在佔坑的Activity啟動完成之後,將它在應用測的記錄替換成為Activity,而這一過程是通過替換Handler當中的mCallback物件,因為在呼叫handleMessage之前,會先去呼叫mCallbackhandleMessage,並且在其不返回true的情況下,會繼續呼叫Handler本身的handleMessage方法:

外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理

對於Small來說,它會對以下四種型別的訊息進行攔截:

外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理

我們以redirectActivity為例,看一下將佔坑的Activity重新替換為真實的Activity的過程。

        private void redirectActivity(Message msg) {
            Object/*ActivityClientRecord*/ r = msg.obj;
            //通過反射獲得啟動該 Activity 的 intent。
            Intent intent = ReflectAccelerator.getIntent(r);
            //就是通過前面放在 Category 中的欄位,來取得真實的 Activity 名字。
            String targetClass = unwrapIntent(intent);
            boolean hasSetUp = Small.hasSetUp();
            if (targetClass == null) {
                if (hasSetUp) return; // nothing to do
                if (intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
                    return;
                }
                Small.setUpOnDemand();
                return;
            }
            if (!hasSetUp) {
                //確保初始化了。
                Small.setUp();
            }
            //重新替換為真實的 Activity
            ActivityInfo targetInfo = sLoadedActivities.get(targetClass);
            ReflectAccelerator.setActivityInfo(r, targetInfo);
        }
複製程式碼

由於handleMessage的返回值為false,按照前面的分析,mHhandleMessage方法也會得到執行。

以上就是整個Hook的過程,簡單的總結下來就是在應用程式與AMS程式的通訊過程的某個節點,通過替換類的方式,插入一些邏輯,以繞過系統的檢查:

  • 從應用程式到AMS所在程式的通訊,是通過替換應用程式中的ActivityThreadmInstrumentation為自定義的ApkBundleLauncher.InstrumentationWrapper,在其中將Intent當中真實的Activity替換成為佔坑的Activity,然後再呼叫原始的mInstrumentation通知AMS
  • AMS所在進行到應用程式的通訊,是通過替換應用程式中的H中的mCallback,在其中將佔坑Activity替換成為真實的Activity,再執行原本的操作。

(3) ActivityThread 內部的 mBoundApplication 變數

這一步沒有進行Hook操作,而是先獲得ActivityThread內部的mBoundApplication例項,然後獲得該例項內部的providers變數,它的型別為List<ProviderInfo>

(4) 備份

最後一步,就是備份一些關鍵變數,用於之後的操作:

        //ActivityThread 例項
        sActivityThread = thread;
        //List<ProviderInfo> 例項
        sProviders = providers;
        //原始的 Instrumentation 例項
        sHostInstrumentation = base;
        //執行 Hook 操作的 Instrumentation 例項
        sBundleInstrumentation = wrapper;
複製程式碼

三、例項分析

以上就是原始碼分析部分,下面,我們通過一個啟動外掛Activity的過程,來驗證一下前面的分析:

3.1 從應用程式到 AMS 程式

通過下面的方法啟動一個外掛Activity

    public void startStubActivity(View view) {
        Small.openUri("upgrade", this);
    }
複製程式碼

按照前面的分析,此時應當會呼叫經過Hook之後的Instrumentation例項的execStartActivity方法,可以看到在wrapIntent方法呼叫之前,我們的目標Activity仍然是真實的UpgradeActivity

外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理
讓斷點繼續往下走,經過wrapIntent之後,Intent的目標物件替換成為了佔坑的Activity
外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理

3.2 從 AMS 程式到應用程式

而當AMS需要通知應用程式時,它第一次回撥的是佔坑的Activity,也就是如下所示:

外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理
通過反射,我們修改ActivityClientRecord中的內容,讓其還原成為真實的Activity
外掛化知識梳理(6)   Small 原始碼分析之 Hook 原理

四、小結

以上就是Small預初始化所做的一些事情,也就是其Hook實現的原理,很多第三方的外掛化都是基於該原理來實現啟動不在AndroidManifest.xml中註冊的元件的,開始的時候,理解起來可能會有點困難,關鍵是要弄清楚應用程式和AMS程式的互動原理,歡迎閱讀 Framework 原始碼解析知識梳理(1) - 應用程式與 AMS 的通訊實現 。


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章