写点什么

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

作者:老A说
  • 2024-01-27
    北京
  • 本文字数:12896 字

    阅读完需:约 42 分钟

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

前言

本章知识点分为上下两节,上章主要介绍下字节流、字符流、NIO、本章介绍下 Dex 文件加密、手写加固框架核心实现;


反编译

什么是反编译?

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

如何进行反编译?

  1. 解压缩 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 文件分成核心代码和非核心代码,将非核心代码展示出来,当非核心代码需要用到核心代码的时候,再把核心代码中的类加载进来,核心代码加固(加密),提升代码安全性(加载的过程中顺带着解密);



源 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. 安装运行,验证是否成功;



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


  • 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 打包流程

  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 对齐;


手写加固框架(非商用)

整体分为四个大的流程

第一步 处理原始 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 加入

还未添加个人简介

评论

发布
暂无评论
如何应对Android面试官->文件IO、手写APK加固框架核心实现(下)_Java’_老A说_InfoQ写作社区