基于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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 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方法,那么进入到对应的类中的这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 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方法 ,那么我们看一下这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 /**
     * @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这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 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,就是一个接口

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

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

1
2
3
4
5
6
7
8
9
//这里是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的对象于是我们就有了如下代码

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//这个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字段,就达到了替换的效果)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 /**
         * 为了拿到 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) // 替换

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
     * 要在执行 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了

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

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

1
2
3
4
5
6
7
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中。