13

VirtualAPP框架原理解析

 4 years ago
source link: https://zhuanlan.zhihu.com/p/151010577
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

VirtualAPP框架原理解析

58同城 Android工程师

为了方便大家理解整个VirutlAPP框架实现的原理及双开APP的Activity是如何启动起来的,文本会着重讲解关键点,对于细节会在后续的文章中分析补充。

先上一张VirtualAPP启动Activity过程时序图

下面对上幅图进行讲解:

1.VA启动后会创建三个类型的进程

io.virtualapp:vapp应用启动的进程,该进程可以理解为Launcher。

io.virtualapp:pN 启动目标应用的进程,我们每点击一个双开应用都会为其创建一个对应的:pN子进程

io.virtualapp:x 虚拟服务子进程,双开应用没有直接与真实系统服务交互,而是与虚拟服务进行交互。虚拟服务实现了真实系统服务的能力。

2.Hook服务代理 替换为 自定义的系统服务代理

:pN进程的目标应用 Hook了所有的系统服务代理,应用进程没有直接与系统服务通信,而是与:x进程的虚拟服务进行通信。与H实例注入callback对象实现msg消息拦截,进行数据替换。可查看InvocationStubManager类

if (VirtualCore.get().isVAppProcess()) {
			addInjector(new LibCoreStub());
			addInjector(new ActivityManagerStub());
			addInjector(new PackageManagerStub());
			addInjector(HCallbackStub.getDefault());
			addInjector(new ISmsStub());
			addInjector(new ISubStub());
			addInjector(new DropBoxManagerStub());
			addInjector(new NotificationManagerStub());
			addInjector(new LocationManagerStub());
			addInjector(new WindowManagerStub());
			addInjector(new ClipBoardStub());
			addInjector(new MountServiceStub());

3. :x进程实现虚拟系统服务

VirtualApp中运行的apk访问系统服务,均为VirtualApp在:x进程模拟的系统服务。上述步骤2中Hook了服务代理后,服务代理的实现会访问:x进程提供服务bunder代理对象.可查看BinderProvider。

public final class BinderProvider extends ContentProvider {
    private final ServiceFetcher mServiceFetcher = new ServiceFetcher();
    @Override
    public boolean onCreate() {       
     IPCBus.register(IPackageManager.class,VPackageManagerService.get());
     IPCBus.register(IActivityManager.class,VActivityManagerService.get());
     IPCBus.register(IUserManager.class,VUserManagerService.get());
     IPCBus.register(IAppManager.class,VAppManagerService.get());

4. AMS服务代理 ActivityInfo信息获取

我们Hook了ASM服务代理,所以每次我们在启动Activity的时候都会获取目标应该的ActivityInfo数据,获取ActvityInfo数据这一个关键步骤。可查看MethodProxies

public Object call(Object who, Method method, Object... args) throws Throwable {
     //获取ActivityInfo信息
     ActivityInfo activityInfo = VirtualCore.get().resolveActivityInfo(intent, userId);
     //调用虚拟服务代理启动Activity
     int res = VActivityManager.get().startActivity(intent, activityInfo, resultTo, options, resultWho, requestCode, VUserHandle.myUserId());

      

5. Activity栈管理

VA采用的也是通过占坑的方式,因为我们的双开应用并没有真实的安装在系统上,所以需要采用占坑的方式,系统启动的是StubActivity,在应用进程的H的Callback进行替换为具体的Activity。这一替换对系统服务是没有感知的,应为系统是通过Binder token来识别某一个Activity的。另外我们可以发现VA为每个应用配置了一个标准启动模式,亲和度为com.lody.virtual.vt的StubActivity,与DroidPlugin不同不需要配置多个启动模式的Activity。这主要依赖于VAMS的栈管理和Intent Flag的配合。下段为大家详细介绍

<activity
            android:name="com.lody.virtual.client.stub.StubActivity$C15"
            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
            android:process=":p15"
            android:taskAffinity="com.lody.virtual.vt"
            android:theme="@style/VATheme" />
   <activity
            android:name="com.lody.virtual.client.stub.StubActivity$C16"
            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
            android:process=":p16"
            android:taskAffinity="com.lody.virtual.vt"
            android:theme="@style/VATheme" />

6. 目标应用进程启动与intent数据替换

我们在跳转到:pN目标应用进程需要将目标应用进程提前启动起来。

  <provider
            android:name="com.lody.virtual.client.stub.StubContentProvider$C9"
            android:authorities="${applicationId}.virtual_stub_9"
            android:exported="false"
            android:process=":p9" />

VAMS记录了启动的进程列表数据,这样就可以知道新启动的APP应该是:pN 进程。

        ProcessRecord app = mProcessNames.get(processName, uid); //是否启动过
        if (app != null && app.client.asBinder().isBinderAlive()) {
            return app;
        }
        int vpid = queryFreeStubProcessLocked();
        if (vpid == -1) {
            return null;
        }
        app = performStartProcessLocked(uid, vpid, info, processName); 
      //启动对应的进程StubContentProvider
        if (app != null) {
            app.pkgList.add(info.packageName);
        }

进程启动之后我们就可以根据targetApp.vpid替换对应的StubActivity

 Intent targetIntent = new Intent();
 targetIntent.setClassName(VirtualCore.get().getHostPkg(), fetchStubActivity(targetApp.vpid, info));
  //intent中添加额为数据方便替换回来
  ComponentName component = intent.getComponent();
  if (component == null) {
            component = ComponentUtils.toComponentName(info);
    }
   targetIntent.setType(component.flattenToString());
   StubActivityRecord saveInstance = new StubActivityRecord(intent, info,
                sourceRecord != null ? sourceRecord.component : null, userId);
   aveInstance.saveToIntent(targetIntent);

7. HCallbackStub 通过intent数据将StubActivityRecord数据替换回来

:PN进程的目标应用户对H实例添加callback(HCallbackStub),这样我们就可以对消息进行处理

 public void inject() throws Throwable {
            otherCallback = getHCallback();
            mirror.android.os.Handler.mCallback.set(getH(), this);
 }

callback会拦截LAUNCH_ACTIVITY消息

private boolean handleLaunchActivity(Message msg) {
            Object r = msg.obj;
            Intent stubIntent = ActivityThread.ActivityClientRecord.intent.get(r);
            StubActivityRecord saveInstance = new StubActivityRecord(stubIntent);
            if (saveInstance.intent == null) {
                return true;
            }
            //获取真实的intent数据与ActivityInfo数据
            Intent intent = saveInstance.intent; 
            ActivityInfo info = saveInstance.info;

            //AcitivityClientRecord中intent数据与AcitivityInfo替换,之后交由H实例处理。
            ActivityThread.ActivityClientRecord.intent.set(r, intent);
            ActivityThread.ActivityClientRecord.activityInfo.set(r, info);

有同学可能就会疑问了,替换之后Activity就可以启动了吗?calssLoder和资源之类的是如何加载的呀?不用classLoader dex注入?LoadAPK对象创建与注入?不用AssetManger.addPath吗?下面为大家解答。

上述步骤执行完成之后会调用系统正常的handleLaunchActivity方法(参考android 7.0代码

 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, "LAUNCH_ACTIVITY");

之后会调用performLaunchActivity方法传递ActivityClientRecord与customIntent 而customIntent为null.所以Activity实例化过程ActivityClientRecord是关键点。

  private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) {  //r.packgeInfo会为空所以为创建新的LoadAPK对象
            //aInfo.applicationInfo对象存储的是插件APK
            // ApplicationInfo数据包含代码路径资源路径等
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        }
        ComponentName component = r.intent.getComponent();
        if (component == null) {
            component = r.intent.resolveActivity(
                mInitialApplication.getPackageManager());
            r.intent.setComponent(component);
        }

        if (r.activityInfo.targetActivity != null) {
            component = new ComponentName(r.activityInfo.packageName,
                    r.activityInfo.targetActivity);
        }

        Activity activity = null;
        try {
            //使用插件的LoadAPK对象获取对应的classloder
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);

上述步骤由于我们已经替换为了插件的ActivityInfo信息,所以创建的LoadAPK对象为插件的apk内存实例对象,获取的classLoader对象也是指定了插件的代码路径与so路径等,所以使用这个classLoader就可以创建目标Activity了。

下面是资源的Resources创建过程

Activity 在ContextImpl对象创建是会创建Resources对象。

 private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
        int displayId = Display.DEFAULT_DISPLAY;
        try {
            displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }

        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, r.token, displayId, r.overrideConfig);

创建Resources对象同样是使用r.packageInfo对象参数进行初始化,当然传递的均为插件apk资源路径数据。

 Resources resources = packageInfo.getResources(mainThread);
        if (resources != null) {
            if (displayId != Display.DEFAULT_DISPLAY
                    || overrideConfiguration != null
                    || (compatInfo != null && compatInfo.applicationScale
                            != resources.getCompatibilityInfo().applicationScale)) {

                if (container != null) {
                    // This is a nested Context, so it can't be a base Activity context.
                    // Just create a regular Resources object associated with the Activity.
                    resources = mResourcesManager.getResources(
                            activityToken,
                            packageInfo.getResDir(),
                            packageInfo.getSplitResDirs(),
                            packageInfo.getOverlayDirs(),
                            packageInfo.getApplicationInfo().sharedLibraryFiles,
                            displayId,
                            overrideConfiguration,
                            compatInfo,
                            packageInfo.getClassLoader());

上述整个过程通过替换ActivityClientRecord的activityInfo的applicationInfo对象为插件的applicationInfo信息以方便创建插件的LoadAPK对象以方便对类和资源进行加载。而ActivityInfo的替换是通过HOOK AMSProxy实现的。可见MethodProxies类的StartActivity方法。

    ActivityInfo activityInfo = VirtualCore.get().resolveActivityInfo(intent, userId);
            
    int res = VActivityManager.get().startActivity(intent, activityInfo, resultTo, 

我们对Activity的启动流程有了整体上的认识。下面我们详细讲解一下VAMS对Activity栈的管理。如何做到一个StubActivity实现各种启动模式的。

VAMS对Activity栈的管理

我们打开两个应用后,通过adb shell dumpsys activity activities 命令可以看出对应两个task,taskid分别为146,114。对于双开应用的Task可以看出系统认为启动的Activity为StubActivity

下面说下实现原理:首先VAMS维护了一个应用的栈的列表如下图所示

以singleTop的模式启动B为例,会怎么做?首先我们会查一下当前栈有没有对应的Task

然后看看栈顶是不是想要的Activity如果是调用onNewIntent方法,如果不是则创建新的实例。

 reuseTask = findTaskByAffinityLocked(userId, affinity);
       if (reuseTask == null) {
            startActivityInNewTaskLocked(userId, intent, info, options);
        } else {
                // Target activity is on top
                if (topRecord != null && !topRecord.marked && topRecord.component.equals(intent.getComponent())) {
                    deliverNewIntentLocked(sourceRecord, topRecord, intent);
                    delivered = true;
                }

通过Flag启动对应的Activity在新的任务栈

Android 5.0引入了 document-centric 模式,可以让Activity在Recent Screen中以多个Task的样式出现。实现也很简单,只需要在startActivity的Intent中加入相应的属性即可。
FLAG_ACTIVITY_NEW_DOCUMENT:如果在startActivity的时候,带上这个参数,系统会寻找所有的Task来查找是否已经存在了需要启动的Activity。如果没有找到,那么被启动的Activity将会被放入一个新的Task中;如果找到了要启动的Activity,则会把该Activity的Task移到前台,并调用该Activity的onNewIntent()方法[这个说法是官方文档里面说的,但是我自己写demo验证,并不会调用onNewIntent,只是单纯的把对应的Task移动到前台。所以最好保证这种Activity是单独存在一个Task中,不然跳转逻辑就会有问题]。
FLAG_ACTIVITY_MULTIPLE_TASK:如果intent带有FLAG_ACTIVITY_NEW_DOCUMENT的同时也添加了FLAG_ACTIVITY_MULTIPLE_TASK,那么系统会每一次都创建一个新的Task来存放要启动的Activity。

private void startActivityInNewTaskLocked(int userId, Intent intent, ActivityInfo info, Bundle options) {
        Intent destIntent = startActivityProcess(userId, null, intent, info);
        if (destIntent != null) {
            destIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            destIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
            destIntent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);

            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                // noinspection deprecation
                destIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
            } else {
                destIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                VirtualCore.get().getContext().startActivity(destIntent, options);
            } else {
                VirtualCore.get().getContext().startActivity(destIntent);
            }
        }
    }

Taskid建立关联关系

通过Intent.Flag的方式启动Activity,到新的Activity栈,但是VAMS并不知道在哪个taskID中,所以在HCallbackStub中会调用VAMS的onActivityCreated方法,传给VAMS当前的Activity在哪个taskid,并创建对应的ActivityRecord建立对应关系。

void onActivityCreated(ProcessRecord targetApp, ComponentName component, ComponentName caller, IBinder token,
                           Intent taskRoot, String affinity, int taskId, int launchMode, int flags) {
        synchronized (mHistory) {
            optimizeTasksLocked();
            TaskRecord task = mHistory.get(taskId);
            if (task == null) {
                task = new TaskRecord(taskId, targetApp.userId, affinity, taskRoot);
                mHistory.put(taskId, task);
            }
            ActivityRecord record = new ActivityRecord(task, component, caller, token, targetApp.userId, targetApp,
                    launchMode, flags, affinity);
            synchronized (task.activities) {
                task.activities.add(record);
            }
        }
    }

Activity 启动模式_KKkeep的专栏-CSDN博客_flag_activity_new_document和flag_activity_new_task应

https://zhuanlan.zhihu.com/p/60870047

http://rk700.github.io/2017/03/15/virtualapp-basic/
https://github.com/prife/VirtualAppDoc
https://m.qq.com/security_lab/news_detail_435.html

http://weishu.me/2016/04/05/understand-plugin-framework-classloader/

https://www.jianshu.com/p/329a9eba3a79


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK