美文网首页
Hook技术 —— 加载完整的Apk

Hook技术 —— 加载完整的Apk

作者: 设计失 | 来源:发表于2018-09-11 17:06 被阅读141次

    通过该demo,我们能了解到如下内容:

    1、 融合不同的 apk dex 文件,
    2、 了解到Element对象以及DexFile 对象
    3、 插件中APK资源的合并

    本文切入点

    1、 融合Element数组
    2、 获取资源文件

    一、 融合 Element 数组

    首先了解一下什么是ClassLoader,以及java 中 ClassLoader与Android 中ClassLoader的区别

    这里很详细的介绍了DexClassLoader

    假设你已经看完了上面的文章~ 下面我们来分析一下java中加载class文件的流程:

    看下下面的代码:
    // 我们反射到系统的api
    Class.forName("android.app.ActivityThread");
    

    这个forName方法最终调用了 Class.java 中的native方法:

    static native Class<?> classForName(String className, boolean shouldInitialize,
                ClassLoader classLoader) throws ClassNotFoundException;
    

    从native层又到了java层中 PathClassLoader ,PathClassLoader 最终又调用了父类BaseDexClassLoader 中的findClass方法:

    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
            Class c = pathList.findClass(name, suppressedExceptions);
            ... 省略
            return c;
        }
    

    我们看 pathList.findClass(); 这个pathList对象是什么?

    private final DexPathList pathList;

    DexPathList对象中有一个findClass 方法:

    
      public Class findClass(String name, List<Throwable> suppressed) {
              // Element数组存放Dex文件
            for (Element element : dexElements) {
                DexFile dex = element.dexFile;
    
                if (dex != null) {
                    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                    if (clazz != null) {
                        return clazz;
                    }
                }
            }
            if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }
            return null;
        }
    

    最终是从Dexfile类中获取到class文件并返回,所以DexFile文件是APK包中dex文件在java中的一个映射~

    以上就是系统加载class文件的过程,我们可以在根源入手,把apk中的dex和原app中的dex合并到一起转换成Element

    融合两个apk中的dex文件

    在我们看到PathClassLoader调用到父类中findClass的时候,可以看到BaseDexClassLoader中的构造方法初始化了DexPathList对象:

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(parent);
            //ClassLoader 类加载器
            //String dexPath, dex文件的路径
            //String libraryPath 库路径
            // File optimizedDirectory 缓存路径
            this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
        }
    

    在DexPathList构造方法中又初始化了Element对象:

      public DexPathList(ClassLoader definingContext, String dexPath,
                String libraryPath, File optimizedDirectory) {
    ... 省略
    
            // 在这里初始化
            this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null,
                                                              suppressedExceptions);
    
            if (suppressedExceptions.size() > 0) {
                this.dexElementsSuppressedExceptions =
                    suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
            } else {
                dexElementsSuppressedExceptions = null;
            }
        }
    
    

    看看 makePathElements() 方法:

     /**
         * Makes an array of dex/resource path elements, one per element of
         * the given array.
         */
        private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                                  List<IOException> suppressedExceptions) {
            List<Element> elements = new ArrayList<>();
            /*
             * Open all files and load the (direct or contained) dex files
             * up front.
             */
            // 打开dex中所有的文件,来加载
            for (File file : files) {
                File zip = null;
                File dir = new File("");
                DexFile dex = null;
                String path = file.getPath();
                String name = file.getName();
    
                if (path.contains(zipSeparator)) {
                    String split[] = path.split(zipSeparator, 2);
                    zip = new File(split[0]);
                    dir = new File(split[1]);
                } else if (file.isDirectory()) {
                    // We support directories for looking up resources and native libraries.
                    // Looking up resources in directories is useful for running libcore tests.
                    // 将文件夹添加进数组
                    elements.add(new Element(file, true, null, null));
                } else if (file.isFile()) {
                    // 如果文件是以 dex 结尾
                    if (name.endsWith(DEX_SUFFIX)) {
                        // Raw dex file (not inside a zip/jar).
                        try {
                            // 获取到一个DexFile文件,不包含 zip/jar
                            dex = loadDexFile(file, optimizedDirectory);
                        } catch (IOException ex) {
                            System.logE("Unable to load dex file: " + file, ex);
                        }
                    } else {
                        // 如果不是以 dex 结尾,则以一个压缩包的形式来解决
                        zip = file;
    
                        try {
                            dex = loadDexFile(file, optimizedDirectory);
                        } catch (IOException suppressed) {
                            /*
                             * IOException might get thrown "legitimately" by the DexFile constructor if
                             * the zip file turns out to be resource-only (that is, no classes.dex file
                             * in it).
                             * Let dex == null and hang on to the exception to add to the tea-leaves for
                             * when findClass returns null.
                             */
                            suppressedExceptions.add(suppressed);
                        }
                    }
                } else {
                    System.logW("ClassLoader referenced unknown path: " + file);
                }
    
                if ((zip != null) || (dex != null)) {
                    // 将文件添加到Element 数组中
                    elements.add(new Element(dir, false, zip, dex));
                }
            }
    
            return elements.toArray(new Element[elements.size()]);
        }
    
    /**
         * Constructs a {@code DexFile} instance, as appropriate depending
         * on whether {@code optimizedDirectory} is {@code null}.
         */
        private static DexFile loadDexFile(File file, File optimizedDirectory)
                throws IOException {
            if (optimizedDirectory == null) {
                return new DexFile(file);
            } else {
                String optimizedPath = optimizedPathFor(file, optimizedDirectory);
                return DexFile.loadDex(file.getPath(), optimizedPath, 0);
            }
        }
    

    到这里,我们可以看到BaseDexClassLoader初始化的时候就给我们实例化了Element[] 数组,所以我们要融合多个dex,只需要实例化一个BaseDexClassLoader!

    首先获取到系统中的Element数组
    //1、  找到系统中的 Elements数组,   private final Element[] dexElements;
    Class myDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
    Field myPathListField = myDexClassLoader.getDeclaredField("pathList");
    myPathListField.setAccessible(true);
    // 获取到 DexPathList
    Object myPathList = myPathListField.get(myDexClassLoader);
    // 获取到dexElements   private final Element[] dexElements;
    Field dexElements = myPathList.getClass().getDeclaredField("dexElements");
    dexElements.setAccessible(true);
    // 获取到系统的apk的Element数组
    Object myElements = dexElements.get(myPathList);
    
    然后获取到插件中的Element数组
    //2、  找到插件的 Element数组,
    PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
    Class pluginDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
    Field pluginPathList = pluginDexClassLoader.getDeclaredField("pathList");
    pluginPathList.setAccessible(true);
    // 获取到 DexPathList
    Object pluginDexPathList = pluginPathList.get(pathClassLoader);
    // 获取到dexElements   private final Element[] dexElements;
    Field pluginDexElements = pluginDexPathList.getClass().getDeclaredField("dexElements");
    dexElements.setAccessible(true);
    // 获取到系统的apk的Element数组
    Object pluginElements = pluginDexElements.get(myPathList);
    
    融合两个数组对象
    //3、 融合两个 Elements 长度为插件和系统两个数组的长, 反射注入到系统中
    // 3_1 获取到两个Elements的长度,得到最新的长度
    int myLength = Array.getLength(myElements);
    int pluginLength = Array.getLength(pluginElements);
    int newLength = myLength + pluginLength;
    // 3_2 每个数组的类型,已经新生成的数组的长度
    // 找到Element的类型
    Class componentType = myElements.getClass().getComponentType();
    Object newElements = Array.newInstance(componentType, newLength);
    // 3_3 融合
    for (int i = 0; i < newLength; i++) {
        if (i < myLength) {
            Array.set(newElements, i, Array.get(myElements, i));
        } else {
            Array.set(newElements, i, Array.get(pluginElements, i - myLength));
        }
    }
    // 3_4 获取到dexElements   private final Element[] dexElements;
    Field elementsField = myPathList.getClass().getDeclaredField("dexElements");
    elementsField.setAccessible(true);
    elementsField.set(myPathList, newElements);
    

    上面就融合了安装APK与插件APK中的Dex文件


    二 、获取到资源文件

    首先我们看下平时我们获取到资源:

     @NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
            CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
            if (res != null) {
                return res;
            }
            throw new NotFoundException("String resource ID #0x"
                    + Integer.toHexString(id));
        }
    

    实际上我们获取到资源是通过AssetManager对象获取,所以我们需要获取到外部存储卡的Resource对象和AssetManager对象:

     // apk路径
    String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "plugin.apk";
    // 初始化一个AssetManager
    assetManager = AssetManager.class.newInstance();
    Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath",String.class);
    addAssetPath.setAccessible(true);
    addAssetPath.invoke(assetManager, apkPath);
    // 初始化Resources 对象,加载资源
    resources = new Resources(
                       assetManager, // AssetManager
                        getApplicationContext().getResources().getDisplayMetrics(), // DisplayMetrics
                        getApplicationContext().getResources().getConfiguration()  // Configruation
                );
    

    上面是初始化了AssetManager对象,以及Resources对象,实际上资源的加载都是通过AssetManager中的addAssetPath方法来注入,所以我们反射获取到AssetManager对象之后,需要invoke到addAssetPath对象,把插件的路径加载~

    通过以上就让插件的资源融合了,但是还有一点,就是获取到资源的时候,没有初始化需要的Block对象,而Block对象是什么呢?

    初始化 block 系列对象

    通过getResouces().getText() 可以获取到一个文字资源,查看源码,我们可以看到 (这里以安卓6.0 api为主)

        /**
         * Retrieve the string value associated with a particular resource
         * identifier for the current configuration / skin.
         */
        /*package*/ final CharSequence getResourceText(int ident) {
            synchronized (this) {
                TypedValue tmpValue = mValue;
                int block = loadResourceValue(ident, (short) 0, tmpValue, true);
                if (block >= 0) {
                    if (tmpValue.type == TypedValue.TYPE_STRING) {
                        // 获取到StringBlock
                        return mStringBlocks[block].get(tmpValue.data);
                    }
                    return tmpValue.coerceToString();
                }
            }
            return null;
        }
    

    而在Resouces构造方法中,并没有初始化StringBlock数组的方法,通过层层追查,我们看到如下方法:

    /*package*/ final boolean getThemeValue(long theme, int ident,
                TypedValue outValue, boolean resolveRefs) {
            int block = loadThemeAttributeValue(theme, ident, outValue, resolveRefs);
            if (block >= 0) {
                if (outValue.type != TypedValue.TYPE_STRING) {
                    return true;
                }
                StringBlock[] blocks = mStringBlocks;
                if (blocks == null) {
                    // 为空时 则调用 ensureStringBlocks() 方法
                    ensureStringBlocks();
                    blocks = mStringBlocks;
                }
                outValue.string = blocks[block].get(outValue.data);
                return true;
            }
            return false;
        }
    

    接下来调用了ensureStringBlocks() 方法:

       /*package*/ final void ensureStringBlocks() {
           // 这里使用了双重校验
            if (mStringBlocks == null) {
                synchronized (this) {
                    if (mStringBlocks == null) {
                        makeStringBlocks(sSystem.mStringBlocks);
                    }
                }
            }
        }
    

    再调用了makeStringBlocks() :

    
        /*package*/ final void makeStringBlocks(StringBlock[] seed) {
            final int seedNum = (seed != null) ? seed.length : 0;
            final int num = getStringBlockCount();
            mStringBlocks = new StringBlock[num];
            if (localLOGV) Log.v(TAG, "Making string blocks for " + this
                    + ": " + num);
            for (int i=0; i<num; i++) {
                if (i < seedNum) {
                    mStringBlocks[i] = seed[i];
                } else {
                    // 通过native 方法来获取到StringBlock数组
                    mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
                }
            }
        }
    

    到此,就初始化了StringBlock ,所以我们需要反射调用 ensureStringBlocks 方法,来初始化:

    // 初始化StringBlock对象
    Method ensureStringBlocks = assetManager.getClass().getDeclaredMethod("ensureStringBlocks");
    ensureStringBlocks.setAccessible(true);
    ensureStringBlocks.invoke(assetManager);
    

    将插件中的添加BaseActivity 重写getResouces() 方法和 getAssets 方法 :

    
    public class BaseActivity extends Activity {
    
        @Override
        public Resources getResources() {
            if(getApplication()!=null && getApplication().getResources()!=null) {
                return getApplication().getResources();
            }
            return super.getResources();
        }
    
        @Override
        public AssetManager getAssets() {
            
            if(getApplication()!=null && getApplication().getAssets()!=null) {
                return getApplication().getAssets();
            }
            return super.getAssets();
        }
    }
    
    最后一步,跳转界面:
    Intent i = new Intent();
    i.setComponent(new ComponentName(pkg, cls));
    startActivity(i);
    

    完~~

    相关文章

      网友评论

          本文标题:Hook技术 —— 加载完整的Apk

          本文链接:https://www.haomeiwen.com/subject/euwugftx.html