Android插件化之Hook Activity

插件化:Android插件化技術,可以實現功能模塊的按需加載和動態更新(從服務器上下載),其本質是動態加載未安裝的apk。從而減小apk的大小。其中最主要的就是Activity的插件化技術,主要是通過hook來實現的。因為Activity的啟動是要經過AMS的校驗的,所以就需要對AMS下功夫。

  • Step1: 在宿主工程的AndroidManifest.xml中預先注冊一個沒有任何功能的Activity進行占坑。

  • Step2.:使用占坑Activity繞過AMS驗證:Activity的啟動,實際會調用Instrumentation類的execStartActvity方法,所以可以對其進行hook,將啟動插件Activity的Intent替換成宿主預注冊的插樁Activity,從而繞過ASM的驗證,并將插件Activity的Intent保存起來。

  • Step3: 還原插件Activity:在 AMS 校驗完畢的時候,通過 binder 告知我們的應用啟動相應 activity 的時候,我們將 插件Activity的intent 的信息再取出來,還原。

本文以 API 27 的源碼為基礎分析

Hook 的選擇點:

靜態變量和單例,因為一旦創建對象,它們不容易變化,非常容易定位。

Hook 過程:

1、尋找 Hook 點,原則是靜態變量或者單例對象,盡量 Hook public 的對象和方法。
選擇合適的代理方式,如果是接口可以用動態代理。
2、用代理對象替換原始對象。
3、Android 的 API 版本比較多,方法和類可能不一樣,所以要做好 API 的兼容工作。

Hook AMS

我們先來 mInstrumentation.execStartActivity 方法

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    Uri referrer = target != null ? target.onProvideReferrer() : null;
    if (referrer != null) {
        intent.putExtra(Intent.EXTRA_REFERRER, referrer);
    }
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                ActivityResult result = null;
                if (am.ignoreMatchingSpecificIntents()) {
                    result = am.onStartActivity(intent);
                }
                if (result != null) {
                    am.mHits++;
                    return result;
                } else if (am.match(who, null, intent)) {
                    am.mHits++;
                    if (am.isBlocking()) {
                        return requestCode >= 0 ? am.getResult() : null;
                    }
                    break;
                }
            }
        }
    }
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        int result = ActivityManager.getService()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}


這里我們留意 ActivityManager.getService().startActivity 這個方法

public static IActivityManager getService() {
    return IActivityManagerSingleton.get();
}

private static final Singleton<IActivityManager> IActivityManagerSingleton =
        new Singleton<IActivityManager>() {
            @Override
            protected IActivityManager create() {
                final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                final IActivityManager am = IActivityManager.Stub.asInterface(b);
                return am;
            }
        };



可以看到 IActivityManagerSingleton 是一個單例對象,因此,我們可以 hook 它。

public static void hookAMSAfter26() throws Exception {
    // 第一步:獲取 IActivityManagerSingleton
    Class<?> aClass = Class.forName("android.app.ActivityManager");
    Field declaredField = aClass.getDeclaredField("IActivityManagerSingleton");
    declaredField.setAccessible(true);
    Object value = declaredField.get(null);
    
    Class<?> singletonClz = Class.forName("android.util.Singleton");
    Field instanceField = singletonClz.getDeclaredField("mInstance");
    instanceField.setAccessible(true);
    Object iActivityManagerObject = instanceField.get(value);
    
    // 第二步:獲取我們的代理對象,這里因為 IActivityManager 是接口,我們使用動態代理的方式
    Class<?> iActivity = Class.forName("android.app.IActivityManager");
    InvocationHandler handler = new AMSInvocationHandler(iActivityManagerObject);
    Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new
            Class<?>[]{iActivity}, handler);
    
    // 第三步:偷梁換柱,將我們的 proxy 替換原來的對象
    instanceField.set(value, proxy);

}



public class AMSInvocationHandler implements InvocationHandler {

    private static final String TAG = "AMSInvocationHandler";

    Object iamObject;

    public AMSInvocationHandler(Object iamObject) {
        this.iamObject = iamObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //            Log.e(TAG, method.getName());
        if ("startActivity".equals(method.getName())) {
            Log.i(TAG, "ready to startActivity");
            for (Object object : args) {
                Log.d(TAG, "invoke: object=" + object);
            }
        }
        return method.invoke(iamObject, args);
    }
}


執行以下測試代碼


try {
    HookHelper.hookAMS();
} catch (Exception e) {
    e.printStackTrace();
}
Intent intent = new Intent(this,TestActivity.class);
startActivity(intent);

接下來我們一起來看一下 API 25 Instrumentation 的代碼(自 API 26 開始 ,Instrumentation execStartActivity 方法有所改變)

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    Uri referrer = target != null ? target.onProvideReferrer() : null;
    if (referrer != null) {
        intent.putExtra(Intent.EXTRA_REFERRER, referrer);
    }
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                if (am.match(who, null, intent)) {
                    am.mHits++;
                    if (am.isBlocking()) {
                        return requestCode >= 0 ? am.getResult() : null;
                    }
                    break;
                }
            }
        }
    }
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        int result = ActivityManagerNative.getDefault()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}




可以看到這里啟動 activity 是調用 ActivityManagerNative.getDefault().startActivity 啟動的。

public abstract class ActivityManagerNative extends Binder implements IActivityManager
{
   
    /**
     * Retrieve the system's default/global activity manager.
     */
    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;
        }
    };
    
}

同理我們看到 ActivityManagerNative 的 gDefault 是一個靜態變量,因此,我們可以嘗試 hook gDefault.

public static void hookAmsBefore26() throws Exception {
    // 第一步:獲取 IActivityManagerSingleton
    Class<?> forName = Class.forName("android.app.ActivityManagerNative");
    Field defaultField = forName.getDeclaredField("gDefault");
    defaultField.setAccessible(true);
    Object defaultValue = defaultField.get(null);

    Class<?> forName2 = Class.forName("android.util.Singleton");
    Field instanceField = forName2.getDeclaredField("mInstance");
    instanceField.setAccessible(true);
    Object iActivityManagerObject = instanceField.get(defaultValue);

    // 第二步:獲取我們的代理對象,這里因為 IActivityManager 是接口,我們使用動態代理的方式
    Class<?> iActivity = Class.forName("android.app.IActivityManager");
    InvocationHandler handler = new AMSInvocationHandler(iActivityManagerObject);
    Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivity}, handler);

    // 第三步:偷梁換柱,將我們的 proxy 替換原來的對象
    instanceField.set(defaultValue, proxy);
}

啟動一個沒有在 AndroidManifest 聲明的 Activity

我們知道我們啟動的 activity 信息都儲存在 intent 中,那么我們若想要 啟動一個沒有在 AndroidManifest 聲明的 Activity,那我們只需要在 某個時機,即調用 startActivity 方法之前欺騙 AMS ,我們的 activity 已經注冊(即替換 intent)。

這里我們重新理一下 Activity 大概的啟動流程:
app 調用 startActivity 方法 -> Instrumentation 類通過 ActivityManagerNative 或者 ActivityManager( API 26以后)將啟動請求發送給 AMS -> AMS 進行一系列檢查并將此請求通過 Binder 派發給所屬 app -> app 通過 Binder 收到這個啟動請求 -> ActivityThread 中的實現將收到的請求進行封裝后送入 Handler -> 從 Handler 中取出這個消息,開始 app 本地的 Activity 初始化和啟動邏輯。

public class HookHelper {
    private static final String TAG = "xiaosanye";

    public static final String EXTRA_TARGET_INTENT = "extra_target_intent";

    public static void hookIActivityManager() {
        //TODO:
//        1. 找到了Hook的點
//        2. hook點 動態代理 靜態?
//        3. 獲取到getDefault的IActivityManager原始對象
//        4. 動態代理 準備classloader 接口
//        5  classloader, 獲取當前線程
//        6. 接口 Class.forName("android.app.IActivityManager");
//        7. Proxy.newProxyInstance() 得到一個IActivityManagerProxy
//        8. IActivityManagerProxy融入到framework

        try {
            Field gDefaultField = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                Class<?> activityManager = Class.forName("android.app.ActivityManager");
                gDefaultField = activityManager.getDeclaredField("IActivityManagerSingleton");
            } else {
                Class<?> activityManager = Class.forName("android.app.ActivityManagerNative");
                //拿到 Singleton<IActivityManager> gDefault
                gDefaultField = activityManager.getDeclaredField("gDefault");
            }

            gDefaultField.setAccessible(true);
            //Singlon<IActivityManager>
           //所有靜態對象的反射可以通過傳null獲取。如果是實列必須傳實例
            Object gDefault = gDefaultField.get(null);

            //拿到Singleton的Class對象
            Class<?> singletonClass = Class.forName("android.util.Singleton");
            Field mInstanceField = singletonClass.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            //獲取到ActivityManagerNative里面的gDefault對象里面的原始的IActivityManager對象
            final Object rawIActivityManager = mInstanceField.get(gDefault);

            //進行動態代理
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            Class<?> iActivityManagerInterface = Class.forName("android.app.IActivityManager");
            //生產IActivityManager的代理對象
            Object proxy = Proxy.newProxyInstance(classLoader, new Class[]{iActivityManagerInterface}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Log.i(TAG, "invoke: method " + method.getName());
                    if ("startActivity".equals(method.getName())) {
                        Log.i(TAG, "準備啟動activity");
                        for (Object obj : args) {
                            Log.i(TAG, "invoke: obj= " + obj);
                        }

                        //偷梁換柱 把Target 換成我們的Stub,欺騙AMS的權限驗證
                        //拿到原始的Intent,然后保存
                        Intent raw = null;
                        int index = 0;
                        for (int i = 0; i < args.length; i++) {
                            if (args[i] instanceof Intent) {
                                raw = (Intent) args[i];
                                index = i;
                                break;
                            }
                        }
                        Log.i(TAG, "invoke: raw= " + raw);

                        //替換成Stub
                        Intent newIntent = new Intent();
                        String stubPackage = "com.xiaosanye.activityhookdemo";
                        newIntent.setComponent(new ComponentName(stubPackage, StubActivity.class.getName()));
                        //把這個newIntent放回到args,達到了一個欺騙AMS的目的
                        newIntent.putExtra(EXTRA_TARGET_INTENT, raw);
                        args[index] = newIntent;

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

            //把我們的代理對象融入到framework
            mInstanceField.set(gDefault, proxy);


        } catch (Exception e) {
            Log.e(TAG, "hookIActivityManager: " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * hook ActivityThread 的 mH,還原未注冊的Activity
     */
    public static void hookHandler() {
        //TODO:
        try {
            Class<?> atClass = Class.forName("android.app.ActivityThread");
            Field sCurrentActivityThreadField = atClass.getDeclaredField("sCurrentActivityThread");
            sCurrentActivityThreadField.setAccessible(true);
            Object sCurrentActivityThread = sCurrentActivityThreadField.get(null);
            //ActivityThread 一個app進程 只有一個,獲取它的mH
            Field mHField = atClass.getDeclaredField("mH");
            mHField.setAccessible(true);
            final Handler mH = (Handler) mHField.get(sCurrentActivityThread);

            //獲取mCallback
            Field mCallbackField = Handler.class.getDeclaredField("mCallback");
            mCallbackField.setAccessible(true);

            mCallbackField.set(mH, new Handler.Callback() {

                @Override
                public boolean handleMessage(Message msg) {
                    Log.i(TAG, "handleMessage: " + msg.what);
                    switch (msg.what) {
                        case 100: {
                            // final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
//                            static final class ActivityClientRecord {
//                                IBinder token;
//                                int ident;
//                                Intent intent;//hook 恢復
                            //恢復真身
                            try {
                                Field intentField = msg.obj.getClass().getDeclaredField("intent");
                                intentField.setAccessible(true);
                                Intent intent = (Intent) intentField.get(msg.obj);
                                Intent targetIntent = intent.getParcelableExtra(EXTRA_TARGET_INTENT);
                                intent.setComponent(targetIntent.getComponent());

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

                        }
                        break;
                        case 159: {
                            Object obj = msg.obj;
                            Log.i(TAG, "handleMessage: obj=" + obj);
                            try {
                                Field mActivityCallbacksField = obj.getClass().getDeclaredField("mActivityCallbacks");
                                mActivityCallbacksField.setAccessible(true);
                                List mActivityCallbacks = (List)mActivityCallbacksField.get(obj);
                                Log.i(TAG, "handleMessage: mActivityCallbacks= " + mActivityCallbacks);
                                if(mActivityCallbacks.size()>0){
                                    Log.i(TAG, "handleMessage: size= " + mActivityCallbacks.size());
                                    String className = "android.app.servertransaction.LaunchActivityItem";
                                    if(mActivityCallbacks.get(0).getClass().getCanonicalName().equals(className)){
                                        Object object = mActivityCallbacks.get(0);
                                        Field intentField =object.getClass().getDeclaredField("mIntent");
                                        intentField.setAccessible(true);
                                        Intent intent = (Intent) intentField.get(object);
                                        Intent targetIntent = intent.getParcelableExtra(EXTRA_TARGET_INTENT);
                                        intent.setComponent(targetIntent.getComponent());
                                    }
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }

                        }
                        break;
                    }
                    mH.handleMessage(msg);
                    return true;
                }
            });

        } catch (Exception e) {
            Log.e(TAG, "hookHandler: " + e.getMessage());
            e.printStackTrace();
        }
    }

}

 public void onBtnHookClicked() {
        HookHelper.hookIActivityManager();
        HookHelper.hookHandler();
        Intent intent = new Intent(this,TestActivity.class);
        startActivity(intent);
    }

運行以上代碼,可以看到我們可以正常啟動沒有在 AndroidManifest 的 activity

小結

啟動沒有在 AndroidManifest 注冊的 Activity 可以分為連個步驟

  • 1、在 AMS 通過 intent 校驗 activity 是否注冊的時候,用已經在 AndroidManifet 注冊的 Activity 欺騙 AMS,繞過 原有 activity 的校驗,并將原有的 intent 信息儲存起來
  • 2、在 AMS 校驗完畢的時候,通過 binder 告知我們的應用啟動相應 activity 的時候,我們將 intent 的信息取出來,還原。

參考文章:https://blog.csdn.net/gdutxiaoxu/article/details/81459910

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念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