插件化-Activity实现

1.插件化

关于插件化的原理和插件化框架之前的优缺点对比,已经有很多的文章,这里不再赘述。

2.Activity的启动

APK在安装的时候,PMS(PackageManagerService)解析Apk中的AndroidManifest.xml文件,根据Apk包路径创建一个对应的资源管理器对象AssetManager,通过该对象来访问Apk包中的资源信息,包括AndroidManifest.xml文件。
Activity的启动由AMS管理,Launcher通知AMS启动Activity,AMS通过PMS去查是否存在该Activity,若不存在,则停止了。若存在,再判断是否已经创建了进程。若未创建,则请求Zygote创建应用进程,然后启动新的进程,在进程中创建ActivityThread对象,执行其中的main函数方法,这里会创建启动主线程,再通知AMS,传入applicationThread以便通讯,AMS再通知应用启动Application,创建启动Activity。

3.插桩Activity

要启动一个Activity必须在AndroidManifest.xml中注册,如果我们要启动一个新的Activity,则必须先在发布的APK内占坑注册。这样,在启动Activity的时候,可以通过AMS的校验。然后将插件里的新Activity来代替之前占坑的Activity,去执行代码。
那么问题来了,hook技术应该hook什么,才能替换Activity。这考验对Android系统工作原理的熟悉程度。
此外,为了保证hook的稳定性,hook点一般找不容易变化的对象,比如单例、静态变量。

3.1Activity的启动分析

启动Activity调用startActivity,最终会调用startActivityForResult。
在内部调用Instrumentation的execStartActivity方法.
Instrumentation:用户监控应用程序与系统的交互。


Activity的startActivityForResult.png

Instrumentation的startActivityForResult在Android的7.0和8.0中代码逻辑是不同的。先看Android7.0。

Android7.0

启动Activity需先通过AMS的校验,则需要获取AMS对象。Android7.0通过
ActivityManagerNative的getDefault来获取AMS的代理对象。


image.png

getDefault通过ServiceManager得到“activity”的Service引用,也就是IBinder类型的AMS的引用。


image.png

将其封装成ActivityManagerProxy类型对象。它用来与AMS进行进程间通信。
image.png

getDefault借助Singleton类来实现单例,且它又是静态的,因此hook IActivityManager。
Android8.0

ActivityManager的getService方法,该方法得到名为“activity的”Servcie的引用,也是IBinder类型,将其转化成IActivityManager类型对象,这里使用了AIDL的方式,IActivityManager类是由AIDL工具在编译时自动生成,与AMS通信,只要集成IActivityManager.Stub并实现对象的方法就可以。因此IActivityManager就是AMS在本地的代理。因此startActivityAsUser实际上调用的是AMS的方法。


Instrumentation的startActivityForResult.png

IActivityManager借助Singleton类实现单例,因此也选择IActivityManager为hook点。


android8.0.png

4.Hook Activity的具体实现(Kotlin)

先写一个插桩Activity(OldActivity),在AndroidManifest内注册。


image.png

再写一个新的Activity(PluginActivity),不注册。
最后写一个测试类AcTestActivity,点击启动PluginActivity。


image.png

下面开始写核心代码
IActivityManager的代理类:
IActivityManagerProxy继承自InvocationHandler

class IActivityManagerProxy :InvocationHandler {
    var mActivityManager:Any

    companion object {
        private val mMethod:String="startActivity"
        private val OLD_INTENT="OLD_ACTIVITY_INTENT"
    }

    constructor(activityManager: Any){
        this.mActivityManager=activityManager
    }
    @Throws(Throwable::class)
    override fun invoke(proxy: Any?, method: Method?, args: Array<Any>): Any ?{
        //判断是否为"startActivity"方法
        if (mMethod == method?.name){
            val intent:Intent
            var index=0
            //获取intent
            for (i in args.indices){
                if (args[i] is Intent){
                    index=i
                    break
                }
            }
            intent=args[index] as Intent
            //替换intent类要启动的Activity为在AndroidManifest内已经注册的Activity
            val newIntent=Intent()
            val packageName=BuildConfig.APPLICATION_ID
            //指定插桩Activity
            val  componentName=ComponentName(packageName,OldActivity::javaClass.name)
            newIntent.component = componentName
            //获取到的intent先保存到新的intent内,后面会用到。
            newIntent.putExtra(OLD_INTENT,intent)
            //将新的intent赋值
            args[index]=newIntent
        }
        return method?.invoke(mActivityManager,*(args))
    }
}

利用反射,将系统原定的IActivityManager对象替换成我们自定义的代理类。

class HookUtil {

    companion object {
        @Throws(Throwable::class)
        fun hookAMS(){
            var  defaultSingleton:Any
            if (Build.VERSION.SDK_INT>=26){
                //android8.0以上,获取hook的IActivityManagerSingleton字段
                val  activityManageClass=Class.forName("android.app.ActivityManager")
                val filed=activityManageClass.getDeclaredField("IActivityManagerSingleton")
                filed.isAccessible=true
                defaultSingleton= filed.get(null)
            }else{
                //android 7.0以下,获取hook的gDefault字段
                val  activityManagerClass=Class.forName("android.app.ActivityManagerNative")
                val  filed=activityManagerClass.getDeclaredField("gDefault")
                filed.isAccessible=true
                defaultSingleton=filed.get(null)
            }
            //获取Singleton中的即将被替换的字段的值--mInstance
            val  singletonClass=Class.forName("android.util.Singleton")
            val  singletonClassFiled=singletonClass.getDeclaredField("mInstance")
            singletonClassFiled.isAccessible=true
            //通过反射,得到即将被代理的对象(即mInstance的实例)--iActivityManager
            val  iActivityManager=singletonClassFiled.get(defaultSingleton)
            //创建代理类对象,该对象持有被代理的对象 :IActivityManagerProxy(iActivityManager)
            // 我们可以在IActivityManagerProxy做一些自定义的操作,其内部持有原来的iActivityManager,
            val  iActivityManagerClass=Class.forName("android.app.IActivityManager")
            val interfaces= arrayOf(iActivityManagerClass)

            val  proxy=Proxy.newProxyInstance(Thread.currentThread().contextClassLoader, interfaces,
                    IActivityManagerProxy(iActivityManager)) as Any
            //用创建的代理类proxy对象来替换Singleton中的mInstance字段原来的对象
            singletonClassFiled.set(defaultSingleton,proxy)

        }
    }

}

自定义Application类(记得替换AndroidManifest内的Application)

class ActivityApplication: Application() {
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        try {
            HookUtil.hookAMS()
        } catch (e:Exception){
            e.printStackTrace()
        }

    }
}

最后点击AcTestActivity内的按钮,启动一个未注册的PluginActivity最终会启动插桩OldActivity。那么就成功“骗过”AMS启动了一个已注册的Activity。

启动新的Activity

上面讲到启动了已注册的Activity,但整个是空的,我们真正想启动的是新Activity而不是插桩Activity。那么这就要找到真正启动Activity的位置,对其进行替换改造。我们知道通过AMS校验后,AMS会通知应用可以启动Activity。AMS支持ApplicationThread的IBinder类IApplicationThread对象,用来进程间通信。
ApplicationThread的scheduleLaunchActivity方法,会调用ActivityThread的sendMessage(H.LAUNCH_ACTIVITY, r)方法。H是Activity的内部类,继承自Handler,是应用程序进程中主线程的消息管理类。Activity的生命周期都是在主线程中执行,所以这里会通过H来切换。


H handler处理Activity的生命周期.png

其中handleLaunchActivity方法最终会调到Activity的onCreate方法。
我们将H的handleMessage的msg进行替换,让其携带的消息为新的Activity。(即从代理类IActivityManagerProxy保存的intent取出来。还记得之前我们将intent内的Activity做了一次替换吗?现在再换回来!)

====未完,待续~~

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

推荐阅读更多精彩内容