Hook介紹

1. 什么是 Hook

Hook 英文翻譯過來就是「鉤子」的意思,那我們?cè)谑裁磿r(shí)候使用這個(gè)「鉤子」呢?在 Android 操作系統(tǒng)中系統(tǒng)維護(hù)著自己的一套事件分發(fā)機(jī)制。應(yīng)用程序,包括應(yīng)用觸發(fā)事件和后臺(tái)邏輯處理,也是根據(jù)事件流程一步步地向下執(zhí)行。而「鉤子」的意思,就是在事件傳送到終點(diǎn)前截獲并監(jiān)控事件的傳輸,像個(gè)鉤子鉤上事件一樣,并且能夠在鉤上事件時(shí),處理一些自己特定的事件。

image

Hook 的這個(gè)本領(lǐng),使它能夠?qū)⒆陨淼拇a「融入」被勾住(Hook)的程序的進(jìn)程中,成為目標(biāo)進(jìn)程的一個(gè)部分。API Hook 技術(shù)是一種用于改變 API 執(zhí)行結(jié)果的技術(shù),能夠?qū)⑾到y(tǒng)的 API 函數(shù)執(zhí)行重定向。在 Android 系統(tǒng)中使用了沙箱機(jī)制,普通用戶程序的進(jìn)程空間都是獨(dú)立的,程序的運(yùn)行互不干擾。這就使我們希望通過一個(gè)程序改變其他程序的某些行為的想法不能直接實(shí)現(xiàn),但是 Hook 的出現(xiàn)給我們開拓了解決此類問題的道路。當(dāng)然,根據(jù) Hook 對(duì)象與 Hook 后處理的事件方式不同,Hook 還分為不同的種類,比如消息 Hook、API Hook 等。

  1. 使用 Java 反射實(shí)現(xiàn) API Hook
    通過對(duì) Android 平臺(tái)的虛擬機(jī)注入與 Java 反射的方式,來改變 Android 虛擬機(jī)調(diào)用函數(shù)的方式(ClassLoader),從而達(dá)到 Java 函數(shù)重定向的目的,這里我們將此類操作稱為 Java API Hook。

下面通過 Hook View 的 OnClickListener 來說明 Hook 的使用方法。

首先進(jìn)入 View 的 setOnClickListener 方法,我們看到 OnClickListener 對(duì)象被保存在了一個(gè)叫做 ListenerInfo 的內(nèi)部類里,其中 mListenerInfo 是 View 的成員變量。ListeneInfo 里面保存了 View 的各種監(jiān)聽事件,比如 OnClickListener、OnLongClickListener、OnKeyListener 等等。

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

ListenerInfo getListenerInfo() {
    if (mListenerInfo != null) {
        return mListenerInfo;
    }
    mListenerInfo = new ListenerInfo();
    return mListenerInfo;
}

我們的目標(biāo)是 Hook OnClickListener,所以就要在給 View 設(shè)置監(jiān)聽事件后,替換 OnClickListener 對(duì)象,注入自定義的操作。

private void hookOnClickListener(View view) {
    try {
        // 得到 View 的 ListenerInfo 對(duì)象
        Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
        getListenerInfo.setAccessible(true);
        Object listenerInfo = getListenerInfo.invoke(view);
        // 得到 原始的 OnClickListener 對(duì)象
        Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
        Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
        mOnClickListener.setAccessible(true);
        View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
        // 用自定義的 OnClickListener 替換原始的 OnClickListener
        View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener);
        mOnClickListener.set(listenerInfo, hookedOnClickListener);
    } catch (Exception e) {
        log.warn("hook clickListener failed!", e);
    }
}

class HookedOnClickListener implements View.OnClickListener {
    private View.OnClickListener origin;

    HookedOnClickListener(View.OnClickListener origin) {
        this.origin = origin;
    }

    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show();
        log.info("Before click, do what you want to to.");
        if (origin != null) {
            origin.onClick(v);
        }
        log.info("After click, do what you want to to.");
    }
}

到這里,我們成功 Hook 了 OnClickListener,在點(diǎn)擊之前和點(diǎn)擊之后可以執(zhí)行某些操作,達(dá)到了我們的目的。下面是調(diào)用的部分,在給 Button 設(shè)置 OnClickListener 后,執(zhí)行 Hook 操作。點(diǎn)擊按鈕后,日志的打印結(jié)果是:Before click → onClick → After click。

    Button btnSend = (Button) findViewById(R.id.btn_send);
    btnSend.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            log.info("onClick");
        }
    });
    hookOnClickListener(btnSend);

我們?cè)賮砜匆粋€(gè)很常見的例子 startActivity
下面我們Hook掉startActivity這個(gè)方法,使得每次調(diào)用這個(gè)方法之前輸出一條日志;(當(dāng)然,這個(gè)輸入日志有點(diǎn)點(diǎn)弱,只是為了展示原理,如果你想可以替換參數(shù),攔截這個(gè)startActivity過程,使得調(diào)用它導(dǎo)致啟動(dòng)某個(gè)別的Activity,指鹿為馬!)
我們知道對(duì)于Context.startActivity,Context的實(shí)現(xiàn)實(shí)際上是ContextImpl;我們看ConetxtImpl類的startActivity方法:

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

這里,實(shí)際上使用了ActivityThread類的mInstrumentation成員的execStartActivity方法;注意到,ActivityThread 實(shí)際上是主線程,而主線程一個(gè)進(jìn)程只有一個(gè),因此這里是一個(gè)良好的Hook點(diǎn)。

接下來就是想要Hook掉我們的主線程對(duì)象,也就是把這個(gè)主線程對(duì)象里面的mInstrumentation給替換成我們修改過的代理對(duì)象;要替換主線程對(duì)象里面的字段,首先我們得拿到主線程對(duì)象的引用,如何獲取呢?ActivityThread類里面有一個(gè)靜態(tài)方法currentActivityThread可以幫助我們拿到這個(gè)對(duì)象類;但是ActivityThread是一個(gè)隱藏類,我們需要用反射去獲取,代碼如下:

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

拿到這個(gè)currentActivityThread之后,我們需要修改它的mInstrumentation這個(gè)字段為我們的代理對(duì)象,我們先實(shí)現(xiàn)這個(gè)代理對(duì)象,由于JDK動(dòng)態(tài)代理只支持接口,而這個(gè)Instrumentation是一個(gè)類,沒辦法,我們只有手動(dòng)寫靜態(tài)代理類,覆蓋掉原始的方法即可。(cglib可以做到基于類的動(dòng)態(tài)代理,這里先不介紹)

public class EvilInstrumentation extends Instrumentation {

    private static final String TAG = "EvilInstrumentation";

    // ActivityThread中原始的對(duì)象, 保存起來
    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");
        }
    }
}

Ok,有了代理對(duì)象,我們要做的就是偷梁換柱!代碼比較簡單,采用反射直接修改:

public static void attactContext() throws Exception{
        // 先獲取到當(dāng)前的ActivityThread對(duì)象
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
        currentActivityThreadField.setAccessible(true);
        Object currentActivityThread = currentActivityThreadField.get(null);

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

        // 創(chuàng)建代理對(duì)象
        Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);

        // 偷梁換柱
        mInstrumentationField.set(currentActivityThread, evilInstrumentation);
    }

好了,我們啟動(dòng)一個(gè)Activity測(cè)試一下,結(jié)果如下:

image

總結(jié)一下:

Hook 過程:
尋找 Hook 點(diǎn),原則是靜態(tài)變量或者單例對(duì)象,盡量 Hook public 的對(duì)象和方法。
選擇合適的代理方式,如果是接口可以用動(dòng)態(tài)代理。
偷梁換柱——用代理對(duì)象替換原始對(duì)象。
Android 的 API 版本比較多,方法和類可能不一樣,所以要做好 API 的兼容工作。

舉個(gè)例子
Android10后添加了ActivityTaskManager

 int result = ActivityTaskManager.getService().startActivity(whoThread,
                    who.getBasePackageName(), who.getAttributionTag(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()), token,
                    target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
int result = ActivityManagerNative.getDefault()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, null, options);

http://www.lxweimin.com/p/8632fdc86009

2. Xposed

通過替換 /system/bin/app_process 程序控制 Zygote 進(jìn)程,使得 app_process 在啟動(dòng)過程中會(huì)加載 XposedBridge.jar 這個(gè) Jar 包,從而完成對(duì) Zygote 進(jìn)程及其創(chuàng)建的 Dalvik 虛擬機(jī)的劫持。
Xposed 在開機(jī)的時(shí)候完成對(duì)所有的 Hook Function 的劫持,在原 Function 執(zhí)行的前后加上自定義代碼。

現(xiàn)在安裝Xposed比較方便,因?yàn)閄posed作者開發(fā)了一個(gè)Xposed Installer App,下載后按照提示傻瓜式安裝(前提是root手機(jī))。其實(shí)它的安裝過程是這個(gè)樣子的:首先探測(cè)手機(jī)型號(hào),然后按照手機(jī)版本下載不同的刷機(jī)包,最后把Xposed刷機(jī)包刷入手機(jī)重啟就好。刷機(jī)包下載 里面有所有版本的刷機(jī)包。
刷機(jī)包解壓打開里面的問件構(gòu)成是這個(gè)樣子的:

META-INF/    里面有文件配置腳本 flash-script.sh 配置各個(gè)文件安裝位置。
system/bin/   替換zygote進(jìn)程等文件
system/framework/XposedBridge.jar jar包位置
system/lib system/lib64 一些so文件所在位置
xposed.prop xposed版本說明文件

所以安裝Xposed的過程就上把上面這些文件放到手機(jī)里相同文件路徑下。
通過查看文件安裝腳本發(fā)現(xiàn):
system/bin/下面的文件替換了app_process等文件,app_process就是zygote進(jìn)程文件。所以Xposed通過替換zygote進(jìn)程實(shí)現(xiàn)了控制手機(jī)上所有app進(jìn)程。因?yàn)樗衋pp進(jìn)程都是由Zygote fork出來的。
Xposed的基本原理是修改了ART/Davilk虛擬機(jī),將需要hook的函數(shù)注冊(cè)為Native層函數(shù)。當(dāng)執(zhí)行到這一函數(shù)是虛擬機(jī)會(huì)優(yōu)先執(zhí)行Native層函數(shù),然后再去執(zhí)行Java層函數(shù),這樣完成函數(shù)的hook。如下圖:

image

通過讀Xposed源碼發(fā)現(xiàn)其啟動(dòng)過程:

  1. 手機(jī)啟動(dòng)時(shí)init進(jìn)程會(huì)啟動(dòng)zygote這個(gè)進(jìn)程。由于zygote進(jìn)程文件app_process已被替換,所以啟動(dòng)的時(shí)Xposed版的zygote進(jìn)程。
  2. Xposed_zygote進(jìn)程啟動(dòng)后會(huì)初始化一些so文件(system/lib system/lib64),然后進(jìn)入XposedBridge.jar中的XposedBridge.main中初始化jar包完成對(duì)一些關(guān)鍵Android系統(tǒng)函數(shù)的hook。
  3. Hook則是利用修改過的虛擬機(jī)將函數(shù)注冊(cè)為native函數(shù)。
  4. 然后再返回zygote中完成原本zygote需要做的工作。
    這只是在宏觀層面稍微介紹了下Xposed,要想詳細(xì)了解需要讀它的源碼了。下面兩篇寫的挺好,要想深入理解的可以看看。

Android基于Linux,第一個(gè)啟動(dòng)的進(jìn)程自然是init進(jìn)程,該進(jìn)程會(huì)
啟動(dòng)所有Android進(jìn)程的父進(jìn)程——Zygote(孵化)進(jìn)程,該進(jìn)程的啟動(dòng)配置在
/init.rc腳本中,而Zygote進(jìn)程對(duì)應(yīng)的執(zhí)行文件是/system/bin/app_process,
該文件完成類庫的加載以及一些函數(shù)的調(diào)用工作。在Zygote進(jìn)程創(chuàng)建后,
再fork出SystemServer進(jìn)程和其他進(jìn)程。
而Xposed Framework呢,就是用自己實(shí)現(xiàn)的app_process替換掉了系統(tǒng)原本
提供的app_process,加載一個(gè)額外的jar包,然后入口從原來的:
com.android.internal.osZygoteInit.main()被替換成了:
de.robv.android.xposed.XposedBridge.main(),
然后創(chuàng)建的Zygote進(jìn)程就變成Hook的Zygote進(jìn)程了,而后面Fork出來的進(jìn)程
也是被Hook過的。這個(gè)Jar包在:
/data/data/de.rbov.android.xposed.installer/bin/XposedBridge.jar

原文鏈接:https://blog.csdn.net/coder_pig/article/details/80031285
Android Hook框架Xposed原理與源代碼分析
https://blog.csdn.net/wxyyxc1992/article/details/17320911

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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