赞
踩
对 Android 安全 专栏进行总结 ;
Android 应用反编译 :
ProGuard 混淆 :
多 dex 加载原理 :
Java 工具开发 :
Application 替换 :
package kim.hsl.multipledex; import android.app.Application; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import java.io.File; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; public class ProxyApplication extends Application { public static final String TAG = "ProxyApplication"; /** * 应用真实的 Application 全类名 */ String app_name; /** * DEX 解密之后的目录名称 */ String app_version; /** * 在 Application 在 ActivityThread 中被创建之后, * 第一个调用的方法是 attachBaseContext 函数. * 该函数是 Application 中最先执行的函数. */ @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); try { Log.i(TAG, "attachBaseContext"); /* 在该 Application 中主要进行两个操作 : 1 . 解密并加载多个 DEX 文件 2 . 将真实的 Application 替换成应用的主 Application */ /* I . 解密与加载多 DEX 文件 先进行解密, 然后再加载解密之后的 DEX 文件 1. 先获取当前的 APK 文件 2. 然后解压该 APK 文件 */ // 获取当前的 APK 文件, 下面的 getApplicationInfo().sourceDir 就是本应用 APK 安装文件的全路径 File apkFile = new File(getApplicationInfo().sourceDir); // 获取在 app Module 下的 AndroidManifest.xml 中配置的元数据, // 应用真实的 Application 全类名 // 解密后的 dex 文件存放目录 ApplicationInfo applicationInfo = null; applicationInfo = getPackageManager().getApplicationInfo( getPackageName(), PackageManager.GET_META_DATA ); Bundle metaData = applicationInfo.metaData; if (metaData != null) { // 检查是否存在 app_name 元数据 if (metaData.containsKey("app_name")) { app_name = metaData.getString("app_name").toString(); } // 检查是否存在 app_version 元数据 if (metaData.containsKey("app_version")) { app_version = metaData.getString("app_version").toString(); } } // 创建用户的私有目录 , 将 apk 文件解压到该目录中 File privateDir = getDir(app_name + "_" + app_version, MODE_PRIVATE); Log.i(TAG, "attachBaseContext 创建用户的私有目录 : " + privateDir.getAbsolutePath()); // 在上述目录下创建 app 目录 // 创建该目录的目的是存放解压后的 apk 文件的 File appDir = new File(privateDir, "app"); // app 中存放的是解压后的所有的 apk 文件 // app 下创建 dexDir 目录 , 将所有的 dex 目录移动到该 dexDir 目录中 // dexDir 目录存放应用的所有 dex 文件 // 这些 dex 文件都需要进行解密 File dexDir = new File(appDir, "dexDir"); // 遍历解压后的 apk 文件 , 将需要加载的 dex 放入如下集合中 ArrayList<File> dexFiles = new ArrayList<File>(); // 如果该 dexDir 不存在 , 或者该目录为空 , 并进行 MD5 文件校验 if (!dexDir.exists() || dexDir.list().length == 0) { // 将 apk 中的文件解压到了 appDir 目录 ZipUtils.unZipApk(apkFile, appDir); // 获取 appDir 目录下的所有文件 File[] files = appDir.listFiles(); Log.i(TAG, "attachBaseContext appDir 目录路径 : " + appDir.getAbsolutePath()); Log.i(TAG, "attachBaseContext appDir 目录内容 : " + files); // 遍历文件名称集合 for (int i = 0; i < files.length; i++) { File file = files[i]; Log.i(TAG, "attachBaseContext 遍历 " + i + " . " + file); // 如果文件后缀是 .dex , 并且不是 主 dex 文件 classes.dex // 符合上述两个条件的 dex 文件放入到 dexDir 中 if (file.getName().endsWith(".dex") && !TextUtils.equals(file.getName(), "classes.dex")) { // 筛选出来的 dex 文件都是需要解密的 // 解密需要使用 OpenSSL 进行解密 // 获取该文件的二进制 Byte 数据 // 这些 Byte 数组就是加密后的 dex 数据 byte[] bytes = OpenSSL.getBytes(file); // 解密该二进制数据, 并替换原来的加密 dex, 直接覆盖原来的文件即可 OpenSSL.decrypt(bytes, file.getAbsolutePath()); // 将解密完毕的 dex 文件放在需要加载的 dex 集合中 dexFiles.add(file); // 拷贝到 dexDir 中 Log.i(TAG, "attachBaseContext 解密完成 被解密文件是 : " + file); }// 判定是否是需要解密的 dex 文件 }// 遍历 apk 解压后的文件 } else { // 已经解密完成, 此时不需要解密, 直接获取 dexDir 中的文件即可 for (File file : dexDir.listFiles()) { dexFiles.add(file); } } Log.i(TAG, "attachBaseContext 解密完成 dexFiles : " + dexFiles); for (int i = 0; i < dexFiles.size(); i++) { Log.i(TAG, i + " . " + dexFiles.get(i).getAbsolutePath()); } // 截止到此处 , 已经拿到了解密完毕 , 需要加载的 dex 文件 // 加载自己解密的 dex 文件 loadDex(dexFiles, privateDir); Log.i(TAG, "attachBaseContext 完成"); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } /** * 加载 dex 文件集合 * 这些 dex 文件已经解密 * 参考博客 : https://hanshuliang.blog.csdn.net/article/details/109608605 * <p> * 创建自己的 Element[] dexElements 数组 * ( libcore/dalvik/src/main/java/dalvik/system/DexPathList.java ) * 然后将 系统加载的 Element[] dexElements 数组 与 我们自己的 Element[] dexElements 数组进行合并操作 */ void loadDex(ArrayList<File> dexFiles, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchFieldException, NoSuchMethodException { Log.i(TAG, "loadDex"); /* 需要执行的步骤 1 . 获得系统 DexPathList 中的 Element[] dexElements 数组 ( libcore/dalvik/src/main/java/dalvik/system/DexPathList.java ) 2 . 在本应用中创建 Element[] dexElements 数组 , 用于存放解密后的 dex 文件 3 . 将 系统加载的 Element[] dexElements 数组 与 我们自己的 Element[] dexElements 数组进行合并操作 4 . 替换 ClassLoader 加载过程中的 Element[] dexElements 数组 ( 封装在 DexPathList 中 ) */ /* 1 . 获得系统 DexPathList 中的 Element[] dexElements 数组 第一阶段 : 在 Context 中调用 getClassLoader() 方法 , 可以拿到 PathClassLoader ; 第二阶段 : 从 PathClassLoader 父类 BaseDexClassLoader 中找到 DexPathList ; 第三阶段 : 获取封装在 DexPathList 类中的 Element[] dexElements 数组 ; 上述的 DexPathList 对象 是 BaseDexClassLoader 的私有成员 Element[] dexElements 数组 也是 DexPathList 的私有成员 因此只能使用反射获取 Element[] dexElements 数组 */ // 阶段一二 : 调用 getClassLoader() 方法可以获取 PathClassLoader 对象 // 从 PathClassLoader 对象中获取 private final DexPathList pathList 成员 Field pathListField = ReflexUtils.reflexField(getClassLoader(), "pathList"); // 获取 classLoader 对象对应的 DexPathList pathList 成员 Object pathList = pathListField.get(getClassLoader()); //阶段三 : 获取封装在 DexPathList 类中的 Element[] dexElements 数组 Field dexElementsField = ReflexUtils.reflexField(pathList, "dexElements"); // 获取 pathList 对象对应的 Element[] dexElements 数组成员 Object[] dexElements = (Object[]) dexElementsField.get(pathList); /* 2 . 在本应用中创建 Element[] dexElements 数组 , 用于存放解密后的 dex 文件 不同的 Android 版本中 , 创建 Element[] dexElements 数组的方法不同 , 这里需要做兼容 */ Method makeDexElements; Object[] addElements = null; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { // 5.0, 5.1 makeDexElements // 反射 5.0, 5.1, 6.0 版本的 DexPathList 中的 makeDexElements 方法 makeDexElements = ReflexUtils.reflexMethod( pathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, optimizedDirectory, suppressedExceptions); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // 7.0 以上版本 makePathElements // 反射 7.0 以上版本的 DexPathList 中的 makeDexElements 方法 makeDexElements = ReflexUtils.reflexMethod(pathList, "makePathElements", List.class, File.class, List.class); ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, optimizedDirectory, suppressedExceptions); } /* 3 . 将 系统加载的 Element[] dexElements 数组 与 我们自己的 Element[] dexElements 数组进行合并操作 首先创建数组 , 数组类型与 dexElements 数组类型相同 将 dexElements 数组中的元素拷贝到 newElements 前半部分, 拷贝元素个数是 dexElements.size 将 addElements 数组中的元素拷贝到 newElements 后半部分, 拷贝元素个数是 dexElements.size */ Object[] newElements = (Object[]) Array.newInstance( dexElements.getClass().getComponentType(), dexElements.length + addElements.length); // 将 dexElements 数组中的元素拷贝到 newElements 前半部分, 拷贝元素个数是 dexElements.size System.arraycopy(dexElements, 0, newElements, 0, dexElements.length); // 将 addElements 数组中的元素拷贝到 newElements 后半部分, 拷贝元素个数是 dexElements.size System.arraycopy(addElements, 0, newElements, dexElements.length, addElements.length); /* 4 . 替换 ClassLoader 加载过程中的 Element[] dexElements 数组 ( 封装在 DexPathList 中 ) */ dexElementsField.set(pathList, newElements); Log.i(TAG, "loadDex 完成"); } @Override public void onCreate() { super.onCreate(); // 如果之前没有替换过 , 执行 Application 替换操作 // 说明没有调用到 createPackageContext 方法 // 该 createPackageContext 方法只有在创建 ContentProvider 时才调用到 // 如果没有调用到 , 说明 AndroidManifest.xml 中没有配置 ContentProvider // 此时需要在此处进行 Application 替换 if (delegate == null){ applicationExchange(); } } @Override public String getPackageName() { if(TextUtils.isEmpty(app_name)){ // 如果 AndroidManifest.xml 中配置的 Application 全类名为空 // 那么 不做任何操作 }else{ // 如果 AndroidManifest.xml 中配置的 Application 全类名不为空 // 为了使 ActivityThread 的 installProvider 方法 // 无法命中如下两个分支 // 分支一 : context.getPackageName().equals(ai.packageName) // 分支二 : mInitialApplication.getPackageName().equals(ai.packageName) // 设置该方法返回值为空 , 上述两个分支就无法命中 return ""; } return super.getPackageName(); } @Override public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException { if(TextUtils.isEmpty(app_name)){ // 如果 AndroidManifest.xml 中配置的 Application 全类名为空 // 说明没有进行 dex 加密操作 , 返回父类方法执行即可 return super.createPackageContext(packageName, flags); }else{ // 只有在创建 ContentProvider 时才调用到该 createPackageContext 方法 , // 如果没有调用到该方法 , 说明该应用中没有配置 ContentProvider ; // 该方法不一定会调用到 // 先进行 Application 替换 applicationExchange(); // Application 替换完成之后 , 再继续向下执行创建 ContentProvider return delegate; } } /** * 调用 applicationExchange 替换 Application * 该成员就是替换后的 Application */ private Application delegate; /** * Application 替换主方法 */ private void applicationExchange(){ try { /* 在此处进行 Application 替换 */ // 先判断是否有配置 Application , // 那么在 Manifest.xml 中的 meta-data 元数据 app_name 不为空 // 如果开发者没有自定义 Application , 没有配置元数据 , 直接退出 if (TextUtils.isEmpty(app_name)) { return; } // 获取上下文对象 , 保存下来 , 之后要使用 Context baseContext = getBaseContext(); // 通过反射获取 Application , 系统也是进行的反射操作 Class<?> delegateClass = Class.forName(app_name); // 创建用户真实配置的 Application delegate = (Application) delegateClass.newInstance(); // 调用 Application 的 attach 函数 // 该函数无法直接调用 , 也需要通过反射调用 // 这里先通过反射获取 Application 的 attach 函数 Method attach = Application.class.getDeclaredMethod("attach", Context.class); // attach 方法是私有的 , 设置 attach 方法允许访问 attach.setAccessible(true); // 获取上下文对象 , // 该 Context 是通过调用 Application 的 attachBaseContext 方法传入的 ContextImpl // 将该上下文对象传入 Application 的 attach 方法中 attach.invoke(delegate, baseContext); /* 参考 : https://hanshuliang.blog.csdn.net/article/details/111569017 博客 查询应该替换哪些对象中的哪些成员 截止到此处, Application 创建完毕 , 下面开始逐个替换下面的 Application ① ContextImpl 的 private Context mOuterContext 成员是 kim.hsl.multipledex.ProxyApplication 对象 ; ② ActivityThread 中的 ArrayList<Application> mAllApplications 集合中添加了 kim.hsl.multipledex.ProxyApplication 对象 ; ③ LoadedApk 中的 mApplication 成员是 kim.hsl.multipledex.ProxyApplication 对象 ; ④ ActivityThread 中的 Application mInitialApplication 成员是 kim.hsl.multipledex.ProxyApplication 对象 ; */ // I . 替换 ① ContextImpl 的 private Context mOuterContext // 成员是 kim.hsl.multipledex.ProxyApplication 对象 Class<?> contextImplClass = Class.forName("android.app.ContextImpl"); // 获取 ContextImpl 中的 mOuterContext 成员 Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext"); // mOuterContext 成员是私有的 , 设置可访问性 mOuterContextField.setAccessible(true); // ContextImpl 就是应用的 Context , 直接通过 getBaseContext() 获取即可 mOuterContextField.set(baseContext, delegate); // II . 替换 ④ ActivityThread 中的 Application mInitialApplication // 成员是 kim.hsl.multipledex.ProxyApplication 对象 ; Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); // 获取 ActivityThread 中的 mInitialApplication 成员 Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); // mInitialApplication 成员是私有的 , 设置可访问性 mInitialApplicationField.setAccessible(true); // 从 ContextImpl 对象中获取其 ActivityThread mMainThread 成员变量 Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread"); mMainThreadField.setAccessible(true); // ContextImpl 就是本应用的上下文对象 , 调用 getBaseContext 方法获得 Object mMainThread = mMainThreadField.get(baseContext); // ContextImpl 就是应用的 Context , 直接通过 getBaseContext() 获取即可 mInitialApplicationField.set(mMainThread, delegate); // III . 替换 ② ActivityThread 中的 ArrayList<Application> mAllApplications // 集合中添加了 kim.hsl.multipledex.ProxyApplication 对象 ; // 获取 ActivityThread 中的 mAllApplications 成员 Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications"); // mAllApplications 成员是私有的 , 设置可访问性 mAllApplicationsField.setAccessible(true); // 获取 ActivityThread 中的 ArrayList<Application> mAllApplications 队列 ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread); // 将真实的 Application 添加到上述队列中 mAllApplications.add(delegate); // IV . 替换 ③ LoadedApk 中的 mApplication // 成员是 kim.hsl.multipledex.ProxyApplication 对象 // 1. 先获取 LoadedApk 对象 // LoadedApk 是 ContextImpl 中的 LoadedApk mPackageInfo 成员变量 // 从 ContextImpl 对象中获取其 LoadedApk mPackageInfo 成员变量 Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo"); mPackageInfoField.setAccessible(true); // ContextImpl 就是本应用的上下文对象 , 调用 getBaseContext 方法获得 Object mPackageInfo = mPackageInfoField.get(baseContext); // 2. 获取 LoadedApk 对象中的 mApplication 成员 Class<?> loadedApkClass = Class.forName("android.app.LoadedApk"); // 获取 ActivityThread 中的 mInitialApplication 成员 Field mApplicationField = loadedApkClass.getDeclaredField("mApplication"); // LoadedApk 中的 mApplication 成员是私有的 , 设置可访问性 mApplicationField.setAccessible(true); // 3. 将 Application 设置给 LoadedApk 中的 mApplication 成员 mApplicationField.set(mPackageInfo, delegate); // V . 下一步操作替换替换 ApplicationInfo 中的 className , 该操作不是必须的 , 不替换也不会报错 // 在应用中可能需要操作获取应用的相关信息 , 如果希望获取准确的信息 , 需要替换 ApplicationInfo // ApplicationInfo 在 LoadedApk 中 Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo"); // 设置该字段可访问 mApplicationInfoField.setAccessible(true); // mPackageInfo 就是 LoadedApk 对象 // mApplicationInfo 就是从 LoadedApk 对象中获得的 mApplicationInfo 字段 ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo); // 设置 ApplicationInfo 中的 className 字段值 mApplicationInfo.className = app_name; // 再次调用 onCreate 方法 delegate.onCreate(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchFieldException exception) { exception.printStackTrace(); } } }
package kim.hsl.multipledex; import android.util.Log; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; public class OpenSSL { static { System.loadLibrary("openssl"); } /** * 从文件中读取 Byte 数组 * @param file * @return * @throws Exception */ public static byte[] getBytes(File file) throws Exception { try { // 创建随机读取文件 RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); // 获取文件字节数 , 创建保存文件数据的缓冲区 byte[] buffer = new byte[(int) randomAccessFile.length()]; // 读取整个文件数据 randomAccessFile.readFully(buffer); // 关闭文件 randomAccessFile.close(); return buffer; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 调用 OpenSSL 解密 dex 文件 * @param data * @param path */ public static native void decrypt(byte[] data, String path); }
package kim.hsl.multipledex; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays; public class ReflexUtils { /** * 通过反射方法获取 instance 类中的 memberName 名称的成员 * @param instance 成员所在对象 * @param memberName 成员变量名称 * @return 返回 Field 类型成员 * @throws NoSuchFieldException */ public static Field reflexField(Object instance, String memberName) throws NoSuchFieldException { // 获取字节码类 Class clazz = instance.getClass(); // 循环通过反射获取 // 可能存在通过反射没有找到成员的情况 , 此时查找其父类是否有该成员 // 循环次数就是其父类层级个数 while (clazz != null) { try { // 获取成员 Field memberField = clazz.getDeclaredField(memberName); // 如果不是 public , 无法访问 , 设置可访问 if (!memberField.isAccessible()) { memberField.setAccessible(true); } return memberField; } catch (NoSuchFieldException exception){ // 如果找不到, 就到父类中查找 clazz = clazz.getSuperclass(); } } // 如果没有拿到成员 , 则直接中断程序 , 加载无法进行下去 throw new NoSuchFieldException("没有在 " + clazz.getName() + " 类中找到 " + memberName + "成员"); } /** * 通过反射方法获取 instance 类中的 参数为 parameterTypes , 名称为 methodName 的成员方法 * @param instance 成员方法所在对象 * @param methodName 成员方法名称 * @param parameterTypes 成员方法参数 * @return * @throws NoSuchMethodException */ public static Method reflexMethod(Object instance, String methodName, Class... parameterTypes) throws NoSuchMethodException { // 获取字节码类 Class clazz = instance.getClass(); // 循环通过反射获取 // 可能存在通过反射没有找到成员方法的情况 , 此时查找其父类是否有该成员方法 // 循环次数就是其父类层级个数 while (clazz != null) { try { // 获取成员方法 Method method = clazz.getDeclaredMethod(methodName, parameterTypes); // 如果不是 public , 无法访问 , 设置可访问 if (!method.isAccessible()) { method.setAccessible(true); } return method; } catch (NoSuchMethodException e) { // 如果找不到, 就到父类中查找 clazz = clazz.getSuperclass(); } } // 如果没有拿到成员 , 则直接中断程序 , 加载无法进行下去 throw new NoSuchMethodException("没有在 " + clazz.getName() + " 类中找到 " + methodName + "成员方法"); } }
package kim.hsl.multipledex; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.util.Enumeration; import java.util.zip.CRC32; import java.util.zip.CheckedOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; public class ZipUtils { /** * 删除文件, 如果有目录, 则递归删除 */ private static void deleteFile(File file){ if (file.isDirectory()){ File[] files = file.listFiles(); for (File f: files) { deleteFile(f); } }else{ file.delete(); } } /** * 解压文件 * @param zip 被解压的压缩包文件 * @param dir 解压后的文件存放目录 */ public static void unZipApk(File zip, File dir) { try { // 如果存放文件目录存在, 删除该目录 deleteFile(dir); // 获取 zip 压缩包文件 ZipFile zipFile = new ZipFile(zip); // 获取 zip 压缩包中每一个文件条目 Enumeration<? extends ZipEntry> entries = zipFile.entries(); // 遍历压缩包中的文件 while (entries.hasMoreElements()) { ZipEntry zipEntry = entries.nextElement(); // zip 压缩包中的文件名称 或 目录名称 String name = zipEntry.getName(); // 如果 apk 压缩包中含有以下文件 , 这些文件是 V1 签名文件保存目录 , 不需要解压 , 跳过即可 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[2048]; int len; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } is.close(); fos.close(); } } // 关闭 zip 文件 zipFile.close(); } catch (Exception e) { e.printStackTrace(); } } /** * 压缩目录为zip * @param dir 待压缩目录 * @param zip 输出的zip文件 * @throws Exception */ public static void zip(File dir, File zip) throws Exception { zip.delete(); // 对输出文件做CRC32校验 CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream( zip), new CRC32()); ZipOutputStream zos = new ZipOutputStream(cos); //压缩 compress(dir, zos, ""); zos.flush(); zos.close(); } /** * 添加目录/文件 至zip中 * @param srcFile 需要添加的目录/文件 * @param zos zip输出流 * @param basePath 递归子目录时的完整目录 如 lib/x86 * @throws Exception */ private static void compress(File srcFile, ZipOutputStream zos, String basePath) throws Exception { if (srcFile.isDirectory()) { File[] files = srcFile.listFiles(); for (File file : files) { // zip 递归添加目录中的文件 compress(file, zos, basePath + srcFile.getName() + "/"); } } else { compressFile(srcFile, zos, basePath); } } private static void compressFile(File file, ZipOutputStream zos, String dir) throws Exception { // temp/lib/x86/libdn_ssl.so String fullName = dir + file.getName(); // 需要去掉temp String[] fileNames = fullName.split("/"); //正确的文件目录名 (去掉了temp) StringBuffer sb = new StringBuffer(); if (fileNames.length > 1){ for (int i = 1;i<fileNames.length;++i){ sb.append("/"); sb.append(fileNames[i]); } }else{ sb.append("/"); } //添加一个zip条目 ZipEntry entry = new ZipEntry(sb.substring(1)); zos.putNextEntry(entry); //读取条目输出到zip中 FileInputStream fis = new FileInputStream(file); int len; byte data[] = new byte[2048]; while ((len = fis.read(data, 0, 2048)) != -1) { zos.write(data, 0, len); } fis.close(); zos.closeEntry(); } }
需要交叉编译 OpenSSL 得到 libcrypto.a 静态库 , 在应用中使用该静态库进行解码操作 ;
#include <jni.h> #include <stdio.h> #include <android/log.h> #include <malloc.h> #include <string.h> #include <openssl/evp.h> #include "logging_macros.h" JNIEXPORT void JNICALL Java_kim_hsl_multipledex_OpenSSL_decrypt(JNIEnv *env, jobject instance, jbyteArray data, jstring path) { // 将 Java Byte 数组转为 C 数组 jbyte *src = (*env)->GetByteArrayElements(env, data, NULL); // 将 Java String 字符串转为 C char* 字符串 const char *filePath = (*env)->GetStringUTFChars(env, path, 0); // 获取 Java Byte 数组长度 int srcLen = (*env)->GetArrayLength(env, data); /* * 下面的代码是从 OpenSSL 源码跟目录下 demos/evp/aesccm.c 中拷贝并修改 */ // 加密解密的上下文 EVP_CIPHER_CTX *ctx; int outlen; // 创建加密解密上下文 ctx = EVP_CIPHER_CTX_new(); /* Select cipher 配置上下文解码参数 * 配置加密模式 : * Java 中的加密算法类型 "AES/ECB/PKCS5Padding" , 使用 ecb 模式 * EVP_aes_192_ecb() 配置 ecb 模式 * AES 有五种加密模式 : CBC、ECB、CTR、OCF、CFB * 配置密钥 : * Java 中定义的密钥是 "kimhslmultiplede" */ EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, "kimhslmultiplede", NULL); // 申请解密输出数据内存, 申请内存长度与密文长度一样即可 // AES 加密密文比明文要长 uint8_t *out = malloc(srcLen); // 将申请的内存设置为 0 memset(out, 0, srcLen); // 记录解密总长度 int totalLen = 0; /* * 解密操作 * int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl); * 解密 inl 长度的 in , 解密为 outl 长度的 out * 解密的输入数据是 src, 长度为 srcLen 字节, 注意该长度是 int 类型 * 解密的输出数据是 out, 长度为 srcLen 字节, 注意该长度是 int* 指针类型 */ EVP_DecryptUpdate(ctx, out, &outlen, src, srcLen); totalLen += outlen; //更新总长度 /* * int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl); * 解密时, 每次解密 16 字节, 如果超过了 16 字节 , 就会剩余一部分无法解密, * 之前的 out 指针已经解密了 outlen 长度, 此时接着后续解密, 指针需要进行改变 out + outlen * 此时需要调用该函数 , 解密剩余内容 */ EVP_DecryptFinal_ex(ctx, out + outlen, &outlen); totalLen += outlen; //更新总长度, 此时 totalLen 就是总长度 // 解密完成, 释放上下文对象 EVP_CIPHER_CTX_free(ctx); // 将解密出的明文, 写出到给定的 Java 文件中 FILE *file = fopen(filePath, "wb"); // 写出 out 指针指向的数据 , 写出个数 totalLen * 1 , 写出到 file 文件中 fwrite(out, totalLen, 1, file); // 关闭文件 fclose(file); // 释放解密出的密文内存 free(out); // 释放 Java 引用 (*env)->ReleaseByteArrayElements(env, data, src, 0); (*env)->ReleaseStringUTFChars(env, path, filePath); }
cmake_minimum_required(VERSION 3.4.1) # 配置编译选项, 编译类型 动态库, C++ 源码为 native-lib.c add_library( openssl SHARED native-lib.c) find_library( log-lib log) # 设置 openssl 函数库的静态库地址 方式一 报错 set(LIB_DIR ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}) add_library(crypto STATIC IMPORTED) # 预编译 openssl 静态库 set_target_properties( crypto PROPERTIES IMPORTED_LOCATION ${LIB_DIR}/libcrypto.a) # 指定头文件 include_directories(${CMAKE_SOURCE_DIR}/include) # 方式一配置完毕 # 设置 openssl 函数库的静态库地址 方式二 # 指定 openssl 头文件查找目录 # CMAKE_SOURCE_DIR 指的是当前的文件地址 #include_directories(${CMAKE_SOURCE_DIR}/include) # 指定 openssl 静态库 # CMAKE_CXX_FLAGS 表示会将 C++ 的参数传给编译器 # CMAKE_C_FLAGS 表示会将 C 参数传给编译器 # 参数设置 : 传递 CMAKE_CXX_FLAGS C+= 参数给编译器时 , 在 该参数后面指定库的路径 # CMAKE_SOURCE_DIR 指的是当前的文件地址 # -L 参数指定动态库的查找路径 #set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L${CMAKE_SOURCE_DIR}/lib/armeabi-v7a") #message("CMake octopus ${CMAKE_SOURCE_DIR} , ${ANDROID_ABI}, {CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}") # 链接动态库 target_link_libraries( openssl crypto android ${log-lib})
package kim.hsl.multiple_dex_tools import java.io.* import java.util.zip.* /** * 此处配置 SDK 根目录绝对路径 * D:/001_Programs/001_Android/002_Sdk/Sdk/ * Y:/001_DevelopTools/002_Android_SDK/ */ val sdkDirectory = "Y:/001_DevelopTools/002_Android_SDK/" @ExperimentalStdlibApi fun main() { /* 1 . 生成 dex 文件 , 该 dex 文件中只包含解密 其它 dex 的功能 编译工程 会生成 Android 依赖库的 aar 文件 生成目录是 module/build/outputs/aar/ 目录下 前提是需要在 菜单栏 / File / Setting / Build, Execution, Deployment / Compiler 设置界面中 , 勾选 Compile independent modules in parallel (may require larger ) 将 D:\002_Project\002_Android_Learn\DexEncryption\multiple-dex-core\build\outputs\aar 路径下的 multiple-dex-core-debug.aar 文件后缀修改为 .zip 解压上述文件 拿到 classes.jar 文件即可 ; */ // 获取 multiple-dex-core-debug.aar 文件对象 var aarFile = File("multiple-dex-core/build/outputs/aar/multiple-dex-core-debug.aar") // 解压上述 multiple-dex-core-debug.aar 文件到 aarUnzip 目录中 // 创建解压目录 var aarUnzip = File("multiple-dex-tools/aarUnzip") // 解压操作 unZip(aarFile, aarUnzip) // 拿到 multiple-dex-core-debug.aar 中解压出来的 classes.jar 文件 var classesJarFile = File(aarUnzip, "classes.jar") // 创建转换后的 dex 目的文件, 下面会开始创建该 dex 文件 var classesDexFile = File(aarUnzip, "classes.dex") // 打印要执行的命令 println("cmd /c ${sdkDirectory}build-tools/30.0.2/dx.bat --dex --output ${classesDexFile.absolutePath} ${classesJarFile.absolutePath}") /* 将 jar 包变成 dex 文件 使用 dx 工具命令 注意 : Windows 命令行命令之前需要加上 "cmd /c " 信息 , Linux 与 MAC 命令行不用添加 */ var process = Runtime.getRuntime().exec("cmd /c ${sdkDirectory}build-tools/30.0.2/dx.bat --dex --output ${classesDexFile.absolutePath} ${classesJarFile.absolutePath}") // 等待上述命令执行完毕 process.waitFor() // 执行结果提示 if(process.exitValue() == 0){ println("生成 dex 操作 , 执行成功"); } else { println("生成 dex 操作 , 执行失败"); } /* 2 . 加密 apk 中的 dex 文件 */ // 解压 apk 文件 , 获取所有的 dex 文件 // 被解压的 apk 文件 var apkFile = File("app/build/outputs/apk/debug/app-debug.apk") // 解压的目标文件夹 var apkUnZipFile = File("app/build/outputs/apk/debug/unZipFile") // 解压文件 unZip(apkFile, apkUnZipFile) // 从被解压的 apk 文件中找到所有的 dex 文件, 小项目只有 1 个, 大项目可能有多个 // 使用文件过滤器获取后缀是 .dex 的文件 var dexFiles : Array<File> = apkUnZipFile.listFiles({ file: File, s: String -> s.endsWith(".dex") }) // 加密找到的 dex 文件 var aes = AES(AES.DEFAULT_PWD) // 遍历 dex 文件 for(dexFile: File in dexFiles){ // 读取文件数据 var bytes = getBytes(dexFile) // 加密文件数据 var encryptedBytes = aes.encrypt(bytes) // 将加密后的数据写出到指定目录 var outputFile = File(apkUnZipFile, "secret-${dexFile.name}") // 创建对应输出流 var fileOutputStream = FileOutputStream(outputFile) // 将加密后的 dex 文件写出, 然后刷写 , 关闭该输出流 fileOutputStream.write(encryptedBytes) fileOutputStream.flush() fileOutputStream.close() // 删除原来的文件 dexFile.delete() } /* 3 . 将代理 Application 中的 classes.dex 解压到上述 app/build/outputs/apk/debug/unZipFile 目录中 */ // 拷贝文件到 app/build/outputs/apk/debug/unZipFile 目录中 classesDexFile.renameTo(File(apkUnZipFile, "classes.dex")) // 压缩打包 , 该压缩包是未签名的压缩包 var unSignedApk = File("app/build/outputs/apk/debug/app-unsigned.apk") // 压缩打包操作 zip(apkUnZipFile, unSignedApk) /* 4 . 对齐操作 */ // 对齐操作的输出结果, 将 app-unsigned.apk 对齐, 对齐后的文件输出到 app-unsigned-aligned.apk 中 var unSignedAlignApk = File("app/build/outputs/apk/debug/app-unsigned-aligned.apk") // 打印要执行的命令 println("cmd /c ${sdkDirectory}build-tools/30.0.2/zipalign -f 4 ${unSignedApk.absolutePath} ${unSignedAlignApk.absolutePath}") /* 将 app-unsigned.apk 对齐 使用 zipalign 工具命令 注意 : Windows 命令行命令之前需要加上 "cmd /c " 信息 , Linux 与 MAC 命令行不用添加 */ process = Runtime.getRuntime().exec("cmd /c ${sdkDirectory}build-tools/30.0.2/zipalign -f 4 ${unSignedApk.absolutePath} ${unSignedAlignApk.absolutePath}") // 等待上述命令执行完毕 process.waitFor() // 执行结果提示 if(process.exitValue() == 0){ println("对齐操作 执行成功"); } else { println("对齐操作 执行失败"); } /* 5 . 签名操作 */ // 签名 apk 输出结果, 将 app-unsigned-aligned.apk 签名, 签名后的文件输出到 app-signed-aligned.apk 中 var signedAlignApk = File("app/build/outputs/apk/debug/app-signed-aligned.apk") // 获取签名 jks 文件 var jksFile = File("dex.jks") // 打印要执行的命令 println("cmd /c ${sdkDirectory}build-tools/30.0.2/apksigner sign --ks ${jksFile.absolutePath} --ks-key-alias Key0 --ks-pass pass:000000 --key-pass pass:000000 --out ${signedAlignApk.absolutePath} ${unSignedAlignApk.absolutePath}") /* 将 app-unsigned.apk 对齐 使用 zipalign 工具命令 注意 : Windows 命令行命令之前需要加上 "cmd /c " 信息 , Linux 与 MAC 命令行不用添加 */ process = Runtime.getRuntime().exec("cmd /c ${sdkDirectory}build-tools/30.0.2/apksigner sign --ks ${jksFile.absolutePath} --ks-key-alias Key0 --ks-pass pass:000000 --key-pass pass:000000 --out ${signedAlignApk.absolutePath} ${unSignedAlignApk.absolutePath}") // 打印错误日志 var br = BufferedReader(InputStreamReader(process.errorStream)) while ( true ){ var line = br.readLine() if(line == null){ break }else{ println(line) } } br.close() // 等待上述命令执行完毕 process.waitFor() // 执行结果提示 if(process.exitValue() == 0){ println("签名操作 执行成功"); } else { println("签名操作 执行失败"); } } /** * 删除文件, 如果有目录, 则递归删除 */ private fun deleteFile(file: File) { if (file.isDirectory) { val files = file.listFiles() for (f in files) { deleteFile(f) } } else { file.delete() } } /** * 解压文件 * @param zip 被解压的压缩包文件 * @param dir 解压后的文件存放目录 */ fun unZip(zip: File, dir: File) { try { // 如果存放文件目录存在, 删除该目录 deleteFile(dir) // 获取 zip 压缩包文件 val zipFile = ZipFile(zip) // 获取 zip 压缩包中每一个文件条目 val entries = zipFile.entries() // 遍历压缩包中的文件 while (entries.hasMoreElements()) { val zipEntry = entries.nextElement() // zip 压缩包中的文件名称 或 目录名称 val name = zipEntry.name // 如果 apk 压缩包中含有以下文件 , 这些文件是 V1 签名文件保存目录 , 不需要解压 , 跳过即可 if (name == "META-INF/CERT.RSA" || name == "META-INF/CERT.SF" || (name == "META-INF/MANIFEST.MF") ) { continue } // 如果该文件条目 , 不是目录 , 说明就是文件 if (!zipEntry.isDirectory) { val file = File(dir, name) // 创建目录 if (!file.parentFile.exists()) { file.parentFile.mkdirs() } // 向刚才创建的目录中写出文件 val fileOutputStream = FileOutputStream(file) val inputStream = zipFile.getInputStream(zipEntry) val buffer = ByteArray(1024) var len: Int while (inputStream.read(buffer).also { len = it } != -1) { fileOutputStream.write(buffer, 0, len) } inputStream.close() fileOutputStream.close() } } // 关闭 zip 文件 zipFile.close() } catch (e: Exception) { e.printStackTrace() } } fun zip(dir: File, zip: File) { // 如果目标压缩包存在 , 删除该压缩包 zip.delete() // 对输出文件做 CRC32 校验 val cos = CheckedOutputStream(FileOutputStream( zip), CRC32()) val zos = ZipOutputStream(cos) // 压缩文件 compress(dir, zos, "") zos.flush() zos.close() } private fun compress(srcFile: File, zos: ZipOutputStream, basePath: String) { if (srcFile.isDirectory) { val files = srcFile.listFiles() for (file in files) { // zip 递归添加目录中的文件 compress(file, zos, basePath + srcFile.name + "/") } } else { compressFile(srcFile, zos, basePath) } } private fun compressFile(file: File, zos: ZipOutputStream, dir: String) { // 拼接完整的文件路径名称 val fullName = dir + file.name // app/build/outputs/apk/debug/unZipFile 路径 val fileNames = fullName.split("/").toTypedArray() // 正确的文件目录名 val sb = StringBuffer() if (fileNames.size > 1) { for (i in 1 until fileNames.size) { sb.append("/") sb.append(fileNames[i]) } } else { sb.append("/") } // 添加 zip 条目 val entry = ZipEntry(sb.substring(1)) zos.putNextEntry(entry) // 读取 zip 条目输出到文件中 val fis = FileInputStream(file) var len: Int val data = ByteArray(2048) while (fis.read(data, 0, 2048).also { len = it } != -1) { zos.write(data, 0, len) } fis.close() zos.closeEntry() } /** * 读取文件到数组中 */ fun getBytes(file: File): ByteArray { // 创建随机方位文件对象 val randomAccessFile = RandomAccessFile(file, "r") // 获取文件大小 , 并创建同样大小的数据组 val buffer = ByteArray(randomAccessFile.length().toInt()) // 读取真个文件到数组中 randomAccessFile.readFully(buffer) // 关闭文件 randomAccessFile.close() return buffer }
package kim.hsl.multiple_dex_tools import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.RandomAccessFile import java.util.zip.* import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec class AES { // Kotlin 类中的静态变量 companion object{ /** * 加密密钥, 16 字节 */ val DEFAULT_PWD = "kimhslmultiplede" } /** * 加密解密算法类型 */ val algorithm = "AES/ECB/PKCS5Padding" /** * 加密算法, 目前本应用中只需要加密, 不需要解密 */ lateinit var encryptCipher: Cipher; /** * 解密算法 */ lateinit var decryptCipher: Cipher; @ExperimentalStdlibApi constructor(pwd: String){ // 初始化加密算法 encryptCipher = Cipher.getInstance(algorithm) // 初始化解密算法 decryptCipher = Cipher.getInstance(algorithm) // 将密钥字符串转为字节数组 var keyByte = pwd.toByteArray() // 创建密钥 val key = SecretKeySpec(keyByte, "AES") // 设置算法类型, 及密钥 encryptCipher.init(Cipher.ENCRYPT_MODE, key); // 设置算法类型, 及密钥 decryptCipher.init(Cipher.DECRYPT_MODE, key); } /** * 加密操作 */ fun encrypt(contet: ByteArray) : ByteArray{ var result : ByteArray = encryptCipher.doFinal(contet) return result } /** * 解密操作 */ fun decrypt(contet: ByteArray) : ByteArray{ var result : ByteArray = decryptCipher.doFinal(contet) return result } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="kim.hsl.dex"> <application android:name="kim.hsl.multipledex.ProxyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <!-- app_name 值是该应用的 Application 的真实全类名 真实 Application : kim.hsl.dex.MyApplication 代理 Application : kim.hsl.multipledex.ProxyApplication --> <meta-data android:name="app_name" android:value="kim.hsl.dex.MyApplication"/> <!-- DEX 解密之后的目录名称版本号 , 完整目录名称为 : kim.hsl.dex.MyApplication_1.0 --> <meta-data android:name="app_version" android:value="\1.0"/> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".MyService" /> <receiver android:name=".MyBroadCastReciver"> <intent-filter> <action android:name="kim.hsl.dex.broadcast" /> </intent-filter> </receiver> <provider android:exported="true" android:name=".MyProvider" android:authorities="kim.hsl.dex.myprovider" /> </application> </manifest>
package kim.hsl.dex; import android.app.Application; import android.util.Log; public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); /* 验证 Application 是否替换成功 打印 Application , ApplicationContext , ApplicationInfo */ Log.i("octopus.MyApplication", "Application : " + this); Log.i("octopus.MyApplication", "ApplicationContext : " + getApplicationContext()); Log.i("octopus.MyApplication", "ApplicationInfo.className : " + getApplicationInfo().className); } }
DEX 加密源码资源 :
val sdkDirectory = "Y:/001_DevelopTools/002_Android_SDK/"
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。