risc-v中文社区

 找回密码
 立即注册
查看: 775|回复: 1

Android dex加密与解密原理及其代码实现

[复制链接]

347

主题

564

帖子

2237

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
2237
发表于 2022-5-17 14:05:53 | 显示全部楼层 |阅读模式
Android dex加密与解密原理及其代码实现
       为什么要进行apk加密?答案是避免apk被有心人反编译,窃取公司重要技术和算法等。但是要给Apk加密要如何实现呢?系统在加载类的时候都是从我们apk的dex文件中加载的。ClassLoader会去维护一个这样的dex文件数组。而我们要做的就是将原apk中的dex都加密,然后将解密部分的代码单独编程成dex文件(我们称这样的dex为壳dex)连带着加密的dex一起加到新apk中。这样新apk安装后系统就能够找到我们应用启动的入口Application了,不至于由于加密导致系统找不到应用程序入口。而在这个程序入口中我们要做的就是解密被加密的dex文件,然后重新插入到ClassLoader维护的dex文件数组中(这里就涉及到大量的反射知识)。

       dex解密与解密分为以下几个步骤:

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

       2)将加密后的dex文件与壳程序Apk的dex文件合并成新的dex文件,然后替换壳程序中的源dex文件,生成新的apk文件。

       3)在壳程序apk的application中进行解密源apk的dex文件。

       知道了原理,下面就是代码实现了。这里有三个工程:

       1)原程序项目 app module(需要加密的APK)。

       2)壳项目 ApkShuck module(解密源程序APK和加载APK)。

       3)对原APK进行加密和壳项目的DEX的合并项目 Encrypt module。

    1. 加密实现
       在Android studio 中创建一个java library module(Encrypt),此module主要用来进行dex的加密工作。

       1. 在开始加密之前首先建立临时目录,在 Encrypt 下建立一个source目录,然后在 source 目录建立 apk 目录和 arr 目录,最后分别在 apk 和 arr 目录下建立一个 temp 目录。apk 目录下放置原apk文件,它的 temp 目录主要用来放置原apk解压之后的文件和原apk中dex加密后的文件。arr目录放置壳apk(其实是一个arr包),它的 temp 目录主要用来放置壳apk解压之后的文件。

       2. 清理临时目录的缓存文件,代码如下所示:

public class ApkEncryptMain {
  public static void main(String[] args) {
        init();
}
    /**
     * 初始化
     */
    private static void init() {
        // 删除缓存
        FileUtils.delFolder(new File("Encrypt/source/apk/temp"));
        FileUtils.delFolder(new File("Encrypt/source/arr/temp"));
    }
}

       3. 加密算法实现,主要采用的是AES加密算法,代码如下:

public class EncryptUtils {  
    private final byte[] KEY = "QUmkLrrISiud6RPU".getBytes(); // 加密使用的key
    private final byte[] IV = "eh7aJlOdHCNsGNcD".getBytes(); // 偏移值
    private final String ALGORITHM = "AES/CBC/PKCS5Padding"; // 加密算法
    private Cipher encryptCipher; // 加密

    /**
     * 使用单例
     */
    private EncryptUtils() {
        try {
            // 初始化加密算法
            encryptCipher = Cipher.getInstance(ALGORITHM);
            SecretKeySpec key = new SecretKeySpec(KEY, "AES");
            encryptCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(IV));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static class SingletonHolder {
        private static final EncryptUtils INSTANCE = new EncryptUtils();
    }

    public static EncryptUtils getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
       4. 解压原apk,并加密原apk中的dex文件。

public class ApkEncryptMain {
    private static final String SOURCE_APK_PATH = "Encrypt/source/apk/app-debug.apk";

    public static void main(String[] args) {
        LogUtils.i("start encrypt");
        init();
        /**
         * 1. 解压源apk文件到 ../source/apk/temp目录下,并加密dex文件
         */
        File sourceApk = new File(SOURCE_APK_PATH);
        File newApkDir = new File(sourceApk.getParent() + File.separator + "temp");
        if (!newApkDir.exists()) {
            newApkDir.mkdirs();
        }
        // 解压Apk并加密dex文件
        EncryptUtils.getInstance().encryptApkFile(sourceApk, newApkDir);
    }
}

public class EncryptUtils {
   /**
     * 加密apk
     *
     * @param srcApkFile 源apk文件的地址
     * @param dstApkFile 新apk文件的地址
     */
    public void encryptApkFile(File srcApkFile, File dstApkFile) {
        if (srcApkFile == null || !srcApkFile.exists()) {
            LogUtils.e("srcAPKFile not exist");
            return;
        }
        // 解压apk到指定文件夹
        ZipUtils.unZip(srcApkFile, dstApkFile);

        // 获取所有的dex(可能存在分包的情况,即有多个dex文件)
        File[] dexFiles = dstApkFile.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File file, String s) {
                // 提取所有的.dex文件
                return s.endsWith(".dex");
            }
        });

       if (dexFiles == null || dexFiles.length <= 0) {
            LogUtils.i("this apk is invalidate");
            return;
        }

        for (File dexFile : dexFiles) {
            // 读取dex中的数据
            byte[] buffer = FileUtils.getBytes(dexFile);
            if (buffer != null) {
                // 加密
                byte[] encryptBytes = encrypt(buffer);
                if (encryptBytes != null) {
                    //修改.dex名为_.dex,避免等会与aar中的.dex重名
                    int indexOf = dexFile.getName().indexOf(".dex");
                    String newName = dexFile.getParent() + File.separator +
                            dexFile.getName().substring(0, indexOf) + "_.dex";
                    // 写数据, 替换原来的数据
                    FileUtils.wirte(new File(newName), encryptBytes);
                    dexFile.delete();
                } else {
                    LogUtils.e("Failed to encrypt dex data");
                    return;
                }
            } else {
                LogUtils.e("Failed to read dex data");
                return;
            }
        }
    }

    /**
     * 加密
     * @param data
     * @return
     */
    private byte[] encrypt(byte[] data) {
        try {
            return encryptCipher.doFinal(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

public class ZipUtils {
    /**
     * 解压zip文件
     *
     * @param srcFile 需要解压的zip文件
     * @param dstFile 解压后的文件
     */
    public static void unZip(File srcFile, File dstFile) {
        if (srcFile == null) {
            LogUtils.e("unZip: srcFile is null");
            return;
        }
        try {
            ZipFile zipFile = new ZipFile(srcFile);
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()){
                ZipEntry zipEntry = entries.nextElement();
                String name = zipEntry.getName();
                if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
                        .equals("META-INF/MANIFEST.MF")) {
                    continue;
                }
                if(!zipEntry.isDirectory()){
                    File file = new File(dstFile, name);
                    if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
                    FileOutputStream fos = new FileOutputStream(file);
                    InputStream is = zipFile.getInputStream(zipEntry);
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = is.read(buffer)) != -1) {
                        fos.write(buffer, 0, len);
                    }
                    is.close();
                    fos.close();
                }
            }
            zipFile.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
       5. 解压arr文件,并生成壳dex。

public class ApkEncryptMain {
    public static void main(String[] args) {

        /**
         * 2. 解压arr文件(不能进行加密的部分),将其中的dex文件拷贝到apk/temp目录中。
         */
        File shuckApk = new File(SHUCK_APK_PATH);
        File newShuckDir = new File(shuckApk.getParent() + File.separator + "temp");
        if (!newShuckDir.exists()) {
            newShuckDir.mkdirs();
        }
        // 解压arr文件,并将arr中的jar文件转化为dex文件
        DxUtils.jar2Dex(shuckApk, newShuckDir);
        // 拷贝arr中的classes.dex 到 apk/temp 目录中
        File copyDstFile = new File("Encrypt/source/apk/temp/classes.dex");
        FileUtils.copyFile(dstDex, copyDstFile);
    }
}

public class DxUtils {
    /**
     * 解压arr并将jar转化为 dex
     *
     * @param srcFile
     * @param dstFile
     */
    public static void jar2Dex(File srcFile, File dstFile) {
        if (srcFile == null || !srcFile.exists()) {
            LogUtils.e("shuck arr file not exist");
            return;
        }
        // 解压apk到指定文件夹
        ZipUtils.unZip(srcFile, dstFile);

        // 获取所有的jar
        File[] jarFiles = dstFile.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File file, String s) {
                // 提取所有的.dex文件
                return s.endsWith(".jar");
            }
        });

        if (jarFiles == null || jarFiles.length <= 0) {
            LogUtils.i("this arr is invalidate");
            return;
        }
        // 一般情况下这个壳arr中只会有一个classes.jar文件,这里classes_jar就是classes.jar文件
        File classes_jar = jarFiles[0];
        // 将classes_jar 转为为 classes.dex
        File dstDex = new File(classes_jar.getParent() + File.separator + "classes.dex");
        // 使用 android tools 里面的dx.bat 命令将 jar 转化为 dex
        dxCommand(classes_jar, dstDex);
    }


    private static void dxCommand(File jarFile, File dexFile) {
        // 这里使用的是dx.bat的绝对路径.请根据实际情况填写。
        String command = "cmd.exe /C E:\\development_tools\\android_sdk\\android_sdk\\build-tools\\29.0.2\\dx --dex --output=" + dexFile.getAbsolutePath()
                + " " + jarFile.getAbsolutePath();
        CmdUtils.execCommand(command);
    }
}

public class CmdUtils {
    public static void execCommand(String command) {
        Runtime runtime = Runtime.getRuntime();
        Process process = null;
        BufferedReader success_buffer = null;
        BufferedReader error_buffer = null;
        try {
            process = runtime.exec(command);
            String line;
            success_buffer = new BufferedReader(new InputStreamReader(process.getInputStream()));
            while ((line = success_buffer.readLine()) != null) {
                LogUtils.i(line);
            }
            process.waitFor();
            if (process.exitValue() != 0) {
                LogUtils.e("exec fail --- " + command);
                error_buffer = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                while ((line = error_buffer.readLine()) != null) {
                    LogUtils.e(line);
                }
            } else {
                LogUtils.i("exec success --- " + command);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (process != null) {
                process.destroy();
                process = null;
            }
            if (success_buffer != null) {
                try {
                    success_buffer.close();
                    success_buffer = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (error_buffer != null) {
                try {
                    error_buffer.close();
                    error_buffer = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
       7. 打包apk/temp目录生成新的未签名的apk文件

                /**
                 *    特别注意!!!
                 *    这里一定要用"/" 千万不要用File.separator
                 *    因为这里是java 工程,它运行在 windows环境,在Windows环境下 File.separator 获取的是 "\"
                 *    而在 Android 系统中 File.separator 获取的是 "/"
                 *    因此在这里如果使用 File.separator 时获取的 "\" 在 android中识别不了,将会导致程序运行不起来。
                 *
                 */

public class ApkEncryptMain {
    private static final String SOURCE_APK_PATH = "Encrypt/source/apk/app-debug.apk";
    private static final String SHUCK_APK_PATH = "Encrypt/source/arr/ApkShuck-release.aar";

    public static void main(String[] args) {
        /**
         * 3. 打包apk/temp目录生成新的未签名的apk文件
         */
        File unsignedApk = new File("Encrypt/result/apk-unsigned.apk");
        unsignedApk.getParentFile().mkdirs();
        unsignedApk.delete();
        ZipUtils.zip(newApkDir, unsignedApk);
    }
}

public class ZipUtils {

    /**
     * 压缩
     * @param sourceFile
     * @param zipFile
     */
    public static void zip(File sourceFile, File zipFile) {
        if (sourceFile == null) {
            LogUtils.e("The original file that needs to be compressed does not exist");
            return;
        }
        zipFile.delete();
        // 对输出文件做CRC32校验
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()));
            compress(sourceFile, zos, "");
            zos.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                    zos = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

   private static void compress(File srcFile, ZipOutputStream zos, String dir) throws IOException {
        if (srcFile.isDirectory()) {
            File[] files = srcFile.listFiles();
            for (File file : files) {
                /**
                 *    特别注意!!!
                 *    这里一定要用"/" 千万不要用File.separator
                 *    因为这里是java 工程,它运行在 windows环境,在Windows环境下 File.separator 获取的是 "\"
                 *    而在 Android 系统中 File.separator 获取的是 "/"
                 *    因此在这里如果使用 File.separator 时获取的 "\" 在 android中识别不了,将会导致程序运行不起来。
                 *
                 */
                compress(file, zos, dir + srcFile.getName() + "/");
            }
        } else {
            compressFile(srcFile, zos, dir);
        }
    }


    private static void compressFile(File file, ZipOutputStream zos, String dir) throws IOException {
        // temp/classes.dex
        String fullName = dir + file.getName();
        // 需要去掉temp
        String[] dirNames = fullName.split("/");
        // 正确的文件目录名(去掉了temp)
        StringBuffer sb = new StringBuffer();
        if (dirNames.length > 1) {
            for (int i = 1; i < dirNames.length; i++) {
                sb.append("/");
                sb.append(dirNames);
            }
        } else {
            sb.append("/");
        }
        ZipEntry entry = new ZipEntry(sb.toString().substring(1));
        zos.putNextEntry(entry);
        FileInputStream fis = new FileInputStream(file);
        int count;
        byte[] bytes = new byte[1024];
        while ((count = fis.read(bytes, 0, 1024)) != -1) {
            zos.write(bytes, 0, count);
        }
        fis.close();
        zos.closeEntry();
    }
}
       8. 对齐

public class ApkEncryptMain {
    public static void main(String[] args) {
        /**
         * 4. 对齐
         */
        File unAlignApk = new File("Encrypt/result/apk-unAlign.apk");
        unAlignApk.getParentFile().mkdirs();
        unAlignApk.delete();
        ZipUtils.zipalign(unsignedApk, unAlignApk);
    }
}

public class ZipUtils{
     /**
     * 对齐
     *
     * @param unsignedApk
     * @param unAlignApk
     */
    public static void zipalign(File unsignedApk, File unAlignApk) {
        if (unsignedApk == null || !unsignedApk.exists()) {
            LogUtils.e("The APK that needs to be align does not exist");
            return;
        }
        // 这里使用的是zipalign.ext的绝对路径.请根据实际情况填写。
        String command = "cmd.exe /C E:\\development_tools\\android_sdk\\android_sdk\\build-tools\\29.0.2\\zipalign -v -p 4 " + unsignedApk.getAbsolutePath()
                + " " + unAlignApk.getAbsolutePath();
        CmdUtils.execCommand(command);
    }
}
       9. 给新的未签名并且已经对齐的apk签名

public class ApkEncryptMain {
    public static void main(String[] args) {

        /**
         * 5 .给新apk添加签名,生成签名apk
         */
        File signedApk = new File("Encrypt/result/apk-signed.apk");
        signedApk.getParentFile().mkdirs();
        signedApk.delete();
        SignUtils.signature(unAlignApk, signedApk);
    }
}

public class SignUtils {
    public static void signature(File unSignApk, File signApk) {
        if (unSignApk == null || !unSignApk.exists()) {
            LogUtils.e("The APK that needs to be signed does not exist");
            return;
        }
        String command = "cmd.exe /C jarsigner -sigalg SHA1withRSA -digestalg SHA1 " +
                "-keystore C:/Users/Administrator/.android/debug.keystore -storepass android -keypass android " +
                "-signedjar " + signApk.getAbsolutePath() + " " + unSignApk.getAbsolutePath() + " androiddebugkey";

        CmdUtils.execCommand(command);
    }
}
       到此dex加密过程就完成了,生成的apk-signed.apk就是签名了的apk,可以直接安装使用。

    2. 解密实现
       要完成解密,我们需要完成如下几个步骤:

       1)找到合适的解密时机。

       2)壳 dex 并没有被加密,需要排除在解密的 dex 文件之外。

       3)解密后的 dex 文件需要重新插入到 ClassLoader 中,这与热修复思想是一样的。

       1. 解密时机

      作为一个被加密的应用,安装的时候我们应用本身是无法控制。所以应用第一次启动的时候就成了我们最佳的解密时机了。 所以我们将解密的逻辑放到Application的attachBaseContext()方法中。

       2. 解压apk、脱壳并解密被加密的原apk中的 dex

       说明:  这里只是为了实现功能而将解密的代码用java实现了,而这部分代码没有被加密,所以还是很容易被反编译查看到解密方法,这样被加密的dex也很容易被破解,因此最好的方案就是把加密和解密代码用JNI方式实现,这样即使人家反编译壳apk的dex文件也没有办法知道加密和解密方法,也就无法破解原apk的dex文件了。

/**
     * 解压apk并解密被加密了的dex文件
     *
     * @param apkFile 被加密了的 apk 文件
     * @param app     存放解压和解密后的apk文件目录
     */
    private void unZipAndDecryptDex(File apkFile, File app) {
        if (!app.exists() || app.listFiles().length == 0) {
            // 当app文件不存在,或者 app 文件是一个空文件夹是需要解压。

            // 解压apk到指定目录
            ZipUtils.unZip(apkFile, app);
            // 获取所有的dex
            File[] dexFiles = app.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File file, String s) {
                    // 提取所有的.dex文件
                    return s.endsWith(".dex");
                }
            });

            if (dexFiles == null || dexFiles.length <= 0) {
                LogUtils.i("this apk is invalidate");
                return;
            }

            for (File file : dexFiles) {
                if (file.getName().equals("classes.dex")) {
                    /**
                     * 我们在加密的时候将不能加密的壳dex命名为classes.dex并拷贝到新apk中打包生成新的apk中了。
                     * 所以这里我们做脱壳,壳dex不需要进行解密操作。
                     */
                } else {
                    /**
                     * 加密的dex进行解密,对应加密流程中的_.dex文件
                     */
                    byte[] buffer = FileUtils.getBytes(file);
                    if (buffer != null) {
                        // 解密
                        byte[] decryptBytes = EncryptUtils.getInstance().decrypt(buffer);
                        if (decryptBytes != null) {
                            //修改.dex名为_.dex,避免等会与aar中的.dex重名
                            int indexOf = file.getName().indexOf(".dex");
                            String newName = file.getParent() + File.separator +
                                    file.getName().substring(0, indexOf) + "new.dex";
                            // 写数据, 替换原来的数据
                            FileUtils.wirte(new File(newName), decryptBytes);
                            file.delete();
                        } else {
                            LogUtils.e("Failed to encrypt dex data");
                            return;
                        }
                    } else {
                        LogUtils.e("Failed to read dex data");
                        return;
                    }
                }
            }
        }
    }
       3. 将解密后的dex文件重新插入dexElements数组中。在这个过程中需要对不同的版本做处理。这里提供一个可以在线查看源码的地址,方便大家阅读源码。http://androidxref.com/

public class LoaderDexUtils {
    public static void loader(ClassLoader loader, ArrayList<File> dexList, File dir) {

        try {
            /**
             * 1. 通过反射找到BaseDexClassLoader中的pathList属性,pathList是DexPathList类型的对象。
             * DexPathList中维护了一个dex文件数组(dexElements数组),ClassLoader加载类的时候就会从这dex数组中去查找。
             * 我们需要将解密出来的dex重新插入到这个数组里面。
             */
            // 这里的loader是PathClassLoader,PathClassLoader继承自BaseDexClassLoader
            Class<?> baseDexClassLoaderClass = loader.getClass().getSuperclass();
            Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object pathList = pathListField.get(loader);


            /**
             * 2. 创建我们自己的dex文件数组,可查看源码中的makeDexElements方法
             */
            ArrayList suppressedExceptions = new ArrayList();
            Class<?> dexPathListClass = pathList.getClass();
            Object[] elements = null;
            if (Build.VERSION.SDK_INT >= 24) {
                Method makeDexElementsMethod = dexPathListClass.getDeclaredMethod("makeDexElements", List.class, File.class, List.class, ClassLoader.class);
                makeDexElementsMethod.setAccessible(true);
                elements = (Object[]) makeDexElementsMethod.invoke(pathList, dexList, dir, suppressedExceptions, loader);
            } else if (Build.VERSION.SDK_INT >= 23) {
                Method makeDexElementsMethod = dexPathListClass.getDeclaredMethod("makePathElements", List.class, File.class, List.class);
                makeDexElementsMethod.setAccessible(true);
                elements = (Object[]) makeDexElementsMethod.invoke(pathList, dexList, dir, suppressedExceptions);
            } else {
                Method makeDexElementsMethod = dexPathListClass.getDeclaredMethod("makeDexElements", ArrayList.class, File.class, ArrayList.class);
                makeDexElementsMethod.setAccessible(true);
                elements = (Object[]) makeDexElementsMethod.invoke(pathList, dexList, dir, suppressedExceptions);
            }

            if (elements == null) {
                LogUtils.e("makeDexElements fail");
                return;
            }
            /**
             * 3. 将解密后的dex文件插入到DexPathList的dexElements数组中。
             */
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object[] oldDexElements = (Object[]) dexElementsField.get(pathList);
            Object[] newDexElements = (Object[]) (Array.newInstance(oldDexElements.getClass()
                    .getComponentType(), oldDexElements.length + elements.length));
            System.arraycopy(oldDexElements, 0, newDexElements, 0, oldDexElements.length);
            System.arraycopy(elements, 0, newDexElements, oldDexElements.length, elements.length);
            dexElementsField.set(pathList, newDexElements);

            // 异常处理
            if (suppressedExceptions.size() > 0) {
                Iterator iterator = suppressedExceptions.iterator();

                while (iterator.hasNext()) {
                    IOException dexElementsSuppressedExceptions = (IOException)
                            iterator.next();
                    Log.w("MultiDex", "Exception in makeDexElement",
                            dexElementsSuppressedExceptions);
                }

                Field suppressedExceptionsField = dexPathListClass.getDeclaredField("dexElementsSuppressedExceptions");
                suppressedExceptionsField.setAccessible(true);
                IOException[] dexElementsSuppressedExceptions = (IOException[])
                        suppressedExceptionsField.get(pathList);
                if (dexElementsSuppressedExceptions == null) {
                    dexElementsSuppressedExceptions = (IOException[]) suppressedExceptions
                            .toArray(new IOException[suppressedExceptions.size()]);
                } else {
                    IOException[] combined = new IOException[suppressedExceptions.size() +
                            dexElementsSuppressedExceptions.length];
                    suppressedExceptions.toArray(combined);
                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                    dexElementsSuppressedExceptions = combined;
                }

                suppressedExceptionsField.set(pathList, dexElementsSuppressedExceptions);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
       4. 替换Application,使用原Apk中的Application替换壳中代理Application。

public class ProxyApplication extends Application {
    private String applicationName;
    private boolean isRestoreRealApp;
    private Application realApp;

    @Override
    public void onCreate() {
        super.onCreate();
        // 替换真实的Application,不然壳的入侵性太强,而且原apk的Application不能运行。
        restoreRealApp();
    }

    private void restoreRealApp() {
        if (isRestoreRealApp) {
            return;
        }
        if (TextUtils.isEmpty(applicationName)) {
            return;
        }

        try {

            // 得到 attachBaseContext(context) 传入的上下文 ContextImpl
            Context baseContext = getBaseContext();
            // 拿到真实 APK Application 的 class
            Class<?> realAppClass = Class.forName(applicationName);
            // 反射实例化,其实 Android 中四大组件都是这样实例化的。
            realApp = (Application) realAppClass.newInstance();

            // 得到 Application attach() 方法 也就是最先初始化的
            Method attach = Application.class.getDeclaredMethod("attach", Context.class);
            attach.setAccessible(true);
            //执行 Application#attach(Context)
            //将真实的 Application 和假的 Application 进行替换。想当于自己手动控制 真实的 Application 生命周期
            attach.invoke(realApp, baseContext);


            // ContextImpl---->mOuterContext(app)   通过Application的attachBaseContext回调参数获取
            Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
            // 获取 mOuterContext 属性
            Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
            mOuterContextField.setAccessible(true);
            mOuterContextField.set(baseContext, realApp);

            //拿到 ActivityThread 变量
            Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
            mMainThreadField.setAccessible(true);
            // 拿到 ActivityThread 对象
            Object mMainThread = mMainThreadField.get(baseContext);

            //  ActivityThread--->>mInitialApplication
            //  反射拿到 ActivityThread class
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            // 得到当前加载的 Application 类
            Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
            mInitialApplicationField.setAccessible(true);
            // 将 ActivityThread 中的 mInitialApplication 替换为 真实的 Application 可以用于接收相应的声明周期和一些调用等
            mInitialApplicationField.set(mMainThread, realApp);


            //   ActivityThread--->mAllApplications(ArrayList)       ContextImpl的mMainThread属性
            //   拿到 ActivityThread 中所有的 Application 集合对象
            Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
            mAllApplicationsField.setAccessible(true);
            ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
            // 删除 ProxyApplication
            mAllApplications.remove(this);
            //  添加真实的 Application
            mAllApplications.add(realApp);

            //  LoadedApk------->mApplication         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);
            //将 LoadedApk 中的 mApplication 替换为 真实的 Application
            mApplicationField.set(mPackageInfo, realApp);

            //修改ApplicationInfo className   LooadedApk
            // 拿到 LoadApk 中的 mApplicationInfo 变量
            Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
            mApplicationInfoField.setAccessible(true);
            ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
            // 将我们真实的 Application ClassName 名称赋值于它
            mApplicationInfo.className = applicationName;

            // 执行真实 Application onCreate 声明周期
            realApp.onCreate();

            //解码完成
            isRestoreRealApp = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getPackageName() {
        if (!TextUtils.isEmpty(applicationName)) {
            return "";
        }
        return super.getPackageName();
    }

    /**
     * 这个函数是如果在 AndroidManifest.xml 中定义了 ContentProvider 那么就会执行此处 : installProvider,简介调用该函数
     *
     * @param packageName
     * @param flags
     * @return
     * @throws PackageManager.NameNotFoundException
     */
    @Override
    public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
        if (TextUtils.isEmpty(applicationName)) {
            return super.createPackageContext(packageName, flags);
        }
        try {
            restoreRealApp();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return realApp;
    }
}
     3. 测试






       从上面的测试过程可以看出,加密dex之后再解密dex可以正常运行,然后加密过后的dex是看不到内容的,而没有加密的dex是可以看得到里面的内容的。

       1. 我们在原apk中使用Application和四大组件,验证application是否替换成功。

       1)Application

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        LogUtils.i("MyApplication onCreate()");
    }
}
       2)Activity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LogUtils.i("activity:" + getApplication());
        LogUtils.i("activity:" + getApplicationContext());
        LogUtils.i("activity:" + getApplicationInfo().className);

        startService(new Intent(this, MyService.class));

        Intent intent = new Intent("com.lx.source_receiver");
        intent.setComponent(new ComponentName(getPackageName(), MyBroadCastReceiver.class.getName()));
        sendBroadcast(intent);

        getContentResolver().delete(Uri.parse("content://com.lx.source.MyProvider"), null,
                null);
    }
}
       3) BroadcastReceiver

public class MyBroadCastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        LogUtils.i("receiver:" + context);
        LogUtils.i("receiver:" + context.getApplicationContext());
        LogUtils.i("receiver:" + context.getApplicationInfo().className);
    }
}
       4)Service

public class MyService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        LogUtils.i("service:" + getApplication());
        LogUtils.i("service:" + getApplicationContext());
        LogUtils.i("service:" + getApplicationInfo().className);
    }
}
      5)ContentProvider      

public class MyProvider extends ContentProvider {

    @Override
    public boolean onCreate() {
        LogUtils.i("provider onCreate:" + getContext());
        LogUtils.i("provider onCreate:" + getContext().getApplicationContext());
        LogUtils.i("provider onCreate:" + getContext().getApplicationInfo().className);
        return true;
    }
    ...
}
       运行日志如下:



       由以上日志可以看出,我们将代理Application成功替换为原Apk中真是的Application。

       BroadcastReceiver中的context是系统又对context进行了封装,将context封装为ReceiverRestrictedContext的对象,目的是防止在onReceive方法中不能再注册广播,源码解析如下

frameworks/base/core/java/android/app/ActivityThread.java

  private void handleReceiver(ReceiverData data) {
        ...
        try {

            receiver = (BroadcastReceiver)cl.loadClass(component).newInstance(); // ... 1
        } catch (Exception e) {
            if (DEBUG_BROADCAST) Slog.i(TAG,
                    "Finishing failed broadcast to " + data.intent.getComponent());
            data.sendFinished(mgr);
            throw new RuntimeException(
                "Unable to instantiate receiver " + component
                + ": " + e.toString(), e);
        }

        try {
            ...
            sCurrentBroadcastIntent.set(data.intent);
            receiver.setPendingResult(data);
            receiver.onReceive(context.getReceiverRestrictedContext(),
                    data.intent);
        } catch (Exception e) {
            if (DEBUG_BROADCAST) Slog.i(TAG,
                    "Finishing failed broadcast to " + data.intent.getComponent());
            data.sendFinished(mgr);
            if (!mInstrumentation.onException(receiver, e)) {
                throw new RuntimeException(
                    "Unable to start receiver " + component
                    + ": " + e.toString(), e);
            }
        } finally {
            sCurrentBroadcastIntent.set(null);
        }

        if (receiver.getPendingResult() != null) {
            data.finish();
        }
    }
       在注释1处创建BroadcastReceiver的对象receiver,在注释2处调用receiver的onReceive方法,注意调用onReceive中传递context对象时,调用了context.getReceiverRestrictedContext()方法得到就是ReceiverRestrictedContext对象。

    4. 总结
      市面上Apk加固中原Apk项目(app)并不会依赖于壳项目(ApkShuck),但是在上面的流程中我们在app项目是依赖了ApkShuck。

      下面来说明一下如何让app不依赖ApkShuck,这样ApkShuck和Encrypt(dex加密项目)更具有通用性,能够跟市面上的apk加固软件一样使用,思路如下

        apk加固流程与上面将的一样,原app的AndroidManifest.xml使用的还是app中的Application。只不过我们在解压原apk的过程中增加一个操作,就是对原app的AndroidManifest.xml进行解析,然后将其修改,

       1)修改Application节点的name属性为壳apk的代理ProxyApplication。

       2)在Application节点中增加 <meta-data> 标签,例如:<meta-data android:name="application_name" android:value="com.lx.source.App"/>

      具体解析修改AndroidManifest.xml的操作省略,有兴趣的同学可以尝试去做一下。这里就只说明实现原理。

代码下载

————————————————
版权声明:本文为CSDN博主「lixiong0713」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lixiong0713/article/details/107840456

回复

使用道具 举报

347

主题

564

帖子

2237

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
2237
 楼主| 发表于 2022-5-17 14:16:50 | 显示全部楼层
工具软件,dex如何转为jar:https://github.com/pxb1988/dex2jar
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



Archiver|手机版|小黑屋|risc-v中文社区

GMT+8, 2024-5-5 00:16 , Processed in 0.016069 second(s), 17 queries .

risc-v中文社区论坛 官方网站

Copyright © 2018-2021, risc-v open source

快速回复 返回顶部 返回列表