Dex加密(上)

作者: 追寻米K | 来源:发表于2019-03-21 16:29 被阅读0次

    App通常都会做混淆防止别人反编译,即使反编译出来也是a、b、c这种,但是这种还是会被一些有心的人还原代码,这样我们需要给dex加密,这样别人就不容易反编译。
    效果:



    别人是没办法看见主工程的代码只能看见解密工程的java代码,因为解密是在C中实现的,所以密钥和解密方法都还是安全的。

    APK本质就是一个zip压缩包,解压之后包含AndroidManifest.xml、class.dex、resources.arsc等等文件


    要把Dex加密,但是系统是不认识我们加密后的Dex的,所以还需要一个解密,我们做一个解密工程生成主Dex(系统能识别的Dex,这个不加密),然后利用这个Dex解密我们加密过的原App的Dex。



    整个工程分三个部分,工程结构:



    不是利用Android Studio的自动打包,而是我们去生成一个apk
    大致步骤:

    1.主APP生成apk,解密工程生成aar,提供给后面使用

    1. 获取解密工程生成的aar中的class.jar包,并把它生成为classes.dex。
      3.获得主APK里面的所有dex,并给所有dex加密成系统不能识别的dex。
    2. 把解密dex放入加密dex的目录,重新生成一个apk,并签名这个apk
    3. 替换回application(也就是AndroidManifest.xml中定义的application)。替换Application

    1. 创建解密工程

    首先在主APP工程中创建一个Module作为解密工程,别忘了在主工程的gradle中添加

    implementation project(':proxy_guard_core')
    

    一个app的入口都是Application,所以我们创建一个Application在这里实现解密,并在主APP的AndroidManifest.xml中使用这个Application。


    准备工作完毕,下面撸解密代码
    Application最先调用的方法是attachBaseContext(Context base)。

    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();
                        }
                    }
                }
    

    系统在加载dex的时候首先加载的是classes.dex,后面我们需要把解密工程生成classes.dex,所以这里遍历得到所以的dex文件,classes.dex除外。Utils.decrypt()调用openssl实现解密

     //加解密的 上下文
        EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
        int outlen;
        unsigned char outbuf[1024];
        //初始化上下文 设置解码参数
        EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, userkey, NULL);
    
        //密文比明文长,所以肯定能保存下所有的明文
        uint8_t *out = malloc(src_len);
        //数据置空
        memset(out, 0, src_len);
        int len;
        //解密   abcdefg  z    z
        EVP_DecryptUpdate(ctx, out, &outlen, src, src_len);
        len = outlen;
        //解密剩余的所有数据 校验
        EVP_DecryptFinal_ex(ctx, out + outlen, &outlen);
        len += outlen;
        EVP_CIPHER_CTX_free(ctx);
    
        //写文件 以二进制形式写出
        FILE *f = fopen(path, "wb");
        fwrite(out, len, 1, f);
        fclose(f);
        free(out);
    

    openSSL的编译看最后面。
    我们把加密的dex解密成普通的dex之后,这个时候系统已经运行完成了多dex自动加载过程,但是我们的dex并没有被加载,所以我们需要自己实现多dex的加载。多dex加载原理看后面,5.0之后开始支持多dex加载,源码MultiDex里也是这样实现的。(但是MultiDex适配19(4.4)以上的之调用下面的第一个if,并没有适配6.x,就能做到19以上系统都能支持多dex加载,具体原因是什么还不清楚,或者我看错了)

     //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);
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT <
                    Build.VERSION_CODES.M) {
                //5.x
                 makeDexElements = Utils.findMethod(pathList, "makeDexElements", ArrayList.class,
                        File.class, ArrayList.class);
                addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,
                        optimizedDirectory,
                        suppressedExceptions);
            } else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.N &&Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
                //6.x
                makeDexElements = Utils.findMethod(pathList, "makePathElements", List.class,
                        File.class, List.class);
                addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,
                        optimizedDirectory,
                        suppressedExceptions);
            }else {
                makeDexElements = Utils.findMethod(pathList, "makeDexElements",
                        List.class, File.class, List.class,ClassLoader.class);
                Field definingContextField = Utils.findField(pathList, "definingContext");
                ClassLoader definingContext = (ClassLoader) definingContextField.get(pathList);
                addElements = (Object[]) makeDexElements.invoke(pathList,dexFiles, optimizedDirectory, suppressedExceptions,definingContext);
            }
    

    合并两个Element[]并替换原来的Element[]

    //创建一个数组
            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);
    

    2. 解密工程生成dex

    生成dex我们需要借助androidSDK\build-tools\28.0.3下的dx.bat工具,这个工具可以把class/jar生成dex。
    一条命令就能生成

    dx --dex --output out.dex in.jar
    

    把之前的解密工程生成aar,拿到其中的classes.jar生成dex

            /**
             * 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");
            }
    

    3. 拿到主工程所有的dex并加密

     /**
             * 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();
            }
    

    4. 放入解密工程的dex,并签名

    https://developer.android.google.cn/studio/publish/app-signing

    /**
             * 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.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
            //官方文档没有 --ks-key-alias等参数 有点坑爹啊
            File signedApk = new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
            File jks = new File("proxy-guard-tools/proxyDex.jks");
            process = Runtime.getRuntime().exec("cmd /c apksigner sign  --ks " + jks.getAbsolutePath
                    () + " --ks-key-alias hz --ks-pass pass:123456 --key-pass  pass:123456 --out" +
                    " " + signedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath());
            process.waitFor();
            //失败
            if (process.exitValue() != 0) {
                throw new RuntimeException("apksigner error");
            }
    

    多Dex加载原理

    Dex的加载是通过ClassLoader来加载,源码目录:
    libcore\dalvik\src\main\java\dalvik\system



    加载一个类我们常常会调用

    getClassLoader().loadClass("类名");
    

    通过getClassLoader()我们实际获得是一个PathClassLoader对象,loadClass方法在抽象类ClassLoader中

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        c = findClass(name);
                    }
                }
                return c;
        }
    

    这里的findClass实际又是PathClassLoader的父类BaseDexClassLoader的一个方法

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
            Class c = pathList.findClass(name, suppressedExceptions);
            if (c == null) {
                ClassNotFoundException cnfe = new ClassNotFoundException(
                        "Didn't find class \"" + name + "\" on path: " + pathList);
                for (Throwable t : suppressedExceptions) {
                    cnfe.addSuppressed(t);
                }
                throw cnfe;
            }
            return c;
        }
    

    类的加载实际是调用pathList的findClass方法通过类名来找到一个类。而pathList是一个DexPathList 对象。

    private final DexPathList pathList;
    

    再来看看DexPathList 的findClass方法:

     public Class<?> findClass(String name, List<Throwable> suppressed) {
            for (Element element : dexElements) {
                Class<?> clazz = element.findClass(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
    
            if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }
            return null;
        }
    
        /**
         * Finds the named class in one of the dex files pointed at by
         * this instance. This will find the one in the earliest listed
         * path element. If the class is found but has not yet been
         * defined, then this method will define it in the defining
         * context that this instance was constructed with.
         */
        public Class<?> findClass(String name, List<Throwable> suppressed) {
            for (Element element : dexElements) {
                Class<?> clazz = element.findClass(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
    
            if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }
            return null;
        }
    

    按照注释的意思就是从一个dex文件中找到class。
    通过遍历dexElements,由此可见一个Element 对应一个dex。
    果然dexElements的注释也证明了。

        /**
         * List of dex/resource (class path) elements.
         * Should be called pathElements, but the Facebook app uses reflection
         * to modify 'dexElements' (http://b/7726934).
         */
        private Element[] dexElements;
    

    所以需要把我们生成的dex加入到dexElements这个数组中就能实现多dex的加载
    先来看看怎么生成dexElements。

        public DexPathList(ClassLoader definingContext, String dexPath,
                String librarySearchPath, File optimizedDirectory) {
            .......
            this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                               suppressedExceptions, definingContext);
            ......
    

    注意:这里是DexPathList的四个参数的构造方法,而不是两个参数的构造方法,两个参数的构造方法,是加载内存中的dexFile。7.0一下源码没有两个参数的构造方法。
    所以我们可以通过反射调用makeDexElements来帮我们生成dexElements。7.0以上都是一样,但是4.4到5.x参数不一样,6.x源码这个方法名就不一样了。

    • 6.x
            public DexPathList(ClassLoader definingContext, String dexPath,
                    String libraryPath, File optimizedDirectory) {
          ......
            this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
                                                           suppressedExceptions);
          ......
    
    • 4.4-5.x
        public DexPathList(ClassLoader definingContext, String dexPath,
                               String libraryPath, File optimizedDirectory) {
            this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                                           suppressedExceptions);
    

    4.4-5.x虽然也是makeDexElements但是只有三个参数。

    编译openSSL

    加密需要openSSL,所以首先先编译一个openSSL。

    1. 下载openSSL
      https://www.openssl.org/source/
      linux 环境下载
    wget https://www.openssl.org/source/openssl-1.1.1b.tar.gz
    

    解压之后编译。官网没有编译教程,github上也没有编译教程
    还好有个wiki
    https://wiki.openssl.org/index.php/Android
    把setenv-android.sh下载下来,并全部复制。
    openSSL源码目录下创建一个build.sh,复制setenv-android.sh中的代码。我的NDK 是r17b版本,然后修改其中的配置。

    _ANDROID_EABI="arm-linux-androideabi-4.9"
    

    源码中是4.8,根据ndk版本来设置,我的ndk中是4.9。
    添加ndk根目录

    export ANDROID_NDK_HOME=/ndk/android-ndk-r17b
    export ANDROID_NDK_ROOT=/ndk/android-ndk-r17b
    

    如果不设置ANDROID_NDK_HOME会报ANDROID_NDK_HOME未定义的错误。
    设置了 ANDROID_NDK_ROOT之后


    这个配置就不用管了。
    这些都是配置,还需要编译模块


    完整的编译脚本在demo中。Dex加密下

    相关文章

      网友评论

        本文标题:Dex加密(上)

        本文链接:https://www.haomeiwen.com/subject/tpsbuqtx.html