Android hook, 以及對插件框架如何實現的開發精要

Hook的概念

*所謂對API的Hook, 其實就是對方法的動態替換. *
采用代理的方式, 創建一個新的對象, 其內部封裝原始對象,通過這種方式,可以修改這個方法的參數以及返回值, 或是在方法中新打印一行log, 達到 方法增強 的目的.

實現方式

在運行時, 采用反射的方式, 用自己新建的代理對象把原始對象給替換掉.
代理對象本質上還是通過原始對象去干事.

對Context.startActivity的hook.

啟動Activity是最常見的操作, Context.startActivity的真正實現是在ContextImpl.java中.

// ContextImpl.java

    @Override
    public void startActivities(Intent[] intents, Bundle options) {
        warnIfCallingFromSystemProcess();
        if ((intents[0].getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
            throw new AndroidRuntimeException(
                    "Calling startActivities() from outside of an Activity "
                    + " context requires the FLAG_ACTIVITY_NEW_TASK flag on first Intent."
                    + " Is this really what you want?");
        }
        mMainThread.getInstrumentation().execStartActivities(
                getOuterContext(), mMainThread.getApplicationThread(), null,
                (Activity) null, intents, options);
    }

可以看到這個API真正的實現是在ActivityThread的成員變量
Instrumentation mInstrumentation;的 execStartActivities()方法.

所以hook的思路就是實現一個Instrumentation的代理類, 在代理類中提供一個新的execStartActivities()方法的實現,
用這個代理類的對象,把ActivityThread的成員變量
Instrumentation mInstrumentation給替換掉.

@hide
public final class ActivityThread {
    Instrumentation mInstrumentation;

    public Instrumentation getInstrumentation() {
        return mInstrumentation;
    }

}

ActivityThread是一個隱藏類,我們需要用反射去獲取,代碼如下:

// 先獲取到當前的ActivityThread對象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

拿到這個currentActivityThread對象之后,我們需要修改它的mInstrumentation這個字段為我們的代理對象.

新建Instrumentation的代理類.

public class EvilInstrumentation extends Instrumentation {

    private static final String TAG = "EvilInstrumentation";

    // ActivityThread中原始的對象, 保存起來
    Instrumentation mBase;

    public EvilInstrumentation(Instrumentation base) {
        mBase = base;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

        // Hook之前, XXX到此一游!
        Log.d(TAG, "\n執行了startActivity, 參數如下: \n" + "who = [" + who + "], " +
                "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
                "\ntarget = [" + target + "], \nintent = [" + intent +
                "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");

        // 開始調用原始的方法, 調不調用隨你,但是不調用的話, 所有的startActivity都失效了.
        // 由于這個方法是隱藏的,因此需要使用反射調用;首先找到這個方法
        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class, 
                    Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mBase, who, 
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            // 某該死的rom修改了  需要手動適配
            throw new RuntimeException("do not support!!! pls adapt it");
        }
    }
}

完整的代碼如下:

package com.ahking.hookdemo;

import android.app.Instrumentation;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        try {
            initHook();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void launchSecondActivity(View view) {
        Intent intent = new Intent(this, SecondActivity.class);
        intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
        this.getApplicationContext().startActivity(intent);
    }

    private void initHook() throws Exception{

        // 先獲取到當前的ActivityThread對象
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);


        // 拿到原始的 mInstrumentation字段
        Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
        mInstrumentationField.setAccessible(true);
        Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);

        // 創建代理對象, 構造時把原始對象作為參數傳進去.
        Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);

        // 偷梁換柱——用代理對象替換原始對象
        mInstrumentationField.set(currentActivityThread, evilInstrumentation);

    }
}




log輸出如下:

com.ahking.hookdemo D/ahking: 執行了startActivity, 參數如下: 
                         who = [android.app.Application@41e6eb20], 
                         contextThread = [android.app.ActivityThread$ApplicationThread@41e68f50], 
                         token = [null], 
                         target = [null], 
                         intent = [Intent { flg=0x10000000 cmp=com.ahking.hookdemo/.SecondActivity }], 
                         requestCode = [-1], 
                         options = [null]

基于這樣的思路, 插件的原型就出來了.
  1. 在host app的AndroidManifest.xml中, 預先注冊一個Activity, 比如叫PluginActivity.
  2. 通過在host app中, hook startActivity(intent)方法, 當host app要啟動plugin app中的某個Activity時(需要明確指出要啟動頁面的完整包名和類名), 在hook了的startActivity中, 把要啟動的頁面修改為PluginActivity, 這樣AMS就不會報錯了.
  3. AMS回調host app進程中的ActivityThread的 handleLaunchActivity(),
    這個方法負責創建Activity的對象, 然后依次調用它的onCreate(), onStart()和onResume().
public final class ActivityThread {

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        Activity a = performLaunchActivity(r, customIntent);
    }

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

        ComponentName component = r.intent.getComponent();


        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }

        mInstrumentation.callActivityOnCreate(activity, r.state);

}

這行代碼很關鍵, 通過Instrumentation創建具體Activity的對象, 這里component.getClassName()的值必然是AMS傳進來的PluginActivity.

            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);

我們可以hook Instrumentation.newActivity()這個方法, 當發現傳進來的參數是PluginActivity時, 并不去創建PluginActivity的對象, 而修改成去創建 plugin app中的Activity的對象, 進而調用這個對象的onCreate(), onStart()和onResume().

如何去創建出 plugin app中的Activity的對象呢? 這就要通過DexClassLoader類.

Instrumentation的原始方法:

public class Instrumentation {

    public Activity newActivity(Class<?> clazz, Context context, 
            IBinder token, Application application, Intent intent, ActivityInfo info, 
            CharSequence title, Activity parent, String id,
            Object lastNonConfigurationInstance) throws InstantiationException, 
            IllegalAccessException {
        Activity activity = (Activity)clazz.newInstance();
        ActivityThread aThread = null;
        activity.attach(context, aThread, this, token, 0, application, intent,
                info, title, parent, id,
                (Activity.NonConfigurationInstances)lastNonConfigurationInstance,
                new Configuration(), null, null);
        return activity;
    }
}

可以看到在原始方法中, 是通過ClassLoader的newInstance()方法, 去創建Activity的對象.

用DexClassLoader類, 可以加載一個apk文件中的classes.dex.

例如這段代碼:

    DexClassLoader classloader = new DexClassLoader("apkPath",
            optimizedDexOutputPath.getAbsolutePath(),
            null, context.getClassLoader());
    Class<?> clazz = classloader.loadClass("com.plugindemo.test");
    Object obj = clazz.newInstance();
    Class[] param = new Class[2];
    param[0] = Integer.TYPE;
    param[1] = Integer.TYPE;
    Method method = clazz.getMethod("add", param);
    method.invoke(obj, 1, 2);

我們可以用DexClassLoader這個類把插件apk中的classes.dex加載進來, 然后調用它的loadClass(“完整的類名”)方法把要啟動的Activity類加載進來, 再調用Class類的newInstance()創建出插件中Activity的對象, 進而再通過調用mInstrumentation.callActivityOnCreate(activity, r.state);啟動這個Activity.

這樣就完成了對插件中頁面的啟動工作, 在host app中要做的, 就是要明確指定好要啟動頁面的完整包名和類名.

用hook機制解決的一個實際問題.

來launcher這邊的公司后, 同事碰到這樣一個棘手問題, 一直沒法解決.
在mediaV廣告模擬點擊后, 出現sdk中使用deeplink打開別的app頁面, 比如京東. 導致這個功能一直無法上線.
我使用上面的代碼, 對startActivity() API進行hook, 把京東這樣的intent給過濾掉, 這樣就完美解決了這個棘手問題.

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

        String intentInfo = intent.toString().toLowerCase();
        if (sMockClick > 0 && (intentInfo.contains("akactivity") || intentInfo.contains("jdmobile"))) {
            sMockClick--;
            Log.i(TAG, "ignore it triggered by mediav");
            return null;
        }

        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mBase, who,
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            throw new RuntimeException("3rd party rom modify this maybe, by ahking");
        }
    }

所以說, 有些知識平時多積累一些, 在一些關鍵時刻就能派上用場, 像activity的啟動流程, hook的實現, 當初學的時候看似無用, 學不學看似對實際的開發并沒有任何的意思, 但如果當初不學, 今天這樣的問題, 打死也想不到可以用這樣的方式去解決.

-------DONE.-------------

refer to:
Android插件化原理解析——Hook機制之動態代理
http://weishu.me/2016/01/28/understand-plugin-framework-proxy-hook/?nsukey=r%2BreMOlnWhDVfOrGukrJH1b%2FDJ9hDbJ0u4hfr6EQY2YIT4RCeJwqR20Lv0rQPVcPyLN4eX%2BgjW3k9fluG6CRgaUj1GyMa1GlVxN1F7%2FU%2FhiikosDgBCklABQCWbrFuXXHL0Q9QnQGDLOcL3demC82ZPcSTFjQrhrm8fEYqxTTxyn9JRzzsfCpZ3CG%2Bn6Z46s

http://zjmdp.github.io/2014/07/22/a-plugin-framework-for-android/

/home/wangxin/src/github/hookDemo (demo代碼的位置)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容