1. Load system resources

1. Resource categories

  • Resource files stored in the res directory. The hexadecimal value of the resource file is generated in the R file at compile time. Resources in the res directory get the Resource object using context. getResource, and then get the Resource using getXXX.
  • Original files stored in assets will not be compiled during compilation. Get the file Resources in the directory using the open method of AssetManager, which comes from the getAssets method of the Resources class

2, the Resources,

(1) the AssetManager

  • AssetManage has an addAssetPath method that passes in the APK path and Resources can access all of the Resources in the current APK. The plug-in APK path can be passed to the addAssetPath method by reflection.
  • AssetManager has an NDK method inside to access files. When apK is packaged, it generates a resources.arsc file, which is a Hash table containing hexadecimal mappings to resources

2. VirtualApk plug-in resource loading

Implementation of resource plug-in:

  • Merge Resources: Merge the Resources of the plug-in into the Resources of the host to access the Resources of the host. There may be duplicate resource ids for plug-ins and hosts. Solution: (1) Modify the aapt command used in the Android packaging process to specify a prefix for the resource ID of the plug-in to avoid conflicts with the host resource ID. (2) Modify the resources.arsc file after Android packaging.
  • Load plug-in Resources separately: Each plug-in constructs its own Resources to load plug-in Resources, without access to host Resources

1, create Resources

#LoadedPluginpublic LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception { ...... this.mResources = createResources(context, getPackageName(), apk); . } protected Resources createResources(Context context, String packageName, File apk) throws Exception {if(Constants.COMBINE_RESOURCES) {// The plug-in resources are incorporated into the host, and the plug-in can access the host resourcesreturn ResourcesManager.createResources(context, packageName, apk);
    } elseHostResources = context.getResources(); hostResources = context.getResources(); AssetManager assetManager = createAssetManager(context, apk);returnnew Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); }}Copy the code

2. Independent plug-in resources

New AssetManager objects are created primarily through reflection, and plug-in resources are loaded through addAssetPath. This applies when the resource is independent and the host resource cannot be invoked

Protected AssetManager createAssetManager(Context Context, File APk) throws Exception {// Create a new AssetManager object by reflection, Through addAssetPath loading plug-ins resources AssetManager am = AssetManager. Class. NewInstance (); Reflector.with(am).method("addAssetPath", String.class).call(apk.getAbsolutePath());
    return am;
}
Copy the code

3. Merge plug-in resources

Get the host resource’s AssetManager first, then add the plug-in resource by calling the addAssetPath of the AssetManager through reflection, and return the new Resources

#ResourcesManagerpublic static synchronized Resources createResources(Context hostContext, String packageName, File APk) throws Exception {// Create Resources objects based on the versionif(build.version.sdk_int >= build.version_codes.n) {// (1)returncreateResourcesForN(hostContext, packageName, apk); } / / (2) the Resources Resources. = ResourcesManager createResourcesSimple (hostContext, apk getAbsolutePath ()); ResourcesManager.hookResources(hostContext, resources);returnresources; } private static Resources createResourcesSimple(Context hostContext, String apk) throws Exception {// hostResources object Resources hostResources = hostcontext.getresources (); Resources newResources = null; AssetManager assetManager; Reflector reflector = Reflector.on(AssetManager.class).method("addAssetPath", String.class);
    if(build.version.sdk_int < build.version_codes.lollipop) {// Create AssetManager by reflection AssetManager = AssetManager.class.newInstance(); reflector.bind(assetManager); final int cookie1 = reflector.call(hostContext.getApplicationInfo().sourceDir);;if (cookie1 == 0) {
            throw new RuntimeException("createResources failed, can't addAssetPath for "+ hostContext.getApplicationInfo().sourceDir); }}else{// Get the host's AssetManager AssetManager = hostResources.getAssets(); reflector.bind(assetManager); } final int cookie2 = reflector.call(apk);if (cookie2 == 0) {
        throw new RuntimeException("createResources failed, can't addAssetPath for " + apk);
    }
    List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
    for (LoadedPlugin plugin : pluginList) {
        final int cookie3 = reflector.call(plugin.getLocation());
        if (cookie3 == 0) {
            throw new RuntimeException("createResources failed, can't addAssetPath for "+ plugin.getLocation()); }} // Create Resources objects from different phone brandsif (isMiUi(hostResources)) {
        newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
    } else if (isVivo(hostResources)) {
        newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
    } else if (isNubia(hostResources)) {
        newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
    } else if (isNotRawResources(hostResources)) {
        newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
    } else {
        // is raw android resources
        newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
    }
    // lastly, sync all LoadedPlugin to newResources
    for (LoadedPlugin plugin : pluginList) {
        plugin.updateResources(newResources);
    }
    
    return newResources;
}
Copy the code

ContextImpl Hook mResources and LoadedApk mResources

public static void hookResources(Context base, Resources resources) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return;
    }
    try {
        Reflector reflector = Reflector.with(base);
        //hook mResources
        reflector.field("mResources").set(resources);
        Object loadedApk = reflector.field("mPackageInfo").get();
        //hook mResources
        Reflector.with(loadedApk).field("mResources").set(resources);

        Object activityThread = ActivityThread.currentActivityThread();
        Object resManager;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            resManager = android.app.ResourcesManager.getInstance();
        } else {
            resManager = Reflector.with(activityThread).field("mResourcesManager").get();
        }
        Map<Object, WeakReference<Resources>> map = Reflector.with(resManager).field("mActiveResources").get(); Object key = map.keySet().iterator().next(); map.put(key, new WeakReference<>(resources)); } catch (Exception e) { Log.w(TAG, e); }}Copy the code

4. The Activity starts resource processing

#VAInstrumentation
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    try {
        cl.loadClass(className);
        Log.i(TAG, String.format("newActivity[%s]", className)); } catch (ClassNotFoundException e) { ...... / / by reflection to the Resources assigned to the Activity of mResources Reflector. QuietReflector. With (Activity). The field ("mResources").set(plugin.getResources());
        return newActivity(activity);
    }

    return newActivity(mBase.newActivity(cl, className, intent));
}
Copy the code

3. Plug-in of SO

There are two ways to plug-in so: based on system.load and based on System.loadLibrary.

1. Implementation of VirtualApk

#LoadedPlugin
protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception {
    File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
    String dexOutputPath = dexOutputDir.getAbsolutePath();
    DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

    if (Constants.COMBINE_CLASSLOADER) {
        DexUtil.insertDex(loader, parent, libsDir);
    }
    return loader;
}

Copy the code

Create a DexClassLoader, parse out the so file in each plug-in APK, extract it to a location, concatenate the paths with commas into a string, and place it in the third argument to the DexClassLoader’s constructor. So in the plug-in, like so in the jniLib directory of the host App, is loaded through the system.loadLibrary method.

#DexUtilpublic static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception { Object baseDexElements = getDexElements(getPathList(baseClassLoader)); Object newDexElements = getDexElements(getPathList(dexClassLoader)); AllDexElements Object allDexElements = combineArray(baseDexElements, newDexElements); Object pathList = getPathList(baseClassLoader); // Replace dexElements with allDexElements reflect.with (pathList).field("dexElements").set(allDexElements);
    
    insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}
Copy the code

So plug-in core code

private static synchronized void insertNativeLibrary(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
    if (sHasInsertedNativeLibrary) {
        return;
    }
    sHasInsertedNativeLibrary = true; Context context = ActivityThread.currentApplication(); BasePathList = getPathList(baseClassLoader);if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
        Reflector reflector = Reflector.with(basePathList);
   
        List<File> nativeLibraryDirectories = reflector.field("nativeLibraryDirectories").get(); nativeLibraryDirectories.add(nativeLibsDir); / / to get to the set of host so Object baseNativeLibraryPathElements = reflector. The field ("nativeLibraryPathElements").get(); final int baseArrayLength = Array.getLength(baseNativeLibraryPathElements); Object newPathList = getPathList(dexClassLoader); / / get the set of plug-ins so Object newNativeLibraryPathElements = reflector. The get (newPathList); Class<? > elementClass = newNativeLibraryPathElements.getClass().getComponentType(); Object allNativeLibraryPathElements = Array.newInstance(elementClass, baseArrayLength + 1); / / the original host so set copy to a new set of System. Arraycopy (allNativeLibraryPathElements baseNativeLibraryPathElements, 0, 0, baseArrayLength); Field soPathField;if (Build.VERSION.SDK_INT >= 26) {
            soPathField = elementClass.getDeclaredField("path");
        } else {
            soPathField = elementClass.getDeclaredField("dir");
        }
        soPathField.setAccessible(true); / / the plugin so set copy to a new set of final int newArrayLength = Array. The getLength (newNativeLibraryPathElements);for (int i = 0; i < newArrayLength; i++) {
            Object element = Array.get(newNativeLibraryPathElements, i);
            String dir = ((File)soPathField.get(element)).getAbsolutePath();
            if (dir.contains(Constants.NATIVE_DIR)) {
                Array.set(allNativeLibraryPathElements, baseArrayLength, element);
                break; }} / / will host and plug-in so up set to replace reflector. The set (allNativeLibraryPathElements); }else {
        Reflector reflector = Reflector.with(basePathList).field("nativeLibraryDirectories"); File[] nativeLibraryDirectories = reflector.get(); final int N = nativeLibraryDirectories.length; File[] newNativeLibraryDirectories = new File[N + 1]; System.arraycopy(nativeLibraryDirectories, 0, newNativeLibraryDirectories, 0, N); newNativeLibraryDirectories[N] = nativeLibsDir; reflector.set(newNativeLibraryDirectories); }}Copy the code

Get the host SO set and get the plug-in SO set. After the combination of the two, replace the original SO set by reflection, and the plug-in SO file can be loaded normally

4. VirtualApk Service plug-in

1. Service startup analysis

  • The Service startup has nothing to do with Instrumentation and cannot be handled by Hook Instrumentation
  • Starting a placeholder Activity multiple times in Standard mode creates multiple activities, but starting a placeholder Service multiple times does not create multiple Service instances
  • Through proxy distribution: Start a unified management of proxy Service, intercept all Service methods, modify startService to proxy Service, manage proxy Service onStartCommond, and create/stop the target Service.

2, Hook IActivityManager

VirtualApk initializes the IActivityManager via ActivityManagerProxy. The startService operation was intercepted by ActivityManagerProxy when the service was started

public class ActivityManagerProxy implements InvocationHandler { protected Object startService(Object proxy, Method method, Object[] args) throws Throwable { IApplicationThread appThread = (IApplicationThread) args[0]; Intent target = (intent) args[1]; / / check Service information ResolveInfo ResolveInfo = this. MPluginManager. ResolveService (target, 0).if (null == resolveInfo || null == resolveInfo.serviceInfo) {
            // is host service
            return method.invoke(this.mActivityManager, args);
        }

        return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
    }

	protected ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
        Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
        return mPluginManager.getHostContext().startService(wrapperIntent);
    }

    protected Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int commandTarget.setcomponent (New ComponentName(ServiceInfo.packagename, ServiceInfo.name)); String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation(); // Determine whether it is a remote service based on processName Booleanlocal= PluginUtil.isLocalService(serviceInfo); Class<? RemoteService Class<? extends Service> delegate =local? LocalService.class : RemoteService.class; Intent Intent = new Intent(); intent.setClass(mPluginManager.getHostContext(), delegate); intent.putExtra(RemoteService.EXTRA_TARGET, target); intent.putExtra(RemoteService.EXTRA_COMMAND,command);
        intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
        if(extras ! = null) { intent.putExtras(extras); }returnintent; }}Copy the code

3, LocalService

public class LocalService extends Service {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
       	......
        switch (command) {
            caseEXTRA_COMMAND_START_SERVICE: {/ / get ActivityThread ActivityThread mainThread = ActivityThread. CurrentActivityThread (); IApplicationThread appThread = mainThread.getApplicationThread(); Service service;if(this. MPluginManager. GetComponentsHandler (.) isServiceAvailable (component)) {/ / get the Service Service = this.mPluginManager.getComponentsHandler().getService(component); }else{try {// Load Service by DexClassLoader Service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance(); Application app = plugin.getApplication(); IBinder token = appThread.asBinder(); Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class); IActivityManager am = mPluginManager.getActivityManager(); Invoke (service, plugin.getpluginContext (), mainThread, component.getClassName(), token, app, am); // Call the Service onCreate method service.oncreate (); this.mPluginManager.getComponentsHandler().rememberService(component, service); } catch (Throwable t) {returnSTART_STICKY; }} // Call the onStartCommand method of service. this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());break; }... }}}Copy the code

4, RemoteService

public class RemoteService extends LocalService {
    
    private static final String TAG = Constants.TAG_PREFIX + "RemoteService";

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent == null) {
            returnsuper.onStartCommand(intent, flags, startId); } / / obtain the target service intent intent target = intent. GetParcelableExtra (EXTRA_TARGET);if(target ! String pluginLocation = intent.getStringExtra(EXTRA_PLUGIN_LOCATION); ComponentName component = target.getComponent(); LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(component);if(plugin == null && pluginLocation ! = null) {try {/ / load the apk plugin File PluginManager. GetInstance (this). LoadPlugin (new File (pluginLocation)); } catch (Exception e) { Log.w(TAG, e); }}}returnsuper.onStartCommand(intent, flags, startId); }}Copy the code

Starting a remote Service is one more step to loading the Service of another plug-in

5. Summary of Service plug-in

  • The IActivityManager is hooked through ActivityManagerProxy during initialization.
  • ActivityManagerProxy intercepts the service to determine whether it is a RemoteService. If it is a RemoteService, start RemoteService; if it is a service in the same process, start LocalService.
  • If LocalService is used, the target Service is loaded through the DexClassLoader, and then the attach method is used to bind the Context. Then the onCreate and onStartCommand methods of the Service are executed
  • If it is RemoteService, the plug-in’s RemoteService is loaded first, and then the same as LocalService.

References:

  • Article VirtualAPK resources
  • Android Plug-in Development Guide
  • Android Advanced Decryption