VirtualApp源码解析

avatar
@字节跳动

作者:字节游戏中台客户端团队 - 聂伟

背景

VirtualApp(以下简称VA)是一款运行在android系统中的沙盒(或者叫轻量andorid虚拟机)产品。

项目地址:github.com/asLody/Virt…

VA相关的功能可以通过github介绍来了解,我们本篇主要就VA的实现原理来分析,源码虽然枯燥,却有很多闪光点值得我们学习。

运行机制

首先来看看VA环境下的运作方式(引自网图):

VA一共会运行在三种进程之中:

  1. 宿主进程,即VA自身的主进程,子进程
  2. Client App进程,即在VA中运行的各种App进程
  3. VA Server进程

通过上图可以看到在Client App进程和原生framework services的通讯过程中,添加了新的一层VA Server,通过V+原service的命名方式新增了VAMS,VPMS等VA service,他们仿造了原framework的部分功能,管理Client App各种会话,自身再和原生framework services通讯。

此外我们还能看到,光AM示例,就有原生AM,mirror.AM,VAM三种,暂时只需要知道他们的作用是hook Client App的各种方法,替换方法参数(包名,uid),或引导方法调用到VA Server中。

这里VA Server端所有的service集中由ServiceFetcher提供,Client端通过Provider.call的跨进程方式获取IServiceFetcher的IBinder句柄,再通过其获取其他Service来完成调用。

为什么要设计中间的这层调用呢?我们用Activity举例说明:

看过Activity源码的同学知道,当我们启动一个新的Activity时,它会先经过AMS,AMS会有一些record记录注册,目标进程是否需要创建,启动模式flags等的处理,最终再回调回ActivityThread,调用Instrumentation的newActivity方法完成创建并执行其OnCreate生命周期。

但是我们通过VA运行的Client App,包名/uid和VA宿主是不一致的(并且没有注册在宿主清单文件中),那么直接启动就会受到系统服务的校验,VA通过预先注册StubActivity(包名就是宿主),然后hook Client App所有关联启动的方法,将方法中的参数(这里指启动intent)替换为宿主,对于AMS的感知就只知道启动了一个宿主的StubActivity,最终VA再拦截mH这个Handler的启动消息,将intent替换为原版intent,完成偷梁换柱的过程。

而多进程的框架设计,可以让子进程的crash不影响宿主进程的运行。

当然,大体原理是如此,实际情况会更复杂一些,接下来我们看看代码包结构:

代码包结构

  1. android:通过建立和android相同的目录来引用一些系统隐藏类,达到欺骗编译器的目的,如android.content.pm.PackageParser。
  2. client:Client App所运行的环境,包含大量的hook代码。
  3. server:VA Server进程相关,仿造了原生framework services部分功能。
  4. mirror:android系统类的镜像包,和系统类同名,封装了反射过程,可以很便捷地直接调用系统类的一些字段和方法。
  5. jni:native hook相关,主要是虚拟机的hook和io重定向(将Client App中的io路径重定向到VA内部)。

其他如os是处理一些环境问题以及多用户的管理,remote是用于aidl传输的各种序列化bean。

从运行机制和代码包结构中,可以看到VA基本的轮廓,下面我们逐一进行分析。

源码分析

mirror

假设我们想hook掉Instrumentation,首先来看看普通的hook实现:

然后是mirror实现:

可以看到普通的实现方式不仅繁琐,而且有大量的模板代码,重复工作,而mirror大大简化了这个过程,仿佛就像是在直接调用系统方法一样。

我们看看mirror下的ActivityThread定义:

所有使用的原ActivityThread的相关成员变量/方法,都以:

public static Ref/RefStatic+属性类型 同名成员变量名/方法名

的方式声明在了mirror.ActivityThread当中,并且在RefClass.load方法中传入当前Class对象和真正的className:

load方法封装了反射获取字段的逻辑,映射同名的属性到mirror类下(正如mirror的英译:镜像),其中Ref/RefStatic+属性类型内部也有同名filed的赋值逻辑,不再赘述。

mirror以简洁而又优雅的设计大大简化了反射注入的成本,使用过程中只需要声明属性即可,而不需要关心其反射细节,使用简单,原理简单。

Java层hook

运行机制有说到,VA会hook原生的AM,来截断部分方法调用到VAM,VAM再去调用自实现的VAMS。我们简单以startActivity方法示例:

原生AM中持有AMS的IBinder句柄,可以从这点入手,创建其动态代理对象:

public class ActivityManagerStub extends MethodInvocationProxy<MethodInvocationStub<IInterface>> {



    public ActivityManagerStub() {

        //基于mirror获取AMS的IActivityManager接口对象,并创建其动态代理对象

        super(new MethodInvocationStub<>(ActivityManagerNative.getDefault.call()));

    }



    @Override

    public void inject() throws Throwable {

        if (BuildCompat.isOreo()) {

            //8.x以上

            Object singleton = ActivityManager.IActivityManagerSingleton.get();

            //替换原AMS对象为我们的动态代理对象

            Singleton.mInstance.set(singleton, getInvocationStub().getProxyInterface());

        } else {

           //8.x以下通过ActivityManagerNative.gDefault.get()获取

        }

    }



    @Override

    protected void onBindMethods() {

        //可以批量添加各种hook方法

        addMethodProxy(new StartActivity());

    }



    //方法hook实现

    static class StartActivity extends MethodProxy {

        @Override

        public String getMethodName() {

            return "startActivity";

        }



        @Override

        public Object call(Object who, Method method, Object... args) throws Throwable {

            //可以对参数进行处理

            int res = VActivityManager.get().startActivity(args);

            return res;

        }

    }

}

最后实例化此Stub类,并执行inject即可,startActivity方法在执行时会引导其调用到VA的VAM中。

VA将方法封装成MethodProxy对象,我们可以很方便的通过方法名来定义各种hook方法,处理方法参数,然后在onBindMethods进行批量的方法添加。

在Stub的基类中又封装了动态代理对象的创建过程,并管理了我们所有添加的MethodProxy,在创建动态代理对象的InvocationHandler中决定执行hook方法还是执行原方法(只会hook相关逻辑的方法,其他方法会正常执行原逻辑)。

最终,通过inject子类实现,真正实现动态代理对象的替换。

我们通过代码结构可以看到java层hook的全貌:

  • VA中实现了大量了Stub来hook各种系统服务(proxies包),并最终添加到InvocationStubManager中统一进行inject调用。
  • 很多场景下的方法只需要替换固定位置的参数(如包名,uid),VA为此提供了数个可以快捷hook方法的衍生类,如ReplaceCallingPkgXXX,只需要简单继承即可。
  • 封装的思路并不难理解,难点在于如何找到hook点(并且还存在android多版本差异的兼容问题),VA几乎接管了整个framework层,任何没有hook到的地方都有可能引发crash,这也是其稳定性比较难做的原因之一。

我们通过下面的图,可以简单了解到大部分系统服务hook所需要做的工作(大多方法只需要替换包名即可):

native层hook

native hook主要分为PLT hook和inline hook两种主流实现方式,VA开源代码中使用的是inline hook类型的开源三方库Cydia Substrate,而在后续的商业版代码中,VA也更新了whale(罗迪自研)和sandHook(目前也归罗盒所有)等native hook框架。

为什么需要native hook?这里以io重定向为例:

所有运行在VA中的Client App(包名即宿主)中对于文件/sp进行读写操作时,所指向的目录都是原本包名下的目录,而android系统对于访问非自身包名做了限制(比如android11的强制分区存储),因此需要把这些目录重定向到VA内部,同时也利用了Client App自身的包名作为父级目录,来达到各App之间文件数据隔离的目的。

而对于android系统来说,文件的读写操作最终都是通过libc.so库函数提供的方法,因此VA需要hook libc.so库函数,修改相关函数的输入参数,我们直接看具体的逻辑:

该方法会在Clint App初始化其application时调用,通过redirectDirectory来添加重定向前后的目录,这里可以看到一部分是data/data为首的原始目录,还有一部分是外置存储的各种目录(VA完全按照其规则在内部也创建了相同的目录结构)。

然后通过NativeEngine.enableIORedirect()来调用到IOUniformer.cpp中的startUniformer方法:

可以看下首个方法faccessat的替换方法声明:

HOOK_DEF会给该方法添加new_前缀,而HOOK_SYMBOL最终宏替换为hook_function,使用MSHookFunction函数来调用三方库Cydia Substrate的hook能力。

当系统执行faccessat时,会被hook到我们声明的new_faccessat方法内,该方法最终完成了替换目录参数的功能,而为何会执行new_faccessat正是Cydia Substrate所做的事情。

因此可以看到这里的核心技术竞争力在于hook框架,剩下的都是利用框架所做的封装和使用。

而Cydia Substrate实际上出现的非常早期,并且已经商业化闭源,不能确保其开源稳定性,一些拓展功能可能需要更稳定的hook框架。

当然,VA的native层hook也不单单做了io重定向的事情,同时还有一些FileSystem和Android VM(如Camera,Audio,Meia,Runtime)相关的hook,感兴趣的同学可以再看看相关逻辑,这里不再进行分析。

安装

安装方法由VirtualCore提供,最终调用远程VAService中的VAppManagerService.installPackage():

public synchronized InstallResult installPackage(String path, int flags, boolean notify) {

    long installTime = System.currentTimeMillis();

    if (path == null) {

        return InstallResult.makeFailure("path = NULL");

    }

    // dex转二进制,安装时可以选择跳过

    boolean skipDexOpt = (flags & InstallStrategy.SKIP_DEX_OPT) != 0;

    File packageFile = new File(path);

    if (!packageFile.exists() || !packageFile.isFile()) {

        return InstallResult.makeFailure("Package File is not exist.");

    }

    VPackage pkg = null;

    try {

        //解析包结构为VPackage(四大组件,权限信息等)并序列化到磁盘中

        pkg = PackageParserEx.parsePackage(packageFile);

    } catch (Throwable e) {

        e.printStackTrace();

    }

    if (pkg == null || pkg.packageName == null) {

        return InstallResult.makeFailure("Unable to parse the package.");

    }

    InstallResult res = new InstallResult();

    res.packageName = pkg.packageName;

    //...省略检测该package是否需要更新代码



    //安装模式,一种是手机中已经安装的app,另外一种直接是apk包

    boolean dependSystem = (flags & InstallStrategy.DEPEND_SYSTEM_IF_EXIST) != 0

            && VirtualCore.get().isOutsideInstalled(pkg.packageName);



    if (existSetting != null && existSetting.dependSystem) {

        dependSystem = false;

    }

    //拷贝so包

    NativeLibraryHelperCompat.copyNativeBinaries(new File(path), libDir);



    //如果安装模式是安装apk包,则需要拷贝apk,这也是直接安装apk包速度更慢的原因

    if (!dependSystem) {

        File privatePackageFile = new File(appDir, "base.apk");

        File parentFolder = privatePackageFile.getParentFile();

        if (!parentFolder.exists() && !parentFolder.mkdirs()) {

            VLog.w(TAG, "Warning: unable to create folder : " + privatePackageFile.getPath());

        } else if (privatePackageFile.exists() && !privatePackageFile.delete()) {

            VLog.w(TAG, "Warning: unable to delete file : " + privatePackageFile.getPath());

        }

        try {

            FileUtils.copyFile(packageFile, privatePackageFile);

        } catch (IOException e) {

            privatePackageFile.delete();

            return InstallResult.makeFailure("Unable to copy the package file.");

        }

        packageFile = privatePackageFile;

    }

    if (existOne != null) {

        PackageCacheManager.remove(pkg.packageName);

    }



    //sd卡上执行bin需要可执行权限

    chmodPackageDictionary(packageFile);



    //新建PackageSetting存储相关信息

    PackageSetting ps;

    if (existSetting != null) {

        ps = existSetting;

    } else {

        ps = new PackageSetting();

    }

    ps.skipDexOpt = skipDexOpt;

    ps.dependSystem = dependSystem;

    ps.apkPath = packageFile.getPath();

    ps.libPath = libDir.getPath();

    ps.packageName = pkg.packageName;

    ps.appId = VUserHandle.getAppId(mUidSystem.getOrCreateUid(pkg));

    if (res.isUpdate) {

        ps.lastUpdateTime = installTime;

    } else {

        ps.firstInstallTime = installTime;

        ps.lastUpdateTime = installTime;

        for (int userId : VUserManagerService.get().getUserIds()) {

            boolean installed = userId == 0;

            ps.setUserState(userId, false/*launched*/, false/*hidden*/, installed);

        }

    }

    //保存pkg信息到磁盘中,内存中

    PackageParserEx.savePackageCache(pkg);

    PackageCacheManager.put(pkg, ps);

    mPersistenceLayer.save();

    BroadcastSystem.get().startApp(pkg);

    //广播通知安装已经完成

    if (notify) {

        notifyAppInstalled(ps, -1);

    }

    res.isSuccess = true;

    return res;

}

安装过程主要就是进行一些必要的拷贝(apk和so包),解析menifest中的各种信息(四大组件,权限等)并保存起来,以备需要时调用。

而当我们再次安装同一个的app时,不会再进行上述逻辑,仅仅是为该app添加一个新的userId:

这里userId存在一个复用逻辑,比如已经安装了同一个app四次,userId分别为0,1,2,3,然后卸载了2,再重新安装会优先复用2(如果已使用就继续递增),最终通过VAppManagerService的installPackageAsUser方法,为该app添加一个userId记录并保存到磁盘即可。

我们安装oppo应用商店两次,来看看安装后的数据目录:

  1. 拷贝该app的所有so包,如果直接安装apk还会在目录下存放拷贝的base.apk。
  2. package和签名信息序列化后保存的文件,包含该app的所有组件信息。
  3. 用户体系,userlist中保存了所有的userId信息,默认user为0。
  4. dex转二进制存放的目录,以及外置存储目录(会通过io重定向将app对外置存储的操作重定向到此目录中)。

启动

运行机制里说过,VA一共运行在三种进程中(最新的介绍中又添加了64位支持的插件进程),宿主进程自不必说,我们主要看看VA Server进程和Client App进程是如何运行起来的:

清单文件中可以看到prcess定义了两种进程,x进程(即VA Server进程)和p进程(即Client App进程),p进程命名为p0,p1,p2等等,这是因为我们可能会运行多个app,或者不同进程的组件,在Client App启动以后,VA会将p进程修改为Client App真正进程的名字。

而每次新进程的创建,意味着Application的重新初始化:

VApp的多次初始化都会调用VirtualCore的startup方法,其中InvocationStubManager通过injectAll添加所有hook类,完成了java层hook的注入:

那么,x进程是如果拉起的呢?这里VA比较巧妙地利用了Provider的call方法自动拉起进程的机制,来看看BinderProvider:

我们发现BinderProvider中添加并初始化了所有的VA service(实际最终添加到ServiceFetcher中),并在call方法中put了ServiceFetcher这个IBinder句柄,ServiceFetcher正是Client端获取VA Service的钥匙。

最后在Client端中可以进行调用获取:

  1. 通过Provider的call方法,获取BinderProvider返回的Bundle,而BinderProvider在清单文件中prcess定义正是:x进程,如果进程未拉起,此时默认会拉起x进程,所有的VA Service运行在x进程之上。
  2. 拿到Bundler以后,就可以获取BinderProvider中put进去的Binder,正是ServiceFetcher。
  3. ServiceFetcher管理着所有的VA Service,通过ServiceFetcher,即是拿到了所有VA Services的调用权。

p进程也是同理,当我们想要启动Client App中的某个四大组件,发现组件所在的进程不存在,那么首先肯定是拉起进程:

这里也初始化了本地的一个VClientImpl,记录了token和vuid,并将Client打包到Bundle以便调用Client端的方法,最终同样也是通过Provider.call的方式进行唤起:

其中VASettings.getStubAuthority(vpid)是用来匹配进程编号所对应的StubContentProvider,在p进程拉起后,会bindApplication进行子程序包的初始化:

 private void bindApplicationNoCheck(String packageName, String processName, ConditionVariable lock) {

    //省略一部分代码

    AppBindData data = new AppBindData();

    //获取安装时保存的apk信息,通过VPMS

    InstalledAppInfo info = VirtualCore.get().getInstalledAppInfo(packageName, 0);

    if (info == null) {

        new Exception("App not exist!").printStackTrace();

        Process.killProcess(0);

        System.exit(0);

    }

    data.appInfo = VPackageManager.get().getApplicationInfo(packageName, 0, getUserId(vuid));

    data.processName = processName;

    data.providers = VPackageManager.get().queryContentProviders(processName, getVUid(), PackageManager.GET_META_DATA);

    Log.i(TAG, "Binding application " + data.appInfo.packageName + " (" + data.processName + ")");

    mBoundApplication = data;

    //设置进程的名字,此时如p0,p1等进程命名正式变为目标进程名字

    VirtualRuntime.setupRuntime(data.processName, data.appInfo);

    int targetSdkVersion = data.appInfo.targetSdkVersion;

    if (targetSdkVersion < Build.VERSION_CODES.GINGERBREAD) {

        StrictMode.ThreadPolicy newPolicy = new StrictMode.ThreadPolicy.Builder(StrictMode.getThreadPolicy()).permitNetwork().build();

        StrictMode.setThreadPolicy(newPolicy);

    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) {

        mirror.android.os.Message.updateCheckRecycle.call(targetSdkVersion);

    }

    if (VASettings.ENABLE_IO_REDIRECT) {

        //io重定向

        startIOUniformer();

    }

    //hook native 函数

    NativeEngine.launchEngine();

    Object mainThread = VirtualCore.mainThread();

    //准备 dex 列表

    NativeEngine.startDexOverride();

    Context context = createPackageContext(data.appInfo.packageName);

    //设置虚拟机系统环境 

    System.setProperty("java.io.tmpdir", context.getCacheDir().getAbsolutePath());

    File codeCacheDir;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

        codeCacheDir = context.getCodeCacheDir();

    } else {

        codeCacheDir = context.getCacheDir();

    }

    //硬件加速相关

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {

        if (HardwareRenderer.setupDiskCache != null) {

            HardwareRenderer.setupDiskCache.call(codeCacheDir);

        }

    } else {

        if (ThreadedRenderer.setupDiskCache != null) {

            ThreadedRenderer.setupDiskCache.call(codeCacheDir);

        }

    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

        if (RenderScriptCacheDir.setupDiskCache != null) {

            RenderScriptCacheDir.setupDiskCache.call(codeCacheDir);

        }

    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {

        if (RenderScript.setupDiskCache != null) {

            RenderScript.setupDiskCache.call(codeCacheDir);

        }

    }

    //修复一些信息

    Object boundApp = fixBoundApp(mBoundApplication);

    mBoundApplication.info = ContextImpl.mPackageInfo.get(context);

    mirror.android.app.ActivityThread.AppBindData.info.set(boundApp, data.info);

    VMRuntime.setTargetSdkVersion.call(VMRuntime.getRuntime.call(), data.appInfo.targetSdkVersion);



    Configuration configuration = context.getResources().getConfiguration();

    Object compatInfo = CompatibilityInfo.ctor.newInstance(data.appInfo, configuration.screenLayout, configuration.smallestScreenWidthDp, false);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {

            DisplayAdjustments.setCompatibilityInfo.call(ContextImplKitkat.mDisplayAdjustments.get(context), compatInfo);

        }

        DisplayAdjustments.setCompatibilityInfo.call(LoadedApkKitkat.mDisplayAdjustments.get(mBoundApplication.info), compatInfo);

    } else {

        CompatibilityInfoHolder.set.call(LoadedApkICS.mCompatibilityInfo.get(mBoundApplication.info), compatInfo);

    }

    //这里配置了一个冲突app列表,有可能会延后进行AppInstrumentation的替换

    boolean conflict = SpecialComponentList.isConflictingInstrumentation(packageName);

    if (!conflict) {

        InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class);

    }

    //利用LoadedApk构建ClientApp的Application

    mInitialApplication = LoadedApk.makeApplication.call(data.info, false, null);

    mirror.android.app.ActivityThread.mInitialApplication.set(mainThread, mInitialApplication);

    ContextFixer.fixContext(mInitialApplication);

    if (Build.VERSION.SDK_INT >= 24 && "com.tencent.mm:recovery".equals(processName)) {

        //单独处理微信的一些问题

        fixWeChatRecovery(mInitialApplication);

    }

    if (data.providers != null) {

        //安装provider

        installContentProviders(mInitialApplication, data.providers);

    }

    if (lock != null) {

        lock.open();

        mTempLock = null;

    }

    VirtualCore.get().getComponentDelegate().beforeApplicationCreate(mInitialApplication);

    try {

        //ClientApp生命周期正式开始

        mInstrumentation.callApplicationOnCreate(mInitialApplication);

        InvocationStubManager.getInstance().checkEnv(HCallbackStub.class);

        if (conflict) {

            InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class);

        }

        Application createdApp = ActivityThread.mInitialApplication.get(mainThread);

        if (createdApp != null) {

            mInitialApplication = createdApp;

        }

    } catch (Exception e) {

        if (!mInstrumentation.onException(mInitialApplication, e)) {

            throw new RuntimeException(

                    "Unable to create application " + mInitialApplication.getClass().getName()

                            + ": " + e.toString(), e);

        }

    }

    //通知完成

    VActivityManager.get().appDoneExecuting();

    VirtualCore.get().getComponentDelegate().afterApplicationCreate(mInitialApplication);

}

Activity

Activity组件可能是我们最关心的了,运行机制里我们解释过如何欺骗AMS启动未注册在清单中的Activity,Java层hook也提到了startActivity方法是如何hook的,接下来我们直接看具体逻辑,hook代码在ActivityManagerStub中:



static class StartActivity extends MethodProxy {



    private static final String SCHEME_FILE = "file";

    private static final String SCHEME_PACKAGE = "package";

    private static final String SCHEME_CONTENT = "content";



    @Override

    public String getMethodName() {

        return "startActivity";

    }



    @Override

    public Object call(Object who, Method method, Object... args) throws Throwable {

        int intentIndex = ArrayUtils.indexOfObject(args, Intent.class, 1);

        if (intentIndex < 0) {

            return ActivityManagerCompat.START_INTENT_NOT_RESOLVED;

        }

        int resultToIndex = ArrayUtils.indexOfObject(args, IBinder.class, 2);

        String resolvedType = (String) args[intentIndex + 1];

        Intent intent = (Intent) args[intentIndex];

        intent.setDataAndType(intent.getData(), resolvedType);

        IBinder resultTo = resultToIndex >= 0 ? (IBinder) args[resultToIndex] : null;

        int userId = XUserHandle.myUserId();



        if (ComponentUtils.isStubComponent(intent)) {

            return method.invoke(who, args);

        }

        

        if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())

                || (Intent.ACTION_VIEW.equals(intent.getAction())

                && "application/vnd.android.package-archive".equals(intent.getType()))) {

            //内部安装拦截自处理,此处代码省略

        } else if ((Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())

                || Intent.ACTION_DELETE.equals(intent.getAction()))

                && "package".equals(intent.getScheme())) {

            //内部卸载拦截自处理,此处代码省略

        } else if (MediaStore.ACTION_IMAGE_CAPTURE.equals(intent.getAction()) ||

                MediaStore.ACTION_VIDEO_CAPTURE.equals(intent.getAction()) ||

                MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(intent.getAction())) {

            handleMediaCaptureRequest(intent);

        }



        String resultWho = null;

        int requestCode = 0;

        Bundle options = ArrayUtils.getFirst(args, Bundle.class);

        if (resultTo != null) {

            resultWho = (String) args[resultToIndex + 1];

            requestCode = (int) args[resultToIndex + 2];

        }



        if (Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES.equals(intent.getAction())) {

            //不需要申请安装apk权限,内部应用更新不走系统安装器,此处代码省略

        }



        if (BuildCompat.isAndroidLevel18()) {

            args[intentIndex - 1] = getHostPkg();

        }

        if (intent.getScheme() != null && intent.getScheme().equals(SCHEME_PACKAGE) && intent.getData() != null) {

            if (intent.getAction() != null && intent.getAction().startsWith("android.settings.")) {

                intent.setData(Uri.parse("package:" + getHostPkg()));

            }

        }



        ActivityInfo activityInfo = VAppManager.get().resolveActivityInfo(intent, userId);

        if (activityInfo == null) {

            VLog.e("VActivityManager", "Unable to resolve activityInfo : %s", intent);

            if (intent.getPackage() != null && isAppPkg(intent.getPackage())) {

                return ActivityManagerCompat.START_INTENT_NOT_RESOLVED;

            }

            return method.invoke(who, args);

        }

        //调用远程VAMS的startActivity方法

        int res = VActivityManager.get().startActivity(intent, activityInfo, resultTo, options, resultWho, requestCode, XUserHandle.myUserId());

        if (res != 0 && resultTo != null && requestCode > 0) {

            VActivityManager.get().sendActivityResult(resultTo, resultWho, requestCode);

        }

        //处理StubActivity的主题和动画为ClientApp中的目标启动Activity的

        if (resultTo != null) {

            ActivityClientRecord r = VActivityManager.get().getActivityRecord(resultTo);

            if (r != null && r.activity != null) {

                try {

                    TypedValue out = new TypedValue();

                    Resources.Theme theme = r.activity.getResources().newTheme();

                    theme.applyStyle(activityInfo.getThemeResource(), true);

                    if (theme.resolveAttribute(android.R.attr.windowAnimationStyle, out, true)) {



                        TypedArray array = theme.obtainStyledAttributes(out.data,

                                new int[]{

                                        android.R.attr.activityOpenEnterAnimation,

                                        android.R.attr.activityOpenExitAnimation

 });



                        r.activity.overridePendingTransition(array.getResourceId(0, 0), array.getResourceId(1, 0));

                        array.recycle();

                    }

                } catch (Throwable e) {

                    // Ignore

                }

            }

        }

        return res;

    }



}

内部安装和卸载,是指如应用宝这样的应用商店安装其他app,或者app自身的升级更新,VA中拦截了该intent自己处理(安装和卸载都在VA内部进行)。

重点关注VActivityManager.get().startActivity这一行,正常的StartActivity方法最终会被引导到VAM,再从p进程到运行在x进程中的远程VAMS中,VAMS再调用自实现ActivityStack的startActivityLocked方法:



int startActivityLocked(int userId, Intent intent, ActivityInfo info, IBinder resultTo, Bundle options,

                        String resultWho, int requestCode) {

    optimizeTasksLocked();



    Intent destIntent;

    ActivityRecord sourceRecord = findActivityByToken(userId, resultTo);

    TaskRecord sourceTask = sourceRecord != null ? sourceRecord.task : null;



    //对启动Flag的处理,此处代码省略

    

    String affinity = ComponentUtils.getTaskAffinity(info);

    TaskRecord reuseTask = null;

    switch (reuseTarget) {

        case AFFINITY:

            reuseTask = findTaskByAffinityLocked(userId, affinity);

            break;

        case DOCUMENT:

            reuseTask = findTaskByIntentLocked(userId, intent);

            break;

        case CURRENT:

            reuseTask = sourceTask;

            break;

        default:

            break;

    }



    boolean taskMarked = false;

    if (reuseTask == null) {

        startActivityInNewTaskLocked(userId, intent, info, options);

    } else {

        boolean delivered = false;

        mAM.moveTaskToFront(reuseTask.taskId, 0);

        boolean startTaskToFront = !clearTask && !clearTop && ComponentUtils.isSameIntent(intent, reuseTask.taskRoot);



        if (clearTarget.deliverIntent || singleTop) {

            taskMarked = markTaskByClearTarget(reuseTask, clearTarget, intent.getComponent());

            ActivityRecord topRecord = topActivityInTask(reuseTask);

            if (clearTop && !singleTop && topRecord != null && taskMarked) {

                topRecord.marked = true;

            }

            // Target activity is on top

            if (topRecord != null && !topRecord.marked && topRecord.component.equals(intent.getComponent())) {

                deliverNewIntentLocked(sourceRecord, topRecord, intent);

                delivered = true;

            }

        }

        if (taskMarked) {

            synchronized (mHistory) {

                scheduleFinishMarkedActivityLocked();

            }

        }

        if (!startTaskToFront) {

            if (!delivered) {

                destIntent = startActivityProcess(userId, sourceRecord, intent, info);

                if (destIntent != null) {

                    startActivityFromSourceTask(reuseTask, destIntent, info, resultWho, requestCode, options);

                }

            }

        }

    }

    return 0;

}

这里其实就是对launch mode以及启动flags等的综合计算,来维护自己的一套Activity栈,然后到了startActivityProcess方法:



private Intent startActivityProcess(int userId, ActivityRecord sourceRecord, Intent intent, ActivityInfo info) {

    intent = new Intent(intent);

    //1

    ProcessRecord targetApp = mService.startProcessIfNeedLocked(info.processName, userId, info.packageName);

    if (targetApp == null) {

        return null;

    }

    Intent targetIntent = new Intent();

    //2

    targetIntent.setClassName(VirtualCore.get().getHostPkg(), fetchStubActivity(targetApp.vpid, info));

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

    //3

    saveInstance.saveToIntent(targetIntent);

    return targetIntent;

}
  1. 当要启动一个所在进程并不存在的Activity时,首先得拉起其所在的进程,启动篇我们也分析过了p进程是如何拉起的。
  2. fetchStubActivity会根据当前的情况,根据vpid去清单文件寻找合适的StubActivity,同一进程下的StubActivity信息会被多次复用。
  3. 将ClientAPP要启动的目标Activity的信息保存在intent中,而此时的intent已经被包装为启动StubActivity的intent。

最终调用ActivityStack.realStartActivityLocked:



private void realStartActivityLocked(IBinder resultTo, Intent intent, String resultWho, int requestCode,

                                     Bundle options) {

    Class<?>[] types = mirror.android.app.IActivityManager.startActivity.paramList();

    Object[] args = new Object[types.length];

    if (types[0] == IApplicationThread.TYPE) {

        args[0] = ActivityThread.getApplicationThread.call(VirtualCore.mainThread());

    }

    int intentIndex = ArrayUtils.protoIndexOf(types, Intent.class);

    int resultToIndex = ArrayUtils.protoIndexOf(types, IBinder.class, 2);

    int optionsIndex = ArrayUtils.protoIndexOf(types, Bundle.class);

    int resolvedTypeIndex = intentIndex + 1;

    int resultWhoIndex = resultToIndex + 1;

    int requestCodeIndex = resultToIndex + 2;



    args[intentIndex] = intent;

    args[resultToIndex] = resultTo;

    args[resultWhoIndex] = resultWho;

    args[requestCodeIndex] = requestCode;

    if (optionsIndex != -1) {

        args[optionsIndex] = options;

    }

    args[resolvedTypeIndex] = intent.getType();

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {

        args[intentIndex - 1] = VirtualCore.get().getHostPkg();

    }

    ClassUtils.fixArgs(types, args);



    mirror.android.app.IActivityManager.startActivity.call(ActivityManagerNative.getDefault.call(),

            (Object[]) args);

}

经过对参数的替换处理,最终将伪造的StubActivity启动intent交给了系统AMS。

从上面的代码里, 我们也看到了一些ProcessRecord,TaskRecord,ActivityRecord的逻辑,这是因为VA比较完整的仿照android源码自实现了ActivityStack的相关功能,当我们启动一个新进程,新taskAffinity的新Activity,VA中也会新建这些Record存放于内存中进行维护,finish同理。

可以简单看一下数据结构,他们之间也存在相互持有关系,为了方便溯源和处理:

至此启动Intent的伪造包装完毕,系统AMS接收到的也是StubActivity的相关信息,经过AMS的各种处理,消息又回到了Client端主线程的mH Handler中,由Instrumentation实例化目标Activity,VA选择在在这个时机拦截消息,恢复Intent:

处理逻辑来到handleLaunchActivity:

private boolean handleLaunchActivity(Message msg) {

    Object r = msg.obj;

    //StubActivity的intent

    Intent stubIntent = ActivityThread.ActivityClientRecord.intent.get(r);

    //StubActivityRecord中会获取之前保留的参数,包含原版intent信息

    StubActivityRecord saveInstance = new StubActivityRecord(stubIntent);

    if (saveInstance.intent == null) {

        return true;

    }

    //原版目标intent

    Intent intent = saveInstance.intent;

    ComponentName caller = saveInstance.caller;

    IBinder token = ActivityThread.ActivityClientRecord.token.get(r);

    ActivityInfo info = saveInstance.info;

    //token为空,则需要拉起组件对应的进程,可参考启动篇p进程的拉起

    if (VClientImpl.get().getToken() == null) {

        InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(info.packageName, 0);

        if (installedAppInfo == null) {

            return true;

        }

        VActivityManager.get().processRestarted(info.packageName, info.processName, saveInstance.userId);

        getH().sendMessageAtFrontOfQueue(Message.obtain(msg));

        return false;

    }

    if (!VClientImpl.get().isBound()) {

        VClientImpl.get().bindApplication(info.packageName, info.processName);

        getH().sendMessageAtFrontOfQueue(Message.obtain(msg));

        return false;

    }

    int taskId = IActivityManager.getTaskForActivity.call(

            ActivityManagerNative.getDefault.call(),

            token,

            false

    );

    //通知VAMS创建完成

    VActivityManager.get().onActivityCreate(ComponentUtils.toComponentName(info), caller, token, info, intent, ComponentUtils.getTaskAffinity(info), taskId, info.launchMode, info.flags);

    ClassLoader appClassLoader = VClientImpl.get().getClassLoader(info.applicationInfo);

    intent.setExtrasClassLoader(appClassLoader);

    //真正替换intent信息的地方,此时intent信息为原版目标信息,又交还给了系统处理

    ActivityThread.ActivityClientRecord.intent.set(r, intent);

    ActivityThread.ActivityClientRecord.activityInfo.set(r, info);

    return true;

}

系统拿到intent之后,所实例化的也就变成了ClientApp中的目标Activity。

最后一个hook点是在Instrumentation.callActivityOnCreate中,此时Activity已经被new出来,VA需要在这个时机恢复真正的主题和屏幕方向,以及修复一些问题。

不过,在android9.0以后,Activity的启动流程发生了一些变化,而VA的开源代码中还没有相关的适配,我们可以简单描述一下处理办法:还是可以通过mH的消息机制来进行拦截,只不过拦截的消息从LAUNCH_ACTIVITY变成了EXECUTE_TRANSACTION,替换intent的地方由ActivityClientRecord变成了LaunchActivityItem,尽管流程上的变化略大,但是应对方式却没有那么复杂,只需要找准hook点即可。

其他三大组件

除去Activity,其他三大组件也各自利用一些技巧规避了问题,因篇幅有限,我们不再做具体的代码分析,只简述一下实现原理,有兴趣的同学可以继续深入了解。

ContentProvider

相比较于StubActivity的占位作用,StubContentProvider却并不是为了占位,其作用是为了调用时可以带起p进程,而在进程拉起后,会进行application的初始化,bindApplicationNoCheck方法中会真正对Client App中的ContentProvider进行安装注册。

当进程A调用进程B(或应用B)中的ContentProvider时,会hook进程A中的getContentProvider方法,判断目标Provider所在进程B是否存在,如果不存在则拉起,这样进程B的Provider安装完毕,返回进程A所需要的目标Provider句柄,完成调用。

Service

由于Service并不像Activity那样有交互有页面,它的生命周期非常简单,并且ClientApp中的Service并不需要暴露给外部(指沙盒外)App使用,因此在VA中,Service不需要让AMS等系统服务知晓。

通过hook startService方法将逻辑直接引导到VAMS中,利用ApplicationThread.scheduleCreateService对目标Service直接进行创建(如果目标进程不存在则拉起进程),如果是bindService则使用ApplicationThread.scheduleBindService等待bind完成。

简而言之就是Client App中Service的创建运行过程,对系统服务屏蔽,利用mirror手段直接调用其生命周期。

BroadcastReceiver

由于广播分静态注册和动态注册,而ClientApp中的静态广播无法被系统AMS所知晓,因此VA使用动态注册来代替静态注册。

当VA Service进程拉起时,VA对所有已安装的APP进行扫描,遍历其所有的Reveiver信息,通过新建StaticBroadcastReceiver代理来接收每个IntentFilter。

然后hook了ClientApp中的broadcastIntent方法,这里其实也做了一个intent包裹,和Activity逻辑类似,当代理广播StaticBroadcastReceiver接收到包装后的intent信息时,解包出真正的intent,回调到ClientApp空间,实例化目标 Receiver(如果目标进程不存在则拉起进程),最终手动进行对其OnCreate和finish的调用。

实际上,各种插件化框架对于四大组件的处理是大同小异的,仅仅是hook点,hook时机略有不同而已。

结语

VA的基本原理,代码层面到此基本结束,剩下的是不断的堆砌完善工作,而基于VA后续也有非常多的拓展,作者有句话说的很好:VA怎么用,一切取决于你的想象力。

如果你对它的其他能力感兴趣,可以看看这篇文章:VirtualApp技术黑产利用研究报告

如果你对免root实现xposed,或者一些基于VA的二次开发项目感兴趣,可以继续了解以下项目:

github.com/android-hac…

github.com/asLody/Sand…

github.com/WindySha/Xp…

github.com/android-hac…

www.taichi-app.com/#/index

笔者在写下此篇时也是依据于当前的理解来分析,如果有不对的地方欢迎指正,或者有兴趣的同学也欢迎一起来交流,感谢大家!

参考文档