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澳腹,指鹿為馬V巍)
我們知道對(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ì)象,我們要做的就是偷梁換柱脖苏!代碼比較簡(jiǎn)單程拭,采用反射直接修改:

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.reibang.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必盖,
該文件完成類庫(kù)的加載以及一些函數(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)系作者
  • 序言:七十年代末擦耀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子涩堤,更是在濱河造成了極大的恐慌眷蜓,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胎围,死亡現(xiàn)場(chǎng)離奇詭異吁系,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)白魂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門汽纤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碧聪,你說我怎么就攤上這事冒版。” “怎么了逞姿?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵辞嗡,是天一觀的道長(zhǎng)捆等。 經(jīng)常有香客問我,道長(zhǎng)续室,這世上最難降的妖魔是什么栋烤? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮挺狰,結(jié)果婚禮上明郭,老公的妹妹穿的比我還像新娘。我一直安慰自己丰泊,他們只是感情好薯定,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瞳购,像睡著了一般话侄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上学赛,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天年堆,我揣著相機(jī)與錄音,去河邊找鬼盏浇。 笑死变丧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绢掰。 我是一名探鬼主播痒蓬,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼曼月!你這毒婦竟也來了谊却?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤哑芹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后捕透,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體聪姿,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年乙嘀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了末购。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡虎谢,死狀恐怖盟榴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情婴噩,我是刑警寧澤擎场,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布羽德,位于F島的核電站,受9級(jí)特大地震影響迅办,放射性物質(zhì)發(fā)生泄漏宅静。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一站欺、第九天 我趴在偏房一處隱蔽的房頂上張望姨夹。 院中可真熱鬧,春花似錦矾策、人聲如沸磷账。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)逃糟。三九已至,卻和暖如春榄鉴,著一層夾襖步出監(jiān)牢的瞬間履磨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工庆尘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留剃诅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓驶忌,卻偏偏與公主長(zhǎng)得像矛辕,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子付魔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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