Android hook, 以及對插件框架如何實(shí)現(xiàn)的開發(fā)精要

Hook的概念

*所謂對API的Hook, 其實(shí)就是對方法的動(dòng)態(tài)替換. *
采用代理的方式柏腻, 創(chuàng)建一個(gè)新的對象微饥, 其內(nèi)部封裝原始對象,通過這種方式咆贬,可以修改這個(gè)方法的參數(shù)以及返回值偷崩, 或是在方法中新打印一行l(wèi)og辟拷, 達(dá)到 方法增強(qiáng) 的目的.

實(shí)現(xiàn)方式

在運(yùn)行時(shí)撞羽, 采用反射的方式阐斜, 用自己新建的代理對象把原始對象給替換掉.
代理對象本質(zhì)上還是通過原始對象去干事.

對Context.startActivity的hook.

啟動(dòng)Activity是最常見的操作, Context.startActivity的真正實(shí)現(xiàn)是在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);
    }

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

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

@hide
public final class ActivityThread {
    Instrumentation mInstrumentation;

    public Instrumentation getInstrumentation() {
        return mInstrumentation;
    }

}

ActivityThread是一個(gè)隱藏類谒出,我們需要用反射去獲取,代碼如下:

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

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

新建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執(zhí)行了startActivity, 參數(shù)如下: \n" + "who = [" + who + "], " +
                "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
                "\ntarget = [" + target + "], \nintent = [" + intent +
                "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");

        // 開始調(diào)用原始的方法, 調(diào)不調(diào)用隨你,但是不調(diào)用的話, 所有的startActivity都失效了.
        // 由于這個(gè)方法是隱藏的,因此需要使用反射調(diào)用;首先找到這個(gè)方法
        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修改了  需要手動(dòng)適配
            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{

        // 先獲取到當(dāng)前的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);

        // 創(chuàng)建代理對象笤喳, 構(gòu)造時(shí)把原始對象作為參數(shù)傳進(jìn)去.
        Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);

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

    }
}




log輸出如下:

com.ahking.hookdemo D/ahking: 執(zhí)行了startActivity, 參數(shù)如下: 
                         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中碌宴, 預(yù)先注冊一個(gè)Activity杀狡, 比如叫PluginActivity.
  2. 通過在host app中, hook startActivity(intent)方法, 當(dāng)host app要啟動(dòng)plugin app中的某個(gè)Activity時(shí)(需要明確指出要啟動(dòng)頁面的完整包名和類名)贰镣, 在hook了的startActivity中呜象, 把要啟動(dòng)的頁面修改為PluginActivity, 這樣AMS就不會(huì)報(bào)錯(cuò)了.
  3. AMS回調(diào)host app進(jìn)程中的ActivityThread的 handleLaunchActivity(),
    這個(gè)方法負(fù)責(zé)創(chuàng)建Activity的對象, 然后依次調(diào)用它的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);

}

這行代碼很關(guān)鍵碑隆, 通過Instrumentation創(chuàng)建具體Activity的對象, 這里component.getClassName()的值必然是AMS傳進(jìn)來的PluginActivity.

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

我們可以hook Instrumentation.newActivity()這個(gè)方法恭陡, 當(dāng)發(fā)現(xiàn)傳進(jìn)來的參數(shù)是PluginActivity時(shí), 并不去創(chuàng)建PluginActivity的對象, 而修改成去創(chuàng)建 plugin app中的Activity的對象, 進(jìn)而調(diào)用這個(gè)對象的onCreate(), onStart()和onResume().

如何去創(chuàng)建出 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()方法休玩, 去創(chuàng)建Activity的對象.

用DexClassLoader類, 可以加載一個(gè)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這個(gè)類把插件apk中的classes.dex加載進(jìn)來劫狠, 然后調(diào)用它的loadClass(“完整的類名”)方法把要啟動(dòng)的Activity類加載進(jìn)來拴疤, 再調(diào)用Class類的newInstance()創(chuàng)建出插件中Activity的對象, 進(jìn)而再通過調(diào)用mInstrumentation.callActivityOnCreate(activity, r.state);啟動(dòng)這個(gè)Activity.

這樣就完成了對插件中頁面的啟動(dòng)工作独泞, 在host app中要做的遥赚, 就是要明確指定好要啟動(dòng)頁面的完整包名和類名.

用hook機(jī)制解決的一個(gè)實(shí)際問題.

來launcher這邊的公司后, 同事碰到這樣一個(gè)棘手問題阐肤, 一直沒法解決.
在mediaV廣告模擬點(diǎn)擊后凫佛, 出現(xiàn)sdk中使用deeplink打開別的app頁面讲坎, 比如京東. 導(dǎo)致這個(gè)功能一直無法上線.
我使用上面的代碼, 對startActivity() API進(jìn)行hook愧薛, 把京東這樣的intent給過濾掉晨炕, 這樣就完美解決了這個(gè)棘手問題.

    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");
        }
    }

所以說, 有些知識(shí)平時(shí)多積累一些毫炉, 在一些關(guān)鍵時(shí)刻就能派上用場瓮栗, 像activity的啟動(dòng)流程, hook的實(shí)現(xiàn)瞄勾, 當(dāng)初學(xué)的時(shí)候看似無用费奸, 學(xué)不學(xué)看似對實(shí)際的開發(fā)并沒有任何的意思, 但如果當(dāng)初不學(xué)进陡, 今天這樣的問題愿阐, 打死也想不到可以用這樣的方式去解決.

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

refer to:
Android插件化原理解析——Hook機(jī)制之動(dòng)態(tài)代理
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代碼的位置)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市趾疚,隨后出現(xiàn)的幾起案子缨历,更是在濱河造成了極大的恐慌,老刑警劉巖糙麦,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辛孵,死亡現(xiàn)場離奇詭異,居然都是意外死亡赡磅,警方通過查閱死者的電腦和手機(jī)魄缚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來焚廊,“玉大人冶匹,你說我怎么就攤上這事〗谥担” “怎么了徙硅?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵疚宇,是天一觀的道長脾猛。 經(jīng)常有香客問我,道長内舟,這世上最難降的妖魔是什么匿乃? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任桩皿,我火速辦了婚禮,結(jié)果婚禮上幢炸,老公的妹妹穿的比我還像新娘泄隔。我一直安慰自己,他們只是感情好宛徊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布佛嬉。 她就那樣靜靜地躺著逻澳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪暖呕。 梳的紋絲不亂的頭發(fā)上斜做,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機(jī)與錄音湾揽,去河邊找鬼瓤逼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛库物,可吹牛的內(nèi)容都是我干的霸旗。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼戚揭,長吁一口氣:“原來是場噩夢啊……” “哼诱告!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起毫目,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蔬啡,失蹤者是張志新(化名)和其女友劉穎诲侮,沒想到半個(gè)月后镀虐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沟绪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年刮便,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绽慈。...
    茶點(diǎn)故事閱讀 39,926評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恨旱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出坝疼,到底是詐尸還是另有隱情搜贤,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布钝凶,位于F島的核電站仪芒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏耕陷。R本人自食惡果不足惜掂名,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哟沫。 院中可真熱鬧饺蔑,春花似錦、人聲如沸嗜诀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至发皿,卻和暖如春融击,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背雳窟。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工尊浪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人封救。 一個(gè)月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓拇涤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親誉结。 傳聞我的和親對象是個(gè)殘疾皇子鹅士,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容