性能优化06-深入解析dex加密框架

性能优化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);
}

demo已放到gitee,有兴趣的同学可以下载!

上一篇:性能优化05-常用的省电优化技巧

下一篇:性能优化07-深入分析进程保活机制

猜你喜欢

转载自blog.csdn.net/dshdhsd/article/details/80037466