VirtualApp 框架浅析

作者: Cat9527 | 来源:发表于2019-07-04 11:59 被阅读0次

    Github地址:VirtualApp

    简介

    VirtualApp是一款运行于Android系统的沙盒产品,可以理解为轻量级的“Android虚拟机”。其产品形态为高可扩展,可定制的集成SDK,您可以基于VA或者使用VA定制开发各种看似不可能完成的项目。VA目前被广泛应用于插件化开发、无感知热更新、云控自动化、多开、手游租号、手游手柄免激活、区块链、移动办公安全、军队政府保密、手机模拟信息、脚本自动化、自动化测试等技术领域。

    VirtualApp可以创建一个虚拟空间,你可以在虚拟空间内任意的安装、启动和卸载APK,这一切都与外部隔离,如同一个沙盒,APK无需在外部安装。

    VirtualApp的特有能力

    • 克隆能力
      可以克隆外部系统中已经安装的App,并在内部运行,互不干扰。典型应用场景为App双开。

    • 免安装能力
      除了克隆已安装App之外,VA可以直接在内部安装(外部无感知)apk,并在内部直接运行。典型应用场景为插件化,独立应用市场等。

    • 多开能力
      VA不仅可以“双开”,独特的多用户模式支持用户在内部无限多开同一个App。

    • 内外隔离能力
      VA是一个标准的沙盒,或者说“虚拟机”,提供了一整套内部与外部的隔离机制,包括但不限于(文件隔离/组件隔离/进程通讯隔离),简单的说VA内部就是一个“完全独立的空间”。在此基础之上,稍作定制即可实现一部手机上的“虚拟手机”。当然您也可以发挥想象,定制成应用于数据加密,数据隔离,隐私保护,企业管理的应用系统。

    • 对于内部App的完全控制能力
      VA对于内部的App具有完全的监控和控制能力,这点在未Root的外部环境中是绝对无法实现的。

    运行机制

    首先,我们来看一下它在开启APP后的进程信息。

    USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME                       
    u0_a645        435   501 2125236 285608 0                   0 S com.duowan.kiwi:yyPushService
    u0_a645      24705   501 1875532  22620 0                   0 S io.virtualapp
    u0_a645      24761   501 1831868  22728 0                   0 S io.virtualapp:x
    u0_a645      26243   501 2770772 147752 0                   0 S com.duowan.kiwi
    

    可以看到,所有被ViralApp打开的应用,都和VirtalApp属于同一个uid:u0_a645。其中,VirtualApp本身有两个进程:io.virtualappio.virtualapp:x

    • io.virtualapp 就是可见的交互界面,同时也负责APK包的管理和安装。
    • io.virtualapp:x 作为一个单独的服务进程,虚拟了一些系统服务。

    以这里安装的虎牙直播为例,查看一下它的进程的内存空间,可以看到相关路径全都被映射到了/data/data/io.virtualapp/virtual下面。

    7f8a8000-7f8a9000 rw-p 00095000 b3:1c 261396     /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libtrustdevice.so
    8393a000-83980000 r--s 00000000 b3:1c 263573     /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/libresources.so
    840fe000-840ff000 rw-p 00027000 b3:1c 261344     /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libjscexecutor.so
    86905000-8691e000 rw-p 0256f000 b3:1c 263574     /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/libmttwebview.so
    86c10000-86c13000 r-xp 00000000 b3:1c 263579     /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/libqb_keystore.so
    86d3e000-86d56000 r-xp 00000000 b3:1c 261313     /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libsecurityenv.so
    8704d000-87050000 r-xp 00000000 b3:1c 263567     /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/libmttwebview_plat_support.so
    87375000-87421000 r-xp 00000000 b3:1c 261301     /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libgnustl_shared.so
    87531000-87c29000 r-xp 00000000 b3:1c 261321     /data/data/io.virtualapp/virtual/data/app/com.duowan.kiwi/lib/libjsc.so
    88386000-883a1000 r--p 00000000 b3:1c 263621     /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/asr_base_dex.dex
    89e4f000-8a6cb000 r--p 00000000 b3:1c 263600     /data/data/io.virtualapp/virtual/data/user/0/com.duowan.kiwi/app_tbs/core_share/tbs_jars_fusion_dex.dex
    

    可见,这里面对路径做过了重新映射。

    注入逻辑

    要想实现对一个APP的虚拟化,就是不直接把APP安装进系统,同时又要提供APP运行过程中所需的一切,从而可以让它误以为自己是运行在正常系统中。这里就需要实现系统服务的虚拟化和相关路径的虚拟化。

    其中,系统服务的虚拟化主要靠注入大量framework组件来实现的。

    @VirtualApp/lib/src/main/java/com/lody/virtual/client/core/InvocationStubManager.java
    private void injectInternal() throws Throwable {
      if (VirtualCore.get().isMainProcess()) {
        return;
      }
      if (VirtualCore.get().isServerProcess()) {
        addInjector(new ActivityManagerStub());
        addInjector(new PackageManagerStub());
        return;
      }
      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());
        addInjector(new BackupManagerStub());
        addInjector(new TelephonyStub());
        addInjector(new TelephonyRegistryStub());
        addInjector(new PhoneSubInfoStub());
        addInjector(new PowerManagerStub());
        addInjector(new AppWidgetManagerStub());
        addInjector(new AccountManagerStub());
        addInjector(new AudioManagerStub());
        addInjector(new SearchManagerStub());
        addInjector(new ContentServiceStub());
        addInjector(new ConnectivityStub());
    
        if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR2) {
          addInjector(new VibratorStub());
          addInjector(new WifiManagerStub());
          addInjector(new BluetoothStub());
          addInjector(new ContextHubServiceStub());
        }
        if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR1) {
          addInjector(new UserManagerStub());
        }
    
        if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR1) {
          addInjector(new DisplayStub());
        }
        if (Build.VERSION.SDK_INT >= LOLLIPOP) {
          addInjector(new PersistentDataBlockServiceStub());
          addInjector(new InputMethodManagerStub());
          addInjector(new MmsStub());
          addInjector(new SessionManagerStub());
          addInjector(new JobServiceStub());
          addInjector(new RestrictionStub());
        }
        if (Build.VERSION.SDK_INT >= KITKAT) {
          addInjector(new AlarmManagerStub());
          addInjector(new AppOpsManagerStub());
          addInjector(new MediaRouterServiceStub());
        }
        if (Build.VERSION.SDK_INT >= LOLLIPOP_MR1) {
          addInjector(new GraphicsStatsStub());
        }
        if (Build.VERSION.SDK_INT >= M) {
          addInjector(new NetworkManagementStub());
        }
        if (Build.VERSION.SDK_INT >= N) {
                  addInjector(new WifiScannerStub());
                  addInjector(new ShortcutServiceStub());
              }
      }
    }
    

    这个注入过程是发生在io.virtualapp.VApp.attachBaseContext中,因此,每次启动一个子进程都会执行到这里,这会区分是isMainProcess(io.virtualapp)或者isServerProcess(io.virtualapp:x)或者isVAppProcess(被安装APP)来进行不同的注入,可以看到,注入最多的还是在被安装APP的进程中。

    可以看到,之前在injectInternal 中addInjector的所有Stub都会调用它的inject方法。

    VirtualApp/lib/src/main/java/com/lody/virtual/client/core/InvocationStubManager.java
    
    void injectAll() throws Throwable {
      for (IInjector injector : mInjectors.values()) {
        injector.inject();
      }
      // XXX: Lazy inject the Instrumentation,
      addInjector(AppInstrumentation.getDefault());
    }
    

    由此实现对各个系统类的替换。

    而在底层,VirtualApp还实现了对原本路径的替换,在java层传入需要重定向的所有路径。

    private void startIOUniformer() {
            ApplicationInfo info = mBoundApplication.appInfo;
            int userId = VUserHandle.myUserId();
            String wifiMacAddressFile = deviceInfo.getWifiFile(userId).getPath();
            NativeEngine.redirectDirectory("/sys/class/net/wlan0/address", wifiMacAddressFile);
            NativeEngine.redirectDirectory("/sys/class/net/eth0/address", wifiMacAddressFile);
            NativeEngine.redirectDirectory("/sys/class/net/wifi/address", wifiMacAddressFile);
            NativeEngine.redirectDirectory("/data/data/" + info.packageName, info.dataDir);
            NativeEngine.redirectDirectory("/data/user/0/" + info.packageName, info.dataDir);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                NativeEngine.redirectDirectory("/data/user_de/0/" + info.packageName, info.dataDir);
            }
            String libPath = new File(VEnvironment.getDataAppPackageDirectory(info.packageName), "lib").getAbsolutePath();
            String userLibPath = new File(VEnvironment.getUserSystemDirectory(userId), "lib").getAbsolutePath();
            NativeEngine.redirectDirectory(userLibPath, libPath);
            NativeEngine.redirectDirectory("/data/data/" + info.packageName + "/lib/", libPath);
            NativeEngine.redirectDirectory("/data/user/0/" + info.packageName + "/lib/", libPath);
    
            NativeEngine.readOnly(VEnvironment.getDataAppDirectory().getPath());
            VirtualStorageManager vsManager = VirtualStorageManager.get();
            String vsPath = vsManager.getVirtualStorage(info.packageName, userId);
            boolean enable = vsManager.isVirtualStorageEnable(info.packageName, userId);
            if (enable && vsPath != null) {
                File vsDirectory = new File(vsPath);
                if (vsDirectory.exists() || vsDirectory.mkdirs()) {
                    HashSet<String> mountPoints = getMountPoints();
                    for (String mountPoint : mountPoints) {
                        NativeEngine.redirectDirectory(mountPoint, vsPath);
                    }
                }
            }
            NativeEngine.hook();
        }
    

    这些路径最终会添加进JNI层的一个映射表中

    void IOUniformer::redirect(const char *orig_path, const char *new_path) {
        LOGI("Start Java_nativeRedirect : from %s to %s", orig_path, new_path);
        add_pair(orig_path, new_path);
    }
    
    static void add_pair(const char *_orig_path, const char *_new_path) {
        std::string origPath = std::string(_orig_path);
        std::string newPath = std::string(_new_path);
        IORedirectMap.insert(std::pair<std::string, std::string>(origPath, newPath));
        if (endWith(origPath, '/')) {
            RootIORedirectMap.insert(
                    std::pair<std::string, std::string>(
                            origPath.substr(0, origPath.length() - 1),
                            newPath.substr(0, newPath.length() - 1))
            );
        }
    }
    

    然后,会hook所有的c库函数,这些函数在调用的时候,就会替换路径为新路径。由于hook的是libc的函数,java层和虚拟机的文件访问最终也会调用到这里,从而受到影响。

    void IOUniformer::startUniformer(int api_level, int preview_api_level) {
        gVars.hooked_process = true;
        HOOK_SYMBOL(RTLD_DEFAULT, vfork);
        HOOK_SYMBOL(RTLD_DEFAULT, kill);
        HOOK_SYMBOL(RTLD_DEFAULT, __getcwd);
        HOOK_SYMBOL(RTLD_DEFAULT, truncate);
        HOOK_SYMBOL(RTLD_DEFAULT, __statfs64);
        HOOK_SYMBOL(RTLD_DEFAULT, execve);
        HOOK_SYMBOL(RTLD_DEFAULT, __open);
        if ((api_level < 25) || (api_level == 25 && preview_api_level == 0)) {
            HOOK_SYMBOL(RTLD_DEFAULT, utimes);
            HOOK_SYMBOL(RTLD_DEFAULT, mkdir);
            HOOK_SYMBOL(RTLD_DEFAULT, chmod);
            HOOK_SYMBOL(RTLD_DEFAULT, lstat);
            HOOK_SYMBOL(RTLD_DEFAULT, link);
            HOOK_SYMBOL(RTLD_DEFAULT, symlink);
            HOOK_SYMBOL(RTLD_DEFAULT, mknod);
            HOOK_SYMBOL(RTLD_DEFAULT, rmdir);
            HOOK_SYMBOL(RTLD_DEFAULT, chown);
            HOOK_SYMBOL(RTLD_DEFAULT, rename);
            HOOK_SYMBOL(RTLD_DEFAULT, stat);
            HOOK_SYMBOL(RTLD_DEFAULT, chdir);
            HOOK_SYMBOL(RTLD_DEFAULT, access);
            HOOK_SYMBOL(RTLD_DEFAULT, readlink);
            HOOK_SYMBOL(RTLD_DEFAULT, unlink);
        }
        HOOK_SYMBOL(RTLD_DEFAULT, fstatat);
        HOOK_SYMBOL(RTLD_DEFAULT, fchmodat);
        HOOK_SYMBOL(RTLD_DEFAULT, symlinkat);
        HOOK_SYMBOL(RTLD_DEFAULT, readlinkat);
        HOOK_SYMBOL(RTLD_DEFAULT, unlinkat);
        HOOK_SYMBOL(RTLD_DEFAULT, linkat);
        HOOK_SYMBOL(RTLD_DEFAULT, utimensat);
        HOOK_SYMBOL(RTLD_DEFAULT, __openat);
        HOOK_SYMBOL(RTLD_DEFAULT, faccessat);
        HOOK_SYMBOL(RTLD_DEFAULT, mkdirat);
        HOOK_SYMBOL(RTLD_DEFAULT, renameat);
        HOOK_SYMBOL(RTLD_DEFAULT, fchownat);
        HOOK_SYMBOL(RTLD_DEFAULT, mknodat);
    //    hook_dlopen(api_level);
    
    #if defined(__i386__) || defined(__x86_64__)
        // Do nothing
    #else
        GodinHook::NativeHook::hookAllRegistered();
    #endif
    }
    

    以chmod函数为例

    // int chmod(const char *path, mode_t mode);
    HOOK_DEF(int, chmod, const char *pathname, mode_t mode) {
        const char *redirect_path = match_redirected_path(pathname);
        if (isReadOnlyPath(redirect_path)) {
            return -1;
        }
        int ret = syscall(__NR_chmod, redirect_path, mode);
        FREE(redirect_path, pathname);
        return ret;
    }
    

    可以看到,它会把原先的pathname,通过match_redirected_path找到映射后的新路径,然后用syscall来调用它,这样就实现了所有路径的重定向。

    运行时结构

    VA 参照原生系统 framework 仿造了一套 framework service,还有配套在 client 端的 framework 库。

    • 系统原生的 framework 运作方式
      简单来说,我们平时所用到的 app 运行空间中的 framework api 最终会通过 Binder 远程调用到 framework service 空间的远程服务。
      而远程服务类似 AMS 中的 Recoder 中会持有 app 空间的 Ibinder token 句柄,通过 token 也可以让 framework service 远程调用到 app 空间。
    • VA 环境下framework 运作方式
      而在 VA 环境下,情况其实也是类似,只不过在 framework service 和 client app 之间还有另外一个 VA 实现的 VAService,VAService 仿造了 framework service 的一些功能。
      因为在 VA 中运行的 Client App 都是没有(也不能注册)在 framework service 的,注册的只有 VA 预先注册在 Menifest 中的 Stub 而已。所以 frameservice 是无法像普通 App 一样管理 VA Client App 的会话的。
      这就要依靠 VA 仿造的另外一套 VAService 完成对 VA 中 Client App 的会话管理了。

    VA初始化

    先看一下代码:
    VirtualCore.startup

    public void startup(Context context) throws Throwable {
            if (!isStartUp) {
                // 确保 MainThread
                if (Looper.myLooper() != Looper.getMainLooper()) {
                    throw new IllegalStateException("VirtualCore.startup() must called in main thread.");
                }
                VASettings.STUB_CP_AUTHORITY = context.getPackageName() + "." + VASettings.STUB_DEF_AUTHORITY;
                ServiceManagerNative.SERVICE_CP_AUTH = context.getPackageName() + "." + ServiceManagerNative.SERVICE_DEF_AUTH;
                this.context = context;
                // 获取 ActivityThread 实例
                mainThread = ActivityThread.currentActivityThread.call();
                unHookPackageManager = context.getPackageManager();
                hostPkgInfo = unHookPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS);
                detectProcessType();
                // hook 系统类
                InvocationStubManager invocationStubManager = InvocationStubManager.getInstance();
                invocationStubManager.init();
                invocationStubManager.injectAll();
                // 修复权限管理
                ContextFixer.fixContext(context);
                isStartUp = true;
                if (initLock != null) {
                    initLock.open();
                    initLock = null;
                }
            }
        }
    

    InvocationStubManager.injectInternal
    主要完成对 Java 层 framework 的 Hook,将其定位到 VA 伪造 VA framework 上去。

    private void injectInternal() throws Throwable {
            // VA 自身的 App 进程不需要 Hook
            if (VirtualCore.get().isMainProcess()) {
                return;
            }
            // VAService 需要 Hook AMS 和 PMS
            if (VirtualCore.get().isServerProcess()) {
                addInjector(new ActivityManagerStub());
                addInjector(new PackageManagerStub());
                return;
            }
            // Client APP 需要 Hook 整个 framework,来使其调用到 VA framework
            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());
                .....................
             }
        }
    

    Client App 的安装

    VirtualCore.installPackage

    public InstallResult installPackage(String apkPath, int flags) {
            try {
                // 调用远程 VAService
                return getService().installPackage(apkPath, flags);
            } catch (RemoteException e) {
                return VirtualRuntime.crash(e);
            }
        }
    

    最终调用 VAServcie 中的 VAppManagerService.installPackage

    public synchronized InstallResult installPackage(String path, int flags, boolean notify) {
            long installTime = System.currentTimeMillis();
            if (path == null) {
                return InstallResult.makeFailure("path = NULL");
            }
            // 是否 OPT 优化(dex -> binary)
            boolean skipDexOpt = (flags & InstallStrategy.SKIP_DEX_OPT) != 0;
            // apk path
            File packageFile = new File(path);
            if (!packageFile.exists() || !packageFile.isFile()) {
                return InstallResult.makeFailure("Package File is not exist.");
            }
            VPackage pkg = null;
            try {
                // 进入解析包结构,该结构是可序列化的,为了持久化在磁盘上
                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;
            // PackageCache holds all packages, try to check if we need to update.
            VPackage existOne = PackageCacheManager.get(pkg.packageName);
            PackageSetting existSetting = existOne != null ? (PackageSetting) existOne.mExtras : null;
            if (existOne != null) {
                if ((flags & InstallStrategy.IGNORE_NEW_VERSION) != 0) {
                    res.isUpdate = true;
                    return res;
                }
                if (!canUpdate(existOne, pkg, flags)) {
                    return InstallResult.makeFailure("Not allowed to update the package.");
                }
                res.isUpdate = true;
            }
            // 获得 app 安装文件夹
            File appDir = VEnvironment.getDataAppPackageDirectory(pkg.packageName);
            // so 文件夹
            File libDir = new File(appDir, "lib");
            if (res.isUpdate) {
                FileUtils.deleteDir(libDir);
                VEnvironment.getOdexFile(pkg.packageName).delete();
                VActivityManagerService.get().killAppByPkg(pkg.packageName, VUserHandle.USER_ALL);
            }
            if (!libDir.exists() && !libDir.mkdirs()) {
                return InstallResult.makeFailure("Unable to create lib dir.");
            }
    
            // 是否基于系统的 apk 加载,前提是安装过的 apk 并且 dependSystem 开关打开
            boolean dependSystem = (flags & InstallStrategy.DEPEND_SYSTEM_IF_EXIST) != 0
                    && VirtualCore.get().isOutsideInstalled(pkg.packageName);
    
            if (existSetting != null && existSetting.dependSystem) {
                dependSystem = false;
            }
            // 复制 so 到 sandbox lib
            NativeLibraryHelperCompat.copyNativeBinaries(new File(path), libDir);
    
            // 如果不基于系统,一些必要的拷贝工作
            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);
            }
    
            // 给上可执行权限,5.0 之后在 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);
                }
            }
            //保存 VPackage Cache 到 Disk
            PackageParserEx.savePackageCache(pkg);
            //保存到 RamCache
            PackageCacheManager.put(pkg, ps);
            mPersistenceLayer.save();
            BroadcastSystem.get().startApp(pkg);
            //发送通知 安装完成
            if (notify) {
                notifyAppInstalled(ps, -1);
            }
            res.isSuccess = true;
            return res;
        }
    

    APk 的安装主要完成以下几件事情:

    • 解析 menifest 拿到 apk 内部信息,包括组件信息,权限信息等。并将这些信息序列化到磁盘和内存中,以备打开时调用。
    • 准备 App 在 VA 沙箱环境中的私有空间,并且复制一些必要的 apk 和 so libs。
    • 最后通知前台安装完成。

    VPackage

    public class VPackage implements Parcelable {
    
        public static final Creator<VPackage> CREATOR = new Creator<VPackage>() {
            @Override
            public VPackage createFromParcel(Parcel source) {
                return new VPackage(source);
            }
    
            @Override
            public VPackage[] newArray(int size) {
                return new VPackage[size];
            }
        };
        public ArrayList<ActivityComponent> activities;
        public ArrayList<ActivityComponent> receivers;
        public ArrayList<ProviderComponent> providers;
        public ArrayList<ServiceComponent> services;
        public ArrayList<InstrumentationComponent> instrumentation;
        public ArrayList<PermissionComponent> permissions;
        public ArrayList<PermissionGroupComponent> permissionGroups;
        public ArrayList<String> requestedPermissions;
        public ArrayList<String> protectedBroadcasts;
        public ApplicationInfo applicationInfo;
        public Signature[] mSignatures;
        public Bundle mAppMetaData;
        public String packageName;
        public int mPreferredOrder;
        public String mVersionName;
        public String mSharedUserId;
        public ArrayList<String> usesLibraries;
        public int mVersionCode;
        public int mSharedUserLabel;
        // Applications hardware preferences
        public ArrayList<ConfigurationInfo> configPreferences = null;
        // Applications requested features
        public ArrayList<FeatureInfo> reqFeatures = null;
        public Object mExtras;
    ..........................
    

    可以看到 VPackage 几乎保存了 apk 中所有的关键信息,尤其是组件的数据结构会在 app 在 VA 中运行的时候给 VAMS,VPMS 这些 VAService 提供 apk 的组件信息。

    Client App 启动

    首先要了解的是 Android App 是组件化的,Apk 其实是 N 多个组件的集合,以及一些资源文件和 Assert,App 的启动有多种情况,只要在一个新的进程中调起了 apk 中任何一个组件,App 将被初始化,Application 将被初始化。

    Activity 启动

    Hook startActivity(重定位 Intent 到 StubActivity)

    首先在 Client App 中,startActivity 方法必须被 Hook 掉,不然 Client App 调用 startActivity 就直指外部 Activity 去了。

    这部分的原理其实与 DroidPlugin 大同小异,由于插件(Client App)中的 Activity 是没有在 AMS 中注册的,AMS 自然无法找到我们的插件 Activity。

    Hook 的目的是我们拿到用户的 Intent,把他替换成指向 VA 在 Menifest 中站好坑的 StubActivity 的 Intent,然后将原 Intent 当作 data 打包进新 Intent 以便以后流程再次进入 VA 时恢复。

    Hook 的方法就是用我们动态代理生成的代理类对象替换系统原来的 ActiityManagerNative.geDefault 对象。

    public void inject() throws Throwable {
            if (BuildCompat.isOreo()) {
                //Android Oreo(8.X)
                Object singleton = ActivityManagerOreo.IActivityManagerSingleton.get();
                Singleton.mInstance.set(singleton, getInvocationStub().getProxyInterface());
            } else {
                if (ActivityManagerNative.gDefault.type() == IActivityManager.TYPE) {
                    ActivityManagerNative.gDefault.set(getInvocationStub().getProxyInterface());
                } else if (ActivityManagerNative.gDefault.type() == Singleton.TYPE) {
                    Object gDefault = ActivityManagerNative.gDefault.get();
                    Singleton.mInstance.set(gDefault, getInvocationStub().getProxyInterface());
                }
            }
            BinderInvocationStub hookAMBinder = new BinderInvocationStub(getInvocationStub().getBaseInterface());
            hookAMBinder.copyMethodProxies(getInvocationStub());
            ServiceManager.sCache.get().put(Context.ACTIVITY_SERVICE, hookAMBinder);
        }
    

    好了,下面只要调用到 startActivity 就会被 Hook 到 call。
    这个函数需要注意以下几点:

    • VA 有意将安装和卸载 APP 的请求重定向到了卸载 VA 内部 APK 的逻辑。
    • resolveActivityInfo 调用到了 VPM 的 resolveIntent,最终会远程调用到 VPMS 的 resolveIntent,然后 VPMS 就会去查询 VPackage 找到目标 Activity 并将信息附加在 ResolveInfo 中返回 VPM。
    • 最后也是最重要的一点,startActivity 会调用到 VAM.startActivity,同样最终会远程调用到 VAMS 的 startActivity。
    static class StartActivity extends MethodProxy {
    
            private static final String SCHEME_FILE = "file";
            private static final String SCHEME_PACKAGE = "package";
    
            @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 = VUserHandle.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()))) {
                    if (handleInstallRequest(intent)) {
                        return 0;
                    }
                } else if ((Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())
                        || Intent.ACTION_DELETE.equals(intent.getAction()))
                        && "package".equals(intent.getScheme())) {
    
                    if (handleUninstallRequest(intent)) {
                        return 0;
                    }
                }
    
                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];
                }
                // chooser 调用选择界面
                if (ChooserActivity.check(intent)) {
                    intent.setComponent(new ComponentName(getHostContext(), ChooserActivity.class));
                    intent.putExtra(Constants.EXTRA_USER_HANDLE, userId);
                    intent.putExtra(ChooserActivity.EXTRA_DATA, options);
                    intent.putExtra(ChooserActivity.EXTRA_WHO, resultWho);
                    intent.putExtra(ChooserActivity.EXTRA_REQUEST_CODE, requestCode);
                    return method.invoke(who, args);
                }
    
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                    args[intentIndex - 1] = getHostPkg();
                }
    
                //解析 ActivityInfo
                ActivityInfo activityInfo = VirtualCore.get().resolveActivityInfo(intent, userId);
                if (activityInfo == null) {
                    VLog.e("VActivityManager", "Unable to resolve activityInfo : " + 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, VUserHandle.myUserId());
                if (res != 0 && resultTo != null && requestCode > 0) {
                    VActivityManager.get().sendActivityResult(resultTo, resultWho, requestCode);
                }
    
                // 处理 Activity 切换动画,因为此时动画还是 Host 的 Stub 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;
            }
    
    
            private boolean handleInstallRequest(Intent intent) {
                IAppRequestListener listener = VirtualCore.get().getAppRequestListener();
                if (listener != null) {
                    Uri packageUri = intent.getData();
                    if (SCHEME_FILE.equals(packageUri.getScheme())) {
                        File sourceFile = new File(packageUri.getPath());
                        try {
                            listener.onRequestInstall(sourceFile.getPath());
                            return true;
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
    
                }
                return false;
            }
    
            private boolean handleUninstallRequest(Intent intent) {
                IAppRequestListener listener = VirtualCore.get().getAppRequestListener();
                if (listener != null) {
                    Uri packageUri = intent.getData();
                    if (SCHEME_PACKAGE.equals(packageUri.getScheme())) {
                        String pkg = packageUri.getSchemeSpecificPart();
                        try {
                            listener.onRequestUninstall(pkg);
                            return true;
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
    
                }
                return false;
            }
    
        }
    

    逻辑最终走到 VAMS 后,VAMS 调用 ActivityStack.startActivityLocked

    // 参考 framework 的实现
        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);
    
            // 根据 Flag 寻找合适的 Task
            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;
        }
    

    然后 call 到了 startActivityProcess ,这就是真正替换 Intent 的地方

    private Intent startActivityProcess(int userId, ActivityRecord sourceRecord, Intent intent, ActivityInfo info) {
            intent = new Intent(intent);
            // 获得 Activity 对应的 ProcessRecorder,如果没有则表示这是 Process 第一个打开的组件,需要初始化 Application
            ProcessRecord targetApp = mService.startProcessIfNeedLocked(info.processName, userId, info.packageName);
            if (targetApp == null) {
                return null;
            }
            Intent targetIntent = new Intent();
    
            // 根据 Client App 的 PID 获取 StubActivity
            String stubActivityPath = fetchStubActivity(targetApp.vpid, info);
    
            Log.e("gy", "map activity:" + intent.getComponent().getClassName() + " -> " + stubActivityPath);
    
            targetIntent.setClassName(VirtualCore.get().getHostPkg(), stubActivityPath);
            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);
            saveInstance.saveToIntent(targetIntent);
            return targetIntent;
        }
    

    fetchStubActivity 会根据相同的进程 id 在 VA 的 Menifest 中找到那个提前占坑的 StubActivity

    private String fetchStubActivity(int vpid, ActivityInfo targetInfo) {
    
            boolean isFloating = false;
            boolean isTranslucent = false;
            boolean showWallpaper = false;
            try {
                int[] R_Styleable_Window = R_Hide.styleable.Window.get();
                int R_Styleable_Window_windowIsTranslucent = R_Hide.styleable.Window_windowIsTranslucent.get();
                int R_Styleable_Window_windowIsFloating = R_Hide.styleable.Window_windowIsFloating.get();
                int R_Styleable_Window_windowShowWallpaper = R_Hide.styleable.Window_windowShowWallpaper.get();
    
                AttributeCache.Entry ent = AttributeCache.instance().get(targetInfo.packageName, targetInfo.theme,
                        R_Styleable_Window);
                if (ent != null && ent.array != null) {
                    showWallpaper = ent.array.getBoolean(R_Styleable_Window_windowShowWallpaper, false);
                    isTranslucent = ent.array.getBoolean(R_Styleable_Window_windowIsTranslucent, false);
                    isFloating = ent.array.getBoolean(R_Styleable_Window_windowIsFloating, false);
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
    
            boolean isDialogStyle = isFloating || isTranslucent || showWallpaper;
    
            // 根据在 Menifest 中注册的 pid
            if (isDialogStyle) {
                return VASettings.getStubDialogName(vpid);
            } else {
                return VASettings.getStubActivityName(vpid);
            }
        }
    

    这里需要特别注意,VA 占坑的方式和 DroidPlugin 有些小不同,VA 没有为每个 Process 注册多个 Activity,也没有为不同的启动方式注册多个 Activity,这里确实是有改进的。
    这里根本原因是因为 VA 对 VAMS 实现的更为完整,实现了原版 AMS 的基本功能,包括完整的 Recorder 管理,Task Stack 管理等,这样的话 StubActivity 的唯一作用便是携带 Client App 真正的 Intent 交给 VAMS 处理。这套机制衍生到其他的组件也是一样的。

    最终, VAMS 调用原生 AM 的 startActivity 向真正的 AMS 发送替换成 StubActivity 的伪造 Intent。

    private void startActivityFromSourceTask(TaskRecord task, Intent intent, ActivityInfo info, String resultWho,
                                                 int requestCode, Bundle options) {
            ActivityRecord top = task.activities.isEmpty() ? null : task.activities.get(task.activities.size() - 1);
            if (top != null) {
                if (startActivityProcess(task.userId, top, intent, info) != null) {
                    realStartActivityLocked(top.token, intent, resultWho, requestCode, options);
                }
            }
        }
    
    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);
        }
    

    恢复原 Intent 重定向到原 Activity

    当 AMS 收到伪装的 Intent 后,就会找到 StubActivity,这时流程回到 VA 里的主线程中的消息队列中。
    Hook 过程就是用我们自己的 Handler 替换 android.os.Handler.mCallback 因为主线程在这里分发一些操作。

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

    handlerMessage 判断是 LAUNCH_ACTIVITY Action 后直接调用了 handlerLaunchActivity 方法,和原版其实很像。

    private boolean handleLaunchActivity(Message msg) {
                Object r = msg.obj;
                Intent stubIntent = ActivityThread.ActivityClientRecord.intent.get(r);
                // 获取原版 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 还没初始化,代表 App 刚刚启动第一个组件
                if (VClientImpl.get().getToken() == null) {
                    VActivityManager.get().processRestarted(info.packageName, info.processName, saveInstance.userId);
                    getH().sendMessageAtFrontOfQueue(Message.obtain(msg));
                    return false;
                }
                // AppBindData 为空,则 App 信息不明
                if (!VClientImpl.get().isBound()) {
                    // 初始化并绑定 Application
                    VClientImpl.get().bindApplication(info.packageName, info.processName);
                    getH().sendMessageAtFrontOfQueue(Message.obtain(msg));
                    return false;
                }
    
                // 获取 TaskId
                int taskId = IActivityManager.getTaskForActivity.call(
                        ActivityManagerNative.getDefault.call(),
                        token,
                        false
                );
    
                // 1.将 ActivityRecorder 加入 mActivities 2.通知服务端 VAMS Activity 创建完成
                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);
                // 将 Host Stub Activity Intent 替换为原版 Intent
                ActivityThread.ActivityClientRecord.intent.set(r, intent);
                // 同上
                ActivityThread.ActivityClientRecord.activityInfo.set(r, info);
                return true;
            }
    

    最后成功从 StubActivity Intent 还原出来的原版 Intent 被继续交给原生的 AM

    // 将 Host Stub Activity Intent 替换为原版 Intent
    ActivityThread.ActivityClientRecord.intent.set(r, intent);
    // 同上
    ActivityThread.ActivityClientRecord.activityInfo.set(r, info);
    

    最后一个 Hook 点在 Instrumentation.callActivityOnCreate:
    因为 AMS 实际上启动的是 StubActivity 的关系,真正的 Activity 的一些信息还不是其真正的信息,比如主题之类的,所以需要在这个时机修复一下,选择这个时间修复的原因也是因为 Activity 已经被 new 出来了,而且资源已经准备完毕。

    public void callActivityOnCreate(Activity activity, Bundle icicle) {
            VirtualCore.get().getComponentDelegate().beforeActivityCreate(activity);
            IBinder token = mirror.android.app.Activity.mToken.get(activity);
            ActivityClientRecord r = VActivityManager.get().getActivityRecord(token);
            // 替换 Activity 对象
            if (r != null) {
                r.activity = activity;
            }
            ContextFixer.fixContext(activity);
            ActivityFixer.fixActivity(activity);
            ActivityInfo info = null;
            if (r != null) {
                info = r.info;
            }
            // 设置主题和屏幕纵横控制
            if (info != null) {
                if (info.theme != 0) {
                    activity.setTheme(info.theme);
                }
                if (activity.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
                        && info.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                    activity.setRequestedOrientation(info.screenOrientation);
                }
            }
            super.callActivityOnCreate(activity, icicle);
            VirtualCore.get().getComponentDelegate().afterActivityCreate(activity);
        }
    

    引用(如有侵权,即刻删除):
    Android虚拟化引擎VirtualApp探究
    Android 双开沙箱 VirtualApp 源码分析

    相关文章

      网友评论

        本文标题:VirtualApp 框架浅析

        本文链接:https://www.haomeiwen.com/subject/llpgcctx.html