如何应对 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 加入
还未添加个人简介







评论