源码探索系列29---插件化基础之启动插件的Activity

昨天的那篇简答的描述了如何利用HOOK来劫持Intent事件,从何来控制关于Activity的开启,
在这个基础上,我们需要来看下怎么开启我们的插件中的Activity

起航

目前DroidPlugin采用的是预先注册占坑,预先注册权限的方式。
利用假的Activity来做“运行”真的PluginActivity。

为何要这样做呢?因为我们在看整个Activity的启动源码就有看到,对于没在AndroidManifest.XML文件注册的Activity,人家是会抛下面这个错误:

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?");

我相信刚开始开发的或多或少有遇到过这个bug的了.
因此,作者对这个占坑问题也说了会预注册一堆各种Launch Mode的Activity.然后是Hook了startActivity和handleLaunchActivity


那么,现在我们应该怎么处理这件事呢?从而让我们的插件在不做大的改动下,也可以正常运行呢?
这先让我们来回顾下大概的Activity启动流程:

图盗自老罗哥的博客

当我们调用Actvity的时候,会经过一大段的流程,最后再回到我们的ActivityThread里面的Handle去,在里面启动我们想要的Activity!详情可以看我写的这篇,源码探索系列3—四大金刚之Activity的启动过程完全解析具体就不贴出来,我们挑着说。

在被缩略的整个一大段流程中重要的内容是处于系统进程(system_server)的
AMS会对我们想启动的Activity做个检测(例如上面提到的bug),看是否在我们的AndroidManifes.xml里面有注册了,这样某种程度避免我们程序受到注入攻击。不过由于系统的进程隔离的存在,我们没办法HOOK掉这个系统进程,但这让我们挑选HOOK点显得好找。去掉中间,那就是两头咯!

思路:

因此我们就这样的思路出来了:

当我们要启动一个插件Activity的时候,把这个intent修改下,传一个真的注册了的Activity信息过去给AMS做检测等工作,等处理完回调到我们的ActivityThread去调用启动Activity的时候,我们再Hook,去启动我们最终想启动的!

demo

有了这样的思路,我们来测试下效果如果

我们创建两个Activity,一个叫RealActivity,另外一个叫StubActvity
然后我们我们在AndroidManifes.xml里面只保留StubActvity的注册信息。

<application 
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name" 
    android:theme="@style/AppTheme">

    <activity android:name=".ui.MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>         

    <activity android:name=".ui.StubActivity">  //<-只有Stub没有realActivity
    </activity> 
</application>

准备好这些后,我们在MainActivity里面发 startActivity到StubActivity

public class MainActivity extends Activity { 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);    
        //现在我们要去我们的插件Activity的,类似于这个RealActivity
        startActivity(new Intent(MainActivity.this, RealActivity.class));
    } 
}

这些准备好后,我们来看下怎么HOOK掉两头的内容,让发到AMS的是真的,但到了另外一头Handler里面就是启动plugin的activty。

假认你看了上一篇源码探索系列28—插件化基础之代理Hook,知道如何写HOOK这个startActivity的方法。

那么我们就只重点的看下invoke里面的内容,后续再贴整个项目的参考代码地址

public class IActivityManagerHook implements InvocationHandler {

    private static final String TAG = IActivityManagerHook.class.getSimpleName();
    private Object originalObject;
    public static final String EXTRA_ORIGINAL_INTENT = "EXTRA_INTENT_data_2";

    public IActivityManagerHook(Object originalObject) {
        this.originalObject = originalObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
     throws Throwable {

        Log.e(TAG, "invoke() called with: " + "proxy = [" + " " + method.getName());

        if (method.getName().equals("startActivity")) {//我们的starActivity方法
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {//找到传过来的intent了!

                    Intent ourFakeIntent = new Intent();
                    ComponentName componentName = new ComponentName(
                                        "com.example.administrator.testdemo", 
                                        StubActivity.class.getName());
                    ourFakeIntent.setComponent(componentName);
                    ourFakeIntent.putExtra(EXTRA_ORIGINAL_INTENT, 
                                            (Intent) args[i]);//存真的intent    
                    args[i] = ourFakeIntent;//替换过去给AMS检测    
                    break;
                }
            }

            return method.invoke(originalObject, args);//  <- 送检

        } else {
            return method.invoke(originalObject, args);
        }
    }
}

根据这部分内容,我们之后需要去hook掉回到我们ActivityThread部分的内容!
依据我们前面在看Activity的启动过程的时候,我们知道,最终他会回到ActivityThread的一个名叫mH的变量的H类(H继承Handle.不知为何要搞这么短的名字,记得大学的时候看到一个日本人写的关于C语言的极简编程的书,大量的利用编译器的特性,这家伙不会也是个日本人吧!还是说受到他的影响呢?),接着他会收到一条LAUNCH_ACTIVITY的消息.

private class H extends Handler {
    public static final int LAUNCH_ACTIVITY         = 100;
    ...

    public void handleMessage(Message msg) {         
         switch (msg.what) {
           case LAUNCH_ACTIVITY: {
               Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, 
                                            "activityStart");
               final ActivityClientRecord r = (ActivityClientRecord) msg.obj;    
               r.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo,                                                                                                 
                                                     r.compatInfo);
               handleLaunchActivity(r, null);//启动Activity
               Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
           } break;
    }
}

因此我们可以Hook掉的点就来了。在去调用handleLaunchActivity()前,我们得替换回我们实际的Activty去!这样就可以达到借尸还魂了。那么得怎么做呢?
好在我们以前有看过Handle的源码,知道这个handleMessage是一个之类需要实现的方法,

/**
 * Subclasses must implement this to receive messages.
 */
public void handleMessage(Message msg) { 

 }    

嗯,我记得看DexClassLoader里面的有一个片段代码,就是下面这个

protected Class<?> findClass(String className) throws ClassNotFoundException {
   throw new ClassNotFoundException(className);              

    //我们的子类必须实现他!所以我们就去看下我们的BaseDexClassLoder
    //不要问我为何他不设置抽象,尽管这个类就是抽象类!
}

在里面也有一个需要之类重写的方法,不然就直接抛异常的。
显然这不是同一个人呢写的。风格不一样。

在他前面还有别人调用它,类似下面这样

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

我们看下这个Handle类的这段代码,如果这个消息自带了callback的话,就去调用它自带的,要不然看有没设置mCallback这个属性,没有才轮到我们重写的 handleMessage()部分!!而ActivityThread的H先生是没设置这个的,我也是看源码才发现有这个属性设置的!所以,我们可以在这里做点文章,这是我们去调用我们的真实Activity的地方!

public static void hookHandler() throws Exception {

    // 先获取到当前的ActivityThread对象
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Field currentActivityThreadField = activityThreadClass.getDeclaredField(
                                       "sCurrentActivityThread");                                           
    currentActivityThreadField.setAccessible(true);
    Object currentActivityThread = currentActivityThreadField.get(null);

    //拿我们的H先生
    Field mHField = activityThreadClass.getDeclaredField("mH");
    mHField.setAccessible(true);
    Handler mH = (Handler) mHField.get(currentActivityThread);

    Field mCallBackField = Handler.class.getDeclaredField("mCallback");
    mCallBackField.setAccessible(true);
    //修改它的callback为我们的,从而HOOK掉
    mCallBackField.set(mH, new ActivityThreadHandlerCallback(mH));

}

好了,有这样的铺垫,我们来看下那个ActivityThreadHandlerCallback的内容

class ActivityThreadHandlerCallback implements Handler.Callback {

    private static final String TAG = ActivityThreadHandlerCallback.
                                       class.getSimpleName();
    public static final int LAUNCH_ACTIVITY = 100;    
    Handler mOriginalHandle;    
    public ActivityThreadHandlerCallback(Handler base) {
        mOriginalHandle = base;
    }    

    @Override
    public boolean handleMessage(Message msg) {

        switch (msg.what) {
            case LAUNCH_ACTIVITY: {
                Object obj = msg.obj;
                try {
                    Field intent = obj.getClass().getDeclaredField("intent");
                    intent.setAccessible(true);
                    Intent raw = (Intent) intent.get(obj);
                    Intent target = raw.getParcelableExtra(
                                IActivityManagerHook.EXTRA_ORIGINAL_INTENT);
                    ComponentName component = new ComponentName(
                                            target.getComponent().getPackageName(), 
                                            target.getComponent().getClassName());

                    //替换我们原本送去AMS的stubActivity为真的RealActivity的 intent!
                    raw.setComponent(component);

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

        //替换好后,再用原来的ActivityThread的mH去处理
        //到此大功告成!
        mOriginalHandle.handleMessage(msg);
        return true;
    }

}

之后的生命和周期呢?

我们前面已经成功做到了启动一个没有在AndroidManifest中注册的Activity,那么接下来对这个Activity的别的生命周期的调用还有效吗?
在看Activity的启动过程我们知道,。AMS与ActivityThread之间对于Activity的生命周期的交互,是使用一个Token来标识,而不是直接使用Activity对象来进行操作管理的!而这个token是Binder对象,因此可以跨进程传递。Activity里面有一个成员变量mToken代表的就是它,token唯一地标识一个Activity对象,它在Activity的attach方法里面被初始化;
我们传给AMS的是我们的假Activity,当在AMS处理Activity的任务栈的时候,使用这个token标记我们传过去的Activity,因此实际上AMS进程里面的token对应的是StubActivity。但到了我们的进程里面时候,我们在他去调用哪
handlehandleMessage前,掉包了那个Activity,换成为了我们的
RealActivity,因此token对应的却是RealActivity!
所以ActivityThread执行回调的时候,能正确地回调到RealActivity相应的方法。因此我们可以确认,通过这种方式启动的Activity有它自己完整生命周期!和真的没什么区别!
这样的设计和我们的服务器的token一样,只要偷到这个,我们就可以做中间人攻击,从而让两端被骗的人以为没事,一切正常进行。

AppCompatActivity与Activity

 Caused by: android.content.pm.PackageManager$NameNotFoundException:      
 ComponentInfo{com.example.administrator.testdemo.ui/
 com.example.administrator.testdemo.ui.RealActivity}     
 atandroid.app.ApplicationPackageManager.getActivityInfo(ApplicationPackageManager.
 java:342)
 at android.support.v4.app.NavUtils.getParentActivityName(NavUtils.java:301) 
 at android.support.v4.app.NavUtils.getParentActivityName(NavUtils.java:281)
 atandroid.support.v7.app.AppCompatDelegateImplV7.onCreate(AppCompatDelegateImplV7.
java:152) 
at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:60) 
at com.example.administrator.testdemo.ui.RealActivity.onCreate(RealActivity.java:12) 

写这篇文章的时候,用AS建的模板程序,写完一直是这个bug!看了好久才发现我继承的是
AppCompatActivity而不是Activity!!!!差点哭晕在厕所,这么低级的错误。

后记

在明白了整个插件的Activity启动的原理流程后,还有些细节需要我们去了解的,例如去获取资源时候涉及到用的R部分的内容,有些是编号规则来的,其中也涉及到AssetsManager等内容!再详细看吧

另外我们都知道Activity是有四种启动模式的,我们都需要占坑,因此会在AndroidManifest写下面的内容

<activity
        android:name=".StandardActivity"
        android:label="@string/title_activity_standard" />

<activity
        android:name=".SingleTopActivity"
        android:label="@string/title_activity_single_top"
        android:launchMode="singleTop" />

<activity
        android:name=".SingleTaskActivity"
        android:label="@string/title_activity_single_task"
        android:launchMode="singleTask" />
 <activity
        android:name=".SingleTaskActivity$SingleTaskActivity1"
        android:label="@string/title_activity_single_task1"
        android:launchMode="singleTask" />

  <activity
        android:name=".SingleInstanceActivity"
        android:label="@string/title_activity_single_instance"
        android:launchMode="singleInstance" />
 <activity
        android:name=".SingleInstanceActivity$SingleInstanceActivity1"
        android:label="@string/title_activity_single_instance1"
        android:launchMode="singleInstance" />

为何有这些.SingleInstanceActivity$SingleInstanceActivity1这样的名字的类,应该和混淆有关。

另外不可避免的是在插件会有很多的Activity被新启动,这需要我们根据业务需要来看下最大的数目,从而确定占坑的数目。

另外,这里的那个RealActivity是在同一个包内的,所以我们前面可以用那样的方式去直接写是没问题的。但显然的,插件时候是不会再在同一个包内的,这时候需要一个类似Java的ClassLoader加载类的工具,在Android上就是DexClassLoader啦。这个在上一篇 源码探索系列27—插件化基础之类加载器DexClassLoader 有做过简单的介绍。下次再看怎么修改这部分内容。

热评文章