51

Hook式插件化

 4 years ago
source link: https://www.tuicool.com/articles/MjiMn2Z
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Hook式插件化

上一篇文章中,通过Hook系统源码实现了不经过AndroidManifest注册也能跳转到对应Activity的功能。这一篇来分析一下怎么通过Hook的方式来实现插件化

从Android类加载的源码开始分析 本文是按照Android9.0源码来,不同系统可能不一样

我们平时跳转Activity的时候比如从MainActivity跳转到LoginActivity中,都是这么写

Intent intent = new Intent(this,LoginActivity.class);
startActivity(intent);

其实还有一种方法,也是可以成功跳转的

Intent intent = new Intent();
intent.setComponent(new ComponentName("com.hsm.hookplugin","com.hsm.hookplugin.LoginActivity"));
startActivity(intent);

上一篇文章Hook进阶中我们已经实现了不经过AndroidManfiest注册就能正常跳转的功能,那如果我们直接传入一个插件包中的包名和类名可以可以完成跳转呢,直接写当然不行,因为我们不知道插件包在哪里,不过我们可以写一下,看看错误信息,然后根据错误信息继续分析

直接写插件包中的包名和全类名

Intent intent = new Intent();
      intent.setComponent(new ComponentName("com.hsm.plugin_package","com.hsm.plugin_package.PluginMainActivity"));
      startActivity(intent);

错误信息如下

java.lang.RuntimeException: Unable to instantiate activity
ComponentInfo{com.hsm.plugin_package/com.hsm.plugin_package.PluginMainActivity}:
java.lang.ClassNotFoundException: Didn't find class
"com.hsm.plugin_package.PluginMainActivity" on path: DexPathList[[zip file
"/data/app/com.hsm.hookplugin-6IZNq_xfampALDCZjZ6YZw==/base.apk"],nativeLibraryDirec
tories=[/data/app/com.hsm.hookplugin-6IZNq_xfampALDCZjZ6YZw==/lib/x86, /system/lib]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:69)
at android.app.Instrumentation.newActivity(Instrumentation.java:1215)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:28 1)

说是在DexPathList这个集合中找不到要跳转的Activity的实例,ClassNotFoundException也就是说在实例化这个Activity的时候找不到它,那我们需要去找一下Activity到底是在哪里实例化的,这就得看一下Activity的启动流程了。(报错信息中已经提示了在BaseDexClassLoader的第134行,不过我们还需要了解到底怎么走到这里,和为啥报错)

Activity的启动流程可以看之前的两篇文章 Activity启动流程(上)Activity启动流程(下)

最终会走到ActivityThread中的performLaunchActivity

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
   //ContextImpl类继承自Context抽象类,
    ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {...}
   ...
}

这里就能看到拿到ClassLoader然后交给mInstrumentation去创建Activity实例并返回, newActivity方法 中传入了ClassLoader,ClassName和intent。

 @Override
public ClassLoader getClassLoader() {
        return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
    }
public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }
static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }
private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }

通过跟踪源码,可以知道这个ClassLoader是一个 PathClassLoader

这时候我们需要了解一下Android中的ClassLoader了,Andorid中的ClassLoader主要有三种

  • BootClassLoader:继承自ClassLoader 主要用来加载FrameWork层的dex文件
  • BaseDexClassLoader:继承自ClassLoader
  • PathClassLoader:继承自BaseDexClassLoader,用来加载已经安装到系统中的apk中的dex文件
  • DexClassLoader:继承自BaseDexClassLoader,用来加载指定的目录中的dex文件(.apk,.zip)

然后在看newActivity这个方法

public Activity newActivity(ClassLoader cl, String className,
            Intent intent)
            throws InstantiationException, IllegalAccessException,
            ClassNotFoundException {
        String pkg = intent != null && intent.getComponent() != null
                ? intent.getComponent().getPackageName() : null;
        return getFactory(pkg).instantiateActivity(cl, className, intent);
    }
//className就是我们最开始通过Intent传过来的
public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
            @Nullable Intent intent)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        return (Activity) cl.loadClass(className).newInstance();
    }

最后通过 cl.loadClass(className).newInstance() 方法来实例化Activity,cl前面也知道了它就是一个PathClassLoader。className就是我们最开始的时候通过Intent传过来的插件包中的Activity的全类名,因为插件包中的这个类找不到所以就会报前面的错误啦。

继续跟下去看看会进入到ClassLoader类中

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

这里面使用了双亲委托模式

  • findLoadedClass(name)检查这个类是否被加载过,最终执行到native方法去查找
  • 如果没有并且其父类不为空,执行父类的loadClass()方法
  • 如果最终还是没有找到,就执行findClass(name)方法自己去加载这个类。

前面创建PathClassLoader的方法中传入了 BootClassLoader.getInstance() 这个就是给parent赋值,我们知道BootClassLoader是用来加载系统FrameWork的文件,这里加载我们Activty返回空,最终会走到最后的findClass方法

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

是一个空方法,最终交给子类BaseDexClassLoader来实现,BaseDexClassLoader在AndroidStudio中看的不全,可以在 https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java 这里看到全部代码,而BaseDexClassLoader的两个子类PathClassLoader和DexClassLoader两个子类都只有构造方法。可以在 https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.javahttps://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java 中看到。所以最终就是执行的BaseDexClassLoader中的findClass方法。

@Override
 protected Class<?> findClass(String name) throws ClassNotFoundException {
     List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
     Class c = pathList.findClass(name, suppressedExceptions);
     if (c == null) {
         ClassNotFoundException cnfe = new ClassNotFoundException(
                 "Didn't find class \"" + name + "\" on path: " + pathList);
         for (Throwable t : suppressedExceptions) {
             cnfe.addSuppressed(t);
         }
         throw cnfe;
     }
     return c;
 }

到这里就可以看到最开始的报错信息了,当c==null的时候抛出这个错误。也就是pathList.findClass方法返回null,pathList变量是DexPathList,所以去DexPathList中看一下findClass方法

https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

public Class<?> findClass(String name, List<Throwable> suppressed) {
       for (Element element : dexElements) {
           Class<?> clazz = element.findClass(name, definingContext, suppressed);
           if (clazz != null) {
               return clazz;
           }
       }
       if (dexElementsSuppressedExceptions != null) {
           suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
       }
       return null;
   }

Element是DexPathList的静态内部类,

public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;

当dexFile不为null的时候才会调用它的loadClassBinaryName方法,否则直接返回null。loadClassBinaryName方法最终会执行到native中。

在DexPathList类中可以看到

this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
                                           
 private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * Open all files and load the (direct or contained) dex files up front.
       */
      for (File file : files) {
          if (file.isDirectory()) {
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
              if (dex != null && isTrusted) {
                dex.setTrusted();
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

dexElements数组是通过传入的一个dexPath的路径来获得的。这时候我们需要分析一下我们的apk文件,直接使用AndroidStudio打开一个apk文件可以看到如图:

这里可以看到一个classes.dex文件,如果我们的应用做了分包处理,这里会看到很多.dex文件,他们的路径会传到上面的代码中,最终解析成为一个Element数组。

这时候我们就知道了,它现在解析的只是当前应用apk下面的.dex文件,而插件apk下面的.dex文件不在这里面,所以上面代码中的返回的Element数组中也就不包含插件中对应的的Element,最终循环完找不到就返回null了。

所以我们可以有这样一个方案,自己解析出插件apk中的Element数组,然后通过反射拿到宿主中的Element数组,把他们两个合成一个在设置回宿主中去,这样宿主中就可以找到对应的类了。

这里还会用到上一篇 Andorid Hook进阶中的一些代码,因为插件中的Activity是在宿主中没有注册的,加载的时候需要绕过AMS的检查。所以可以接着上一篇的代码继续来。

在Application中在添加一个融合Element的方法

/**
    * 插件中的Element融入到宿主中
    */
   private void pluginToAppAction() throws Exception{
       //1. 找到宿主中的dexElements数组
       PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
       Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
       Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
       pathListField.setAccessible(true);
       Object dexPathList = pathListField.get(pathClassLoader);

       Field dexElementsField = dexPathList.getClass().getDeclaredField("dexElements");
       dexElementsField.setAccessible(true);
       Object appDexElements = dexElementsField.get(dexPathList);
       //2.  找到插件中的dexElements数组
       File pluginFile = new File(Environment.getExternalStorageDirectory() + File.separator + "p.apk");
       if (!pluginFile.exists()) {
           Log.i(TAG,"插件包不存在");
           return;
       }
       String pluginPath = pluginFile.getAbsolutePath();
       File pluginDir = this.getDir("pluginDir",MODE_PRIVATE);
       DexClassLoader dexClassLoader = new DexClassLoader(pluginPath,pluginDir.getAbsolutePath(),null,getClassLoader());

       Class<?> pluginBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
       Field pluginPathListField = pluginBaseDexClassLoaderClass.getDeclaredField("pathList");
       pluginPathListField.setAccessible(true);
       Object pluginDexPathList = pluginPathListField.get(dexClassLoader);
       Field dexElementsFieldPlugin = pluginDexPathList.getClass().getDeclaredField("dexElements");
       dexElementsFieldPlugin.setAccessible(true);
       Object pluginDexElements = dexElementsFieldPlugin.get(pluginDexPathList);

       //3. 把两个数组合并成一个新的数组 newElement
       int appLength = Array.getLength(appDexElements);
       int pluginLength = Array.getLength(pluginDexElements);
       int sum = appLength+pluginLength;
       //创建一个新的数组 两个参数,一个是类型  一个是长度
       Object newDexElement = Array.newInstance(appDexElements.getClass().getComponentType(), sum);
       //进行融合
       for (int i = 0; i < sum; i++) {
           if(i<appLength){
               Array.set(newDexElement,i,Array.get(appDexElements,i));
           }else {
               Array.set(newDexElement,i,Array.get(pluginDexElements,i-appLength));
           }
       }
       //4. 把新的数组设置回宿主中
       dexElementsField.set(dexPathList,newDexElement);

       //5. 处理布局文件
       handlePluginLayout();
   }
       private void handlePluginLayout() throws Exception{
       assetManager = AssetManager.class.newInstance();

       // 把插件的路径 给 AssetManager
       File pluginFile = new File(Environment.getExternalStorageDirectory() + File.separator + "p.apk");
       if (!pluginFile.exists()) {
           Log.i(TAG,"插件包不存在");
           return;
       }
       String pluginPath = pluginFile.getAbsolutePath();

       //执行addAssetPath方法吧路径添加进去 assetManager才能加载资源
       Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath",String.class);
       addAssetPathMethod.setAccessible(true);
       addAssetPathMethod.invoke(assetManager,pluginPath);

       //宿主的Resources
       Resources r = getResources();
       //创建新的resources用来加载插件中的资源
       resources = new Resources(assetManager,r.getDisplayMetrics(),r.getConfiguration());

   }
   private Resources resources;
   private AssetManager assetManager;
    @Override
   public Resources getResources() {
       return resources == null ? super.getResources() : resources;
   }

   @Override
   public AssetManager getAssets() {
       return assetManager == null ? super.getAssets() : assetManager;
   }

全部代码就在上面了目的很明确

  1. 找到宿主中的dexElements数组 通过PathClassLoader寻找
  2. 找到插件中的dexElements数组 通过DexClassLoader寻找
  3. 把两个数组合并成一个新的数组 newElement
  4. 把新的数组设置回宿主中
  5. 处理布局文件,我们需要创建自己的Resources和AssetManager来加载插件的布局文件
  6. 创建完之后,重写系统的getResources和getAssets方法,如果自己创建的不为空就使用自己创建的来加载。

将插件打包成apk放到根目录下,运行宿主app

效果:

r6NzumM.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK