Android Hook技術分析

一.簡介

??????Hook技術是一種用于改變API執(zhí)行結果的技術,Android系統(tǒng)中有一套自己的事件分發(fā)機制,所有的代碼調用和回調都是按照一定順序執(zhí)行的,Hook技術存在的意義就在于,Hook可以幫助我們在Android中在SDK源代碼邏輯執(zhí)行過程中,通過代碼手動攔截執(zhí)行該邏輯,加入自己的代碼邏輯。
??????為了保證hook的穩(wěn)定性,一般攔截的點都會選擇比較容易找到并且不易發(fā)生變化的對象,比如靜態(tài)變量和單例。
??????提到Hook,就不得不說一下Java的反射機制。

二.反射

??????Java反射機制主要提供了以下功能:
????????????在運行時判斷任意一個對象所屬的類
????????????在運行時構造任意一個類的對象
????????????在運行時判斷任意一個類所具有的成員變量和方法
????????????在運行時調用任意一個對象的方法
????????????生成動態(tài)代理
??????在運行狀態(tài)中,對于任意一個類,都能夠獲取到這個類的所有屬性和方法,對于任意一個對象,都能夠調用它的任意一個方法和屬性(包括私有的方法和屬性),這種動態(tài)獲取的信息以及動態(tài)調用對象的方法的功能就稱為java語言的反射機制。

反射.png

三.Hook使用案例分析

1.實現啟動未注冊的activity

??????比如我們想啟動一個activity,如果未在AndroidManifest.xml里面注冊的話,調用Context.startActivity()時會出現以下異常:

1-25 10:44:54.811 E/AndroidRuntime(25309): Caused by: android.content.ActivityNotFoundException: Unable to 
find explicit activity class {com.hly.learn/com.hly.learn.HookActivity}; have you declared this activity in 
your AndroidManifest.xml?

??????下面就一起來實現啟動一個未在AndroidManifest.xml注冊的activity。

??????a.尋找hook點

??????對于Context.startActivity,由于Context的實現類為ContextImpl,因此直接分析ContextImpl類的startActivity()的方法:

   @Override
    public void startActivity(Intent intent) {
        warnIfCallingFromSystemProcess();
        startActivity(intent, null);
    }

    @Override
    public void startActivity(Intent intent, Bundle options) {
        ......
        mMainThread.getInstrumentation().execStartActivity(
                getOuterContext(), mMainThread.getApplicationThread(), null,
                (Activity) null, intent, -1, options);
    }

??????從上面可以看到,最終會調用mMainThread.getInstrumentation().execStartActivity(),mMainThread是ActivityThread實例,getInstrumentation()返回的是Instrumentation實例,實際上使用了ActivityThread類的mInstrumentation成員的execStartActivity方法;而ActivityThread 實際上是主線程,因為主線程一個進程只有一個,所以這里是一個良好的Hook點,即Hook主線程對象。

??????b.選擇合適代理方式

??????要將這個主線程對象里面的mInstrumentation替換成修改過的代理對象;要替換主線程對象里面的字段,得先拿到主線程對象的引用,如何獲取呢?
??????ActivityThread類里面有一個靜態(tài)方法currentActivityThread,通過它可以拿到這個對象類;但ActivityThread是一個隱藏類,需用反射去獲取拿到currentActivityThread后,要修改它的mInstrumentation字段為修改后的代理對象。實現如下:

    public static void hookInstrumentation(Context context) {
        try {
            //step1:先獲取到當前的ActivityThread對象, 該對象是mInstrumentation的持有者
            Class<?> activityThreadClz = Class.forName("android.app.ActivityThread");
            Method currentActivityThreadMethod = activityThreadClz.getDeclaredMethod("currentActivityThread");
            currentActivityThreadMethod.setAccessible(true);
            Object currentActivityThread = currentActivityThreadMethod.invoke(null);

            //step2:從ActivityThread里面拿到原始的mInstrumentation
            Field instrumentation = activityThreadClz.getDeclaredField("mInstrumentation");
            instrumentation.setAccessible(true);
            Instrumentation mInstrumentation = (Instrumentation) instrumentation.get(currentActivityThread);

            //step3:創(chuàng)建Instrumentation的代理對象[接下來會講到]
            Instrumentation proxyInstrumentation = new ProxyInstrumentation(mInstrumentation, context.getPackageManager());

            //step4:將持有的Instrumentation原始對象替換成代理對象[將currentActivityThread里面的instrumentation變量替換為proxyInstrumentation]
            instrumentation.set(currentActivityThread, proxyInstrumentation);
        } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }

    }

??????接下來實現這個代理對象,由于JDK動態(tài)代理只支持接口,而這個Instrumentation是一個類,因此只能手寫一個靜態(tài)代理類,用來覆蓋掉原始的方法,實現如下:

public class ProxyInstrumentation extends Instrumentation {

    private Instrumentation mBase;
    private PackageManager mPm;

    public ProxyInstrumentation(Instrumentation base, PackageManager pm) {
        mBase = base;
        mPm = pm;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        Log.d("Seven", "------Hook打印execStartActivity------");
        List<ResolveInfo> resolveInfos = mPm.queryIntentActivities(intent, PackageManager.MATCH_ALL);

        //檢查要跳轉的activity是否在Manifest.xml里面注冊
        if (resolveInfos == null || resolveInfos.size() == 0) {
            //把要跳轉的activity記錄下來,在接下來newActivity還原的時候要拿來還原
            intent.putExtra("intent_name", intent.getComponent().getClassName());
            //把要跳轉的activity改成已經在Manifest.xml里面注冊過的MainActivity
            intent.setClassName(who, "com.hly.learn.MainActivity");
        }

        // 開始調用Instrumentation原始的方法,由于這個方法是隱藏的,因此需要使用反射調用;
        try {
            //找到這個方法
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            //通過反射調用Instrumentation的execStartActivity方法
            return (ActivityResult) execStartActivity.invoke(mBase, who,
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            throw new RuntimeException("do not support!");
        }
    }

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        //取出上步記錄下來的要跳轉的activity并進行替換,來啟動未注冊的activity
        String intentName = intent.getStringExtra("intent_name");
        Log.d("Seven", "------newActivity內進行替換------");
        if (!TextUtils.isEmpty(intentName)) {
            return super.newActivity(cl, intentName, intent);
        }
        return super.newActivity(cl, className, intent);
    }
}
??????c.啟動運行
HookUtils.hookInstrumentation(mContext);
private void hookActivity() {
    Intent i = new Intent();
    i.setClass(mContext, HookActivity.class);
    i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    mContext.getApplicationContext().startActivity(i);
}
1-25 11:28:37.627 D/Seven   ( 1852): ------Hook打印execStartActivity------
1-25 11:28:37.665 D/Seven   ( 1852): ------newActivity內進行替換------

??????至此,一個未在AndroidManifest.xml里面注冊的activity通過hook技術方式就可以啟動了。

2.攔截點擊事件統(tǒng)計點擊次數
??????a.尋找hook點

??????對一個view設置點擊事件的調用流程如下:

    hkBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
            }
        });

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

    static class ListenerInfo {
        ......
        /**
         * Listener used to dispatch click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        public OnClickListener mOnClickListener;
        ......
}

??????Hook操作onClickListener實例,需要首先獲取到onClickListener的擁有者,即:ListenerInfo,最后通過ListenerInfo獲取到原始的mOnClickListener。

??????b.選擇合適的代理方式

??????從上面的流程可以發(fā)現,要將ListenerInfo類里面的mOnClickListener替換成修改過的代理對象;要替換ListenerInfo里面的字段,得先拿到ListenerInfo,View類中有一個方法getListenerInfo() ,通過它可以拿到這個ListenerInfo,實現如下:

    public static void hookOnClickListener(View view) {
        //step1:反射執(zhí)行View類的getListenerInfo()方法,拿到view的mListenerInfo對象,這個對象是點擊事件mOnClickListener的持有者
        try {
            Class<?> viewClz = Class.forName("android.view.View");
            Method method = viewClz.getDeclaredMethod("getListenerInfo");
            //由于getListenerInfo()方法并不是public的,所以要加這個代碼來保證訪問權限
            method.setAccessible(true);
            //拿到mListenerInfo,也就是點擊事件的持有者
            Object listenerInfo = method.invoke(view);

            //step2:找到mListenerInfo持有的點擊事件對象mOnClickListener
            //內部類的表示方法:android.view.View$ListenerInfo
            Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
            Field onClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
            final View.OnClickListener onClickListenerInstance = (View.OnClickListener) onClickListener.get(
                    listenerInfo);

            //step3:創(chuàng)建自己點擊事件的OnClickListener代理類
             動態(tài)或靜態(tài)創(chuàng)建OnClickListener的代理類proxyOnClickListener

            //step4:將持有者擁有的點擊事件替換成代理對象[將listenerInfo里面的onClickListener變量替換為proxyOnClickListener]
            onClickListener.set(listenerInfo, proxyOnClickListener);
        } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException | NoSuchFieldException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
    }

??????接下來要創(chuàng)建OnClickListener的代理對象,由于OnClickListener是一個接口,因此可以使用JDK動態(tài)代理方式Java動態(tài)代理,也可以用靜態(tài)代理類實現,實現方式如下:

    //方式1:自己實現代理類,將原始的View.OnClickListener對象onClickListenerInstance作為參數傳入
    ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
    static class ProxyOnClickListener implements View.OnClickListener{

        private View.OnClickListener listener;
        private int clickCount = 0;
        ProxyOnClickListener(View.OnClickListener listener){
            this.listener = listener;
        }

        @Override
        public void onClick(View v) {
            clickCount++;
            Log.d("Seven", "Hook OnClickListener 2 click count " + clickCount);
            if(this.listener != null){
                this.listener.onClick(v);
            }
    }
            
    //方式2:由于View.OnClickListener是一個接口,所以可以直接用動態(tài)代理模式
    //參數:類的加載器,要代理實現的接口(用Class數組表示,支持多接口),代理類的實際邏輯封裝在new出來的InvocationHandler內
    Object proxyOnClickListener = Proxy.newProxyInstance(View.OnClickListener.class.getClassLoader(),
                    new Class[]{View.OnClickListener.class}, new InvocationHandler() {
                private int clickCount = 0;
                @Override
                 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                         clickCount++;
                         Log.d("Seven", "Hook OnClickListener 1 click count " + clickCount);
                         return method.invoke(onClickListenerInstance, args);
                }
    });
??????c.啟動運行
hkBtn.setOnClickListener(this);
HookUtils.hookonClickListener(hkBtn);

??????運行結果如下:

1-25 10:51:12.190 D/Seven   (26125): Hook OnClickListener 1 click count 1
1-25 10:51:21.428 D/Seven   (26125): Hook OnClickListener 1 click count 2
1-25 10:51:21.607 D/Seven   (26125): Hook OnClickListener 1 click count 3
1-25 10:51:21.770 D/Seven   (26125): Hook OnClickListener 1 click count 4
1-25 10:51:21.918 D/Seven   (26125): Hook OnClickListener 1 click count 5
1-25 10:51:22.068 D/Seven   (26125): Hook OnClickListener 1 click count 6
1-25 10:51:22.217 D/Seven   (26125): Hook OnClickListener 1 click count 7
1-25 10:51:22.394 D/Seven   (26125): Hook OnClickListener 1 click count 8
1-25 10:51:22.543 D/Seven   (26125): Hook OnClickListener 1 click count 9
1-25 10:51:22.722 D/Seven   (26125): Hook OnClickListener 1 click count 10
1-25 10:51:22.854 D/Seven   (26125): Hook OnClickListener 1 click count 11
1-25 10:51:23.009 D/Seven   (26125): Hook OnClickListener 1 click count 12
1-25 10:51:23.158 D/Seven   (26125): Hook OnClickListener 1 click count 13

??????至此已經實現了對點擊事件的hook。

3.總結

??????針對以上兩個案例分析,我們可以看到在進行hook時主要分為以下幾步:
??????a.尋找合適的hook點;
??????b.找到Hook點對應的class;
??????c.直接通過class或class內部的method找到對應的filed;
??????d.替換對象是interface的話,直接用動態(tài)代理方式創(chuàng)建對象;否則,直接寫個類繼承要替換的類;
??????e.調用filed.set(obj, proxy)來對變量進行替換;
??????f.針對hook點進行相應的處理;

??????hook技術涉及到的知識點主要有反射、代理及android源碼的熟練程度。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容