preface

In Android P Preview, reflection calls to the @hide API are restricted. For details, see This article. Recently the team has been sharing plugins and hotfixes as well. So write an essay and take good notes.

Prepare knowledge

  • Reflection, dynamic proxy
  • Several related classloaders in Android. Note that PathClassLoader can load uninstalled APK on ART vm, but not on Dalvik vm.
  • The principles of the four components of Android
  • PackageManagerServer
  • Load and package resources
  • other

The code mentioned in this article has passed Nexus 5(Dalvik VIRTUAL Machine) Android 6.0 test

All the resources mentioned in this article are under this repository

In particular, this blog will not explain too much of the original. If the reader does not have the relevant knowledge, it is recommended to read the blogs of Weishu and Gityuan first, and Lao Luo’s blog for the knowledge of resource packaging.

  • Weishu’s Notes
  • gityuan

Plug-in the Activity

First of all, it is not possible to start an Activity that is not registered with AndroidManifest at all. Because during the startup process, there is a verification process, and the verification is done by the PMS, which we cannot interfere with. For this reason, most plugins for activities use the idea of pit capture. The difference is how to replace before validation and restore when the object is generated. For now, there are two better options:

  • Hook Instrumentation solutions
  • Interferes with methods such as startActivity and the ClassLoader findClass scheme

Hook Instrumentation here. According to the idea mentioned above, we need to bypass the inspection first, so how do we bypass the inspection? Instrumentation#execStartActivity instrumentality #execStartActivity instrumentality #execStartActivity instrumentality #execStartActivity instrumentality #execStartActivity instrumentality #execStartActivity Enable boot parameters to pass system checks. So, how do we do that? First, we need to check whether the Intent we want to launch is matched. If not, change the ClassName to the spoof Activity configured in the AndroidManifest and put the current ClassName in the extra of the current Intent. So I can do the recovery later, so let’s look at the code.

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
        if(infos = = null | | infos. The size () = = 0) {/ / up blank, PutExtra (TARGET_ACTIVITY, intent.getComponent().getClassName())); intent.setClassName(who,"com.guolei.plugindemo.StubActivity");
        }

        Class instrumentationClz = Instrumentation.class;
        try {
            Method execMethod = instrumentationClz.getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
            return (ActivityResult) execMethod.invoke(mOriginInstrumentation, who, contextThread, token,
                    target, intent, requestCode, options);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
Copy the code

We have to bypass the detection, now need to solve problem is reduction, we know that system start the Activity at the end of the call to ActivityThread, here, through Instrumentation# newActivity method to reflect object, constructing an Activity as a result, We just need to undo here. The code is as follows:

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        if(! TextUtils.isEmpty(intent.getStringExtra(TARGET_ACTIVITY))) {return super.newActivity(cl, intent.getStringExtra(TARGET_ACTIVITY), intent);
        }
        return super.newActivity(cl, className, intent);
    }
Copy the code

With everything in place, our final problem is how to replace the Instrumentation of the system. This is as simple as replacing the mInstrumentation field in the ActivityThread.

    private void hookInstrumentation() {
        Context context = getBaseContext();
        try {
            Class contextImplClz = Class.forName("android.app.ContextImpl");
            Field mMainThread = contextImplClz.getDeclaredField("mMainThread");
            mMainThread.setAccessible(true);
            Object activityThread = mMainThread.get(context);
            Class activityThreadClz = Class.forName("android.app.ActivityThread");
            Field mInstrumentationField = activityThreadClz.getDeclaredField("mInstrumentation");
            mInstrumentationField.setAccessible(true);
            mInstrumentationField.set(activityThread,
                    new HookInstrumentation((Instrumentation) mInstrumentationField.get(activityThread),
                            context.getPackageManager()));
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("plugin"."hookInstrumentation: error"); }}Copy the code

This way, we can start an Activity that is not registered in the AndroidManifest file, but it is important to note that since the ClassLoader we are using here is the host’s ClassLoader, we need to add the dex file of the plugin to our host. This is important. There are several implementations of the multi-classLoader architecture, and the code here needs to be changed.

Service plug-in

Starting an unregistered Service does not crash and exit, but with a warning. Also, service startup is handled directly by ContextImpl to AMS, so let’s look at the code.

    private ComponentName startServiceCommon(Intent service, UserHandle user) {
        try {
            validateServiceIntent(service);
            service.prepareToLeaveProcess(this);
            ComponentName cn = ActivityManagerNative.getDefault().startService(
                mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
                            getContentResolver()), getOpPackageName(), user.getIdentifier());
            if(cn ! = null) {if (cn.getPackageName().equals("!")) {
                    throw new SecurityException(
                            "Not allowed to start service " + service
                            + " without permission " + cn.getClassName());
                } else if (cn.getPackageName().equals("!!!!!")) {
                    throw new SecurityException(
                            "Unable to start service " + service
                            + ":"+ cn.getClassName()); }}returncn; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}Copy the code

And the process of creating the object is not created by Instrumentation, but directly generated in ActivityThread#handleCreateService reflection. So, we can not use the idea of Activity, how to do? Since we can’t do the replacement restore, we can consider the proxy, we start a real registered Service, we start the Service, and let the Service, as the system Service Service, process our plug-in Service exactly as it does.

Let’s take startService as an example. The first thing we need to do is hook AMS, because when AMS starts the service, what do we need to do if we want to start the plug-in service? By replacing the plug-in service with the real proxy Service, the proxy Service is started. In the proxy Service, we build the plug-in service and call attach, onCreate, and other methods.

Hook AMS code is as follows:

    private void hookAMS() {
        try {
            Class activityManagerNative = Class.forName("android.app.ActivityManagerNative");
            Field gDefaultField = activityManagerNative.getDeclaredField("gDefault");
            gDefaultField.setAccessible(true);
            Object origin = gDefaultField.get(null);
            Class singleton = Class.forName("android.util.Singleton");
            Field mInstanceField = singleton.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            Object originAMN = mInstanceField.get(origin);
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class[]{Class.forName("android.app.IActivityManager")},
                    new ActivityManagerProxy(getPackageManager(),originAMN));
            mInstanceField.set(origin, proxy);
            Log.e(TAG, "hookAMS: success" );
        } catch (Exception e) {
            Log.e(TAG, "hookAMS: "+ e.getMessage()); }}Copy the code

Let’s look at the ActivityManagerProxy proxy.

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("startService")) {
            Intent intent = (Intent) args[1];
            List<ResolveInfo> infos = mPackageManager.queryIntentServices(intent, PackageManager.MATCH_ALL);
            if (infos == null || infos.size() == 0) {
                intent.putExtra(TARGET_SERVICE, intent.getComponent().getClassName());
                intent.setClassName("com.guolei.plugindemo"."com.guolei.plugindemo.StubService"); }}return method.invoke(mOrigin, args);
    }
Copy the code

The code is clear and simple enough that we don’t need to do any more, so let’s look at how the proxy Service starts and calls our plug-in Service.

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onStartCommand: stub service ");
        if(intent ! = null && ! IsEmpty (intent.getStringExtra(TARGET_SERVICE)) {// Start the actual service String serviceName = intent.getStringExtra(TARGET_SERVICE); try { Class activityThreadClz = Class.forName("android.app.ActivityThread");
                Method getActivityThreadMethod = activityThreadClz.getDeclaredMethod("getApplicationThread");
                getActivityThreadMethod.setAccessible(true); // Get ActivityThread Class contextImplClz = class.forname ("android.app.ContextImpl");
                Field mMainThread = contextImplClz.getDeclaredField("mMainThread");
                mMainThread.setAccessible(true); Object activityThread = mMainThread.get(getBaseContext()); Object applicationThread = getActivityThreadMethod.invoke(activityThread); // Obtain the token value Class iInterfaceClz = class.forname ("android.os.IInterface");
                Method asBinderMethod = iInterfaceClz.getDeclaredMethod("asBinder");
                asBinderMethod.setAccessible(true); Object token = asBinderMethod.invoke(applicationThread); Class serviceClz = class.forname ("android.app.Service");
                Method attachMethod = serviceClz.getDeclaredMethod("attach",
                        Context.class, activityThreadClz, String.class, IBinder.class, Application.class, Object.class);
                attachMethod.setAccessible(true);
                Class activityManagerNative = Class.forName("android.app.ActivityManagerNative");
                Field gDefaultField = activityManagerNative.getDeclaredField("gDefault");
                gDefaultField.setAccessible(true);
                Object origin = gDefaultField.get(null);
                Class singleton = Class.forName("android.util.Singleton");
                Field mInstanceField = singleton.getDeclaredField("mInstance");
                mInstanceField.setAccessible(true); Object originAMN = mInstanceField.get(origin); Service targetService = (Service) Class.forName(serviceName).newInstance(); attachMethod.invoke(targetService, this, activityThread, intent.getComponent().getClassName(), token, getApplication(), originAMN); / / service oncreate Method onCreateMethod = serviceClz. GetDeclaredMethod ("onCreate");
                onCreateMethod.setAccessible(true);
                onCreateMethod.invoke(targetService);
                targetService.onStartCommand(intent, flags, startId);
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "onStartCommand: "+ e.getMessage()); }}return super.onStartCommand(intent, flags, startId);
    }
Copy the code

The code is longer and the logic is as follows:

  • The need to start the plug-in Service was detected
  • Parameters needed to build the plug-in Service Attach method
  • Construct a plug-in Service
  • Call the attach method of the plug-in Service
  • Call the onCreate method of the plug-in Service

Thus, a plug-in Service is started.

Add-on of BroadcastReceiver

BroadcastReceiver is divided into two types, static registration and dynamic registration. Statically registered is when PMS scans APK during installation or system startup, parses the configuration file, and stores it in the PMS side, which we cannot interfere with, and because our plug-in is not installed, statically registered can not be loaded through the normal behavior of the system. And dynamic registration, because there is no detection of this step, therefore, we do not need to intervene. We now need to solve the problem of how to load the static registration in the plug-in.

We can register this ourselves by parsing the configuration file and calling the dynamic registration method.

I won’t post the code here, but post it with the ContentProvider below.

The plug-in of ContentProvider

Unlike the other three components, ContentProvider is installed in the process startup portal, ActivityThread. So we can follow this idea, to carry out the installation operation.

Here’s the code.

          Field providersField = packageClz.getDeclaredField("providers");
            providersField.setAccessible(true);
            ArrayList providers = (ArrayList) providersField.get(packageObject);

            Class providerClz = Class.forName("android.content.pm.PackageParser$Provider");
            Field providerInfoField = providerClz.getDeclaredField("info");
            providersField.setAccessible(true);
            List<ProviderInfo> providerInfos = new ArrayList<>();
            for (int i = 0; i < providers.size(); i++) {
                ProviderInfo providerInfo = (ProviderInfo) providerInfoField.get(providers.get(i));
                providerInfo.applicationInfo = getApplicationInfo();
                providerInfos.add(providerInfo);
            }
            Class contextImplClz = Class.forName("android.app.ContextImpl");
            Field mMainThread = contextImplClz.getDeclaredField("mMainThread");
            mMainThread.setAccessible(true);
            Object activityThread = mMainThread.get(this.getBaseContext());
            Class activityThreadClz = Class.forName("android.app.ActivityThread");
            Method installContentProvidersMethod = activityThreadClz.getDeclaredMethod("installContentProviders", Context.class, List.class);
            installContentProvidersMethod.setAccessible(true);
            installContentProvidersMethod.invoke(activityThread, this, providerInfos);
Copy the code

Post the whole code here, including the Multidex method plus dex, BroadcastReceiver plugin and ContentProvider plugin.

    private void loadClassByHostClassLoader() {
        File apkFile = new File("/sdcard/plugin_1.apk");
        ClassLoader baseClassLoader = this.getClassLoader();
        try {
            Field pathListField = baseClassLoader.getClass().getSuperclass().getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object pathList = pathListField.get(baseClassLoader);

            Class clz = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = clz.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true); Object[] dexElements = (Object[]) dexElementsField.get(pathList); Class elementClz = dexElements.getClass().getComponentType(); Object[] newDexElements = (Object[]) Array.newInstance(elementClz, dexElements.length + 1); Constructor<? > constructor = elementClz.getConstructor(File.class, boolean.class, File.class, DexFile.class); File file = new File(getFilesDir(),"test.dex");
            if (file.exists()) {
                file.delete();
            }
            file.createNewFile();
            Object pluginElement = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), file.getAbsolutePath(), 0)); Object[] toAddElementArray = new Object[]{pluginElement}; System.arraycopy(dexElements, 0, newDexElements, 0, dexElements.length); Arraycopy (toAddElementArray, 0, newDexElements, dexElements. Length, toAddElementArray.length); dexElementsField.set(pathList, newDexElements); AssetManager assetManager = getResources().getAssets(); Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            method.invoke(assetManager, apkFile.getPath());

//            PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_RECEIVERS);
//            if(packageInfo ! = null) { //for (ActivityInfo info : packageInfo.receivers) {
//                    Log.e(TAG, "loadClassByHostClassLoader: " + info.name );
//
//                }
//            }
            Class packageParseClz = Class.forName("android.content.pm.PackageParser");
            Object packageParser = packageParseClz.newInstance();
            Method parseMethod = packageParseClz.getDeclaredMethod("parsePackage", File.class, int.class);
            parseMethod.setAccessible(true);
            Object packageObject = parseMethod.invoke(packageParser, apkFile, 1 << 2);
            Class packageClz = Class.forName("android.content.pm.PackageParser$Package");
            Field receiversField = packageClz.getDeclaredField("receivers");
            receiversField.setAccessible(true);
            ArrayList receives = (ArrayList) receiversField.get(packageObject);

            Class componentClz = Class.forName("android.content.pm.PackageParser$Component");
            Field intents = componentClz.getDeclaredField("intents");
            intents.setAccessible(true);
            Field classNameField = componentClz.getDeclaredField("className");
            classNameField.setAccessible(true);
            for(int i = 0; i < receives.size(); i++) { ArrayList<IntentFilter> intentFilters = (ArrayList<IntentFilter>) intents.get(receives.get(i)); String className = (String) classNameField.get(receives.get(i)); registerReceiver((BroadcastReceiver) getClassLoader().loadClass(className).newInstance(), intentFilters.get(0)); } / / install ContentProvider Field providersField = packageClz. GetDeclaredField ("providers");
            providersField.setAccessible(true);
            ArrayList providers = (ArrayList) providersField.get(packageObject);

            Class providerClz = Class.forName("android.content.pm.PackageParser$Provider");
            Field providerInfoField = providerClz.getDeclaredField("info");
            providersField.setAccessible(true);
            List<ProviderInfo> providerInfos = new ArrayList<>();
            for (int i = 0; i < providers.size(); i++) {
                ProviderInfo providerInfo = (ProviderInfo) providerInfoField.get(providers.get(i));
                providerInfo.applicationInfo = getApplicationInfo();
                providerInfos.add(providerInfo);
            }
            Class contextImplClz = Class.forName("android.app.ContextImpl");
            Field mMainThread = contextImplClz.getDeclaredField("mMainThread");
            mMainThread.setAccessible(true);
            Object activityThread = mMainThread.get(this.getBaseContext());
            Class activityThreadClz = Class.forName("android.app.ActivityThread");
            Method installContentProvidersMethod = activityThreadClz.getDeclaredMethod("installContentProviders", Context.class, List.class);
            installContentProvidersMethod.setAccessible(true);
            installContentProvidersMethod.invoke(activityThread, this, providerInfos);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "loadClassByHostClassLoader: "+ e.getMessage()); }}Copy the code

So far, a little bit of the plug-in approach for the four components has been introduced, although only one approach has been introduced for each component. The above content ignores most of the source details. This part of the content needs you to fill.

Plug-in solutions for resources

There are two plug-in schemes for resources

  • Consolidated resource options
  • Each plug-in constructs its own resource schema

Today, we will introduce the first solution, merge resource solution, merge resource solution, we just need to call addAsset in the existing AssetManager to add a resource, of course, there are more adaptive problems, we ignore for the moment. The biggest problem of merging resource schemes is resource conflict. There are two ways to resolve resource conflicts.

  • Modify AAPT, can freely modify PP section
  • Interfere with the compilation process and modify the ASRC and R files

For a simple demonstration, I’ll just use the VirtualApk compiled plug-in directly. In fact, the compiled plug-in for VirtualApk comes from the compiled plug-in for Small. This should be easy to write as long as you’re familiar with the file format.

            AssetManager assetManager = getResources().getAssets();
            Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            method.invoke(assetManager, apkFile.getPath());
Copy the code

We only need the above simple code, can complete the plug-in of resources. Of course, version differences are ignored here.

Plug-in scheme of SO

Plug-in scheme of SO, HERE I introduce the scheme of modifying DexpathList. What are we going to do? Only need to add SO nativeLibraryPathElements Element, and it is ok to add SO nativeLibraryDirectories path. Here’s the code.

            Method findLibMethod = elementClz.getDeclaredMethod("findNativeLibrary",String.class);
            findLibMethod.setAccessible(true);
//            Object soElement = constructor.newInstance(new File("/sdcard/"), true, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(),
//                    file.getAbsolutePath(), 0));
//            findLibMethod.invoke(pluginElement,System.mapLibraryName("native-lib"));
            ZipFile zipFile = new ZipFile(apkFile);
            ZipEntry zipEntry = zipFile.getEntry("lib/armeabi/libnative-lib.so");
            InputStream inputStream = zipFile.getInputStream(zipEntry);
            File outSoFile = new File(getFilesDir(), "libnative-lib.so");
            if (outSoFile.exists()) {
                outSoFile.delete();
            }
            FileOutputStream outputStream = new FileOutputStream(outSoFile);
            byte[] cache = new byte[2048];
            int count = 0;
            while((count = inputStream.read(cache)) ! = -1) { outputStream.write(cache, 0, count); } outputStream.flush(); outputStream.close(); inputStream.close(); SoElement = constructive.newinstance (getFilesDir(),true, null, null);
//            findLibMethod.invoke(soElement,System.mapLibraryName("native-lib")); / / will soElement filling into nativeLibraryPathElements, Field soElementField = CLZ. GetDeclaredField ("nativeLibraryPathElements");
            soElementField.setAccessible(true); Object[] soElements = (Object[]) soElementField.get(pathList); Object[] newSoElements = (Object[]) Array.newInstance(elementClz, soElements.length + 1); Object[] toAddSoElementArray = new Object[]{soElement}; System.arraycopy(soElements, 0, newSoElements, 0, soElements.length); System. arrayCopy (toAddSoElementArray, 0, newSoElements, soElements. toAddSoElementArray.length); soElementField.set(pathList, newSoElements); // Fill a nativeLibraryDirectories with Field libDir = clz.getDeclaredField("nativeLibraryDirectories");
            libDir.setAccessible(true);
            List libDirs = (List) libDir.get(pathList);
            libDirs.add(getFilesDir());
            libDir.set(pathList,libDirs);
Copy the code

conclusion

Under the previous careful study, plug-in scheme has been very mature. The difficulty of plug-in scheme is mainly in adaptation. The rest is fine.

PS: The knowledge related to thermal repair has been written in PPT, and the next chapter will analyze the thermal repair.