VirtualApp 框架淺析

Github地址:VirtualApp

簡介

VirtualApp是一款運行于Android系統(tǒng)的沙盒產(chǎn)品,可以理解為輕量級的“Android虛擬機”。其產(chǎn)品形態(tài)為高可擴展,可定制的集成SDK,您可以基于VA或者使用VA定制開發(fā)各種看似不可能完成的項目。VA目前被廣泛應用于插件化開發(fā)、無感知熱更新、云控自動化、多開、手游租號、手游手柄免激活、區(qū)塊鏈、移動辦公安全、軍隊政府保密、手機模擬信息、腳本自動化、自動化測試等技術領域。

VirtualApp可以創(chuàng)建一個虛擬空間,你可以在虛擬空間內(nèi)任意的安裝、啟動和卸載APK,這一切都與外部隔離,如同一個沙盒,APK無需在外部安裝。

VirtualApp的特有能力

  • 克隆能力
    可以克隆外部系統(tǒng)中已經(jīng)安裝的App,并在內(nèi)部運行,互不干擾。典型應用場景為App雙開。

  • 免安裝能力
    除了克隆已安裝App之外,VA可以直接在內(nèi)部安裝(外部無感知)apk,并在內(nèi)部直接運行。典型應用場景為插件化,獨立應用市場等。

  • 多開能力
    VA不僅可以“雙開”,獨特的多用戶模式支持用戶在內(nèi)部無限多開同一個App。

  • 內(nèi)外隔離能力
    VA是一個標準的沙盒,或者說“虛擬機”,提供了一整套內(nèi)部與外部的隔離機制,包括但不限于(文件隔離/組件隔離/進程通訊隔離),簡單的說VA內(nèi)部就是一個“完全獨立的空間”。在此基礎之上,稍作定制即可實現(xiàn)一部手機上的“虛擬手機”。當然您也可以發(fā)揮想象,定制成應用于數(shù)據(jù)加密,數(shù)據(jù)隔離,隱私保護,企業(yè)管理的應用系統(tǒng)。

  • 對于內(nèi)部App的完全控制能力
    VA對于內(nèi)部的App具有完全的監(jiān)控和控制能力,這點在未Root的外部環(huán)境中是絕對無法實現(xiàn)的。

運行機制

首先,我們來看一下它在開啟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 作為一個單獨的服務進程,虛擬了一些系統(tǒng)服務。

以這里安裝的虎牙直播為例,查看一下它的進程的內(nèi)存空間,可以看到相關路徑全都被映射到了/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

可見,這里面對路徑做過了重新映射。

注入邏輯

要想實現(xiàn)對一個APP的虛擬化,就是不直接把APP安裝進系統(tǒng),同時又要提供APP運行過程中所需的一切,從而可以讓它誤以為自己是運行在正常系統(tǒng)中。這里就需要實現(xiàn)系統(tǒng)服務的虛擬化和相關路徑的虛擬化。

其中,系統(tǒng)服務的虛擬化主要靠注入大量framework組件來實現(xiàn)的。

@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());
          }
  }
}

這個注入過程是發(fā)生在io.virtualapp.VApp.attachBaseContext中,因此,每次啟動一個子進程都會執(zhí)行到這里,這會區(qū)分是isMainProcess(io.virtualapp)或者isServerProcess(io.virtualapp:x)或者isVAppProcess(被安裝APP)來進行不同的注入,可以看到,注入最多的還是在被安裝APP的進程中。

可以看到,之前在injectInternal 中addInjector的所有Stub都會調(diào)用它的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());
}

由此實現(xiàn)對各個系統(tǒng)類的替換。

而在底層,VirtualApp還實現(xiàn)了對原本路徑的替換,在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庫函數(shù),這些函數(shù)在調(diào)用的時候,就會替換路徑為新路徑。由于hook的是libc的函數(shù),java層和虛擬機的文件訪問最終也會調(diào)用到這里,從而受到影響。

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函數(shù)為例

// 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來調(diào)用它,這樣就實現(xiàn)了所有路徑的重定向。

運行時結構

VA 參照原生系統(tǒng) framework 仿造了一套 framework service,還有配套在 client 端的 framework 庫。

  • 系統(tǒng)原生的 framework 運作方式
    簡單來說,我們平時所用到的 app 運行空間中的 framework api 最終會通過 Binder 遠程調(diào)用到 framework service 空間的遠程服務。
    而遠程服務類似 AMS 中的 Recoder 中會持有 app 空間的 Ibinder token 句柄,通過 token 也可以讓 framework service 遠程調(diào)用到 app 空間。
  • VA 環(huán)境下framework 運作方式
    而在 VA 環(huán)境下,情況其實也是類似,只不過在 framework service 和 client app 之間還有另外一個 VA 實現(xiàn)的 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 系統(tǒng)類
            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,來使其調(diào)用到 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 {
            // 調(diào)用遠程 VAService
            return getService().installPackage(apkPath, flags);
        } catch (RemoteException e) {
            return VirtualRuntime.crash(e);
        }
    }

最終調(diào)用 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 優(yōu)化(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.");
        }

        // 是否基于系統(tǒng)的 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);

        // 如果不基于系統(tǒng),一些必要的拷貝工作
        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);
        }

        // 給上可執(zhí)行權限,5.0 之后在 SD 卡上執(zhí)行 bin 需要可執(zhí)行權限
        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);
        //發(fā)送通知 安裝完成
        if (notify) {
            notifyAppInstalled(ps, -1);
        }
        res.isSuccess = true;
        return res;
    }

APk 的安裝主要完成以下幾件事情:

  • 解析 menifest 拿到 apk 內(nèi)部信息,包括組件信息,權限信息等。并將這些信息序列化到磁盤和內(nèi)存中,以備打開時調(diào)用。
  • 準備 App 在 VA 沙箱環(huán)境中的私有空間,并且復制一些必要的 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 中所有的關鍵信息,尤其是組件的數(shù)據(jù)結構會在 app 在 VA 中運行的時候給 VAMS,VPMS 這些 VAService 提供 apk 的組件信息。

Client App 啟動

首先要了解的是 Android App 是組件化的,Apk 其實是 N 多個組件的集合,以及一些資源文件和 Assert,App 的啟動有多種情況,只要在一個新的進程中調(diào)起了 apk 中任何一個組件,App 將被初始化,Application 將被初始化。

Activity 啟動

Hook startActivity(重定位 Intent 到 StubActivity)

首先在 Client App 中,startActivity 方法必須被 Hook 掉,不然 Client App 調(diào)用 startActivity 就直指外部 Activity 去了。

這部分的原理其實與 DroidPlugin 大同小異,由于插件(Client App)中的 Activity 是沒有在 AMS 中注冊的,AMS 自然無法找到我們的插件 Activity。

Hook 的目的是我們拿到用戶的 Intent,把他替換成指向 VA 在 Menifest 中站好坑的 StubActivity 的 Intent,然后將原 Intent 當作 data 打包進新 Intent 以便以后流程再次進入 VA 時恢復。

Hook 的方法就是用我們動態(tài)代理生成的代理類對象替換系統(tǒng)原來的 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);
    }

好了,下面只要調(diào)用到 startActivity 就會被 Hook 到 call。
這個函數(shù)需要注意以下幾點:

  • VA 有意將安裝和卸載 APP 的請求重定向到了卸載 VA 內(nèi)部 APK 的邏輯。
  • resolveActivityInfo 調(diào)用到了 VPM 的 resolveIntent,最終會遠程調(diào)用到 VPMS 的 resolveIntent,然后 VPMS 就會去查詢 VPackage 找到目標 Activity 并將信息附加在 ResolveInfo 中返回 VPM。
  • 最后也是最重要的一點,startActivity 會調(diào)用到 VAM.startActivity,同樣最終會遠程調(diào)用到 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 調(diào)用選擇界面
            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);
            }

            // 調(diào)用遠程 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 調(diào)用 ActivityStack.startActivityLocked

// 參考 framework 的實現(xiàn)
    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);

        // 根據(jù) 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();

        // 根據(jù) 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 會根據(jù)相同的進程 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;

        // 根據(jù)在 Menifest 中注冊的 pid
        if (isDialogStyle) {
            return VASettings.getStubDialogName(vpid);
        } else {
            return VASettings.getStubActivityName(vpid);
        }
    }

這里需要特別注意,VA 占坑的方式和 DroidPlugin 有些小不同,VA 沒有為每個 Process 注冊多個 Activity,也沒有為不同的啟動方式注冊多個 Activity,這里確實是有改進的。
這里根本原因是因為 VA 對 VAMS 實現(xiàn)的更為完整,實現(xiàn)了原版 AMS 的基本功能,包括完整的 Recorder 管理,Task Stack 管理等,這樣的話 StubActivity 的唯一作用便是攜帶 Client App 真正的 Intent 交給 VAMS 處理。這套機制衍生到其他的組件也是一樣的。

最終, VAMS 調(diào)用原生 AM 的 startActivity 向真正的 AMS 發(fā)送替換成 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 因為主線程在這里分發(fā)一些操作。

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

handlerMessage 判斷是 LAUNCH_ACTIVITY Action 后直接調(diào)用了 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 創(chuàng)建完成
            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 被繼續(xù)交給原生的 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 已經(jīng)被 new 出來了,而且資源已經(jīng)準備完畢。

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 源碼分析

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容