基于Android9.0的Hook Activity 的启动(插件化)

前言

本文主要是记录Hook activity的知识点,涉及的内容比较多,读完本文读者将会了解,activity的启动,动态代理,合并Dex文件,动态加载资源等,本文的目的是手写一个简易插件化框架,实现宿主app 启动一个插件中的Activity(没有在Manifest文件中注册的Activity)

activity的启动

9.0 activity的启动和早期的版本稍有不同,不过大体流程都是一样的主要是这样几步骤

  • Launcher进程请求AMS
  • AMS发送创建应用进程请求
  • Zygote进程接受请求并孵化应用进程
  • 应用进程启动ActivityThread

目的

启动一个插件中的Activity,那么我们面临两个问题

  • 插件中的apk是没有安装的
  • 要启动的Activity是没有在Manifest中注册的

分析

当启动一个Activity时候 AMS会检查是否在Manifest中注册,如果没有注册就会抛出异常
跟进源码 startActivity ->startActivityForResult

 public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
          //这里是重点
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            ...省略
    }

从startActivityForResult中可以看出 activity的启动是交给了Instrumentation 的execStartActivity方法,那么进入到对应的类中的这个方法

 public ActivityResult  execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
            .....
      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); //检查Activity 
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
  ......
}

上面代码中通过ActivityManager.getService()得到一个result 然后丢给了checkStartActivityResult方法 ,那么我们看一下这个方法

public static void checkStartActivityResult(int res, Object intent) {
        if (!ActivityManager.isStartResultFatalError(res)) {
            return;
        }

        switch (res) {
            case ActivityManager.START_INTENT_NOT_RESOLVED:
            case ActivityManager.START_CLASS_NOT_FOUND:
                if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
                    throw new ActivityNotFoundException(
                            "Unable to find explicit activity class "
                            + ((Intent)intent).getComponent().toShortString()
                            + "; have you declared this activity in your AndroidManifest.xml?");
                throw new ActivityNotFoundException(
                        "No Activity found to handle " + intent);
}

我们发现了 传进来的res就是上面的result,并且通过判断,会抛出我们熟悉的异常
";have you declared this activity in your AndroidManifest.xml?",假如我们启动没有注册的activity那么返回的这个result 走到这里肯定会抛出异常,于是我们的hook点就来了,当代码走到这里的时候我们需要替换成可以通过检查的activity,那么如何替换呢,我们继续分析这个result是如何产生的,回到execStartActivity 这个方法

 public ActivityResult  execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
            .....
      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); //检查Activity 
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
  ......
}

我们发现activity的启动这个时候交给了ActivityManager,点进去看ActivityManger.getService()

 /**
     * @hide
     */
    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;
                }
            };

这里我在贴一下这个singleton这个类

/**
 * Singleton helper class for lazily initialization.
 *
 * Modeled after frameworks/base/include/utils/Singleton.h
 *
 * @hide
 */
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;
        }
    }
}

我们发现singleton的get方法其实就是调用了 create()方法,也就是说,ActivityManager.getService 返回的是IActivityManager.Stub.asInterface(b); 看到这个应该会很熟悉,这就是AIDL,目的是获得AMS的binder对象,然后就可以发起AMS的远程调用了,也就是说Activity的启动交给了AMS。
熟悉AIDL的盆友们很容易就知道,getService返回的IActivityManager,就是一个接口

ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options)

这里就是拿到接口的对象,然后调用接口的startActivity方法,之后代码会进入AMS的startActivity方法中

//这里是ActivityManagerService中的方法
 @Override
    public final int startActivity(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle bOptions) {
        return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,
                resultWho, requestCode, startFlags, profilerInfo, bOptions,
                UserHandle.getCallingUserId());
    }

后续的我们先不跟进(有兴趣的同学继续跟着源码往下走就可以了),这里看一下这个方法的第三个参数 intent ,我们发现启动activity的时候需要一个intent,那么当我们启动一个没有注册的activity的时候,我们是不是可以把这个intent替换掉,替换成已经注册的Intent,那么怎么替换呢?

替换

我们现在已经知道代理走到了 IACtivityManager.startActivity(参数1,参数2,参数3是intent...),IACtivityManager是一个接口,于是我们想到用动态代理去,去拦截这个接口做hook。要代理这个接口,就需要这个接口的对象,我们发现ActivityManger.getService()刚好返回的就是IActivityManager的对象于是我们就有了如下代码

 val mActivityManagerClass = Class.forName("android.app.ActivityManager")
        val mIActivityManager = mActivityManagerClass.getMethod("getService").invoke(null)

这样就拿到了IActivityManager接口的对象,接下来使用动态代理

//这个mIActivityManagerProxy本质就是IActivityManager
val mIActivityManagerProxy = Proxy.newProxyInstance(

            HookApplication::class.java.classLoader,

            arrayOf(mIActivityManagerClass) // 要监听的接口

        ) { proxy, method, args ->
            // IActivityManager 接口的回调方法
            // public int startActivity(IApplicationThread caller, String callingPackage,
            //            Intent intent, String resolvedType, IBinder resultTo, String resultWho,
            //            int requestCode, int flags, String profileFile,
            //            ParcelFileDescriptor profileFd, Bundle options) throws RemoteException;

            if ("startActivity" == method.name) {//接口中有很多方法,但是我们只处理startActivity
                // 这里偷梁换柱,换了startActivity的方法中的第三个参数
                // 换成 可以 通过 AMS检查的 ProxyActivity
                val intent = Intent(this, ProxyActivity::class.java) //这里ProxyActivity是注册过得
                intent.putExtra("actionIntent", args[2] as Intent)
                args[2] = intent
            }

            Log.d("hook", "拦截到了IActivityManager里面的方法" + method.name)
            // 让系统继续正常往下执行
            method.invoke(mIActivityManager, *args)
        }

这里我们把接口的方法中的参数intent给换掉了,然后得到了一个mIActivityManagerProxy对象,然后我们需要把这个对象,重新赋值给singleton中的instance对象,(这里解释一下,由于原来的代码就是通过getService来获取IActivityManager接口的对象,而这个getService获取的就是singleton中的mInstance字段,所以我们把我们拦截后得到的IActivityManager对象,重新设置给mInstance字段,就达到了替换的效果)

 /**
         * 为了拿到 Singleton的对象
         * 通过 ActivityManager 拿到 IActivityManagerSingleton 变量(对象)
         */

        val mSingletonField = mActivityManagerClass.getDeclaredField("IActivityManagerSingleton")
        mSingletonField.isAccessible = true
        val singletonObj = mSingletonField.get(null)//拿到singleton对象
    
        // 替换点
        val mSingletonClass = Class.forName("android.util.Singleton")
        // 获取此字段 mInstance
        val mInstanceField = mSingletonClass.getDeclaredField("mInstance")
        mInstanceField.isAccessible = true // 让虚拟机不要检测 权限修饰符
        // 替换
        mInstanceField.set(singletonObj, mIActivityManagerProxy) // 替换

这里稍微说一下反射注意的点,反射的关键点就是需要静态变量(看客姥爷们如果看不懂上面的,就复习一下反射)。
下面贴一下完整的代码

/**
     * 要在执行 AMS之前,替换可用的 Activity,替换在AndroidManifest里面配置的Activity
     */
    @Throws(Exception::class)
    private fun hookAmsAction() {
        // 动态代理
        val mIActivityManagerClass = Class.forName("android.app.IActivityManager")

        // 我们要拿到IActivityManager对象,才能让动态代理里面的 invoke 正常执行下
        val mActivityManagerClass = Class.forName("android.app.ActivityManager")
        val mIActivityManager = mActivityManagerClass.getMethod("getService").invoke(null)

        // 本质是IActivityManager
        val mIActivityManagerProxy = Proxy.newProxyInstance(

            HookApplication::class.java.classLoader,

            arrayOf(mIActivityManagerClass) // 要监听的接口

        ) { proxy, method, args ->
            // IActivityManager 接口的回调方法
            // public int startActivity(IApplicationThread caller, String callingPackage,
            //            Intent intent, String resolvedType, IBinder resultTo, String resultWho,
            //            int requestCode, int flags, String profileFile,
            //            ParcelFileDescriptor profileFd, Bundle options) throws RemoteException;

            if ("startActivity" == method.name) {
                // 做自己的业务逻辑
                // 换成 可以 通过 AMS检查的 ProxyActivity
                val intent = Intent(this, ProxyActivity::class.java)
                intent.putExtra("actionIntent", args[2] as Intent)
                args[2] = intent
            }
            
            Log.d("hook", "拦截到了IActivityManager里面的方法" + method.name)
            // 让系统继续正常往下执行
            method.invoke(mIActivityManager, *args)
        }

        /**
         * 为了拿到 Singleton的对象
         * 通过 ActivityManager 拿到 IActivityManagerSingleton 变量(对象)
         */

        val mSingletonField = mActivityManagerClass.getDeclaredField("IActivityManagerSingleton")
        mSingletonField.isAccessible = true
        val singletonObj = mSingletonField.get(null)
        // 替换点
        val mSingletonClass = Class.forName("android.util.Singleton")
        // 获取此字段 mInstance
        val mInstanceField = mSingletonClass.getDeclaredField("mInstance")
        mInstanceField.isAccessible = true // 让虚拟机不要检测 权限修饰符
        // 替换
        mInstanceField.set(singletonObj, mIActivityManagerProxy) // 替换是需要gDefault
    }

把这个方法放到Application的oncreate方法中,我们就可以启动一个没有注册的Activity了

val intent = Intent(this, TestActivity::class.java)
        startActivity(intent)

这里的TestActivity并没有注册,但是程序不会崩溃,他会跳转到ProxyActivity中(想想我们替换的过程)ProxyActivity一定是注册的Activity

if ("startActivity" == method.name) {
                // 做自己的业务逻辑
                // 换成 可以 通过 AMS检查的 ProxyActivity
                val intent = Intent(this, ProxyActivity::class.java)
                intent.putExtra("actionIntent", args[2] as Intent)
                args[2] = intent
            }

到这里我们已经实现了一部分,今天先休息,明天继续写后续,如何把这个ProxyActivity再替换回来,使得它启动的时候跳转到TestActivity中。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容