likes
comments
collection
share

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

作者站长头像
站长
· 阅读数 4

 前言

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

反编译

什么是反编译?

利用编译程序从源语言编写的源程序产生目标程序的过程;

如何进行反编译?

  1. 解压缩 apk 文件;
  • 如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

  • META-INF  存放签名文件

  • classes.dex  需要将这个文件反编译成 jar

  • res

  • resources.arsc

  1. dex2jar,将 class.dex 转换成 jar;
  2. 使用 JD-GUI 查看 jar;

MAC 下反编译可以查看这篇博客:mac下反编译

加固方案

反模拟器

模拟器运行 APK,可以用模拟器监控到 APK 的各种行为,所以在实际的加固 APK 运行中,一旦发现模拟器在运行该 APK,就停止核心代码的运行;

代码虚拟化

代码虚拟化在桌面平台应用保护中已经是非常的常见了,主要思路是自建一个虚拟执行引擎,把原生的可执行代码转换成自定义的指令进行虚拟执行;

加密

样本的部分可执行代码是以压缩或者加密的形式存在的,比如:被保护过的代码被切割成多个小段,前面的一段代码先把后面的代码片段在内存中解密,然后再去执行解密之后的代码,如此一块一块的迭代执行;(微信就是基于此的一种加固方式)本章的手写 APK 加固的核心实现也是基于此方案实现;

基于加密方案的加固原理

我们所有的 Dex 经过 classloader 加载到内存后,执行运行,但是 Dex 并不是全部都会进行加载,而是需要用之前加载;

所以我们可以将 Dex 文件分成核心代码和非核心代码,将非核心代码展示出来,当非核心代码需要用到核心代码的时候,再把核心代码中的类加载进来,核心代码加固(加密),提升代码安全性(加载的过程中顺带着解密);

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

源 Dex 进行加密,壳 Dex 不加密,用壳 Dex 对源 Dex 进行解密;

加固框架总体思路

  1. 将 APK 文件分为 Dex 一类和其他一类;

  2. 文件解压缩(unzip),对文件进行分类(过滤)「一类是 Dex,一类是非 Dex」;

  3. 对 Dex 进行加密操作,将 Dex 文件中的二进制进行读取,读取出来之后进行 AES 加密,加密完成之后再写回去「文件 IO 操作」;

  4. 创建一个壳 Dex 文件;

  5. 将壳 Dex 和加密后的 Dex 文件合并;

  6. 将合并后的 Dex 与其他非 Dex 文件进行合并成一个新的 APK 文件;

  7. 对新的 APK 文件进行重新签名;

  8. 安装运行,验证是否成功;

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

这是加固前的流程,加固中和加固后的流程呢?

  • Dex 文件可以随意拼凑吗?
  • 壳 Dex 怎么来的?
  • 如何签名?
  • 如何运行新的 APK(脱壳)?

Dex 文件可以随意拼凑吗?

我们对 Dex 进行操作的时候,需要了解 Dex 的文件格式

Dex 文件基本格式

文件头 header

  • checksum
  • signnature
  • file_size
  • 如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

索引区

  • 字符串的索引 string_ids

  • 类型的索引 type_ids

  • 方法原型的索引 proto_ids

  • 域的索引 field_ids

  • 方法的索引 method_ids

数据区

  • 类的定义区 class_defs
  • 数据区 data
  • 链接数据区 link_data

对 Dex 的修改,我们必须要按照 Dex 的格式来进行,例如我们往数据区的 data 写入了一段数据,那么我们就需要按照格式修改 header 区的 file_size signnature checksum(checksum 是对整个文件的一个校验)

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

APK 打包流程

  1. 资源文件通过 aapt 工具生成 R.java 文件,由 gradle 自动调用构建工具;

  2. .aidl File 通过 aidl 工具自动生成 java 代码;

  3. 开发者创建的 java 代码 + aidl 创建的 java 代码以及资源文件生成的 java 代码,通过 java compiler 生成 class「也就是 javac 的操作」;

  4. 用 dx.bat 工具 将 class 文件生成 dex 文件;

  5. 用 apkbuilder 工具将资源文件和 dex 文件其他文件打包成一个 APK 文件;

  6. 用 jarsigner + keystore 对 apk 进行签名,获取签名文件;

  7. zipalign 对齐;

  • 如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

手写加固框架(非商用)

整体分为四个大的流程

第一步 处理原始 APK & 加密 Dex;

原始 APK 的处理,其实就是文件的IO操作,解压 APK,并读取其中的 Dex 文件;

APK 解压,并读取 Dex 文件

public class ZipUtils {
    public static void unZip(File zip, File dir) {
        try {
            dir.delete();
            // 适用 jdk 提供的 ZipFile API 进行解压读取操作
            ZipFile zipFile = new ZipFile(zip);
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry zipEntry = entries.nextElement();
                String name = zipEntry.getName();
                // 过滤非 dex 文件
                if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
                        .equals("META-INF/MANIFEST.MF")) {
                    continue;
                }
                if (!zipEntry.isDirectory()) {
                    File file = new File(dir, name);
                    if (!file.getParentFile().exists()) {
                        file.getParentFile().mkdirs();
                    }
                    FileOutputStream fos = new FileOutputStream(file);
                    InputStream is = zipFile.getInputStream(zipEntry);
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = is.read(buffer)) != -1) {
                        fos.write(buffer, 0, len);
                    }
                    // 关闭相关输入输出流
                    is.close();
                    fos.close();
                }
            }
            zipFile.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

对 Dex 文件加密

public static File encryptAPKFile(File srcAPKfile, File dstApkFile) throws Exception {
    if (srcAPKfile == null) {
        System.out.println("encryptAPKFile :srcAPKfile null");
        return null;
    }
    // File disFile = new File(srcAPKfile.getAbsolutePath() + "unzip");
    // ZipUtls.unZip(srcAPKfile, disFile);
    // ZipUtils.unZip(srcAPKfile, dstApkFile);
        
    // 获得所有的dex (需要处理分包情况)   
    File[] dexFiles = dstApkFile.listFiles(new FilenameFilter() {
        @Override
        public boolean accept(File file, String s) {
            return s.endsWith(".dex");
        }
    });

    File mainDexFile = null;
    byte[] mainDexData = null;

    for (File dexFile: dexFiles) {
        // 读数据,dex 转 byte
        byte[] buffer = Utils.getBytes(dexFile);
        // 加密,对 byte 进行加密
        byte[] encryptBytes = AESUtils.encrypt(buffer);

        if (dexFile.getName().endsWith("classes.dex")) {
            mainDexData = encryptBytes;
            mainDexFile = dexFile;
        }
        // 写数据,替换原来的数据
        FileOutputStream fos = new FileOutputStream(dexFile);
        fos.write(encryptBytes);
        fos.flush();
        fos.close();
    }
    return mainDexFile;
}

public static byte[] encrypt(byte[] content) {
    try {
        byte[] result = encryptCipher.doFinal(content);
        return result;
    } catch (IllegalBlockSizeException e) {
        e.printStackTrace();
    } catch (BadPaddingException e) {
        e.printStackTrace();
    }
    return null;
}

public static byte[] getBytes(File dexFile) throws Exception {
   RandomAccessFile fis = new RandomAccessFile(dexFile, "r");
   byte[] buffer = new byte[(int)fis.length()];
   fis.readFully(buffer);
   fis.close();
   return buffer;
}

对加密后的 Dex 进行重命名,区分壳 Dex;

if (newApkFile.isDirectory()) {
    return;			
}
File[] listFiles = newApkFile.listFiles();
for (File file : listFiles) {
    if (file.isFile()) {
	if (file.getName().endsWith(".dex")) {
            String name = file.getName();
	    System.out.println("rename step1:"+name);
	    int cursor = name.indexOf(".dex");
	    String newName = file.getParent() + File.separator + name.substring(0, cursor) + "_" + ".dex";
	    System.out.println("rename step2: " + newName);
	    file.renameTo(new File(newName));
	}
    }
}

第二步、第三步直接在main中进行执行即可;

public class HandleProguard {

    public static void main(String[] args) throws Exception {
    	
    	byte[] mainDexData; //存储源apk中的源dex文件 
    	byte[] aarData;     // 存储壳中的壳dex文件
    	byte[] mergeDex;    // 存储壳dex 和源dex 的合并的新dex文件
    	
    	
    	File tempFileApk = new File("src/source/apk/temp");
    	if (tempFileApk.exists()) {
	    File[]files = tempFileApk.listFiles();
	    for(File file: files){
		if (file.isFile()) {
		    file.delete();
		}
	    }
	}
    	
    	File tempFileAar = new File("src/source/aar/temp");
    	if (tempFileAar.exists()) {
    	    File[]files = tempFileAar.listFiles();
	    for(File file: files){
		if (file.isFile()) {
		    file.delete();
		}
	    }
	}
    	
        /**
         * 第一步 处理原始apk 加密dex
         *
         */
        AES.init(AES.DEFAULT_PWD);
        File apkFile = new File("src/source/apk/app-debug.apk");
        File newApkFile = new File(apkFile.getParent() + File.separator + "temp");
        if(!newApkFile.exists()) {
            newApkFile.mkdirs();
        }
        // 解压 apk
        Zip.unZip(apkFile, newApkFile);
        File mainDexFile = AES.encryptAPKFile(apkFile, newApkFile);
        if (!newApkFile.isDirectory()) {
	    return;
	}
        File[] listFiles = newApkFile.listFiles();
	for (File file : listFiles) {
	    if (file.isFile()) {
		if (file.getName().endsWith(".dex")) {
		    String name = file.getName();
		    System.out.println("rename step1:"+name);
		    int cursor = name.indexOf(".dex");
		    String newName = file.getParent()+ File.separator + name.substring(0, cursor) + "_" + ".dex";
		    System.out.println("rename step2:"+newName);
		    file.renameTo(new File(newName));
		}
	    }
	}
    }
}

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

执行的结果:在 src/source 目录下生成 temp 文件夹,以及 APK 解压之后的相关文件以及加密之后的 Dex 文件,重命名是为了区分壳 Dex 和源 Dex;

第二步 处理 AAR 中的 JAR 获得壳 Dex;

/**
  * 第二步 处理aar 获得壳dex
  */
File aarFile = new File("src/source/aar/mylibrary-debug.aar");
// 在 aar 目录下创建 temp 目录
File fakeDex = new File(aarFile.getParent() + File.separator + "temp");
System.out.println("jar2Dex: aarFile.getParent(): " + aarFile.getParent());
// 解压 aar 到 fakeDex 目录
ZipUtils.unzip(aarFile, fakeDex);
// 获取 aar 中的 jar 并转换成 dex
File aarDex = DxUtils.jar2Dex(aarFile);       
File tempMainDex = new File(newApkFile.getPath() + File.separator + "classes.dex");
if (!tempMainDex.exists()) {
    tempMainDex.createNewFile();
}
FileOutputStream fileOutputStream = new FileOutputStream(tempMainDex);
byte[] bytes = ByteUtils.getBytes(aarDex);
fileOutputStream.write(bytes);
fileOutputStream.flush();
fileOutputStream.close();

// jar2dex 的命令
public class DxUtils {    
    public static File jar2Dex(File aarFile, File fakeDex) throws IOException {        
        if (fakeDex == null) {            
            return null;        
        }        
        File[] files = fakeDex.listFiles(new FilenameFilter() {            
            @Override            
            public boolean accept(File dir, String name) {                
                return name.equals("classes.jar");            
            }        
        });        
        if (files == null || files.length <= 0) {            
            throw new RuntimeException("the aar is invalidate");        
        }        
        File classes_jar = files[0];        
        File aarDex = new File(classes_jar.getParentFile(), "classes.dex");        
        DxUtils.dxCommand(aarDex, classes_jar);        
        return aarDex;    
    }    

    public static void dxCommand(File aarDex, File classes_jar) throws IOException {        
        Runtime runtime = Runtime.getRuntime();            
        
        // windows 下 dx.bat 的执行命令
        // Process process = runtime.exec("cmd.exe /C dx --dex --output=" + aarDex.getAbsolutePath() + " "
        // + classes_jar.getAbsolutePath());        
        
        // mac 下 dx.bat 的执行命令        
        Process process = runtime.exec("dx --dex --output=" + aarDex.getAbsolutePath() + " "                
                            + classes_jar.getAbsolutePath());        
        try {            
            process.waitFor();        
        } catch (InterruptedException e) {            
            e.printStackTrace();        
        }        
        if (process.exitValue() != 0) {            
            InputStream inputStream = process.getErrorStream();            
            int len;            
            byte[] buffer = new byte[1024];            
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();            
            while ((len = inputStream.read(buffer)) != -1) {                
                byteArrayOutputStream.write(buffer, 0, len);            
            }            
            System.out.println(new String(byteArrayOutputStream.toByteArray(), "GBK"));            
            throw new RuntimeException("dx process error");        
        }        
        process.destroy();    
    }
}

mac 下需要配置 dx 的环境变量,配置成功之后,需要重启 idea,然后执行我们的程序即可

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

执行将 classes.jar 转换成 classes.dex 并复制到 src/source/apk/temp 目录下;

aar 壳 Dex 中主要是 Application 的子类,用来替换我们在 AndroidManifest.xml 中的默认 Application;

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

因为 壳 Dex 是没有被加密的,当我们安装了这个壳 Dex 之后,启动这个 ShellApplication 的时候,我们需要在 ShellApplication 中进行脱壳(解密)操作,hook dexElements 数组,保证我们的源 Dex 可以正常安装和 load;

第三步 打包签名;

/** 
  * 合并壳dex 和源dex 打包签名 
  * */
File unsignedApkFile = new File("src/result/apk-unsigned.apk");
unsignedApkFile.getParentFile().mkdirs();
ZipUtils.zip(newApkFile, unsignedApkFile);
// 签名
File signedApkFile = new File("src/result/apk-signed.apk");
SignatureUtils.signature(unsignedApkFile, signedApkFile);

public class SignatureUtils {    
    public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {        
        // windows 平台下的签名命令 
        // String cmd[] = {"cmd.exe", "/C ","jarsigner",  "-sigalg", "MD5withRSA",                
                        // "-digestalg", "SHA1",                
                        // "-keystore", "src/source/keystore/debug.keystore",                
                        // "-storepass", "android",                
                        // "-keypass", "android",                
                        // "-signedjar", signedApk.getAbsolutePath(),                
                        // unsignedApk.getAbsolutePath(),                
                        // "androiddebugkey"}; 
        // mac 平台下的签名命令
        String cmd[] = {"jarsigner",  "-sigalg", "MD5withRSA",        
                        "-digestalg", "SHA1",        
                        "-keystore", "src/source/keystore/debug.keystore",        
                        "-storepass", "android",        
                        "-keypass", "android",        
                        "-signedjar", signedApk.getAbsolutePath(),        
                        unsignedApk.getAbsolutePath(),        
                        "androiddebugkey"};              Process process = Runtime.getRuntime().exec(cmd);        
        System.out.println("start sign");        
        try {            
            int waitResult = process.waitFor();            
            System.out.println("waitResult: " + waitResult);        
        } catch (InterruptedException e) {            
            e.printStackTrace();            
            throw e;        
        }        
        System.out.println("process.exitValue() " + process.exitValue() );        
        if (process.exitValue() != 0) {            
            InputStream inputStream = process.getErrorStream();            
            int len;            
            byte[] buffer = new byte[2048];            
            ByteArrayOutputStream bos = new ByteArrayOutputStream();            
            while((len=inputStream.read(buffer)) != -1) {                
                bos.write(buffer,0,len);            
            }            
            System.out.println(new String(bos.toByteArray(),"GBK"));            
            throw new RuntimeException("sign error");        
        }        
        System.out.println("finish signed");        
        process.destroy();    
    }
}

执行合并打包和签名

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

生成签名的 APK,运行 adb install apk-signed.apk 执行安装;

配置了 dx 环境变量,理论上 jarsinger 也就不用配置了,它们都在同一个目录下

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

在Mac/Linux系统中,deubg.keystore文件默认储存在 ~/.android/ 路径下;

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

release 的一般存放在公司自己的打包服务器上;

我们将签名后的 APK(apk-signed.apk) 直接拖到 Android Studio 的编辑窗口内,可以直接看 Dex 文件

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

点击源 Dex,我们发现

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

源 Dex 因为加密,Android Studio 解析失败

而我们的壳 Dex 是可以解析成功的;

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

第四步:脱壳

核心就是在运行的时候进行解密操作: ShellApplication 的 attachBaseContext 中获取加密后的 Dex 进行解密,并 Hook dexElements 数组进行加载;

ClassLoader、dexElements 等后面插件化、热修复的时候会详细讲解;

接下里的操作,基本就是热修复的流程(反射,关于 dex 的加载等等);

public class ShellApplication extends Application {
    
    private static final String TAG = "ShellApplication";

    public static String getPassword() {
        // 这里要和加密的密码保持一致,否则会解密失败;
        return "abcdefghijklmnop";
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        AES.init(getPassword());
        File apkFile = new File(getApplicationInfo().sourceDir);
        // 解压到这个目录下 data/data/包名/files/fake_apk/ 不需要权限
        File unZipFile = getDir("fake_apk", MODE_PRIVATE);
        File app = new File(unZipFile, "app");
        if (!app.exists()) {
            ZipUtils.unZip(apkFile, app);
            File[] files = app.listFiles();
            for (File file : files) {
                String name = file.getName();
                if (name.endsWith(".dex")) {
                    try {
                        byte[] bytes = getBytes(file);
                        FileOutputStream fos = new FileOutputStream(file);
                        // 执行解密操作
                        byte[] decrypt = AES.decrypt(bytes);
                        fos.write(decrypt);
                        fos.flush();
                        fos.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        List list = new ArrayList<>();
        Log.d(TAG, Arrays.toString(app.listFiles()));
        for (File file : app.listFiles()) {
            if (file.getName().endsWith(".dex")) {
                list.add(file);
            }
        }

        Log.d(TAG, list.toString());        try {
            // 这里可以参考 Tinker 热修复,进行不同版本的适配 
            V19.install(getClassLoader(), list, unZipFile);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    private static Field findField(Object instance, String name) throws NoSuchFieldException {
        Class clazz = instance.getClass();

        while (clazz != null) {
            try {
                Field e = clazz.getDeclaredField(name);
                if (!e.isAccessible()) {
                    e.setAccessible(true);
                }

                return e;
            } catch (NoSuchFieldException var4) {
                clazz = clazz.getSuperclass();
            }
        }

        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

    private static Method findMethod(Object instance, String name, Class... parameterTypes)
            throws NoSuchMethodException {
        Class clazz = instance.getClass();
        while (clazz != null) {
            try {
                Method e = clazz.getDeclaredMethod(name, parameterTypes);
                if (!e.isAccessible()) {
                    e.setAccessible(true);
                }

                return e;
            } catch (NoSuchMethodException var5) {
                clazz = clazz.getSuperclass();
            }
        }
        throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList
                (parameterTypes) + " not found in " + instance.getClass());
    }

    private static void expandFieldArray(Object instance, String fieldName, Object[]
            extraElements) throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field jlrField = findField(instance, fieldName);
        Object[] original = (Object[]) ((Object[]) jlrField.get(instance));
        Object[] combined = (Object[]) ((Object[]) Array.newInstance(original.getClass()
                .getComponentType(), original.length + extraElements.length));
        System.arraycopy(original, 0, combined, 0, original.length);
        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
        jlrField.set(instance, combined);
    }

    private static final class V19 {
        private V19() {
        }

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory) throws IllegalArgumentException,
                IllegalAccessException, NoSuchFieldException, InvocationTargetException,
                NoSuchMethodException {

            Field pathListField = findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList suppressedExceptions = new ArrayList();
            Log.d(TAG, "Build.VERSION.SDK_INT " + Build.VERSION.SDK_INT);
            // 参考 Tinker 进行不同版本的适配
            if (Build.VERSION.SDK_INT >= 23) {
                expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList, new
                                ArrayList(additionalClassPathEntries), optimizedDirectory,
                        suppressedExceptions));
            } else {
                expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new
                                ArrayList(additionalClassPathEntries), optimizedDirectory,
                        suppressedExceptions));
            }

            if (suppressedExceptions.size() > 0) {
                Iterator suppressedExceptionsField = suppressedExceptions.iterator();

                while (suppressedExceptionsField.hasNext()) {
                    IOException dexElementsSuppressedExceptions = (IOException)
                            suppressedExceptionsField.next();
                    Log.w("MultiDex", "Exception in makeDexElement",
                            dexElementsSuppressedExceptions);
                }

                Field suppressedExceptionsField1 = findField(loader,
                        "dexElementsSuppressedExceptions");
                IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[])
                        suppressedExceptionsField1.get(loader));
                if (dexElementsSuppressedExceptions1 == null) {
                    dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions
                            .toArray(new IOException[suppressedExceptions.size()]);
                } else {
                    IOException[] combined = new IOException[suppressedExceptions.size() +
                            dexElementsSuppressedExceptions1.length];
                    suppressedExceptions.toArray(combined);
                    System.arraycopy(dexElementsSuppressedExceptions1, 0, combined,
                            suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
                    dexElementsSuppressedExceptions1 = combined;
                }

                suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
            }

        }

        private static Object[] makeDexElements(Object dexPathList,
                                                ArrayList<File> files, File
                                                        optimizedDirectory,
                                                ArrayList<IOException> suppressedExceptions) throws
                IllegalAccessException, InvocationTargetException, NoSuchMethodException {

                Method makeDexElements = findMethod(dexPathList, "makeDexElements", new
                        Class[]{ArrayList.class, File.class, ArrayList.class});
                return ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files,
                        optimizedDirectory, suppressedExceptions}));
           }
    }

   
    private static Object[] makePathElements(
            Object dexPathList, ArrayList<File> files, File optimizedDirectory,
            ArrayList<IOException> suppressedExceptions)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        Method makePathElements;
        try {
            makePathElements = findMethod(dexPathList, "makePathElements", List.class, File.class,
                    List.class);
        } catch (NoSuchMethodException e) {
            Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
            try {
                makePathElements = findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
            } catch (NoSuchMethodException e1) {
                Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
                try {
                    Log.e(TAG, "NoSuchMethodException: try use v19 instead");
                    return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
                } catch (NoSuchMethodException e2) {
                    Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
                    throw e2;
                }
            }
        }
        return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
    }

    private byte[] getBytes(File file) throws Exception {
        RandomAccessFile r = new RandomAccessFile(file, "r");
        byte[] buffer = new byte[(int) r.length()];
        r.readFully(buffer);
        r.close();
        return buffer;
    }
}

Tinker  的 SystemClassLoaderAdapter 进行了不同版本的适配,这里可以参考下;

如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)

简历润色

简历上可写:深度理解文件IO,可基于 IO 操作手写 APK 加固核心实现;

下一章预告

带你玩转JVM内存管理;