如何应对 Android 面试官 ->文件 IO、手写 APK 加固框架核心实现 (下)
- 2024-01-27  北京
 本文字数:12896 字
阅读完需:约 42 分钟

前言
本章知识点分为上下两节,上章主要介绍下字节流、字符流、NIO、本章介绍下 Dex 文件加密、手写加固框架核心实现;
 反编译
什么是反编译?
利用编译程序从源语言编写的源程序产生目标程序的过程;
如何进行反编译?
解压缩 apk 文件;
META-INF 存放签名文件
classes.dex 需要将这个文件反编译成 jar
res
resources.arsc
dex2jar,将 class.dex 转换成 jar;
使用 JD-GUI 查看 jar;
MAC 下反编译可以查看这篇博客:mac下反编译
加固方案
反模拟器
模拟器运行 APK,可以用模拟器监控到 APK 的各种行为,所以在实际的加固 APK 运行中,一旦发现模拟器在运行该 APK,就停止核心代码的运行;
代码虚拟化
代码虚拟化在桌面平台应用保护中已经是非常的常见了,主要思路是自建一个虚拟执行引擎,把原生的可执行代码转换成自定义的指令进行虚拟执行;
加密
样本的部分可执行代码是以压缩或者加密的形式存在的,比如:被保护过的代码被切割成多个小段,前面的一段代码先把后面的代码片段在内存中解密,然后再去执行解密之后的代码,如此一块一块的迭代执行;(微信就是基于此的一种加固方式)本章的手写 APK 加固的核心实现也是基于此方案实现;
基于加密方案的加固原理
我们所有的 Dex 经过 classloader 加载到内存后,执行运行,但是 Dex 并不是全部都会进行加载,而是需要用之前加载;
所以我们可以将 Dex 文件分成核心代码和非核心代码,将非核心代码展示出来,当非核心代码需要用到核心代码的时候,再把核心代码中的类加载进来,核心代码加固(加密),提升代码安全性(加载的过程中顺带着解密);
 源 Dex 进行加密,壳 Dex 不加密,用壳 Dex 对源 Dex 进行解密;
加固框架总体思路
将 APK 文件分为 Dex 一类和其他一类;
文件解压缩(unzip),对文件进行分类(过滤)「一类是 Dex,一类是非 Dex」;
对 Dex 进行加密操作,将 Dex 文件中的二进制进行读取,读取出来之后进行 AES 加密,加密完成之后再写回去「文件 IO 操作」;
创建一个壳 Dex 文件;
将壳 Dex 和加密后的 Dex 文件合并;
将合并后的 Dex 与其他非 Dex 文件进行合并成一个新的 APK 文件;
对新的 APK 文件进行重新签名;
安装运行,验证是否成功;
 这是加固前的流程,加固中和加固后的流程呢?
Dex 文件可以随意拼凑吗?
壳 Dex 怎么来的?
如何签名?
如何运行新的 APK(脱壳)?
Dex 文件可以随意拼凑吗?
我们对 Dex 进行操作的时候,需要了解 Dex 的文件格式
Dex 文件基本格式
文件头 header
checksum
signnature
file_size
索引区
字符串的索引 string_ids
类型的索引 type_ids
方法原型的索引 proto_ids
域的索引 field_ids
方法的索引 method_ids
数据区
类的定义区 class_defs
数据区 data
链接数据区 link_data
对 Dex 的修改,我们必须要按照 Dex 的格式来进行,例如我们往数据区的 data 写入了一段数据,那么我们就需要按照格式修改 header 区的 file_size signnature checksum(checksum 是对整个文件的一个校验)
 APK 打包流程
资源文件通过 aapt 工具生成 R.java 文件,由 gradle 自动调用构建工具;
.aidl File 通过 aidl 工具自动生成 java 代码;
开发者创建的 java 代码 + aidl 创建的 java 代码以及资源文件生成的 java 代码,通过 java compiler 生成 class「也就是 javac 的操作」;
用 dx.bat 工具 将 class 文件生成 dex 文件;
用 apkbuilder 工具将资源文件和 dex 文件其他文件打包成一个 APK 文件;
用 jarsigner + keystore 对 apk 进行签名,获取签名文件;
zipalign 对齐;
手写加固框架(非商用)
整体分为四个大的流程
第一步 处理原始 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));    }      }  }    }}
 执行的结果:在 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 并转换成 dexFile 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,然后执行我们的程序即可
 执行将 classes.jar 转换成 classes.dex 并复制到 src/source/apk/temp 目录下;
aar 壳 Dex 中主要是 Application 的子类,用来替换我们在 AndroidManifest.xml 中的默认 Application;
 因为 壳 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();        }}
执行合并打包和签名
 生成签名的 APK,运行 adb install apk-signed.apk 执行安装;
配置了 dx 环境变量,理论上 jarsinger 也就不用配置了,它们都在同一个目录下
 在 Mac/Linux 系统中,deubg.keystore 文件默认储存在 ~/.android/ 路径下;
 release 的一般存放在公司自己的打包服务器上;
我们将签名后的 APK(apk-signed.apk) 直接拖到 Android Studio 的编辑窗口内,可以直接看 Dex 文件
 点击源 Dex,我们发现
 源 Dex 因为加密,Android Studio 解析失败;
而我们的壳 Dex 是可以解析成功的;
 第四步:脱壳
核心就是在运行的时候进行解密操作: 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 进行了不同版本的适配,这里可以参考下;
 简历润色
简历上可写:深度理解文件 IO,可基于 IO 操作手写 APK 加固核心实现;
下一章预告
带你玩转 JVM 内存管理;
老A说
还未添加个人签名 2018-11-26 加入
还未添加个人简介







    
评论