introduce

First of all, VirtualApp is not VirtualApk, didi’s open source plug-in framework.

VirtualApp is one of the darker things, it can create a virtual space, you can install, start and uninstall APK in the virtual space, all isolated from the outside, like a sandbox, APK does not need to be installed outside.

A profound

When you start VirtualApp, the interface looks like this.

The APK that has been installed through VirtualApp is displayed. You can choose to install the APK directly from the SD card or existing APK in the system. After installation, just click on the icon to open the APP as if it was installed externally. On a brief trial run, Nexus 6P, Android 7.0, Zhihu and Weibo all worked fine, and ran at the same speed as external installations. And still can install many same application, realize many open effect.

From a

First of all, let’s take a look at its progress information after opening the APP,

u0_a200 22932 494 1034396 84008 SyS_epoll_ 0000000000 S io.virtualapp u0_a200 22955 494 1064388 70408 SyS_epoll_ 0000000000 S io.virtualapp:x u0_a200 22983 494 1530416 266948 0000000000 R com.zhihu.android u0_a200 23320 494 1410736 214680 SyS_epoll_ 0000000000 S com.sina.weibo u0_a200 23387 494 1174928 76848 SyS_epoll_ 0000000000 S com.sina.weibo.image u0_a200 23415 494 1186076 81648 SyS_epoll_ 0000000000 S com.sina.weibo:remote u0_a200 23455 494 1173888 76572 SyS_epoll_ 0000000000 S com.sina.weibo.imageservant u0_a200 24028 494 1182780 74408 SyS_epoll_ 0000000000 S com.sina.weibo.servant u0_a200 24425 494 1027636 66116 SyS_epoll_ 0000000000 S com.taobao.sophix_android u0_a200 24492  494 1334412 174708 SyS_epoll_ 0000000000 S com.zhihu.androidCopy the code

All applications opened by ViralApp belong to the same uid as VirtalApp: u0_a200. VirtualApp has two processes: IO. VirtualApp and IO. VirtualApp 😡

IO. Virtualapp is the visible interface and also responsible for the management and installation of APK packages. IO. Virtualapp 😡 is a single service process that virtualizes some system services. We’ll come back to that later.

Here to install weibo, for example, take a look at it’s process of memory space, you can see all relative path is mapped to the/data/data/IO virtualapp/virtual below,

. . b6d0f000-b7017000 r--p 00000000 fd:00 410335 /data/data/io.virtualapp/virtual/data/user/0/com.sina.weibo/Plugin/com.weibo.app.movie/dalvik-cache/base-1.dex b7017000-b71d4000 r-xp 00308000 fd:00 410335 /data/data/io.virtualapp/virtual/data/user/0/com.sina.weibo/Plugin/com.weibo.app.movie/dalvik-cache/base-1.dex ... . bb745000-bb831000 r--p 00000000 fd:00 410247 /data/data/io.virtualapp/virtual/data/user/0/com.sina.weibo/code_cache/secondary-dexes/composer1312fd1cbada0e5074c9f9961 b16aefb.dex bb831000-bb8f0000 r-xp 000ec000 fd:00 410247 /data/data/io.virtualapp/virtual/data/user/0/com.sina.weibo/code_cache/secondary-dexes/composer1312fd1cbada0e5074c9f9961 b16aefb.dex ... . bf448000-bf978000 r-xp 00000000 fd:00 410129 /data/data/io.virtualapp/virtual/data/app/com.sina.weibo/lib/libweiboffmpeg.so bf978000-bf979000 ---p 00000000 00:00 0 bf979000-bf9ab000 r--p 00530000 fd:00 410129 /data/data/io.virtualapp/virtual/data/app/com.sina.weibo/lib/libweiboffmpeg.so bf9ab000-bf9af000 rw-p 00562000 fd:00 410129 /data/data/io.virtualapp/virtual/data/app/com.sina.weibo/lib/libweiboffmpeg.so ... . c335a000-c33a9000 r-xp 00000000 fd:00 410127 /data/data/io.virtualapp/virtual/data/app/com.sina.weibo/lib/libweiboplayer.so c33aa000-c33ad000 r--p 0004f000 fd:00 410127 /data/data/io.virtualapp/virtual/data/app/com.sina.weibo/lib/libweiboplayer.so c33ad000-c33ae000 rw-p 00052000 fd:00 410127 /data/data/io.virtualapp/virtual/data/app/com.sina.weibo/lib/libweiboplayer.so ... .Copy the code

So we’ve remapped the path. Let’s take a look at the code.

Injection logic

Virtualizing an APP means not installing the APP directly into the system, but at the same time providing everything the APP needs to run, so that it can be fooled into thinking it is running on a normal system. You need to virtualize system services and related paths.

Among them, the virtualization of system services is mainly realized by injecting a large number of framework components.

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

The injection process is happened in IO. Virtualapp. VApp. AttachBaseContext, therefore, every time they start a child process execution here. This distinguishes between isMainProcess (IO. Virtualapp) or isServerProcess (IO. Virtualapp :x) or isVAppProcess (installed APP) for different injections. The most injections are still in the installed APP process.

As you can see, all the stubs of addInjector in injectInternal previously called its Inject method.

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()); }Copy the code

This enables the replacement of various system classes.

At the bottom, VirtualApp also implements a replacement of the original path, passing in all paths that need to be redirected at the Java layer.

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(); }Copy the code

These paths are eventually added to a mapping table in the JNI layer

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)) ); }}Copy the code

It then hooks all the C library functions that, when called, replace the path with the new one. Since the hook is a liBC function, the Java layer and virtual machine file access will eventually be called here, which will be affected.

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
}Copy the code

Take the chmod function for example,

// 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;
}Copy the code

As you can see, it takes the old pathname, match_redirected_path, finds the mapped new path, and then calls it with syscall, which redirects all paths.

Start the principle

Finally, let’s look at how an APP is launched in VirtualApp.

Launching an app is in the launch method of LoadingActivity

public static void launch(Context context, String packageName, int userId) {
    Intent intent = VirtualCore.get().getLaunchIntent(packageName, userId);
    if (intent != null) {
        Intent loadingPageIntent = new Intent(context, LoadingActivity.class);
        loadingPageIntent.putExtra(PKG_NAME_ARGUMENT, packageName);
        loadingPageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        loadingPageIntent.putExtra(KEY_INTENT, intent);
        loadingPageIntent.putExtra(KEY_USER, userId);
        context.startActivity(loadingPageIntent);
    }
}Copy the code

It is then called to the startActivity of the VActivityManager

public int startActivity(Intent intent, ActivityInfo info, IBinder resultTo, Bundle options, String resultWho, int requestCode, int userId) { try { return getService().startActivity(intent, info, resultTo, options, resultWho, requestCode, userId); } catch (RemoteException e) { return VirtualRuntime.crash(e); }}Copy the code

Binder will eventually find the VActivityManagerService for the IO. Virtualapp 😡 process.

@Override public int startActivity(Intent intent, ActivityInfo info, IBinder resultTo, Bundle options, String resultWho, int requestCode, int userId) { synchronized (this) { return mMainStack.startActivityLocked(userId, intent, info, resultTo, options, resultWho, requestCode); }}Copy the code

Service side will call startActivity ActivityStack startActivityLocked, then call to startActivityInNewTaskLocked method

private void startActivityInNewTaskLocked(int userId, Intent intent, ActivityInfo info, Bundle options) { Intent destIntent = startActivityProcess(userId, null, intent, info); DestIntent if (destIntent! = null) { destIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); destIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); destIntent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // noinspection deprecation destIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); } else { destIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { VirtualCore.get().getContext().startActivity(destIntent, options); } else { VirtualCore.get().getContext().startActivity(destIntent); }}}Copy the code

The intent is replaced at startActivityProcess

private Intent startActivityProcess(int userId, ActivityRecord sourceRecord, Intent intent, ActivityInfo info) { intent = new Intent(intent); ProcessRecord targetApp = mService.startProcessIfNeedLocked(info.processName, userId, info.packageName); if (targetApp == null) { return null; } Intent targetIntent = new Intent(); => 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); saveInstance.saveToIntent(targetIntent); return targetIntent; }Copy the code

TargetIntent. SetClassName will be set to VirtualApp package name, at the same time fetchStubActivity used to retrieve the Activity of pile

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; if (isDialogStyle) { return VASettings.getStubDialogName(vpid); } else { return VASettings.getStubActivityName(vpid); }}Copy the code

Eventually return here is VASettings getStubActivityName

     public static String STUB_ACTIVITY = StubActivity.class.getName();

    public static String getStubActivityName(int index) {
        return String.format(Locale.ENGLISH, "%s$C%d", STUB_ACTIVITY, index);
    }Copy the code

Visible, finally return to the Activity name is com lody. Virtual. The client. The stub. StubActivity $C? ,? Indicates a specific number.

And they’re pre-written in AndroidManifest.

<activity android:name="com.lody.virtual.client.stub.StubActivity$C0" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|scr eenSize|smallestScreenSize|fontScale" android:process=":p0" android:taskAffinity="com.lody.virtual.vt" android:theme="@style/VATheme" /> <activity android:name="com.lody.virtual.client.stub.StubActivity$C1" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|scr eenSize|smallestScreenSize|fontScale" android:process=":p1" android:taskAffinity="com.lody.virtual.vt" android:theme="@style/VATheme" /> <activity android:name="com.lody.virtual.client.stub.StubActivity$C2" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|scr eenSize|smallestScreenSize|fontScale" android:process=":p2" android:taskAffinity="com.lody.virtual.vt" android:theme="@style/VATheme" /> <activity android:name="com.lody.virtual.client.stub.StubActivity$C3" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|scr eenSize|smallestScreenSize|fontScale" android:process=":p3" android:taskAffinity="com.lody.virtual.vt" android:theme="@style/VATheme" />Copy the code

That way, startActivity will eventually start up in StubActivity here.

And each time it creates a new child process, p, okay? Started a process in, will perform to the IO. Virtualapp. VApp. AttachBaseContext, in this way, will go just mentioned injectInternal method, implement all injection logic, all interacts with the system to replace. StubActivity’s onCreate method, though declared in the code, never executes:

public abstract class StubActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // The savedInstanceState's classLoader is not exist.
        super.onCreate(null);
        finish();
        // It seems that we have conflict with the other Android-Plugin-Framework.
        Intent stubIntent = getIntent();
        // Try to acquire the actually component information.
        StubActivityRecord r = new StubActivityRecord(stubIntent);
        if (r.intent != null) {
            if (TextUtils.equals(r.info.processName, VirtualRuntime.getProcessName()) && r.userId == VUserHandle.myUserId()) {
                // Retry to inject the HCallback to instead of the exist one.
                InvocationStubManager.getInstance().checkEnv(HCallbackStub.class);
                Intent intent = r.intent;
                startActivity(intent);
            } else {
                // Start the target Activity in other process.
                VActivityManager.get().startActivity(r.intent, r.userId);
            }
        }
    }

    public static class C0 extends StubActivity {
    }

    public static class C1 extends StubActivity {
    }

    public static class C2 extends StubActivity {
    }

    public static class C3 extends StubActivity {
    }
    
    ... ...
}Copy the code

After the substitution, the execution flow is changed, and you can see that the injected code can be found before the actual Application of the APP is executed:

com.taobao.sophix_app.MyApplication.onCreate(MyApplication.java:33)
android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1024)
com.lody.virtual.client.hook.delegate.InstrumentationDelegate.callApplicationOnCreate(InstrumentationDelegate.java:225)
com.lody.virtual.client.hook.delegate.AppInstrumentation.callApplicationOnCreate(AppInstrumentation.java:137)
com.lody.virtual.client.VClientImpl.bindApplicationNoCheck(VClientImpl.java:312)
com.lody.virtual.client.VClientImpl.bindApplication(VClientImpl.java:192)
com.lody.virtual.client.hook.proxies.am.HCallbackStub.handleLaunchActivity(HCallbackStub.java:114)
com.lody.virtual.client.hook.proxies.am.HCallbackStub.handleMessage(HCallbackStub.java:71)
android.os.Handler.dispatchMessage(Handler.java:98)
android.os.Looper.loop(Looper.java:154)
android.app.ActivityThread.main(ActivityThread.java:6077)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)Copy the code

thinking

We have roughly analyzed how VirtualApp works, of course, there are a lot of details are not covered, interested students can do their own research.

In my opinion, VirtualApp can load features from external APK, which can inspire endless imagination.

For simple unhulls, you can use VirtualApp to load the APK and dump the unhulled DEX files in memory during runtime.

In addition, since the Sophix hotfix is also non-invasive, if you integrate Sophix into VirtualApp, you can load a patch before starting the APP to replace some of the classes in the original APP, enabling changes to the logic of the original APP without repackaging the original APK.