美文网首页Android开发经验谈
性能优化06-多dex加密

性能优化06-多dex加密

作者: 最爱的火 | 来源:发表于2018-05-18 10:24 被阅读227次

    性能优化06-多dex加密

    dex加密是为了提高apk的安全性,保护源码。

    一、dex加密的原理

    使用加密库对apk的所有dex文件加密,然后把加密库打包成dex文件,把它和apk的dex文件放在一起重新打包成apk文件。这样apk的原dex文件就无法被反编译了。

    apk的原dex文件加密后,Android系统也无法解析,所以需要先对其解密。由于app运行时最先启动Applicaiton,所以需要设置一个代理Applicaiton,在代理Applicaiton做解密操作。解密完成后,使用hook技术,替换真正的Applicaiton即可。

    下面介绍本人设计的dex加密框架

    二、使用说明

    使用时,需要先使用加密框架加密dex:

    1. 在app的build.gradle中引入加密库Tool
    2. 编译加密库Tool,获得加密库Tool的aar文件和app的apk文件
    3. 在EncryptAndSigin的main()中手动修改initKeyStore(),来配置签名文件。注意:配置环境变量后,需要重启AS。
    4. 执行EncryptAndSigin的main().会在app/build/outputs/apk/debug/下生成签名过的apk,即为加密的apk

    然后,配置代理的Applicaiton:

    1. 在app的清单文件注册ProxyApp
    2. 添加额外数据:app_name(表示真实Applicaiton的全类名)和app_version(表示app的版本,使用int值)

    三、dex加密的实现

    dex加密的实现分为四步:dex分包、加密apk、替换Applicaiton、解密apk。

    1.dex分包

    当APP的方法数超过65536时,一般采用dex分包来解决。如果不需要分包,就跳过此步骤。

    系统支持使用multidex分包。Android5.0以下没有multidex库,所以需要手动引入。

    android {
        defaultConfig {
            multiDexEnabled true
        }
    }
    
    dependencies {
        compile 'com.android.support:multidex:1.0.0'
    }
    

    2.加密apk

    加密apk,不仅要对apk的dex文件加密,还需要将加密ku打包成dex文件,并与apk文件一起打包成新的apk。

    制作加密库的dex 文件

    先将加密库编译成aar文件,然后对其解压得到classes.jar,再通过dx命令将其打包成dex文件。

    private static File unZipAar() throws IOException, InterruptedException {
        File aarFile = new File(dexPath + "/build/outputs/aar/" + dexPath + "-debug.aar");
        File aarTemp = new File(dexPath + "/temp");
        //解压aar 获得classes.jar
        Zip.unZip(aarFile, aarTemp);
        File classesJar = new File(aarTemp, "classes.jar");
        //执行dx命令 将jar变成dex文件
        File classesDex = new File(aarTemp, "classes.dex");
        String cmd = "cmd /c dx --dex --output " + classesDex.getAbsolutePath() + " " + classesJar.getAbsolutePath();
        //执行cmd命令。1.windows中需要以cmd /c开头,linux/mac不需要。2.需要把dx添加环境变量,并重启AS。
        exec(cmd);
        return classesDex;
    }
    

    加密apk中所有dex文件

    解压apk,得到所有的dex文件,然后使用AES加密。

    private static File encryptDex() throws Exception {
        //2.1 解压apk 获得所有的dex文件
        File apkFile = new File(appPath + "/build/outputs/apk/debug/app-debug.apk");
        File apkTemp = new File(appPath + "/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();
        }
        return apkTemp;
    }
    

    把classes.dex 放入 apk解压目录 在压缩成apk

    将加密库的dex文件和apk的dex文件打包成新的apk,即为加密后的apk。

    private static File zipDex(File classesDex, File apkTemp) throws Exception {
        classesDex.renameTo(new File(apkTemp, "classes.dex"));
        File unSignedApk = new File(appPath +"/build/outputs/apk/debug/app-unsigned.apk");
        Zip.zip(apkTemp, unSignedApk);
        return unSignedApk;
    }
    

    对齐并签名apk

    先对加密后的apk对齐,压缩其体积,然后再进行签名。

    private static void siginApk(File unSignedApk) throws IOException, InterruptedException {
        //对齐apk文件:压缩apk体积。26.0.2不认识-p参数 zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
        File alignedApk = new File(appPath + "/build/outputs/apk/debug/app-unsigned-aligned.apk");
        String cmd = "cmd /c zipalign -f 4 " + unSignedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath();
        exec(cmd);
        //签名:apksigner sign  --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out  out.apk in.apk
        File signedApk = new File(appPath + "/build/outputs/apk/debug/app-signed-aligned.apk");
        File jks = new File(dexPath + "/src/main/java/gsw/dex/jks/proxy.jks");
        //注意:apksigner工具在build-tools26.0.0中没有,可以用26.0.3的
        String cmd2 = "cmd /c apksigner sign  --ks " + jks.getAbsolutePath() + " --ks-key-alias lance --ks-pass pass:p123456 --key-pass " +
                "pass:p654321 --out" + " " + signedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath();
        exec(cmd2);
    
        //删除临时文件
        File aarTemp = new File(dexPath + "/temp");
        Utils.deleteDir(aarTemp);
        File apkTemp = new File(appPath + "/build/outputs/apk/debug/temp");
        Utils.deleteDir(apkTemp);
        //删除未签名的文件
        Utils.deleteDir(unSignedApk);
        //删除对齐的文件
        if (signedApk.exists()) {
            Utils.deleteDir(alignedApk);
        }
    }
    

    3.替换Applicaiton

    由于使用了代理的Applicaiton,所以需要先从清单文件从获取真实Applicaiton的全类名,然后通过hook技术替换真实的Applicaiton。

    获取真实Applicaiton的信息

    通过在清单配置的meta-data,获取app_name(表示真实Applicaiton的全类名)和app_version(表示app的版本)。

    public void getMetaData() {
        try {
            ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo
                    (getPackageName(), PackageManager.GET_META_DATA);
            Bundle metaData = applicationInfo.metaData;
            //是否设置app_name 与 app_version
            if (null != metaData) {
                //是否存在name为app_name的meta-data数据
                if (metaData.containsKey("app_name")) {
                    app_name = metaData.getString("app_name");
                }
                if (metaData.containsKey("app_version")) {
                    app_version = metaData.getString("app_version");
                }
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }
    

    替换真实的Applicaiton

    首先根据全类名生成真实Applicaiton的实例,然后通过hook技术替换真实的Applicaiton。由于ContextImpl、ActivityThread、LoadedApk都持有Applicaiton的引用,所以需要进行3个替换。

    private void bindRealApplication() throws Exception {
        if (isBindReal) {
            return;
        }
        //如果用户(使用这个库的开发者) 没有配置Application 就不用管了
        if (TextUtils.isEmpty(app_name)) {
            return;
        }
        //这个就是attachBaseContext传进来的 ContextImpl
        Context baseContext = getBaseContext();
        //反射创建出真实的 用户 配置的Application
        Class<?> delegateClass = Class.forName(app_name);
        delegate = (Application) delegateClass.newInstance();
        //反射获得 attach函数
        Method attach = Application.class.getDeclaredMethod("attach", Context.class);
        //设置允许访问
        attach.setAccessible(true);
        attach.invoke(delegate, baseContext);
    
        /**
         *  1.替换ContextImpl:ContextImpl -> mOuterContext ProxyApp->MyApplication
         */
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        //获得 mOuterContext 属性
        Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
        mOuterContextField.setAccessible(true);
        mOuterContextField.set(baseContext, delegate);
    
        /**
         * 2.替换ActivityThread:  mAllApplications 与 mInitialApplication
         */
        //获得ActivityThread对象 ActivityThread 可以通过 ContextImpl 的 mMainThread 属性获得
        Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
        mMainThreadField.setAccessible(true);
        Object mMainThread = mMainThreadField.get(baseContext);
    
        //替换 mInitialApplication
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field mInitialApplicationField = activityThreadClass.getDeclaredField
                ("mInitialApplication");
        mInitialApplicationField.setAccessible(true);
        mInitialApplicationField.set(mMainThread, delegate);
    
        //替换 mAllApplications
        Field mAllApplicationsField = activityThreadClass.getDeclaredField
                ("mAllApplications");
        mAllApplicationsField.setAccessible(true);
        ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
        mAllApplications.remove(this);
        mAllApplications.add(delegate);
    
    
        /**
         * 3.替换LoadedApk:LoadedApk -> mApplication ProxyApp
         */
        //LoadedApk 可以通过 ContextImpl 的 mPackageInfo 属性获得
        Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
        Object mPackageInfo = mPackageInfoField.get(baseContext);
    
        Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
        Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
        mApplicationField.setAccessible(true);
        mApplicationField.set(mPackageInfo, delegate);
    
        //修改ApplicationInfo className LoadedApk
        Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
        mApplicationInfoField.setAccessible(true);
        ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
        mApplicationInfo.className = app_name;
    
        delegate.onCreate();
        isBindReal = true;
    }
    

    4.解密dex

    解密dex

    先把加密后的dex使用AES进行解密,然后使用hook技术添加到系统的dexElements中,让其生效。

    private void decryptApk() {
        //获得当前的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<File> 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目录
                        AES.init(AES.DEFAULT_PWD);
                        bytes = AES.decrypt(bytes);
                        FileOutputStream fos = new FileOutputStream(file);
                        fos.write(bytes);
                        fos.close();
                        dexFiles.add(file);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            //已经解密过了
        } else {
            for (File file : dexDir.listFiles()) {
                dexFiles.add(file);
            }
        }
        try {
            loadDex(dexFiles, versionDir);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    加载dex

    先获取系统 classloader中的dexElements数组,然后把解密后的dex添加到前面。

    private void loadDex(List<File> dexFiles, File optimizedDirectory) throws
            NoSuchFieldException, IllegalAccessException, NoSuchMethodException,
            InvocationTargetException {
        getClassLoader();
        /**
         * 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 需要做版本兼容
        Method makeDexElements = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            makeDexElements = Utils.findMethod(pathList, "makeDexElements", List.class, File.class, List.class);
        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
            makeDexElements = Utils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            makeDexElements = Utils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);
        }
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        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);
    }
    

    最后

    代码地址:https://gitee.com/yanhuo2008/Common/tree/master/ToolDex

    性能优化专题:https://www.jianshu.com/nb/25128595

    喜欢请点赞,谢谢!

    相关文章

      网友评论

        本文标题:性能优化06-多dex加密

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