Written by: Nie Wei, Byte Game China Client Team

background

VirtualApp (VA) is a sandbox (or lightweight AndorID virtual machine) product that runs on android.

Project address: github.com/asLody/Virt…

VA related functions can be introduced through github to understand, we mainly on the implementation of the VA principle to analyze, source code although boring, but there are a lot of flash points worth learning.

Operation mechanism

Let’s take a look at how it works in the VA environment (from the web map) :

VA runs in three processes:

  1. Host process, that is, VA’s own main process, child process
  2. Client App processes are various App processes running in the VA
  3. VA Server process

It can be seen from the figure above that a new layer of VA Server is added during the communication between Client App process and native Framework Services. VA services such as VAMS and VPMS are added by naming the original service with V+. They copy some of the functionality of the original framework, manage various sessions of the Client App, and communicate with the original Framework Services themselves.

In addition, we can also see that the light AM example, there are native AM, mirror.AM, VAM three, only need to know that their function is hook Client App various methods, replace method parameters (package name, UID), or boot method call to VA Server.

All services on the VA Server are provided by ServiceFetcher. The Client obtains the IBinder handle of IServiceFetcher in the Provider. Call cross-process mode, and then obtains other services to complete the call.

Why design the middle call? Let’s use the Activity example:

For those of you who have seen the source code of Activity, when we start a new Activity, it will first go through AMS, AMS will have some record registration, whether the target process needs to be created, start mode flags, etc., and finally call back to ActivityThread. Call the newActivity method of Instrumentation to complete the creation and execution of its OnCreate lifecycle.

However, we run the Client App through VA, the package name/UID is inconsistent with the VA host (and is not registered in the host list file), so the direct startup will be verified by the system service, VA through pre-registered StubActivity (package name is the host), Then hook Client App is associated with all the start methods, and the parameters in the method (here refers to the start intent) are replaced with the host. AMS only knows that StubActivity of a host is started, and finally VA intercepts the start message of mH Handler. Replace an intent with the original intent.

The multi-process framework design can make the crash of the child process not affect the running of the host process.

Of course, the general principle is true, but the reality is a little more complicated, so let’s look at the package structure:

Code package structure

  1. Android: by establishing the same directory as the android hidden classes to refer to some system, achieve the purpose of cheating compiler, such as android. Content. PM. PackageParser.
  2. Client: The environment in which the client App runs contains a large number of hook codes.
  3. Server: VA Server process related, mimics some functionality of native Framework Services.
  4. Mirror: An Android system class mirror package with the same name as the system class. It encapsulates the reflection process and can easily call some fields and methods of the system class directly.
  5. Jni: Native hook is related to vm hook and I/O redirection (redirecting I/O paths in Client App to VA internal).

Others, such as OS, handle some environmental issues and multi-user management, and Remote is a variety of serialized beans for AIDL transport.

From the operational mechanism and code package structure, you can see the basic outline of VA, which we will analyze one by one.

Source code analysis

mirror

Suppose we want to hook off Instrumentation, first take a look at the ordinary hook implementation:

Then there is the mirror implementation:

As you can see, the normal implementation method is not only tedious, but also a lot of template code, repetitive work, while mirror greatly simplifies the process, as if directly calling system methods.

Let’s look at the ActivityThread definition under the mirror:

All member variables/methods associated with the original ActivityThread used start with:

Public static Ref/RefStatic+ Member variable name/method name of the property type

The refClass. load method passes in the current Class object and the real className:

The load method encapsulates the logic of reflecting fields and maps the properties of the same name to the mirror class. The Ref/RefStatic+ attribute type also has the assignment logic of the same name.

Mirror with concise and elegant design greatly simplifies the cost of reflection injection, the use of the process only need to declare the properties, and do not need to care about its reflection details, simple to use, simple principle.

Java layer hook

VA will hook native AM to truncate some method calls to VAM, and THEN VAM will call self-implemented VAMS. Let’s briefly use the startActivity method as an example:

A native AM holds an IBinder handle to AMS. We can create a dynamic proxy object for AMS from this point:

public class ActivityManagerStub extends MethodInvocationProxy<MethodInvocationStub<IInterface>> { public ActivityManagerStub() {// Get the AMS IActivityManager interface object based on mirror, And create the dynamic proxy objects super (new MethodInvocationStub < > (ActivityManagerNative. GetDefault. Call ())); } @override public void inject() throws Throwable {if (buildCompat.isoreo ()) {//8.x Object singleton = ActivityManager.IActivityManagerSingleton.get(); // Replace the AMS object with our dynamic proxy Singleton.minstance. Set (Singleton, getInvocationStub().getProxyInterface()); } else {/ / 8. X following through ActivityManagerNative. GDefault. The get () to obtain}} @ Override protected void onBindMethods () {/ / you can add a variety of bulk hook method  addMethodProxy(new StartActivity()); } static class StartActivity extends MethodProxy {@override public String getMethodName() {return "startActivity"; } @Override public Object call(Object who, Method method, Object... Args) throws Throwable {// Parameters can be processed int res = vActivityManager.get ().startActivity(args); return res; }}}Copy the code

Finally, instantiate the Stub class and inject it. The startActivity method will bootstrap it into the VAM of the VA when executed.

VA encapsulates methods as MethodProxy objects. We can easily define hook methods by method names, process method parameters, and add methods in batches in onBindMethods.

The Stub base class encapsulates the creation of the dynamic proxy object and manages all of our added methodProxies. In the InvocationHandler that creates the dynamic proxy object, we decide whether to execute the hook method or the original method. The other methods execute the original logic as normal.

Finally, implement dynamic proxy object replacement by inject subclass.

We can see the full picture of Java layer hooks from the code structure:

  • VA has implemented a large number of stubs to hook various system services (proxies package), and finally added to the InvocationStubManager for inject invocation.
  • In many cases, methods only need to replace fixed position parameters (such as package name, UID). VA provides several derivative classes that can quickly hook methods, such as ReplaceCallingPkgXXX, which only need simple inheritance.
  • The idea of encapsulation is not difficult to understand, but the difficulty lies in how to find hook points (and there are compatibility problems with different Android versions). VA almost takes over the whole framework layer, and any place not hooked may cause crash, which is one of the reasons why its stability is difficult to do.

In the following figure, we can get a simple idea of what most system service hooks need to do (most methods only need to replace the package name) :

Native layer hook

Native Hook is mainly divided into PLT hook and Inline Hook. VA open source code uses Cydia Substrate, an open source tripartite library of inline Hook type, while in subsequent commercial code, VA has also updated native Hook frameworks such as Whale (developed by Roddy) and sandHook (currently also owned by Luo Box).

Why native Hook? The following uses I/O redirection as an example:

When the Client App (package name is host) runs in VA to read and write the file/SP, it points to the directory of the original package name. However, android system restricts access to non-own package name (such as mandatory partition storage in Android11). Therefore, these directories need to be redirected to the VA, and the package name of the Client App itself is used as the parent directory to achieve the purpose of file data isolation between apps.

For the Android system, file read and write operations are ultimately provided by the libc.so library function, so VA needs to hook libc.so library function, modify the input parameters of relevant functions, we directly look at the specific logic:

This method is called when Clint App initializes its application and adds the directories before and after the weighting via redirectDirectory. Here you can see some of the original directories headed by data/data. Another part is the various directories stored externally (the VA creates the same directory structure internally as well, following its own rules).

Then through NativeEngine. EnableIORedirect () to invoke to IOUniformer. The CPP in startUniformer method:

Look at the substitution declaration for the first method, Faccessat:

HOOK_DEF prefixes the method with new_, and HOOK_SYMBOL eventually replaces the macro with hook_function, using the MSHookFunction function to call the hook capabilities of triadic library Cydia Substrate.

When the system performs a Faccessat, it will be hook into our declared new_Faccessat method, which ultimately performs the function of replacing directory parameters, and why new_Faccessat is executed is exactly what Cydia Substrate does.

Therefore, it can be seen that the core technical competitiveness here lies in the Hook framework, and the rest is the encapsulation and use made by the framework.

In fact, Cydia Substrate appears very early and has been commercialized as a closed source, which cannot ensure its open source stability. Some expanded functions may require a more stable Hook framework.

Of course, VA native hook does not only do IO redirection, there are also some FileSystem and Android VM (such as Camera, Audio, Meia, Runtime) related hooks, interested students can look at the relevant logic, no analysis here.

The installation

Installation method is provided by VirtualCore and eventually call remote VAService VAppManagerService. InstallPackage () :

public synchronized InstallResult installPackage(String path, int flags, boolean notify) { long installTime = System.currentTimeMillis(); if (path == null) { return InstallResult.makeFailure("path = NULL"); SkipDexOpt = (flags & installStrategy.skip_dex_opt)! = 0; File packageFile = new File(path); if (! packageFile.exists() || ! packageFile.isFile()) { return InstallResult.makeFailure("Package File is not exist."); } VPackage pkg = null; Try {/ / analytic package structure for VPackage (four components, permission information, etc.) and serialized to disk 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; / /... Omit to check whether the package needs to update the code // Installation mode, one is the app already installed in the mobile phone, Boolean dependSystem = (flags & installStrategy.depend_system_if_exist)! = 0 && VirtualCore.get().isOutsideInstalled(pkg.packageName); if (existSetting ! = null && existSetting.dependSystem) { dependSystem = false; } / / copy so package NativeLibraryHelperCompat. CopyNativeBinaries (new File (path), libDir); // If the installation mode is to install the APK package, you need to copy the APK package, which is why it is slower to install the APK package directly. 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); } // chmodPackageDictionary(packageFile) is required for executing bin on the SD card. // Create a new PackageSetting app. 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); }} / / save the PKG information to disk, memory PackageParserEx. SavePackageCache (PKG); PackageCacheManager.put(pkg, ps); mPersistenceLayer.save(); BroadcastSystem.get().startApp(pkg); If (notify) {notifyAppInstalled(ps, -1); } res.isSuccess = true; return res; }Copy the code

The installation process is all about making the necessary copies (APK and SO packages), parsing the various information in Menifest (four major components, permissions, etc.) and saving it in case you need to call it.

When we install the same app again, we do not apply the above logic, just add a new userId to the app:

Here there is a reuse logic for userId. For example, if the same APP has been installed for four times, userId is 0, 1, 2, and 3 respectively. After uninstalling 2, it will reuse 2 preferentially (if it has been used, it will continue to increase). Finally, use the installPackageAsUser method of VAppManagerService to add a userId record to the app and save it to disk.

Let’s install the Oppo App Store twice and look at the data directory after installation:

  1. Copy all the so packages of this app. If you install APk directly, you will also store the copied base.apk in the directory.
  2. Package and signature information serialized file that contains information about all components of the app.
  3. In the user system, userList stores all userIDS. The default user is 0.
  4. Dex to the directory stored in binary and the external storage directory (the operation of external storage of APP will be redirected to this directory through IO redirect).

Start the

The VA Server process and the Client App process run in the same way as the VA Server process and the Client App process.

As you can see in the manifest file, Prcess defines two types of process, x (VA Server process) and P (Client App process). P is named P0, P1, P2, etc. This is because we may be running multiple apps, or components of different processes. After the Client App starts, VA will change process P to the name of the real process of the Client App.

Each time a new process is created, the Application is reinitialized:

VirtualCore’s Startup method is called for multiple VApp initializations, where InvocationStubManager adds all hook classes through injectAll to complete the Java layer hook injection:

So, how does x process pull up? Here VA makes clever use of the Provider’s call method to automatically pull up the process. Take a look at BinderProvider:

We found that the BinderProvider added and initialized all VA services (actually added to ServiceFetcher eventually) and put the IBinder handle ServiceFetcher in the call method. ServiceFetcher is the key for the Client to obtain the VA Service.

Finally, call can be made on the Client side to obtain:

  1. The call method of the Provider is used to retrieve the Bundle returned by BinderProvider. The Prcess definition of BinderProvider in the manifest file is 😡 process. If the process is not pulled, x process will be pulled by default. All VA services run on the X process.
  2. Once you’ve got a Bundler, you can get the Binder you put into BinderProvider, ServiceFetcher.
  3. ServiceFetcher manages all VA Services. By using ServiceFetcher, you can call all VA Services.

The same is true for the p process. When we want to start one of the four major components in the Client App and find that the process of the component does not exist, we must start the process first:

Here, a local VClientImpl is initialized, tokens and vuids are recorded, and the Client is packaged into a Bundle to invoke client-side methods, which are also invoked via provider. call:

Including VASettings. GetStubAuthority process number (vpid) is used to match the corresponding StubContentProvider, after p process pull up, will launch a subroutine package bindApplication initialization:

private void bindApplicationNoCheck(String packageName, String processName, ConditionVariable lock) {conditionBindData data = new AppBindData(); To obtain apK information saved during installation, run VPMS InstalledAppInfo info = virtualCore.get ().getInstalledAppInfo(packageName, 0); if (info == null) { new Exception("App not exist!" ).printStackTrace(); Process.killProcess(0); System.exit(0); } data.appInfo = VPackageManager.get().getApplicationInfo(packageName, 0, getUserId(vuid)); data.processName = processName; data.providers = VPackageManager.get().queryContentProviders(processName, getVUid(), PackageManager.GET_META_DATA); Log.i(TAG, "Binding application " + data.appInfo.packageName + " (" + data.processName + ")"); mBoundApplication = data; / / set the name of process, such as p0, p1 named officially become a target such as process name VirtualRuntime setupRuntime (data. ProcessName, data. AppInfo); int targetSdkVersion = data.appInfo.targetSdkVersion; if (targetSdkVersion < Build.VERSION_CODES.GINGERBREAD) { StrictMode.ThreadPolicy newPolicy = new StrictMode.ThreadPolicy.Builder(StrictMode.getThreadPolicy()).permitNetwork().build(); StrictMode.setThreadPolicy(newPolicy); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) { mirror.android.os.Message.updateCheckRecycle.call(targetSdkVersion); } if (VASettings.ENABLE_IO_REDIRECT) {// IO redirect (); } / / hook native function NativeEngine. LaunchEngine (); Object mainThread = VirtualCore.mainThread(); / / prepare dex list NativeEngine startDexOverride (); Context context = createPackageContext(data.appInfo.packageName); // Set the VM System environment system.setProperty ("java.io.tmpdir", context.getcacheDir ().getabsolutePath ()); File codeCacheDir; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { codeCacheDir = context.getCodeCacheDir(); } else { codeCacheDir = context.getCacheDir(); } / / hardware acceleration if (Build) VERSION) SDK_INT < Build. VERSION_CODES. N) {if (HardwareRenderer setupDiskCache! = null) { HardwareRenderer.setupDiskCache.call(codeCacheDir); } } else { if (ThreadedRenderer.setupDiskCache ! = null) { ThreadedRenderer.setupDiskCache.call(codeCacheDir); } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (RenderScriptCacheDir.setupDiskCache ! = null) { RenderScriptCacheDir.setupDiskCache.call(codeCacheDir); } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (RenderScript.setupDiskCache ! = null) { RenderScript.setupDiskCache.call(codeCacheDir); Object boundApp = fixBoundApp(mBoundApplication); mBoundApplication.info = ContextImpl.mPackageInfo.get(context); mirror.android.app.ActivityThread.AppBindData.info.set(boundApp, data.info); VMRuntime.setTargetSdkVersion.call(VMRuntime.getRuntime.call(), data.appInfo.targetSdkVersion); Configuration configuration = context.getResources().getConfiguration(); Object compatInfo = CompatibilityInfo.ctor.newInstance(data.appInfo, configuration.screenLayout, configuration.smallestScreenWidthDp, false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { DisplayAdjustments.setCompatibilityInfo.call(ContextImplKitkat.mDisplayAdjustments.get(context), compatInfo); } DisplayAdjustments.setCompatibilityInfo.call(LoadedApkKitkat.mDisplayAdjustments.get(mBoundApplication.info), compatInfo); } else { CompatibilityInfoHolder.set.call(LoadedApkICS.mCompatibilityInfo.get(mBoundApplication.info), compatInfo); } // A list of conflicting apps is configured, Is likely to delay the AppInstrumentation replacement of Boolean conflict = SpecialComponentList. IsConflictingInstrumentation (packageName); if (! conflict) { InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class); } / / use LoadedApk construct ClientApp Application mInitialApplication = LoadedApk. MakeApplication. Call (data. The info, false, null); mirror.android.app.ActivityThread.mInitialApplication.set(mainThread, mInitialApplication); ContextFixer.fixContext(mInitialApplication); If (build.version.sdk_int >= 24&& "com.0ceh.mm :recovery".equals(processName)) {// Handle some problems of wechat alone fixWeChatRecovery(mInitialApplication); } if (data.providers ! = null) {// Install provider installContentProviders(mInitialApplication, data.providers); } if (lock ! = null) { lock.open(); mTempLock = null; } VirtualCore.get().getComponentDelegate().beforeApplicationCreate(mInitialApplication); Try {/ / ClientApp. The official start of the life cycle mInstrumentation callApplicationOnCreate (mInitialApplication); InvocationStubManager.getInstance().checkEnv(HCallbackStub.class); if (conflict) { InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class); } Application createdApp = ActivityThread.mInitialApplication.get(mainThread); if (createdApp ! = null) { mInitialApplication = createdApp; } } catch (Exception e) { if (! mInstrumentation.onException(mInitialApplication, e)) { throw new RuntimeException( "Unable to create application " + mInitialApplication.getClass().getName() + ": " + e.toString(), e); }} // Tell vactivityManager.get ().appDoneWed (); VirtualCore.get().getComponentDelegate().afterApplicationCreate(mInitialApplication); }Copy the code

Activity

The Activity component is probably the one we care most about. We explained how to trick AMS into starting an Activity that is not registered in the list. Java layer Hook also mentioned how to hook the startActivity method. The hook code is in ActivityManagerStub:

static class StartActivity extends MethodProxy { private static final String SCHEME_FILE = "file"; private static final String SCHEME_PACKAGE = "package"; private static final String SCHEME_CONTENT = "content"; @Override public String getMethodName() { return "startActivity"; } @Override public Object call(Object who, Method method, Object... args) throws Throwable { int intentIndex = ArrayUtils.indexOfObject(args, Intent.class, 1); if (intentIndex < 0) { return ActivityManagerCompat.START_INTENT_NOT_RESOLVED; } int resultToIndex = ArrayUtils.indexOfObject(args, IBinder.class, 2); String resolvedType = (String) args[intentIndex + 1]; Intent intent = (Intent) args[intentIndex]; intent.setDataAndType(intent.getData(), resolvedType); IBinder resultTo = resultToIndex >= 0 ? (IBinder) args[resultToIndex] : null; int userId = XUserHandle.myUserId(); if (ComponentUtils.isStubComponent(intent)) { return method.invoke(who, args); } if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction()) || (Intent.ACTION_VIEW.equals(intent.getAction()) && "Application/VND. Android. Package - archive". The equals (intent. GetType ()))) {/ / internal installation to intercept the processing, Code omitted here} else if ((Intent) ACTION_UNINSTALL_PACKAGE) equals (Intent. The getAction () | | Intent.action_delete.equals (intent.getAction())) &&" package".equals(intent.getScheme())) {intent.action_delete.equals (intent.getScheme())); Code omitted here} else if (MediaStore. ACTION_IMAGE_CAPTURE. Equals (intent. The getAction () | | MediaStore.ACTION_VIDEO_CAPTURE.equals(intent.getAction()) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(intent.getAction())) { handleMediaCaptureRequest(intent); } String resultWho = null; int requestCode = 0; Bundle options = ArrayUtils.getFirst(args, Bundle.class); if (resultTo ! = null) { resultWho = (String) args[resultToIndex + 1]; requestCode = (int) args[resultToIndex + 2]; } settings.action_manage_unknown_app_sources.equals (intent.getAction())) {settings.action_manage_unknown_app_sources.equals (intent.getAction())) Code omitted here} the if (BuildCompat isAndroidLevel18 ()) {args [intentIndex - 1] = getHostPkg (); } if (intent.getScheme() ! = null && intent.getScheme().equals(SCHEME_PACKAGE) && intent.getData() ! = null) { if (intent.getAction() ! = null && intent.getAction().startsWith("android.settings.")) { intent.setData(Uri.parse("package:" + getHostPkg())); } } ActivityInfo activityInfo = VAppManager.get().resolveActivityInfo(intent, userId); if (activityInfo == null) { VLog.e("VActivityManager", "Unable to resolve activityInfo : %s", intent); if (intent.getPackage() ! = null && isAppPkg(intent.getPackage())) { return ActivityManagerCompat.START_INTENT_NOT_RESOLVED; } return method.invoke(who, args); Int res = vactivityManager.get (). StartActivity (intent, activityInfo, resultTo, options, resultWho, requestCode, XUserHandle.myUserId()); if (res ! = 0 && resultTo ! = null && requestCode > 0) { VActivityManager.get().sendActivityResult(resultTo, resultWho, requestCode); } // Handle StubActivity themes and animations to start the Activity's 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; }}Copy the code

Internal install and uninstall refers to an intent that an app store, such as App Store, installs another app or upgrades the app itself. The intent is intercepted and handled by the VA itself.

Focus on the line vactivityManager.get ().startActivity. The normal startActivity method is eventually booted to the VAM and then from the P process to the remote VAMS running in the X process. VAMS then calls startActivityLocked, which implements ActivityStack:

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; / / to start the processing of Flag, code omitted here String affinity. = ComponentUtils getTaskAffinity (info); TaskRecord reuseTask = null; switch (reuseTarget) { case AFFINITY: reuseTask = findTaskByAffinityLocked(userId, affinity); break; case DOCUMENT: reuseTask = findTaskByIntentLocked(userId, intent); break; case CURRENT: reuseTask = sourceTask; break; default: break; } boolean taskMarked = false; if (reuseTask == null) { startActivityInNewTaskLocked(userId, intent, info, options); } else { boolean delivered = false; mAM.moveTaskToFront(reuseTask.taskId, 0); boolean startTaskToFront = ! clearTask && ! clearTop && ComponentUtils.isSameIntent(intent, reuseTask.taskRoot); if (clearTarget.deliverIntent || singleTop) { taskMarked = markTaskByClearTarget(reuseTask, clearTarget, intent.getComponent()); ActivityRecord topRecord = topActivityInTask(reuseTask); if (clearTop && ! singleTop && topRecord ! = null && taskMarked) { topRecord.marked = true; } // Target activity is on top if (topRecord ! = null && ! topRecord.marked && topRecord.component.equals(intent.getComponent())) { deliverNewIntentLocked(sourceRecord, topRecord, intent); delivered = true; } } if (taskMarked) { synchronized (mHistory) { scheduleFinishMarkedActivityLocked(); } } if (! startTaskToFront) { if (! delivered) { destIntent = startActivityProcess(userId, sourceRecord, intent, info); if (destIntent ! = null) { startActivityFromSourceTask(reuseTask, destIntent, info, resultWho, requestCode, options); } } } } return 0; }Copy the code

The startActivityProcess method is used to calculate the launch mode and flags to maintain the Activity stack:

private Intent startActivityProcess(int userId, ActivityRecord sourceRecord, Intent intent, ActivityInfo info) { intent = new Intent(intent); //1 ProcessRecord targetApp = mService.startProcessIfNeedLocked(info.processName, userId, info.packageName); if (targetApp == null) { return null; } Intent targetIntent = new Intent(); //2 targetIntent.setClassName(VirtualCore.get().getHostPkg(), fetchStubActivity(targetApp.vpid, info)); ComponentName component = intent.getComponent(); if (component == null) { component = ComponentUtils.toComponentName(info); } targetIntent.setType(component.flattenToString()); StubActivityRecord saveInstance = new StubActivityRecord(intent, info, sourceRecord ! = null ? sourceRecord.component : null, userId); //3 saveInstance.saveToIntent(targetIntent); return targetIntent; }Copy the code
  1. To start an Activity in a process that does not exist, you must first start the process. In startup, we analyzed how the P process is started.
  2. FetchStubActivity will find the appropriate StubActivity according to the current situation, according to the VPID list file, StubActivity information in the same process will be reused multiple times.
  3. Store information about the target Activity that ClientAPP wants to launch in the intent, which is already wrapped as the intent that starts StubActivity.

The final call ActivityStack. RealStartActivityLocked:

private void realStartActivityLocked(IBinder resultTo, Intent intent, String resultWho, int requestCode, Bundle options) { Class<? >[] types = mirror.android.app.IActivityManager.startActivity.paramList(); Object[] args = new Object[types.length]; if (types[0] == IApplicationThread.TYPE) { args[0] = ActivityThread.getApplicationThread.call(VirtualCore.mainThread());  } int intentIndex = ArrayUtils.protoIndexOf(types, Intent.class); int resultToIndex = ArrayUtils.protoIndexOf(types, IBinder.class, 2); int optionsIndex = ArrayUtils.protoIndexOf(types, Bundle.class); int resolvedTypeIndex = intentIndex + 1; int resultWhoIndex = resultToIndex + 1; int requestCodeIndex = resultToIndex + 2; args[intentIndex] = intent; args[resultToIndex] = resultTo; args[resultWhoIndex] = resultWho; args[requestCodeIndex] = requestCode; if (optionsIndex ! = -1) { args[optionsIndex] = options; } args[resolvedTypeIndex] = intent.getType(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { args[intentIndex - 1] = VirtualCore.get().getHostPkg(); } ClassUtils.fixArgs(types, args); mirror.android.app.IActivityManager.startActivity.call(ActivityManagerNative.getDefault.call(), (Object[]) args); }Copy the code

After the parameter is replaced, the fake StubActivity launch intent is finally delivered to system AMS.

ProcessRecord, TaskRecord, ActivityRecord, ProcessRecord, TaskRecord, ActivityRecord The new Activity of the new taskAffinity, VA will also create these records and store them in memory for maintenance, as will Finish.

We can take a brief look at the data structure. There are also mutual holding relationships between them. For easy traceability and processing:

The system AMS receives information about StubActivity. After a variety of AMS processing, the message is returned to the mH Handler of the main thread of the Client. The target Activity is instantiated by Instrumentation. VA selects to intercept the Intent at this time, resuming the Intent:

Processing logic goes to handleLaunchActivity:

private boolean handleLaunchActivity(Message msg) { Object r = msg.obj; . / / StubActivity intent intent stubIntent = ActivityThread ActivityClientRecord. Intent. Get (r); StubActivityRecord saveInstance = new StubActivityRecord(stubIntent); if (saveInstance.intent == null) { return true; } // Intent intent intent = saveInstance. ComponentName caller = saveInstance.caller; IBinder token = ActivityThread.ActivityClientRecord.token.get(r); ActivityInfo info = saveInstance.info; // If the token is empty, the process corresponding to the component needs to be pulled up. If (vclientimppl.get ().getToken() == null) {InstalledAppInfo InstalledAppInfo = VirtualCore.get().getInstalledAppInfo(info.packageName, 0); if (installedAppInfo == null) { return true; } VActivityManager.get().processRestarted(info.packageName, info.processName, saveInstance.userId); getH().sendMessageAtFrontOfQueue(Message.obtain(msg)); return false; } if (! VClientImpl.get().isBound()) { VClientImpl.get().bindApplication(info.packageName, info.processName); getH().sendMessageAtFrontOfQueue(Message.obtain(msg)); return false; } int taskId = IActivityManager.getTaskForActivity.call( ActivityManagerNative.getDefault.call(), token, false ); / / notice VAMS created VActivityManager. The get (). OnActivityCreate (ComponentUtils. ToComponentName (info), the caller, token, info, intent, ComponentUtils.getTaskAffinity(info), taskId, info.launchMode, info.flags); ClassLoader appClassLoader = VClientImpl.get().getClassLoader(info.applicationInfo); intent.setExtrasClassLoader(appClassLoader); / / replace real information of intent, intent for the original target information, and returned to the system processing ActivityThread. ActivityClientRecord. Intent. Set (r, intent); ActivityThread.ActivityClientRecord.activityInfo.set(r, info); return true; }Copy the code

Once the system picks up the intent, the instantiated Activity becomes the target Activity in ClientApp.

Finally a hook point is in Instrumentation. CallActivityOnCreate, at this point the Activity has been the new VA needs at this time to restore the true theme and the screen direction, and fix some problems.

After android9.0, there are some changes in the Activity startup process, and there is no corresponding adaptation in the VA open source code. We can simply describe the handling method: It is still possible to intercept messages using mH, but instead of intercepting a message from LAUNCH_ACTIVITY it becomes EXECUTE_TRANSACTION, replacing the intent with ActivityClientRecord it becomes LaunchActivityItem. Although the process changes slightly, the way to deal with it is not that complicated, just need to find the right hook points.

Other three components

In addition to the Activity, the other three components also use some skills to avoid the problem. Due to the limited space, we will not do the specific code analysis, just a brief introduction of the implementation principle, interested students can continue to understand.

ContentProvider

Compared with StubActivity placeholder function, StubContentProvider is not for placeholder, its function is to call can take p process, and after the process pulled up, the application initialization. The bindApplicationNoCheck method actually registers the installation of the ContentProvider in the Client App.

When process A calls the ContentProvider in process B (or application B), process A will hook the getContentProvider method in process A to determine whether process B, where the target Provider is located, exists. If not, process B will pull up, and then process B’s Provider is installed. Return the target Provider handle required by process A to complete the call.

Service

Since a Service does not have an interactive page like an Activity, its life cycle is very simple, and the Service in ClientApp does not need to be exposed to external (i.e. sandbox) apps. Therefore, in VA, Service does not need to be known to AMS and other system services.

Through the hook startService method will lead directly to the VAMS logic, using ApplicationThread. ScheduleCreateService of target Service directly create does not exist, it is pulled (if the target process), If it is use the ApplicationThread bindService. ScheduleBindService waiting for bind to finish.

In short, it is the process of creating and running the Service in the Client App. It shields the system Service and calls its life cycle directly by using the mirror method.

BroadcastReceiver

Since broadcasts are divided into static and dynamic registrations, and static broadcasts in ClientApp cannot be known by AMS, VA uses dynamic registrations instead of static registrations.

When the VA Service process is pulled up, VA scans all installed apps, iterates over all their Reveiver information, and receives each IntentFilter by creating a StaticBroadcastReceiver agent.

When the host broadcastreceiver receives a wrapped intent, it sends the broadcastIntent message to the ClientApp. When the host broadcastreceiver receives a wrapped intent, it sends the broadcastIntent message to the ClientApp. Unpack the actual intent, call back to the ClientApp space, instantiate the target Receiver (which pulls up the target process if it doesn’t exist), and finally call its OnCreate and Finish manually.

In fact, each plug-in framework treats the four components in the same way, just hook the point, hook the timing is slightly different.

conclusion

The basic principle of VA, the code level is basically over, the rest is the continuous stacking and improvement work, and based on VA there are a lot of follow-up expansion, the author has a very good words: how to use VA, everything depends on your imagination.

If you’re interested in its other capabilities, check out this article: Research Report on The Exploitation of VirtualApp technology.

If you are interested in implementing Xposed without root, or some vA-BASED secondary development projects, you can continue to understand the following projects:

Github.com/android-hac…

Github.com/asLody/Sand…

Github.com/WindySha/Xp…

Github.com/android-hac…

www.taichi-app.com/#/index

The author wrote this article is also based on the current understanding to analyze, if there is something wrong, welcome to correct, or interested students are also welcome to communicate with us, thank you!

Reference documentation

  • Design of enterprise Mobile Application Security Platform based on Sandbox Technology
  • Ximsfei. Making. IO / 2017/07/13 /…
  • Blog.csdn.net/ganyao93954…
  • Mabin004. Making. IO / 2019/02/09 /…
  • VirtualApp -2 -2 InvocationStubManager injection hook details – Dreamtalk four leaves – Blog garden
  • www.52pojie.cn/thread-8562…
  • www.coolapk.com/feed/220757…