Android登錄攔截:動態(tài)代理+Hook的實現(xiàn)

一、動態(tài)代理 + Hook 的實現(xiàn)

在之前的文章我們講過插件化的實現(xiàn)有點類似,插件化一般是替換系統(tǒng)的 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類型的靜態(tài)常量,它的get()方法返回的是Singleton類中的private T mInstance ,這個mInstance的創(chuàng)建又是在gDefault實例化時通過create()方法實現(xiàn)。gDefault.get()獲取到的mInstance實例就是ActivityManagerService(AMS)實例。由于gDefault是一個靜態(tài)常量,因此可以通過反射獲取到它的實例,同時它是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系統(tǒng)以下 ,8.0系統(tǒng) - 9.0系統(tǒng),10系統(tǒng) - 12系統(tǒng) 的實現(xiàn)均有差異,需要做一下兼容性處理。我們通過下面的工具類方法實現(xiàn)如何使用反射 + Hook + 動態(tài)代理實現(xiàn)效果:

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

            Field singletonField;
            Class<?> iActivityManager;            // 1,獲取Instrumentation中調(diào)用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;
            }            //開始動態(tài)代理
            Object proxy = Proxy.newProxyInstance(
                    Thread.currentThread().getContextClassLoader(),                    new Class[]{iActivityManager},                    new AmsHookBinderInvocationHandler(mInstance));            //現(xiàn)在替換掉這個對象
            mInstanceField.set(singleton, proxy);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }    //動態(tài)代理執(zhí)行類
    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);                //設(shè)置新的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 {        //啟動動態(tài)代理
         DynamicProxyUtils.hookAms()

        gotoActivity<ProfileDemoActivity>()
    }

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

二、Itent的攔截與處理

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

我們修改動態(tài)代理的回調(diào)方法:

//動態(tài)代理執(zhí)行類


    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())) {                //如果已經(jīng)登錄-直接放行
                if (LoginManager.isLogin()){                    return method.invoke(obj, args);
                }                //如果未登錄-獲取到原始意圖,再替換Intent攜帶數(shù)據(jù)到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);                //設(shè)置新的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 {            //啟動動態(tài)代理
            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) })   //設(shè)置Result

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

                finish()

            }, 500)

        }

總結(jié)

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

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

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

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

優(yōu)點與缺點

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

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

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

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容