一個極簡的RePlugin

jeepc發表於2018-07-09

本文將通過一個極簡的例子來說明RePlugin的one hook原理。文章相關程式碼:github.com/jeepc/Simpl…

所謂one hook,即hook classloader。為什麼hook classloader就能實現外掛化,這要從Android四大元件啟動原理說起(這裡只說Activity,其他讀者自行探究)。

熟悉Android framework層的讀者應該知道,一次startActivity的呼叫,會通過Binder IPC呼叫到AMS的startActivity方法,AMS會進行一系列檢驗工作,其中包括該Activity是否在AndroidManifest.xml中註冊過。只有通過檢驗才能返回到App自己的程式來建立Activity。RePlugin的做法非常巧妙,就是一開始傳遞的Activity為在AndroidManifest.xml預註冊的StubActivity(大部分的外掛化框架都會用這個方案,巧妙的在後頭),以此來騙過AMS的檢驗,返回App建立Activity時,由於我們hook了classloader,所以我們就可以為所欲為,替換成自己想啟動的Activity(巧妙之處在於只hook一個地方,相容性更好,不易出錯)。

hook相關程式碼(核心程式碼都來自RePlugin):

public class App extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        hookClassloader();

    }

    private void hookClassloader() {
        try {
            Context oBase = this.getBaseContext();
            Object oPackageInfo = null;

            oPackageInfo = ReflectUtils.readField(oBase, "mPackageInfo");

            // 獲取mPackageInfo.mClassLoader
            ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");
            if (oClassLoader == null) {
                return;
            }
            ClassLoader cl = new SRPClassloader(oClassLoader.getParent(), oClassLoader);
            // 將新的ClassLoader寫入mPackageInfo.mClassLoader
            ReflectUtils.writeField(oPackageInfo, "mClassLoader", cl);

            // 設定執行緒上下文中的ClassLoader為RePluginClassLoader
            // 防止在個別Java庫用到了Thread.currentThread().getContextClassLoader()時,“用了原來的PathClassLoader”,或為空指標
            Thread.currentThread().setContextClassLoader(cl);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

程式碼很簡單就是利用反射,將我們自定義的Classloader替換掉原來mPackageInfo中的Classloader,因為後面類的載入都會用到mPackageInfo的Classloader,更深層的原因可以參考這篇文章《唯一外掛化Replugin原始碼及原理深度剖析--唯一Hook點原理》。自定義的SRPClassloader裡面具體實現,我們接著說。

 public SRPClassloader(ClassLoader parent, ClassLoader orig) {

        // 由於PathClassLoader在初始化時會做一些Dir的處理,所以這裡必須要傳一些內容進來
        // 但我們最終不用它,而是拷貝所有的Fields
        super("", "", parent);
        mOrig = orig;

        // 將原來宿主裡的關鍵欄位,拷貝到這個物件上,這樣騙系統以為用的還是以前的東西(尤其是DexPathList)
        // 注意,這裡用的是“淺拷貝”(淺拷貝就是複製引用,確保物件一模一樣)
        // Added by Jiongxuan Zhang
        copyFromOriginal(orig);

        initMethods(orig);
    }
    
     private void copyFromOriginal(ClassLoader orig) {

            // Android 4.0以上只需要複製pathList即可
            // 以下方法在較慢的手機上用時:1ms
            copyFieldValue("pathList", orig);
    }
    
    private void copyFieldValue(String field, ClassLoader orig) {
        try {
            Field f = ReflectUtils.getField(orig.getClass(), field);
            if (f == null) {
                return;
            }

            // 刪除final修飾符
            ReflectUtils.removeFieldFinalModifier(f);

            // 複製Field中的值到this裡
            Object o = ReflectUtils.readField(f, orig);
            ReflectUtils.writeField(f, this, o);
        } catch (IllegalAccessException e) {
        }
    }
複製程式碼

以上程式碼註釋已經解釋很清楚了,就是利用反射將原來宿主裡的關鍵欄位,拷貝到自定的classloader物件中。

以上過程都是初始化,後面就是實際呼叫的過程了。

我們在MainActivity啟動StubActivity,StubAcitivity一行程式碼都沒有,只是在AndroidManifest.xml註冊過,以此欺騙AMS,後面就會呼叫到自定義的SRPClassloader的loadClass方法。

  @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        //
        Class<?> c = null;
        //這裡直接用最簡單的方法hook(實際上啟動外掛的Activity,有很多工作要做,有很多問題要解決)
        if(className.contains("StubActivity")){

            c = mOrig.loadClass("com.jeepc.simplifiedrp.RealActivity");
        }
        if (c != null) {
            return c;
        }
        //
        try {
            c = mOrig.loadClass(className);
            return c;
        } catch (Throwable e) {
            //
        }
        //
        return super.loadClass(className, resolve);
    }
複製程式碼

這裡是我自己寫的程式碼,簡直讓人不忍直視,哈哈哈。就是判斷如果類名包含"StubActivity",就替換為RealActivity,所以這只是一個demo用來說明道理的,沒有實用價值。

最後再補充一下,為什麼這樣替換,RealActivity具有正常的生命週期呢?原因是因為建立RealActivity的時候會傳入一個token物件,這個token物件是一個IBinder用來和AMS通訊的。AMS後面就只認這個IBinder,不認Activity的名字了。所以RealActivity生命週期相關的方法能正常的執行。詳細可以參考這篇文章《Android 外掛化原理解析——Activity生命週期管理》

總結:這篇文章涉及到的內容很多,其中有關於java動態代理,以及binder、AMS等Android framework等知識。因為篇幅有限(其實是作者水平有限),未能深入剖析,讀者可以自行探究。而且關於RePlugin的原理,遠不止這些,該文章只是提及一小部分。不過通過幾天的學習,我發現RePlugin的程式碼寫得真心漂亮,如果有Android framework的基礎,深入學習也不是問題。以下參考連結都是非常好的學習資料,感謝!

參考:《Android外掛化原理解析——概要》

          《唯一外掛化RePlugin原始碼及原理深度剖析--工程職責及大綱》

相關文章