1. Introduction

There are advantages and disadvantages of Hook plug-in in the aforementioned exploration of Android plug-in (ii). The advantage is that components such as activities in plug-ins do not need to depend on the environment of the host APP, and can freely use context objects such as this and context. However, the disadvantages are also very obvious, that is, the more plug-ins, the dexElements array in memory will become larger and larger, which may cause exceptions such as memory overflow. Today’s LoadedApk plug-in solves the problem of Hook plug-in.

2. StartActivity source analysis

Let’s start with startActivity again.

startActivity() --> Activity.startActivity() --> Activity.startActivityForResult() --> Instrumentation. ExecStartActivity () - > ActivityTaskManager. GetService () startActivity (AMS) ActivityThread. HandleLaunchActivity () - > performLaunchActivity () (this) in themselves to handle LoadedApkCopy the code

We know that,ActivitycallstartActivityAnd then at the endActivityThreadperformhandleLaunchActivityWe’ll follow it straight to oursActivityInitialization is defined byLoadedApkThe inside of themClassLoaderFor loading. From this we get our Hook point, how can we customize a LoadedApk and put themClassLoaderReplace it with our ownClassLoaderYou are ready to load the plug-in’s classes. As you can see in the figure below, we are pulling backhandleLaunchActivity()It would have passed beforegetPackageInfoNoCheck()Method to initialize ourLoadedApkObject and store variables such as globalmPackagesSo we’re going to create a plug-inLoadedApkAnd add it tomPackagesI’ll do it in this set.

Let’s sketch the process briefly:

3. Code to achieve custom LoadedApk

LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk: LoadedApk Only the dexElements array that fuses hosts and plug-ins is masked. If you haven’t seen Hook plug-in, you can click here to check it out.

ActivityThread has a set of LoadedApk mPackages, so we first get this variable, and then customize our LoadedApk. LoadedApk is not open to developers. The LoadedApk instance is returned by getPackageInfoNoCheck(), and the LoadedApk instance is returned by a ClassLoader. MClassLoader is replaced with our own ClassLoader. Finally, store our custom LoadedApk in mPackages.

Simple process analysis is as follows:

1. Reflect the mPackages of ActivityThread

2. Customize a LoadedApk

3. Customize a ClassLoader

4. Reflect LoadedApk’s mClassLoader and assign a custom ClassLoader to it

5. Store custom LoadedApk in mPackages

Now we follow the above process step by step code implementation.

3.1 Obtaining the mPackages of ActivityThread

This is a little bit easier, so I’ll just paste the code here.

// Get ActivityThread Class<? > mActivityThreadClass = Class.forName("android.app.ActivityThread"); // Obtain the currentActivityThread() Method currentActivityThread = mActivityThreadClass.getDeclaredMethod("currentActivityThread"); currentActivityThread.setAccessible(true); / / get ActivityThread instance Object mActivityThread = currentActivityThread. Invoke (null); //final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>(); / / get mPackages attribute Field mPackagesField = mActivityThreadClass. GetDeclaredField (" mPackages "); mPackagesField.setAccessible(true); ArrayMap<String, Object> mPackages = (ArrayMap<String, Object>) mPackagesField. Get (mActivityThread);Copy the code

3.2 Customize a LoadedApk

Customizing our LoadedApk is a bit more complicated, so let’s focus on this. First we know that to get a LoadedApk instance we can use reflection to call the getPackageInfoNoCheck(ApplicationInfo ai,CompatibilityInfo compatInfo) method. This method passes two arguments. The first type is ApplicationInfo and the first type is CompatibilityInfo. How do we get instances of these two classes?

3.2.1 Obtaining the CompatibilityInfo Instance

The second parameter is easier to get, which is easier to get later. To follow upCompatibilityInfoThe source code for this class is available, and it has a static variableDEFAULT_COMPATIBILITY_INFOIt’s its own instance object, and we can get that variable by reflection, and then we can solve the second argument problem.

3.2.2 Obtaining the ApplicationInfo Instance

The second parameter, mentioned in the first article, is available in the source codePackageParser.javaClass, which has a method in itgenerateApplicationInfo()You can return aApplicationInfoInstance, we can get one by reflecting this methodApplicationInfoInstance, but in the method, you need to pass three arguments of typePackage(the class isPackageParserInner class), and int as wellPackageUserState. Where do we get instances of these three types?

To viewPackageParserSource code discovery is available throughparsePackage()Method can return onePackageAnd simply pass in our plugin’s File instance and an int value.So the first argument takes care of itself, and the third argument we can get the class by reflection and executenewInstance()To get an instance, we’ll just pass a 0 for the second argument, and then we’ll get oneApplicationInfoExample, as follows:

Private ApplicationInfo getAppInfo(File File) throws Exception {/* Run this method to obtain ApplicationInfo public static ApplicationInfo generateApplicationInfo(Package p, int flags,PackageUserState state) */ Class<? > mPackageParserClass = Class.forName("android.content.pm.PackageParser"); Class<? > mPackageClass = Class.forName("android.content.pm.PackageParser$Package"); Class<? > mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState"); / / get generateApplicationInfo Method generateApplicationInfoMethod = mPackageParserClass.getDeclaredMethod("generateApplicationInfo", mPackageClass, int.class, mPackageUserStateClass); / / create instances Object PackageParser mPackageParser = mPackageParserClass. NewInstance (); Public Package parsePackage(File packageFile, Int flags) * / / / get parsePackage Method parsePackageMethod = mPackageParserClass. GetDeclaredMethod (" parsePackage ", File.class, int.class); / / execution parsePackage method to get the Package instance Object mPackage = parsePackageMethod. Invoke (mPackageParser, file, PackageManager.GET_ACTIVITIES); // Execute generateApplicationInfo, Get ApplicationInfo instance ApplicationInfo ApplicationInfo = (ApplicationInfo) generateApplicationInfoMethod. Invoke (null, mPackage, 0, mPackageUserStateClass.newInstance()); / / we get ApplicationInfo the default path is not set, we want to set up / / ApplicationInfo sourceDir = plugin path; / / applicationInfo. PublicSourceDir = plugin path; applicationInfo.sourceDir = file.getAbsolutePath(); applicationInfo.publicSourceDir = file.getAbsolutePath(); return applicationInfo; }Copy the code

3.2.3 Obtaining the LoadedApk instance

Now that we have two instances of LoadedApk, we can get an instance of LoadedApk as follows:

// Execute the following method to return a LoadedApk, We simulate system performing this method / * this packageInfo = client. GetPackageInfoNoCheck (activityInfo applicationInfo, compatInfo); public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo, int flags) */ Class<? > mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo"); Method getLoadedApkMethod = mActivityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, mCompatibilityInfoClass); /* public static final CompatibilityInfo DEFAULT_COMPATIBILITY_INFO = new CompatibilityInfo() {}; * / / / the above comment is to obtain the default CompatibilityInfo instance Field mCompatibilityInfoDefaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO"); Object mCompatibilityInfo = mCompatibilityInfoDefaultField.get(null); ApplicationInfo ApplicationInfo = getAppInfo(file); // Execute this method, To get a LoadedApk Object mLoadedApk = getLoadedApkMethod. Invoke (mActivityThread, applicationInfo mCompatibilityInfo);Copy the code

3.3 Customizing a ClassLoader

This is relatively simple, the previous article also mentioned, not to go into the code:

// Customize a ClassLoader String optimizedDirectory = context.getDir("plugin", context.mode_private).getabsolutePath (); DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDirectory, null, context.getClassLoader());Copy the code

3.4 Replacing the mClassLoader of LoadedApk with a user-defined ClassLoader

This is also very simple, directly on the code:

MClassLoaderField = mLoadedapk.getClass ().getDeclaredField("mClassLoader"); mClassLoaderField.setAccessible(true); // Set custom classLoader to mClassLoader property McLassloaderfield. set(mLoadedApk, classLoader);Copy the code

3.5 Save custom LoadedApk to mPackages

WeakReference loadApkReference = new WeakReference(mLoadedApk); / / add custom LoadedApk mPackages. Put (applicationInfo packageName, loadApkReference); // Reset mPackages mPackagesField. Set (mActivityThread, mPackages);Copy the code

By following the steps above, we successfully saved our custom LoadedApk to mPackages. Complete the code to come in a wave:

public void customLoadApkAction() throws Exception { File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "plugin2.apk"); if (! File.exists ()) {throw new FileNotFoundException(" Plug-in package does not exist "); } // Get ActivityThread Class<? > mActivityThreadClass = Class.forName("android.app.ActivityThread"); // Obtain the currentActivityThread() Method currentActivityThread = mActivityThreadClass.getDeclaredMethod("currentActivityThread"); currentActivityThread.setAccessible(true); / / get ActivityThread instance Object mActivityThread = currentActivityThread. Invoke (null); //final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>(); / / get mPackages attribute Field mPackagesField = mActivityThreadClass. GetDeclaredField (" mPackages "); mPackagesField.setAccessible(true); ArrayMap<String, Object> mPackages = (ArrayMap<String, Object>) mPackagesField.get(mActivityThread); // Execute the following method to return a LoadedApk, We simulate system performing this method / * this packageInfo = client. GetPackageInfoNoCheck (activityInfo applicationInfo, compatInfo); public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo, int flags) */ Class<? > mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo"); Method getLoadedApkMethod = mActivityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, mCompatibilityInfoClass); /* public static final CompatibilityInfo DEFAULT_COMPATIBILITY_INFO = new CompatibilityInfo() {}; * / / / the above comment is to obtain the default CompatibilityInfo instance Field mCompatibilityInfoDefaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO"); Object mCompatibilityInfo = mCompatibilityInfoDefaultField.get(null); ApplicationInfo ApplicationInfo = getAppInfo(file); // applicationInfo.uid = context.getApplicationInfo().uid; // Execute this method, To get a LoadedApk Object mLoadedApk = getLoadedApkMethod. Invoke (mActivityThread, applicationInfo mCompatibilityInfo); // Customize a ClassLoader String optimizedDirectory = context.getDir("plugin", context.mode_private).getabsolutePath (); DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDirectory, null, context.getClassLoader()); //private ClassLoader mClassLoader; MClassLoaderField = mLoadedapk.getClass ().getDeclaredField("mClassLoader"); mClassLoaderField.setAccessible(true); // Set custom classLoader to mClassLoader property McLassloaderfield. set(mLoadedApk, classLoader); WeakReference loadApkReference = new WeakReference(mLoadedApk); / / add custom LoadedApk mPackages. Put (applicationInfo packageName, loadApkReference); // Reset mPackages mPackagesField. Set (mActivityThread, mPackages); Thread.sleep(2000); }Copy the code

4. An error is reported

The code was written, it was run, and by some miracle, it crashed.

Take a look at the crash log. My project is running on devices running Android10.0 and will not report this exception on devices running earlier versions.The reason is that my plugin does not belong to this process, check the system API method called, finally located inContentProviderNative.call()As this is a remote Binder, we can’t hook it, so we have to go backandroid.provider.Settings.NameValueCache.getStringForUser(Settings.java:2374)This section, tracking this discovery, reported the wrong location.And what we found is,NameValueCacheThis class ofmProviderHolderProperty can return oneIContentProviderInstance,IContentProviderIt’s an interface that we can replace with a dynamic proxycall()The method’s package name should avoid the above exception. The log call stack shows that we passed firstSettings$Global.getStringForUser() From the source code, the Global class has these two key attributes that allow us to reflect the call method that caused the error. The following steps are used to obtain reflection:

Get the sProviderHolder property of the Settings$Global class

  	 Field sProviderHolderFiled = Settings.Global.class.getDeclaredField("sProviderHolder");
  	 sProviderHolderFiled.setAccessible(true);
  	 Object sProviderHolder = sProviderHolderFiled.get(null);
Copy the code

Get Settings$ContentProviderHolder’s getProvider() method

 	Method getProviderMethod = sProviderHolder.getClass().getDeclaredMethod("getProvider", ContentResolver.class);
        getProviderMethod.setAccessible(true);
Copy the code

3. Obtain the original IContentProvider instance object

	 final Object iContentProvider = getProviderMethod.invoke(sProviderHolder, context.getContentResolver());
Copy the code

Settings$gets the mContentProvider property of the ContentProviderHolder class

	Field mContentProviderFiled = sProviderHolder.getClass().getDeclaredField("mContentProvider");
        mContentProviderFiled.setAccessible(true);
Copy the code

5. Obtain the IContentProvider class

Class<? > mIContentProviderClass = Class.forName("android.content.IContentProvider");Copy the code

6. Create our own proxy object

Object mContentProviderProxy = Proxy.newProxyInstance( context.getClassLoader(), new Class[]{mIContentProviderClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("call")) { Log.d("yuongzw", method.getName()); Args [0] = context.getPackagename (); } return method.invoke(iContentProvider, args); }});Copy the code

7. Set our proxy object to the mContentProvider property

	mContentProviderFiled.set(sProviderHolder, mContentProviderProxy);
Copy the code

After completing our fix to the error log in these 7 steps, run it again to see what happens.

There is an error again, should not ah? Didn’t I fix it already? I looked at the error log, the error reported is not quite the same as last time, this time isSettings$SystemFor this class, we hook some attributes of this class again according to the previous 7 steps. At this point, all the error messages have been fixed, and the last image is:Project Address:LoadApkDemo