The foreword 0.

VirtualApp is a virtual framework running on the Android operating system that allows you to create a virtual space and run other applications in this virtual space with full control over the application.

This article will be with the source code of VirtualApp, a simple introduction to the basic principle of VirtualApp without installation to start the Activity of APK, as well as the related detection scheme.

1. Noun conventions

noun Referred to as” note
VirtualApp VA
VActivityManager VAM VirtualApp’s own ActivityManager
VActivityManagerService VAMS VirtualApp’s own ActivityMangerService
VPackageManager VPM VirtualApp PackageManager
VPackageManagerService VPMS VirtualApp’s own PackageManagerService
ActivityManager AM The android SDK ActivityManger
ActivityManagerService AMS The android SDK ActivityMangerService
PackageManager PM The android SDK PackageManager
PackageManagerService PMS The android SDK PackageManagerService
Virtual application An application running in VirtualApp

2. A few questions

To start an APK Activity in a virtual space without installation, I consider the following issues:

  1. How to parse the information of four components in APK package?
  2. How to solve the problem of loading code and resources when starting an application?
  3. After starting an application, how do I start the four components?
  4. After starting the application, how to achieve the full Hook ability of the app?

Let’s examine the Installation and startup process of VirtualApp to answer these questions.

3. The installation

Installing a virtual application inside VA will eventually call the installPackage method of VPMS, skimping the compatibility code and keeping the key flow:

// VPackageInstallerService public synchronized InstallResult installPackage(String path, int flags, boolean notify) { ... File packageFile = new File(path); . VPackage pkg = null; Try {// 1. Echo create android.pm.PackageParser object, Resolve the apk package of the four major components and other related information / / stored in VPackage object PKG = PackageParserEx parsePackage (packageFile); } catch (Throwable e) { e.printStackTrace(); }... / / 2. The copy so libraries to the/data/data/IO virtualapp/virtual / $packageName/lib directory File appDir = VEnvironment.getDataAppPackageDirectory(pkg.packageName); File libDir = new File(appDir, "lib"); NativeLibraryHelperCompat.copyNativeBinaries(new File(path), libDir); . / / 3. Keep the front through the android. PM. PackageParser parsing out information PackageParserEx. SavePackageCache (PKG); PackageCacheManager.put(pkg, ps); . / / 4. Copy the apk to the/data/data/IO virtualapp/virtual / $packageName directory File privatePackageFile = new File (appDir, "base.apk"); FileUtils.copyFile(packageFile, privatePackageFile); }Copy the code

As you can see, installing virtual applications inside the VA, VA does a few things

  1. Reflection to createandroid.pm.PackageParserExamples, analyze the four components of virtual application APK package and other information;
  2. Copy the so library to the virtual path of the corresponding package;
  3. Save and persist part of APK package data to hard disk;
  4. Copy the APK package to the corresponding virtual path.

In the internal installation of virtual application, all core logic to reflection to create android.pm.PackageParser instance implementation, VirtualApp just do so file and APK file copy, and persistent information.

The information to be persisted includes key information such as appId, package name, and so library path, so that you can use the Android.pm. PackageParser instance to parse internal application information when starting up next time.

Through the VA internal installation logic, we can get the information of the four components in the virtual application, and then get the launching Intent, and start to have the launching capability. Let’s look at how VirtualApp handles the startup logic.

4. Start

VA has some logic embedded in it when it starts. In a word, VA runs virtual applications in its own process by injecting instances + dynamic proxy + four component pegs. Let’s take a look at the VA embedded code:

4.0. Inject objects

One of the keys to starting the virtual application is to replace the instance of the mCallback field in ActivityThread.mh:

// HCallbackStub.java public class HCallbackStub implements Handler.Callback, IInjector { @Override public void inject() throws Throwable { otherCallback = getHCallback(); / / the this, namely HCallbackStub into ActivityThread instances of mH mCallback field in the mirror.. Android OS. Handler. MCallback. Set (getH (), this); } private static Handler getH() {return activityThread.mh. get(virtualCore.mainThread ());} private static Handler getH() {return activityThread.mh. get(virtualCore.mainThread ()); } private static Handler.Callback getHCallback() { try { Handler handler = getH(); / / get the singleton ActivityThread mCallack examples of instances of mH return mirror.. Android OS. Handler. MCallback. Get (Handler); } catch (Throwable e) { e.printStackTrace(); } return null; } @Override public boolean handleMessage(Message msg) { ... }}Copy the code

As you can see, the VA through reflection, into your own Handler. The Callback to ActivityThread. MH. MCallback, in order to achieve

  1. Intercept the message
  2. Process the message
  3. Determines whether to forward to mH

The role of.

The return value of handleMessage can be used to determine whether to forward the handleMessage to mH.

// Handler.java public void dispatchMessage(@NonNull Message msg) { if (msg.callback ! = null) { handleCallback(msg); } else { if (mCallback ! If (McAllback.handlemessage (MSG)) {if (McAllback.handlemessage (MSG)) {if (McAllback.handlemessage (MSG)); } } handleMessage(msg); }}Copy the code

4.1. Dynamic proxy

Injection instances can intercept methods because the Handler provides a Callback interface that allows the developer to control its execution. The reality may not be so rosy and universal. A dynamic proxy is more general, it can proxy interface methods and return you an instance of the proxy. Let’s see what VA does with dynamic proxies. Take the Hook Activity start as an example:

// MethodProxies.java class MethodProxies { static class StartActivity extends MethodProxy { @Override public String getMethodName() { return "startActivity"; } public Object call(Object who, Method method, Object... args) throws Throwable { ... int res = VActivityManager.get().startActivity(intent, activityInfo, resultTo, options, resultWho, requestCode, VUserHandle.myUserId()); . return res; }}}Copy the code

A lot of details have been omitted here, leaving only the most important parts. As you can see, the startActivity method is intercepted and the logic is forwarded to the VAM. Which instance of startActivity is intercepted? Look at the initialization process:

// ActivityManagerStub. Java // Runtime level annotations, runtime instantiation of all classes in MethodProxies, Add it to a table @Inject(MethodProxies. Class) public Class ActivityManagerStub extends MethodInvocationProxy<MethodInvocationStub<IInterface>> { @Override public void inject() throws Throwable { if (BuildCompat isOreo ()) {/ / Android Oreo (8 X) / / IActivityManagerSingleton Object Object of ActivityManager singleton = ActivityManagerOreo.IActivityManagerSingleton.get(); // Replace this object's mInstance with our own proxy object, Namely ProxyInterface Singleton. MInstance. Set (Singleton, getInvocationStub () getProxyInterface ()); } else {the if (ActivityManagerNative. GDefault. Type () = = IActivityManager. Type) {/ / in the same way ActivityManagerNative.gDefault.set(getInvocationStub().getProxyInterface()); } else if (ActivityManagerNative. GDefault. Type () = = Singleton. Type) {/ / the same Object gDefault = ActivityManagerNative.gDefault.get(); Singleton.mInstance.set(gDefault, getInvocationStub().getProxyInterface()); }}}}Copy the code

As you can see, here is actually the singleton within the proxy objects into ActivityManager IActivityManagerSingleton mInstance (below 8.0 is gDefault) field. By injecting a proxy object, the specified method is implemented:

  1. Interception;
  2. Decide whether to forward;

The purpose of.

4.2. Four components of pile insertion

The four components of a virtual application must not be declared in the host application’s AndroidManifest. One problem with this is that starting a component that is not declared in AndroidManifest will cause the current process to crash.

VA’s solution to this problem is to declare in AndroidManifest some of the four components used for piling, running in 100 processes.

<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" /> ... <provider android:name="com.lody.virtual.client.stub.StubContentProvider$C0" android:authorities="${applicationId}.virtual_stub_0" android:exported="false" android:process=":p0" /> <provider android:name="com.lody.virtual.client.stub.StubContentProvider$C1" android:authorities="${applicationId}.virtual_stub_1"  android:exported="false" android:process=":p1" /> ...Copy the code

The startup of a Service is special, so there is no need to declare a Service.

Broadcast is also special, if declared in AndroidManifest, it is statically registered, so there is no declaration of the post Broadcast.

When a component in APK is started, a new component is created according to its running process, and the information of the apK component to be started is serialized in the intent of the component and sent to AMS. After the operation of AMS, the IApplicationThread of the component process is called, and the thread is cut through Handler. In activityThread.mh, go to the HCallbackStub embedded in the VirtualApp implementation. In the HCallbackStub, extract the actual component that needs to be launched from the Intent and start it.

4.3. Get an Intent that can be launched

This is sample code that calls the VA interface to start a virtual application that has been installed internally.

public void launchTargetApp(String packageName, int userId) { Intent targetIntent = VirtualCore.get().getLaunchIntent(packageName, userId) if (targetIntent ! = null) { VirtualCore.get().startActivity(intent) } }Copy the code

Let’s take a look inside the VA to get an Intent with the ability to start. Calling VirtualCore.getLaunchintent eventually leads to the queryIntentActivities method of VPMS:

public List<ResolveInfo> queryIntentActivities(Intent intent, String resolvedType, int flags, int userId) { ... ComponentName comp = Intent.getComponent (); . if (comp ! = null) { final List<ResolveInfo> list = new ArrayList<ResolveInfo>(1); // 1. In this case, the package list maintained internally by VPMS is passed. // getActivityInfo and return final activityInfo ai = getActivityInfo(comp, flags, userId); if (ai ! = null) { final ResolveInfo ri = new ResolveInfo(); ri.activityInfo = ai; list.add(ri); } return list; }... final String pkgName = intent.getPackage(); If (pkgName == null) {// 2. ActivityInfoList return mActivities. QueryIntent (Intent, resolvedType, flags, userId); } final VPackage pkg = mPackages.get(pkgName); if (pkg ! Intent_filter = intent_filter = intent_filter = intent_filter = intent_filter Just added to the package name filter conditions return mActivities. QueryIntentForPackage (intent, resolvedType, flags, PKG. Activities, userId); } return Collections.emptyList(); }Copy the code

There are three kinds of logic to get an intent

  1. Specify the component
  2. Specifies the intent-filter inside the intent (specified with intent.addcategory ())
  3. Specify the packet name + specify the intent-filter

Inside virtualCore.getLaunchintent, the package name and the category with the value LAUNCHER are specified, so the third logic is used to filter out activities that are not LAUNCHER categories based on the given VPackage. Return a List of one element to the caller.

After obtaining the Intent, the next step is to call VAMS to start the activity.

4.4. Real startup logic

Ability of Intent, the obtained has invoked VirtualCore. StartActivity, eventually call startActivity of VAMS method, hand over to start the task ActivityStack startActivityLocked.

// VActivityManagerService.java int startActivityLocked(int userId, Intent intent, ActivityInfo info, IBinder resultTo, Bundle options, String resultWho, int requestCode) { TaskRecord reuseTask = null; // Determine which existing stack of tasks can be started using the launch mode, flags in the Intent... If (reuseTask = = null) {/ / no task stack available, just start in the new task stack startActivityInNewTaskLocked (userId, intent, info, options); } else {// Move the available task stack to the foreground mam. moveTaskToFront(reusetask.taskid, 0); . // According to ActivityInfo's processName, DestIntent = startActivityProcess(userId) destIntent = startActivityProcess(userId, sourceRecord, intent, info); The activity / / / / in the corresponding process started pile end to realStartActivityLocked startActivityFromSourceTask (reuseTask destIntent, info, resultWho, requestCode, options); } return 0; } private Intent startActivityProcess(int userId, ActivityRecord sourceRecord, Intent intent, ActivityInfo info) {// Depending on the activity process, Assign a process ProcessRecord targetApp = mService. StartProcessIfNeedLocked (info. ProcessName, userId, info. PackageName); . Intent targetIntent = new Intent(); // According to the process vpID, To find the corresponding pile activity targetIntent. SetClassName (VirtualCore. The get (). GetHostPkg (), fetchStubActivity (targetApp vpid, info)); . // Store the original activityInfo in the intent corresponding to the post activity. StubActivityRecord saveInstance = new StubActivityRecord(Intent, info, sourceRecord! = null ? sourceRecord.component : null, userId); saveInstance.saveToIntent(targetIntent); return targetIntent; } private void realStartActivitiesLocked(IBinder resultTo, Intent[] intents, String[] resolvedTypes, Bundle options) { Class<? >[] types = IActivityManager.startActivities.paramList(); Object[] args = new Object[types.length]; . / / go directly to local ActivityManager start pile activity IActivityManager. StartActivities. Call (ActivityManagerNative. GetDefault. Call (), (Object[]) args); }Copy the code

There are several main things done here:

  1. Query all current stacks to see if there are any stacks available for the activity to start.
  2. If you don’t create a new one, there’s a way to tune AMS to bring this stack to the foreground;
  3. Assign a virtual PID (vpID) based on the activityInfo package name and process name.
  4. According to vpID, the corresponding pile Activity Intent is obtained.
  5. Stuff information about the activity to start into the intent;
  6. Call AMS to start the pile activity

After a series of OPERATIONS of AMS, the process corresponding to the pile activity has been started. At this point the process does the following:

  1. Go to ActivityThread’s main method and call Attach to notify AMS that I’ve started;
  2. AMS uses the IBinder token callback to tell the process that it needs to start the stub activity;
  3. Return the IApplicationThread of the stub activity process via IPC;
  4. The Handler calls back to the main thread to enter the embedded HCallbackStub

At this point the logic goes to the code that was preburied in VirtualApp. Let’s see what HCallbackStub does:

// HCallbackStub.java @Override public boolean handleMessage(Message msg) { if (LAUNCH_ACTIVITY == msg.what) { if (! handleLaunchActivity(msg)) { return true; } } return false; } private boolean handleLaunchActivity(Message msg) { Object r = msg.obj; Intent stubIntent = ActivityThread.ActivityClientRecord.intent.get(r); StubActivityRecord saveInstance = new StubActivityRecord(stubIntent); Intent intent = saveInstance.intent; ActivityInfo info = saveInstance.info; . if (! Vclientimpl.get ().isbound ()) {// The apK application has not been initialized yet. VClientImpl.get().bindApplication(info.packageName, info.processName); / / the Message is inserted into the Message queue head getH () sendMessageAtFrontOfQueue (Message. Obtain (MSG)); // do not let Handler handle return false; }... AppClassLoader = vclientimpl.get ().getClassLoader(info.applicationInfo); appClassLoader = vcliEntimpl.get ().getClassLoader(info.applicationInfo); intent.setExtrasClassLoader(appClassLoader); / / replace intent ActivityThread. ActivityClientRecord. Intent. Set (r, intent); / / replace need to start the Activity ActivityThread. ActivityClientRecord. ActivityInfo. Set (r, info); return true; }Copy the code

Here, HCallbackStub does a few things:

  1. If the application is not initialized, initialize it;
  2. Deserialize the activity that actually needs to be started;
  3. Initialize the application in APK and perform other application-level logic;
  4. Replace the Intent and activityInfo in ActivityClientRecord

At this point, the Android SDK takes over the rest of the startup logic, which is normal startup logic.

5. Problem solving

Review the questions raised above

  1. How to parse the information of four components in APK package?
  2. How to solve the problem of loading code and resources when starting an application?
  3. After starting an application, how do I start the four components?
  4. After starting the application, how to achieve the full Hook ability of the app?

5.0. Parse the APK package

Parses the information of the four major components of APK. VPMS parses the information of the four major components of APK by calling the PackageParser in the Android SDK, and then serializes the package name, apK file path, and so library file path to the local, so that the next startup can call PackageParser again. Restore the information of the four components.

5.1. Code loading

The key to solving the code loading problem is to get an instance of the LoadedApk object corresponding to the APK package.

What is LoadedApk?

The LoadedApk object is an in-memory representation of an APK file. Information about Apk files, such as the code and resources of Apk files, and even information about components such as activities and services in the code can be obtained through this object.

Before starting the four components, VirtualApp checks in HCallbackStub to see if the APK application is initialized, and initializes it if it is not:

// VClientImpl.java private void bindApplicationNoCheck(String packageName, String processName, ConditionVariable lock) { AppBindData data = new AppBindData(); // Initialize applicationInfo data.appInfo = vPackagemanager.get ().getApplicationInfo(packageName, 0, getUserId(vuid)); // initialize the processName data.processName = processName; . mBoundApplication = data; Context context = createPackageContext(data.appinfo.packagename); . Object boundApp = fixBoundApp(mBoundApplication); mBoundApplication.info = ContextImpl.mPackageInfo.get(context); // inject LoadedApk, // data.info is mboundApplication.info, / / mBoundApplication. The info is the context of mPackageInfo mirror. The android. App. ActivityThread. AppBindData. Info. Set (boundApp, data.info); . / / initializes the application within the apk mInitialApplication = LoadedApk. MakeApplication. Call (data. The info, false, null); / / into ActivityThread mInitialApplication fields in the mirror.. Android app. ActivityThread. MInitialApplication. Set (mainThread, mInitialApplication); . } private Context createPackageContext(String packageName) { try { Context hostContext = VirtualCore.get().getContext();  / / / / CONTEXT_INCLUDE_CODE representative including code CONTEXT_IGNORE_SECURITY representative ignore security warnings return hostContext. CreatePackageContext (packageName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); VirtualRuntime.crash(new RemoteException()); } throw new RuntimeException(); }Copy the code

VA initializes the application in the APK package, most crucially by calling the Android SDK’s createPackageContext method. This method takes the LoadedApk object and initializes the Application.

The four components are also similar. Take Activity as an example:

private boolean handleLaunchActivity(Message msg) { ActivityInfo info = saveInstance.info; . / / here to set up in ActivityThread activityInfo ActivityClientRecord. ActivityInfo. Set (r, info); Return true; }Copy the code

After replacing the activityInfo, it is forwarded to the mH, which is forwarded to performLaunchActivity:

/** Core implementation of activity launch. */ private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ActivityInfo aInfo = r.activityInfo; if (r.packageInfo == null) { r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE); }... ContextImpl appContext = createBaseContextForActivity(r); Activity activity = null; java.lang.ClassLoader cl = appContext.getClassLoader(); / / by this load corresponding activity class, and reflection to create activity = mInstrumentation newActivity (cl, component getClassName (), r.i ntent); }Copy the code

And then there’s CONTEXT_INCLUDE_CODE to load the LoadedApk object, and with that logic, you can use LoadedApk to load and initialize the appContext, and then the classloader of the appContext, You have the ability to load the activity class.

5.2. Resource loading

LoadedApk is also key to resource loading issues. In performLaunchActivity, a context is created for the activity:

/** Core implementation of activity launch. */ private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ActivityInfo aInfo = r.activityInfo; if (r.packageInfo == null) { r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE); }... ContextImpl appContext = createBaseContextForActivity(r); . } private ContextImpl createBaseContextForActivity(ActivityClientRecord r) { ContextImpl appContext = ContextImpl.createActivityContext( this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig); . return appContext; } static ContextImpl createActivityContext(ActivityThread mainThread, LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId, Configuration overrideConfiguration) { ... // Create the base resources for which all configuration contexts for this Activity // will be rebased upon. context.setResources(resourcesManager.createBaseTokenResources(activityToken, packageInfo.getResDir(), splitDirs, packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfiguration, compatInfo, classLoader, packageInfo.getApplication() == null ? null : packageInfo.getApplication().getResources().getLoaders())); context.mDisplay = resourcesManager.getAdjustedDisplay(displayId, context.getResources()); return context; }Copy the code

The resource is also dependent on LoadedApk, and LoadedApk has been created in advance, so the resource load can go down normally.

5.3. The hooks

Because virtual applications run within VA’s own processes, they are theoretically fully Hook capable.

6. Test

VA has the following characteristics through reading the source code:

  1. Virtual applications run in VA processes;
  2. AppDir Path Contains the appDir path of VA
  3. Some key objects were replaced with proxies;

(There are many other features of VA, just a few of which are listed here.)

Based on the three features, the following solutions can be used to check whether the current application is running under VA:

6.0. Check whether key objects are replaced

VA can control the process by replacing some key objects. Taking AM as an example, IT can detect whether AM is replaced:

private fun isAmProxy(): Boolean {
    val clazz = Class.forName("android.app.ActivityManager")
    val field = clazz.getDeclaredField("IActivityManagerSingleton")
    field.isAccessible = true
    val singleton = field.get(null)
    val singletonClazz = Class.forName("android.util.Singleton")
    val get = singletonClazz.getDeclaredMethod("get")
    val am = get.invoke(singleton)
    return am is Proxy
}
Copy the code

Under normal circumstances, an AM cannot be a proxy instance. By checking whether an AM is a Proxy, you can determine whether the environment is normal.

6.1. Detect all processes under the same UID

VA runs the virtual application under its own process. With this feature, we can iterate through the current process with the same UID. If the package name of another package appears, we can conclude that the environment is abnormal:

private fun runningBadEnvironment(): Boolean { val am = getSystemService(ACTIVITY_SERVICE) as ActivityManager val runningProcesses = am.runningAppProcesses RunningProcesses. ForEach {// Add a whitelist if (! it.processName.contains(packageName)) { return true } } return false }Copy the code

6.2. Check whether all parent paths of appDir have read and write permissions

VA places the virtual application’s dataDir directory in its dataDir subdirectory. We can use that to test.

private fun appDirAccessible() { var parent = File(dataDir.parent ? : "") var accessible = false while (parent.absolutePath ! = File.separator) { accessible = accessible or parent.canRead() parent = File(parent.parent ? : File.separator) } return accessible }Copy the code

This is done by checking whether all the parent directories of the appDir directory have read permission. If you have the read permission, the environment is abnormal.

7. To summarize

It can be seen from the above introduction that VA provides the ability of external interaction during the running of virtual applications by replacing local agents in the system and replacing instances in key processes, so that virtual applications can run in their own containers to achieve the purpose of virtualization.

8. References

  1. VirtualApp
  2. Android plug-in loading mechanism
  3. Android plug-in principle analysis — Activity lifecycle management
  4. Android apps open more practices