性能优化 (七) APK 加固之 Dex 加解密,反编译都看不到项目主要代码。

性能优化系列

APP 启动优化html

UI 绘制优化java

内存优化android

图片压缩git

长图优化github

电量优化数组

Dex 加解密性能优化

动态替换 Application架构

APP 稳定性之热修复原理探索app

APP 持续运行之进程保活实现ide

ProGuard 对代码和资源压缩

APK 极限压缩

简介

如今随意在应用市场下载一个 APK 文件而后反编译,95% 以上基本上都是通过混淆,加密,或第三方加固(第三方加固也是这个原理),那么今天咱们就对 Dex 来进行加密解密。让反编译没法正常阅读项目源码。

加密后的结构

APK 分析

经过 AS 工具分析加密后的 APK 文件,查看 dex 是报错的,要的就是这个效果。

反编译效果

想要对 Dex 加密 ,先来了解什么是 64 K 问题

想要详细了解 64 k 的问题能够参考官网

随着 Android 平台的持续成长,Android 应用的大小也在增长。当您的应用及其引用的库达到特定大小时,您会遇到构建错误,指明您的应用已达到 Android 应用构建架构的极限。早期版本的构建系统按以下方式报告这一错误:

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
复制代码

较新版本的 Android 构建系统虽然显示的错误不一样,但指示的是同一问题:

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
复制代码

这些错误情况都会显示下面这个数字:65,536。这个数字很重要,由于它表明的是单个 Dalvik Executable (DEX) 字节码文件内的代码可调用的引用总数。本节介绍如何经过启用被称为 Dalvik 可执行文件分包的应用配置来越过这一限制,使您的应用可以构建并读取 Dalvik 可执行文件分包 DEX 文件。

关于 64K 引用限制

Android 5.0 以前版本的 Dalvik 可执行文件分包支持

Android 5.0(API 级别 21)以前的平台版本使用 Dalvik 运行时来执行应用代码。默认状况下,Dalvik 限制应用的每一个 APK 只能使用单个 classes.dex 字节码文件。要想绕过这一限制,您可使用 Dalvik 可执行文件分包支持库,它会成为您的应用主要 DEX 文件的一部分,而后管理对其余 DEX 文件及其所包含代码的访问。

Android 5.0 及更高版本的 Dalvik 可执行文件分包支持

Android 5.0(API 级别 21)及更高版本使用名为 ART 的运行时,后者原生支持从 APK 文件加载多个 DEX 文件。ART 在应用安装时执行预编译,扫描 classesN.dex 文件,并将它们编译成单个 .oat 文件,供 Android 设备执行。所以,若是您的 minSdkVersion 为 21 或更高值,则不须要 Dalvik 可执行文件分包支持库。

解决 64K 限制

  1. 若是您的 minSdkVersion 设置为 21 或更高值,您只需在模块级 build.gradle 文件中将 multiDexEnabled 设置为 true,如此处所示:

    android {
        defaultConfig {
            ...
            minSdkVersion 21 
            targetSdkVersion 28
            multiDexEnabled true
        }
        ...
    }
    复制代码

    可是,若是您的 minSdkVersion 设置为 20 或更低值,则您必须按以下方式使用 Dalvik 可执行文件分包支持库

    • 修改模块级 build.gradle 文件以启用 Dalvik 可执行文件分包,并将 Dalvik 可执行文件分包库添加为依赖项,如此处所示

      android {
          defaultConfig {
              ...
              minSdkVersion 15 
              targetSdkVersion 28
              multiDexEnabled true
          }
          ...
      }
      
      dependencies {
        compile 'com.android.support:multidex:1.0.3'
      }
      复制代码
    • 当前 Application extends MultiDexApplication {...} 或者 MultiDex.install(this);

  2. 经过混淆 开启 ProGuard 移除未使用的代码,构建代码压缩。

  3. 减小第三方库的直接依赖,尽量下载源码,须要什么就用什么不必依赖整个项目。

Dex 加密与解密

流程:

  1. 拿到 APK 解压获得全部的 dex 文件。
  2. 经过 Tools 来进行加密,并把加密后的 dex 和代理应用 class.dex 合并,而后从新签名,对齐,打包。
  3. 当用户安装 APK 打开进入代理解密的 Application 时,反射获得 dexElements 并将解密后的 dex 替换 DexPathList 中的 dexElements .

Dex 文件加载过程

既然要查 Dex 加载过程,那么得先知道从哪一个源码 class 入手,既然不知道那么咱们就先打印下 ClassLoader ;

下面就以一个流程图来详细了解下 Dex 加载过程吧

最后咱们得知在 findClass(String name,List sup) 遍历 dexElements 找到 Class 并交给 Android 加载。

Dex 解密

如今咱们知道 dex 加载流程了 , 那么咱们怎么进行来对 dex 解密勒,刚刚咱们得知须要遍历 dexElements 来找到 Class 那么咱们是否是能够在遍历以前 ,初始化 dexElements 的时候。反射获得 dexElements 将咱们解密后的 dex 交给 dexElements 。下面咱们就经过代码来进行解密 dex 并替换 DexPathList 中的 dexElements;

  1. 获得当前加密了的 APK 文件 并解压

    //获得当前加密了的APK文件
    File apkFile=new File(getApplicationInfo().sourceDir);
    //把apk解压 app_name+"_"+app_version目录中的内容须要boot权限才能用
    File versionDir = getDir(app_name+"_"+app_version,MODE_PRIVATE);
    File appDir=new File(versionDir,"app");
    File dexDir=new File(appDir,"dexDir");
    复制代码
  2. 获得咱们须要加载的 Dex 文件

    //把apk解压到appDir
    Zip.unZip(apkFile,appDir);
    //获取目录下全部的文件
    File[] files=appDir.listFiles();
    for (File file : files) {
         String name=file.getName();
         if(name.endsWith(".dex") && !TextUtils.equals(name,"classes.dex")){
         try{
            AES.init(AES.DEFAULT_PWD);
            //读取文件内容
            byte[] bytes=Utils.getBytes(file);
            //解密
            byte[] decrypt=AES.decrypt(bytes);
            //写到指定的目录
            FileOutputStream fos=new FileOutputStream(file);
            fos.write(decrypt);
            fos.flush();
            fos.close();
            dexFiles.add(file);
    
         }catch (Exception e){
             e.printStackTrace();
         }
      }
    }
    复制代码
  3. 把解密后的 dex 加载到系统

    private void loadDex(List<File> dexFiles, File versionDir) throws Exception{
            //1.获取pathlist
            Field pathListField = Utils.findField(getClassLoader(), "pathList");
            Object pathList = pathListField.get(getClassLoader());
            //2.获取数组dexElements
            Field dexElementsField=Utils.findField(pathList,"dexElements");
            Object[] dexElements=(Object[])dexElementsField.get(pathList);
            //3.反射到初始化dexElements的方法
            Method makeDexElements=Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);
    
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            Object[] addElements=(Object[])makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions);
    
            //合并数组
            Object[] newElements= (Object[])Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+addElements.length);
            System.arraycopy(dexElements,0,newElements,0,dexElements.length);
            System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);
    
            //替换 DexPathList 中的 element 数组
            dexElementsField.set(pathList,newElements);
        }
    复制代码

    解密已经完成了,下面来看看加密吧,这里为何先说解密勒,由于 加密涉及到 签名,打包,对齐。因此留到最后讲。

Dex 加密

  1. 制做只包含解密代码的 dex

    1. sdk\build-tools 中执行下面命令 会获得包含 dex 的 jar
    dx --dex --output out.dex in.jar
    2. 经过 exec 执行
    File aarFile=new File("proxy_core/build/outputs/aar/proxy_core-debug.aar");
            File aarTemp=new File("proxy_tools/temp");
            Zip.unZip(aarFile,aarTemp);
            File classesJar=new File(aarTemp,"classes.jar");
            File classesDex=new File(aarTemp,"classes.dex");
            String absolutePath = classesDex.getAbsolutePath();
            String absolutePath1 = classesJar.getAbsolutePath();
            //dx --dex --output out.dex in.jar
            //dx --dex --output //D:\Downloads\android_space\DexDEApplication\proxy_tools\temp\classes.dex //D:\Downloads\android_space\DexDEApplication\proxy_tools\temp\classes.jar
            Process process=Runtime.getRuntime().exec("cmd /c dx --dex --output "+classesDex.getAbsolutePath()
                                        +" "+classesJar.getAbsolutePath());
            process.waitFor();
            if(process.exitValue()!=0){
                throw new RuntimeException("dex error");
            }
    复制代码
  2. 加密 apk 中的 dex 文件

    File apkFile=new File("app/build/outputs/apk/debug/app-debug.apk");
            File apkTemp=new File("app/build/outputs/apk/debug/temp");
            Zip.unZip(apkFile,apkTemp);
            //只要dex文件拿出来加密
            File[] dexFiles=apkTemp.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File file, String s) {
                    return s.endsWith(".dex");
                }
            });
            //AES加密了
            AES.init(AES.DEFAULT_PWD);
            for (File dexFile : dexFiles) {
                byte[] bytes = Utils.getBytes(dexFile);
                byte[] encrypt = AES.encrypt(bytes);
                FileOutputStream fos=new FileOutputStream(new File(apkTemp,
                        "secret-"+dexFile.getName()));
                fos.write(encrypt);
                fos.flush();
                fos.close();
                dexFile.delete();
    }
    复制代码
  3. 把 dex 放入 apk 加压目录,从新压成 apk 文件

    File apkTemp=new File("app/build/outputs/apk/debug/temp");
            File aarTemp=new File("proxy_tools/temp");
            File classesDex=new File(aarTemp,"classes.dex");
            classesDex.renameTo(new File(apkTemp,"classes.dex"));
            File unSignedApk=new File("app/build/outputs/apk/debug/app-unsigned.apk");
            Zip.zip(apkTemp,unSignedApk);
    复制代码

    如今能够看下加密后的文件,和未加密的文件

    未加密 apk:

    加密后的 apk (如今只能看见代理 Application )

打包

对齐

//apk整理对齐工具 未压缩的数据开头均相对于文件开头部分执行特定的字节对齐,减小应用运行内存。
zipalign -f 4 in.apk out.apk 

//比对 apk 是否对齐
zipalign -c -v 4 output.apk

//最后提示 Verification succesful 说明对齐成功了
  236829 res/mipmap-xxxhdpi-v4/ic_launcher.png (OK - compressed)
  245810 res/mipmap-xxxhdpi-v4/ic_launcher_round.png (OK - compressed)
  260956 resources.arsc (OK - compressed)
  317875 secret-classes.dex (OK - compressed)
 2306140 secret-classes2.dex (OK - compressed)
 2477544 secret-classes3.dex (OK - compressed)
Verification succesful
复制代码

签名打包 apksigner

//sdk\build-tools\24.0.3 以上,apk签名工具
apksigner sign  --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out  out.apk in.apk
复制代码

总结

其实原理就是把主要代码经过命令 dx 生成 dex 文件,而后把加密后的 dex 合并在代理 class.dex 中。这样虽然仍是能看见代理中的代码,可是主要代码已经没有暴露出来了,就已经实现了咱们想要的效果。若是封装的好的话(JNI 中实现主要解密代码),基本上就哈也看不见了。ClassLoader 仍是很重要的,热修复跟热加载都是这原理。学到这里 DEX 加解密已经学习完了,若是想看本身试一试能够参考个人代码

代码传送阵