Android登錄攔截:動態代理+Hook的實現

一、動態代理 + Hook 的實現

在之前的文章我們講過插件化的實現有點類似,插件化一般是替換系統的 mInstrumentation 為自己的 Instrumentation 。

而我們這里沒有這么麻煩,我們這里需要Hook的是ASM ,是Android啟動頁面過程中的一個 mInstance 對象,它就是ActivityManagerService。

startActivity()最終會進入Instrumentation:

@Overridepublic void startActivityForResult(
        String who, Intent intent, int requestCode, @Nullable Bundle options) {
    ...
    Instrumentation.ActivityResult ar =
        mInstrumentation.execStartActivity(            this, mMainThread.getApplicationThread(), mToken, who,
            intent, requestCode, options);
    ...
}
Instrumentation的execStartActivity代碼:


public ActivityResult execStartActivity(
    Context who, IBinder contextThread, IBinder token, String target,
    Intent intent, int requestCode, Bundle options) {
    ...    try {
        ...        int result = ActivityManagerNative.getDefault()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target, requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {        throw new RuntimeException("Failure from system", e);
    }    return null;
}

gDefault是一個Singleton類型的靜態常量,它的get()方法返回的是Singleton類中的private T mInstance ,這個mInstance的創建又是在gDefault實例化時通過create()方法實現。gDefault.get()獲取到的mInstance實例就是ActivityManagerService(AMS)實例。由于gDefault是一個靜態常量,因此可以通過反射獲取到它的實例,同時它是Singleton類型的,因此可以獲取到其中的mInstance。

static public IActivityManager getDefault() {    
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {    protected IActivityManager create() {  
        IBinder b = ServiceManager.getService("activity");        if (false) {
            Log.v("ActivityManager", "default service binder = " + b);
        }        IActivityManager am = asInterface(b);        if (false) {
            Log.v("ActivityManager", "default service = " + am);
        }        return am;
    }
};public abstract class Singleton<T> {    private T mInstance;    protected abstract T create();    public final T get() {        synchronized (this) {            if (mInstance == null) {

                mInstance = create();
            }            return mInstance;
        }
    }
}

由于8.0系統以下 ,8.0系統 - 9.0系統,10系統 - 12系統 的實現均有差異,需要做一下兼容性處理。我們通過下面的工具類方法實現如何使用反射 + Hook + 動態代理實現效果:

public class DynamicProxyUtils {    //修改啟動模式
    public static void hookAms() {        try {

            Field singletonField;
            Class<?> iActivityManager;            // 1,獲取Instrumentation中調用startActivity(,intent,)方法的對象
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {                // 10.0以上是ActivityTaskManager中的IActivityTaskManagerSingleton
                Class<?> activityTaskManagerClass = Class.forName("android.app.ActivityTaskManager");
                singletonField = activityTaskManagerClass.getDeclaredField("IActivityTaskManagerSingleton");
                iActivityManager = Class.forName("android.app.IActivityTaskManager");
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {                // 8.0,9.0在ActivityManager類中IActivityManagerSingleton
                Class activityManagerClass = ActivityManager.class;
                singletonField = activityManagerClass.getDeclaredField("IActivityManagerSingleton");
                iActivityManager = Class.forName("android.app.IActivityManager");
            } else {                // 8.0以下在ActivityManagerNative類中 gDefault
                Class<?> activityManagerNative = Class.forName("android.app.ActivityManagerNative");
                singletonField = activityManagerNative.getDeclaredField("gDefault");
                iActivityManager = Class.forName("android.app.IActivityManager");
            }
            singletonField.setAccessible(true);            Object singleton = singletonField.get(null);            // 2,獲取Singleton中的mInstance,也就是要代理的對象
            Class<?> singletonClass = Class.forName("android.util.Singleton");            Field mInstanceField = singletonClass.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);            Method getMethod = singletonClass.getDeclaredMethod("get");            Object mInstance = getMethod.invoke(singleton);            if (mInstance == null) {                return;
            }            //開始動態代理
            Object proxy = Proxy.newProxyInstance(
                    Thread.currentThread().getContextClassLoader(),                    new Class[]{iActivityManager},                    new AmsHookBinderInvocationHandler(mInstance));            //現在替換掉這個對象
            mInstanceField.set(singleton, proxy);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }    //動態代理執行類
    public static class AmsHookBinderInvocationHandler implements InvocationHandler {        private Object obj;        public AmsHookBinderInvocationHandler(Object rawIActivityManager) {
            obj = rawIActivityManager;
        }        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {            if ("startActivity".equals(method.getName())) {

                Intent raw;                int index = 0;                for (int i = 0; i < args.length; i++) {                    if (args[i] instanceof Intent) {
                        index = i;                        break;
                    }
                }                //原始意圖
                raw = (Intent) args[index];
                YYLogUtils.w("原始意圖:" + raw);                //設置新的Intent-直接制定LoginActivity
                Intent newIntent = new Intent();                String targetPackage = "com.guadou.kt_demo";                ComponentName componentName = new ComponentName(targetPackage, LoginDemoActivity.class.getName());
                newIntent.setComponent(componentName);

                YYLogUtils.w("改變了Activity啟動");

                args[index] = newIntent;

                YYLogUtils.w("攔截activity的啟動成功" + " --->");                return method.invoke(obj, args);

            }            //如果不是攔截的startActivity方法,就直接放行
            return method.invoke(obj, args);
        }

    }
}

使用的時候我們可以在Application中使用,也可以就在方法中啟動:

 mBtnProfile.click {        //啟動動態代理
         DynamicProxyUtils.hookAms()

        gotoActivity<ProfileDemoActivity>()
    }

這樣我們就可以把應用類全部的Activity跳轉都替換為我們的LoginActivity了...太壞了。下一步怎么做?

二、Itent的攔截與處理

其實和之前Intent的攔截處理有點類似了,我們判斷是否登錄,如果已經登錄了,直接放行,如果沒有登錄,我們拿到原始的Intent,當做參數傳給新的LoginIntent。登錄執行完成了讓LoginActivity幫我們做后續的意圖。

我們修改動態代理的回調方法:

//動態代理執行類


    public static class AmsHookBinderInvocationHandler implements InvocationHandler {        private Object obj;        public AmsHookBinderInvocationHandler(Object rawIActivityManager) {
            obj = rawIActivityManager;
        }        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {            if ("startActivity".equals(method.getName())) {                //如果已經登錄-直接放行
                if (LoginManager.isLogin()){                    return method.invoke(obj, args);
                }                //如果未登錄-獲取到原始意圖,再替換Intent攜帶數據到LoginActivity中
                Intent raw;                int index = 0;                for (int i = 0; i < args.length; i++) {                    if (args[i] instanceof Intent) {
                        index = i;                        break;
                    }
                }                //原始意圖
                raw = (Intent) args[index];
                YYLogUtils.w("原始意圖:" + raw);                //設置新的Intent-直接制定LoginActivity
                Intent newIntent = new Intent();                String targetPackage = "com.guadou.kt_demo";                ComponentName componentName = new ComponentName(targetPackage, LoginDemoActivity.class.getName());
                newIntent.setComponent(componentName);
                newIntent.putExtra("targetIntent", raw);

                YYLogUtils.w("改變了Activity啟動");

                args[index] = newIntent;

                YYLogUtils.w("攔截activity的啟動成功" + " --->");                return method.invoke(obj, args);

            }            //如果不是攔截的startActivity方法,就直接放行
            return method.invoke(obj, args);
        }

    }

使用邏輯:

 mBtnProfile.click {            //啟動動態代理
            DynamicProxyUtils.hookAms()

            gotoProfilePage()
        }

Login頁面的處理:

    private var mTargetIntent: Intent? = null
    private var mTargetType = 0

     override fun init() {
        mTargetIntent = intent.getParcelableExtra("targetIntent")
        mTargetType = intent.getIntExtra("type", 0)
    }    fun doLogin() {
            showStateLoading()

            CommUtils.getHandler().postDelayed({
                showStateSuccess()

                SP().putString(Constants.KEY_TOKEN, "abc")

                setResult(-1, Intent().apply { putExtra("type", mTargetType) })   //設置Result

                if (mTargetIntent != null) {
                    startActivity(mTargetIntent)
                }

                finish()

            }, 500)

        }

總結

其實我們可以加入一個黑名單,白名單的集合來管理,例如我們使用注解標記哪一些頁面需要校驗登錄,然后把這些注解的頁面放入一個集合中,在動態代理的回調中,我們判斷如果在這些集合中的頁面才會判斷是否登錄,否則直接放行。

如果需要管理的頁面太多,我們可以使用APT代碼生成,或者ASM字節碼注入等多種方式來實現。網上有一些方案是基于APT代碼生成的示例。

當然如果大家有需求可以自行擴展與實現,比如頁面不多的話,可以自己管理一個黑名單集合,如果多的話可以使用APT生成代碼。

主要注意的是,注解的方案只用于跳轉頁面的場景,如果是彈窗,或者切換Tab的場景就無法實現,還是不夠靈活。

優點與缺點

相比Intent的方案,使用Hook+動態代理的方法對攔截登錄頁面進行了封裝和處理,集中處理的方式在使用起來更加的便捷,后面的繼續執行的邏輯還是和Intent方案一樣的邏輯。
可以說是Intent的進化版,缺點還是和Intent一樣,在繼續執行這一塊還是使用起來麻煩,如果有跳轉頁面之外的邏輯還是免不了各種type區分和定義。除此之外基于Hook的實現跟系統版本有關系,目前只是兼容到Android12版本,如果后期Androd13 14又有修改,那么可能就無法運行了。

總的來說,個人不是很推薦這樣的方案,當然如果大家使用的是定制設備,系統版本是固定的,那么這樣的方案也不是不能用,所以大家需要按需選擇。

來自:https://www.androidos.net.cn/doc/2022/9/1/71.html

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容