Android app dex加密原理

目前android app反编译工具越来越牛逼,大点的app都会有加固技术。这里简单介绍下dex加密原理.

我们可能都听说过app加固,是对我们的app加了一层壳保护,但是这个壳是神马东西呢,今天主要介绍下这个东系西。

加固分为几个部分(步骤)

1,把源apk(要加固的apk  s.apk)中的dex文件加密。加密之后就不再是正常的 dex文件,那么市面上的反编译工具就不能按照

正常的dex去解析了。

2,在项目工程中实现一个application(即壳,也没有什么神秘的)用于解密dex。加密之后android系统也不认识了,要想运行就必须要 解密 。

3,把解密的dex和壳的dex合并之后替换原来BaseDexClassLoader->DexPathList->Element[] dexElements属性,dexElements可是个好东西,里面存放的是应用的dex列表。这个东西在Tinker热修复中也起了关键作用(下篇文章会详细介绍)。

4,替换application。

下面看具体实现

/**
         * 1、制作只包含解密代码的dex 文件
         */
        //1.1 解压aar 获得classes.jar
        File aarFile = new File("proxy-guard-core/build/outputs/aar/proxy-guard-core-debug.aar");
        File aarTemp = new File("proxy-guard-tools/temp");
        Zip.unZip(aarFile, aarTemp);
        File classesJar = new File(aarTemp, "classes.jar");
        //1.2 执行dx命令 将jar变成dex文件
        File classesDex = new File(aarTemp, "classes.dex");
        //执行命令  windows:cmd /c  linux/mac不需要(cmd /c)
        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文件
         */
        //2.1 解压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 dex : dexFiles) {
            //读取文件数据
            byte[] bytes = getBytes(dex);
            //加密
            byte[] encrypt = AES.encrypt(bytes);
            //写到指定目录
            FileOutputStream fos = new FileOutputStream(new File(apkTemp, "secret-"
                    + dex.getName()));
            fos.write(encrypt);
            fos.flush();
            fos.close();
            dex.delete();
        }

        /**
         * 3、把classes.dex 放入 apk解压目录 在压缩成apk
         */
        classesDex.renameTo(new File(apkTemp, "classes.dex"));
        File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
        Zip.zip(apkTemp, unSignedApk);

        /**
         * 4、对齐与签名
         */
        //4.1 对齐
//       26.0.2不认识-p参数 zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
        File alignedApk = new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
        process = Runtime.getRuntime().exec("cmd /c zipalign -f 4 " + unSignedApk
                .getAbsolutePath() + " " +
                alignedApk.getAbsolutePath());
        process.waitFor();
        //失败
        if (process.exitValue() != 0) {
            throw new RuntimeException("zipalign error");
        }

        //4.2 签名
//        apksigner sign  --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass
// pass:别名密码 --out  out.apk in.apk
        File signedApk = new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
        File jks = new File("proxy-guard-tools/proxy.jks");
        process = Runtime.getRuntime().exec("cmd /c apksigner sign  --ks " + jks.getAbsolutePath
                () + " --ks-key-alias lance --ks-pass pass:p123456 --key-pass  pass:p654321 --out" +
                " " + signedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath());
        process.waitFor();
        //失败
        if (process.exitValue() != 0) {
            throw new RuntimeException("apksigner error");
        }

这样就把一个加密的apk生成了。

下边看看壳的application干了什么:

getMetaData();
//获得当前的apk文件
File apkFile = new File(getApplicationInfo().sourceDir);
//apk zip 解压到 appDir这个目录 /data/data/packagename/
File versionDir = getDir(app_name + "_" + app_version, MODE_PRIVATE);
File appDir = new File(versionDir, "app");
//提取apk中 需要解密的所有dex放入到这个目录
File dexDir = new File(appDir, "dexDir");
//需要我们加载的dex
List dexFiles = new ArrayList<>();
//需要解密 (MD5 文件校验)
if (!dexDir.exists() || dexDir.list().length == 0) {
    //把apk解压 到 appDir
    Zip.unZip(apkFile, appDir);
    //获取目录下的所有文件
    File[] files = appDir.listFiles();
    for (File file : files) {
        String name = file.getName();
        //文件名是 .dex结尾, 并且不是主dex 放入 dexDir 目录
        if (name.endsWith(".dex") && !TextUtils.equals(name, "classes.dex")) {
            try {
                //从文件中读取 byte数组 加密后的dex数据
                byte[] bytes = Utils.getBytes(file);
                //将dex 文件 解密 并且写入 原文件file目录
                Utils.decrypt(bytes, file.getAbsolutePath());
                dexFiles.add(file);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
} else {
    //已经解密过了
    for (File file : dexDir.listFiles()) {
        dexFiles.add(file);
    }
}
可以看到就是把apk中dex问价解密然后替换系统的dex列表,具体怎么做的呢:

 private void loadDex(List dexFiles, File optimizedDirectory) throws
            NoSuchFieldException, IllegalAccessException, NoSuchMethodException,
            InvocationTargetException {
        /**
         * 1.获得 系统 classloader中的dexElements数组
         */
        //1.1  获得classloader中的pathList => DexPathList
        Field pathListField = Utils.findField(getClassLoader(), "pathList");
        Object pathList = pathListField.get(getClassLoader());
        //1.2 获得pathList类中的 dexElements
        Field dexElementsField = Utils.findField(pathList, "dexElements");
        Object[] dexElements = (Object[]) dexElementsField.get(pathList);
        /**
         * 2.创建新的 element 数组 -- 解密后加载dex
         */
        //5.x 需要适配6.x 7.x
        Method makeDexElements;
//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT <
//                Build.VERSION_CODES.M) {
             makeDexElements = Utils.findMethod(pathList, "makeDexElements", ArrayList.class,
                    File.class, ArrayList.class);
//        } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
//            makeDexElements = Utils.findMethod(pathList, "makePathElements", ArrayList.class,
//                    File.class, ArrayList.class);
//        }
        ArrayList suppressedExceptions = new ArrayList();
        Object[] addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,
                optimizedDirectory,
                suppressedExceptions);
        /**
         * 3.合并两个数组
         */
        //创建一个数组
        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);
        /**
         * 4.替换classloader中的 element数组
         */
        dexElementsField.set(pathList, newElements);
    }
和好的至此上边的1,2,3步就完成了,最后一步替换application还不知道怎么实现,网上有不少例子,也在也就之中

后续会补充。

你可能感兴趣的:(Android app dex加密原理)