1. 程式人生 > >Hook簡述以及Hook AMS 實現統一登入

Hook簡述以及Hook AMS 實現統一登入

什麼是hook?

hook 翻譯成中文是 名詞鉤子、掛鉤,動詞 鉤住的意思。

在程式中 Hook 是一種技術,也成為鉤子函式。

實際上,一個處理訊息的程式段,通過系統的呼叫,把它掛入系統。 在系統沒有呼叫到該函式之前,鉤子程式先捕獲該訊息,先得到控制權,然後加工處理,然後再扔給系統做後續的處理。

比如說,有一輛貨車 每天從A倉庫拉貨(一個集裝箱的蘋果) 到 B 水果分發站。 我們的鉤子函式的呢,就是在 該貨車拉 集裝箱之前,開啟集裝箱 做一些自己的操作,比如把蘋果換成其他的水果。然後再把集裝箱放回原地,讓貨車拉走。 對於這個過程 貨車是無法感知到的。

上面的例子比較… 但可以幫助理解hook技術。

hook的作用?

在我們開發過程中,有時候某些API 不能滿足我們的需求時,我們就可以使用Hook 來進而改變一個 系統api 的原有功能。
比如通過hook AMS 實現統一登入等。

如何hook?

我們這裡介紹一下 java層的hook 方式。有兩種:

1.利用系統內部提供的介面,通過實現該介面,然後注入自己的操作邏輯 (只在特定的場景下適用)

2.動態代理(所有場景)

如何查詢hook點

找到hook 點: 靜態的變數或者單例,生命週期跨度較長,不會輕易重新建立。

Hook 過程

  • 找到要Hook的點,通過反射或者其他方式 拿到該引用。
  • 選擇合適的方式處理邏輯,動態代理 或者 介面方式。
  • 用我們修改過的物件,替換原來的物件(即上面提到的,把集裝箱放回原位置,等貨車拉走)

ok,這裡只是簡述一下 hook 相關的概念

Hook AMS 實現應用統一登入

整體思路:

1 . 我們使用動態代理的方式 hook 到 AMS 中的startActivity() 方法。當外部啟動頁面時,比如,啟動購物車頁面(未在AndroidManifest中註冊)時。我們在代理的 invoke 方法中, 替換準備啟動的 Intent物件為 空殼Activity(已在AndroidManifest中註冊),並把啟動購物車的Intent 傳遞過去, 然後讓系統做後續的啟動操作。

2 . hook 到 ActivityThread 中的 mH(Handler,負責系統訊息的處理) ,並給他設定 CallBack, 在 callback 中 我們便可以 獲取到啟動 Activity的訊息 LAUNCH_ACTIVITY 即100, 然後我們可以取出 intent 替換為我們想要啟動的購物車介面, 當然可以在其中加入一些判斷登入的邏輯來實現統一的登入校驗.

上面的思路,可能稍微有些抽象,下面我們來上程式碼:

我們先新增一些基本的頁面以及簡單的跳轉操作程式碼:

/**
     *  跳轉到購物車介面
     * @param view
     */
    public void toCartPage(View view) {

        startActivity(new Intent(MainActivity.this,CartPageActivity.class));
    }

    /**
     *  跳轉到我的介面
     * @param view
     */
    public void toMinePage(View view) {

        startActivity(new Intent(MainActivity.this,MinePageActivity.class));
    }

上面的 CartPageActivityMinePageActivity 介面均未在AndroidManifest.xml 中註冊。

我們需要新增一個 空殼頁面, 這個頁面呢,沒有任何的實際作用,只是為了能讓 未註冊的頁面可以正常的通過系統的檢查。

public class ProxyActivity extends Activity{

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

ok,下面我們來上 hook 部分的程式碼。

 /**
     *  hook ams 中的 startActivity 方法,代理該方法,新增我們自己的操作,然後再拋給 系統做後續的流程
     */
    public void hookStartActivity() {

        try {
            Class<?> mManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");

            //hook到 gDefault 這個單例 Singleton
            Field mIActivityManagerSington = mManagerNativeClazz.getDeclaredField("gDefault");
            mIActivityManagerSington.setAccessible(true);
            Object gDefaultValue = mIActivityManagerSington.get(null);

            Class<?> mSingletonClazz = Class.forName("android.util.Singleton");
            Field mSingletonField = mSingletonClazz.getDeclaredField("mInstance");
            mSingletonField.setAccessible(true);

            //通過反射的方式 獲取到 我們的 IActivityManager 引用
            Object iActivityManagerObj = mSingletonField.get(gDefaultValue);


            CheckStartActivityHandler checkStartActivityHandler = new CheckStartActivityHandler(iActivityManagerObj);

            Class<?> mIActivityMangerInterface = Class.forName("android.app.IActivityManager");

            // 此處通過動態代理的方式 生成 代理IActivityManager中介面的代理物件 mProxyActivityManager 並設定給了系統
            Object mProxyActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{mIActivityMangerInterface}, checkStartActivityHandler);

            mSingletonField.set(gDefaultValue,mProxyActivityManager);

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

    }

我們來說說上面都幹了些啥事:

1.反射的方式獲取到了ActivityManageNative 中的 gDefault欄位 即 Singleton<IActivityManager>,它的內部持有IActivityManager的引用。 我們的 AMSIActivityManager 的具體實現。

2.從 gDefault 取出了我們的 IActivityManager 通過下面這幾行程式碼:

  Class<?> mSingletonClazz = Class.forName("android.util.Singleton");
  Field mSingletonField = mSingletonClazz.getDeclaredField("mInstance");
  mSingletonField.setAccessible(true);
  //通過反射的方式 獲取到 我們的 IActivityManager 引用
  Object iActivityManagerObj = mSingletonField.get(gDefaultValue);

3.建立了動態代理的Handler裝置,即實現 InvocationHandler ,這是硬性要求。沒的選。同時,我們傳入了我們的 IActivityManager 引用.

 CheckStartActivityHandler checkStartActivityHandler = new CheckStartActivityHandler(iActivityManagerObj);

4.動態代理系統 IActivityManager 中的介面,並返回 生成的代理物件 即 mProxyActivityManager 並設定給系統,就是我們說的,我們還得把集裝箱還回去。所有 IActivityManager 中的介面都呼叫 都會被委託到 我們的 checkStartActivityHandler中, 呼叫 invoke 方法。

 Class<?> mIActivityMangerInterface = Class.forName("android.app.IActivityManager");

            // 此處通過動態代理的方式 生成 代理IActivityManager中介面的代理物件 mProxyActivityManager 並設定給了系統
            Object mProxyActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{mIActivityMangerInterface}, checkStartActivityHandler);

            mSingletonField.set(gDefaultValue,mProxyActivityManager);

簡單提一下動態代理的幾個引數含義:

loader:類載入器型別,由哪個類載入器來載入代理物件

interfaces :一組介面,代理物件會實現該介面。

invocationHandler:當代理物件呼叫方法時,會關聯到Handler 並呼叫該 invoke 方法。

上面的暫且說到這,我們繼續來看代理中的處理程式碼:

 /**
     *  動態代理 所需要實現的, 每一個代理的介面方法 都會委託給我們 這個handler 裝置 來進行處理,即都會呼叫到 invoke 方法
     */
    class CheckStartActivityHandler implements InvocationHandler {


        private Object mOldActivityManager;

        public CheckStartActivityHandler(Object iOldActivityManger) {
            this.mOldActivityManager = iOldActivityManger;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            Log.e(TAG,"hook method "+method);

            if ("startActivity".equals(method.getName())){  //如果方法時 啟動Activity
                Intent mIntent = null;
                int argIndex = 0;
                for (int i = 0; i < args.length; i++) {
                    Object arg = args[i];
                    if (arg instanceof Intent){
                        mIntent = (Intent) arg;
                        argIndex = i;
                    }
                }

                Intent newIntent = new Intent();
                newIntent.setComponent(new ComponentName(mContext,ProxyActivity.class));
                //把startActivity 中的 Intnet 傳遞過去, mIntent 為想要開啟的頁面。
                newIntent.putExtra("oldIntent",mIntent);
                args[argIndex] = newIntent; // 替換啟動的intent
            }

            return method.invoke(mOldActivityManager,args);
        }
    }

說下 invoke方法中的幾個引數:

proxy : 生成的代理物件

method : 我們所要呼叫方法的Method物件

args : 該Mehod所需要的方法引數

你也看到了,我們只是判斷了方法的名稱, 然後取出對應的 Intent 然後替換為我們的 空殼Activity,這裡只是為了可以通過AMS的相關檢查。 然後把我們的 想要啟動的頁面傳遞過去(比如購物車介面,注意,購物車介面並未註冊)。

第一步的操作到這裡就完成了。我們也可以歇一會,讓 Activity 先飛一會。

接下來,我們要進行第二步的操作,即通過回撥的方式 hook 系統的 訊息處理Handler。

/**
     *  通過設定回撥的方式, hook M  即訊息Handler 判斷是否是啟動Activity 的訊息 即 100;
     */
    public void hookMHandler(){

        try {
            Class<?> mActivityThreadClazz = Class.forName("android.app.ActivityThread");

            Method currentActivityThread = mActivityThreadClazz.getDeclaredMethod("currentActivityThread");
            Object mActivityThreadInstance = currentActivityThread.invoke(null);


            Field mHandlerField = mActivityThreadClazz.getDeclaredField("mH");
            mHandlerField.setAccessible(true);

            Handler mActivityThreadHandler = (Handler) mHandlerField.get(mActivityThreadInstance);
            Field mHandlerCallBackField = Handler.class.getDeclaredField("mCallback");
            mHandlerCallBackField.setAccessible(true);

            mHandlerCallBackField.set(mActivityThreadHandler,new HandlerCallBack(mActivityThreadHandler));

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

上面做了3件事:

1.反射獲取到 ActivityThreadmActivityThreadInstance

2.獲取 ActivityThread內部的訊息處理Handler mH,並獲取其內部的 Field域 mCallback

3.給該欄位,設定Callback.即下面這段程式碼:

mHandlerCallBackField.set(mActivityThreadHandler,new HandlerCallBack(mActivityThreadHandler));

下面我們貼上CallBack 內部的處理程式碼:

class HandlerCallBack implements Handler.Callback {


        private Handler mActivityThreadH;

        public HandlerCallBack(Handler mActivityThreadHandler) {
            this.mActivityThreadH = mActivityThreadHandler;
        }

        @Override
        public boolean handleMessage(Message msg) {

            if (msg.what == 100){       // 當前是正在 啟動Activity   以及過了 AMS的相關檢測
                launchRealActivity(msg);
            }
            mActivityThreadH.handleMessage(msg);
            return true;
        }

        private void launchRealActivity(Message msg) {
            // 此處的 obj 為 ActivityClientRecord 物件 儲存了 Activity 啟動的資訊
            Object mActivityRecordObj = msg.obj;

            try {
                Field mIntentField = mActivityRecordObj.getClass().getDeclaredField("intent");
                mIntentField.setAccessible(true);
                // 此處我們獲取到的是 啟動Activity 時的 代理intent
                Intent mProxyIntent = (Intent) mIntentField.get(mActivityRecordObj);

                Intent mOldIntent = mProxyIntent.getParcelableExtra("oldIntent");


                //TODO: 此處判斷登入的邏輯,如果未登入 則跳轉到登入介面。否則 開啟傳遞的 Intent 即 mOldIntent;


                mProxyIntent.setComponent(mOldIntent.getComponent());

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

        }
    }

判斷訊息是否是啟動Activity,如果是,則從Intent中取出我們真正要啟動的 Activity(比如購物車頁面)。然後啟動即可以了。正如你所看到的 TODO ,我們可以在這裡加入登入校驗的邏輯。

最後,我們只需要在 Application 中呼叫即可:

public class BaseApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        LoginHookCheckUtil hookCheckUtil = new LoginHookCheckUtil(this);
        hookCheckUtil.hookStartActivity();
        hookCheckUtil.hookMHandler();
    }
}

到這裡呢,就結束了。

注意

有一個問題,我目前尚未處理, 仍在尋找解決方式。即 CartPageActivityMinePageActivity 只能繼承 Activity . 如果繼承 AppCompatActivity 會報錯。 請注意~~~~!!!

程式碼地址:

https://download.csdn.net/download/mike_cui_ls/10342043