Android Apk加固

2023-08-14  本文已影响0人  冷野明

一、加固原理

加固原理相对简单,首先对apk进行解压获取到原dex, 接着对原dex 进行加密,制作并生成壳dex(加载时用来解密原dex), 并从新打包成apk, 运行时利用壳dex对加密的dex进行解密并加载到内存中。 是不是很简单? 当然,这只是大概的原理,下面我们将详细叙述。

1.1 加密

加密的方式有很多种,如RSA,AES等,加固中常用的加密算法是AES,由于加密算法不是本文的重点,读者可自行去了解相关算法的区别。 这里我使用gradle插件的方式在编译的时候自动解压加密并重新打包,避免了手动加密的繁琐。 解压加密的核心代码处理如下:

 // 解压 apk 文件 , 获取所有的 dex 文件

    // 被解压的 apk 文件
    var apkFile = File(apk)
    // 解压的目标文件夹
    var apkUnZipFile = File("app/build/outputs/apk/release/unZipFile")

    // 解压文件
    var rawPathList=unZip(apkFile, apkUnZipFile)
//    println(Arrays.asList(rawPathList))

    // 从被解压的 apk 文件中找到所有的 dex 文件, 小项目只有 1 个, 大项目可能有多个
    // 使用文件过滤器获取后缀是 .dex 的文件
    var dexFiles : Array<File> = apkUnZipFile.listFiles({ file: File, s: String ->
        s.endsWith(".dex")
    })

    // 加密找到的 dex 文件
    var aes = AES(AES.DEFAULT_PWD)
    // 遍历 dex 文件
    for(dexFile: File in dexFiles){
        // 读取文件数据
        var bytes = getBytes(dexFile)
        // 加密文件数据
        var encryptedBytes = aes.encrypt(bytes)

        // 将加密后的数据写出到指定目录
        var outputFile = File(apkUnZipFile, "secret-${dexFile.name}")
        // 创建对应输出流
        var fileOutputStream = FileOutputStream(outputFile)

        // 将加密后的 dex 文件写出, 然后刷写 , 关闭该输出流
        fileOutputStream.write(encryptedBytes)
        fileOutputStream.flush()
        fileOutputStream.close()

        // 删除原来的文件
        dexFile.delete()
    }

1.2 制作壳dex

为了在点击桌面icon首先执行我们的壳Application, 首先要在打包过程中,将原Application 替换成 壳程序的Application, 同时使用Meta-Data 记录原Application的全路径名,最终的实现如下:

   <!-- 写入权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:name="com.test.multipledex.ProxyApplication"
        android:theme="@style/Theme.AppEncrypton">

        <!-- app_name 值是该应用的 Application 的真实全类名
    真实 Application : kim.hsl.dex.MyApplication
    代理 Application : kim.hsl.multipledex.ProxyApplication -->
        <meta-data android:name="app_name" android:value="com.test.appencrypton.MyApplication"/>
        <!-- DEX 解密之后的目录名称版本号 , 完整目录名称为 :
                kim.hsl.dex.MyApplication_1.0 -->
        <meta-data android:name="app_version" android:value="1.0"/>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

1.3 解密

首先获取到安装APK 文件的加密dex,然后将加密后的dex 文件进行解密 并加载到内存中。


            /*
                I . 解密与加载多 DEX 文件
                    先进行解密, 然后再加载解密之后的 DEX 文件

                    1. 先获取当前的 APK 文件
                    2. 然后解压该 APK 文件
             */

                // 获取当前的 APK 文件, 下面的 getApplicationInfo().sourceDir 就是本应用 APK 安装文件的全路径
                File apkFile = new File(getApplicationInfo().sourceDir);

                // 获取在 app Module 下的 AndroidManifest.xml 中配置的元数据,
                // 应用真实的 Application 全类名
                // 解密后的 dex 文件存放目录
                ApplicationInfo applicationInfo = null;
                packageName=getPackageName();
                applicationInfo = getPackageManager().getApplicationInfo(
                        packageName,
                        PackageManager.GET_META_DATA
                );

                Bundle metaData = applicationInfo.metaData;
                if (metaData != null) {
                    // 检查是否存在 app_name 元数据
                    if (metaData.containsKey("app_name")) {
                        app_name = metaData.getString("app_name").toString();
                    }
                    // 检查是否存在 app_version 元数据
                    if (metaData.containsKey("app_version")) {
                        app_version = metaData.getString("app_version").toString();
                    }
                }

                // 创建用户的私有目录 , 将 apk 文件解压到该目录中
                File privateDir = getDir(app_name + "_" + app_version, MODE_PRIVATE);

                Log.i(TAG, "attachBaseContext 创建用户的私有目录 : " + privateDir.getAbsolutePath());

                // 在上述目录下创建 app 目录
                // 创建该目录的目的是存放解压后的 apk 文件的
                File appDir = new File(privateDir, "app");

                // app 中存放的是解压后的所有的 apk 文件
                // app 下创建 dexDir 目录 , 将所有的 dex 目录移动到该 dexDir 目录中
                // dexDir 目录存放应用的所有 dex 文件
                // 这些 dex 文件都需要进行解密
                File dexDir = new File(appDir, "dexDir");

                // 遍历解压后的 apk 文件 , 将需要加载的 dex 放入如下集合中
                ArrayList<File> dexFiles = new ArrayList<File>();
                // 如果该 dexDir 不存在 , 或者该目录为空 , 并进行 MD5 文件校验
                if (!dexDir.exists() || dexDir.list().length == 0) {
                    // 将 apk 中的文件解压到了 appDir 目录
                    ZipUtils.unZipApk(apkFile, appDir);
                    if (!dexDir.exists()){
                        dexDir.mkdir();
                    }

                    // 获取 appDir 目录下的所有文件
                    File[] files = appDir.listFiles();

//                Log.i(TAG, "attachBaseContext appDir 目录路径 : " + appDir.getAbsolutePath());
//                Log.i(TAG, "attachBaseContext appDir 目录内容 : " + files);

                    // 遍历文件名称集合
                    for (int i = 0; i < files.length; i++) {
                        File file = files[i];

//                    Log.i(TAG, "attachBaseContext 遍历 " + i + " . " + file);

                        // 如果文件后缀是 .dex , 并且不是 主 dex 文件 classes.dex
                        // 符合上述两个条件的 dex 文件放入到 dexDir 中
                        if (file.getName().endsWith(".dex") &&
                                !TextUtils.equals(file.getName(), "classes.dex")) {
                            // 筛选出来的 dex 文件都是需要解密的
                            // 解密需要使用 OpenSSL 进行解密

                            // 获取该文件的二进制 Byte 数据
                            // 这些 Byte 数组就是加密后的 dex 数据
                            byte[] bytes = OpenSSL.getBytes(file);

                            // 解密该二进制数据, 并替换原来的加密 dex, 直接覆盖原来的文件即可
                            File temp=new File(dexDir,file.getName());
                            Log.i(TAG, "temp: " + temp.getAbsolutePath());
                            OpenSSL.decrypt(bytes, temp);

                            // 将解密完毕的 dex 文件放在需要加载的 dex 集合中
                            dexFiles.add(temp);

                            // 拷贝到 dexDir 中

                            Log.i(TAG, "attachBaseContext 解密完成 被解密文件是 : " + temp);

                        }// 判定是否是需要解密的 dex 文件
                    }// 遍历 apk 解压后的文件

                } else {
                    Log.i(TAG,  "再次启动");
                    // 已经解密完成, 此时不需要解密, 直接获取 dexDir 中的文件即可
                    for (File file : dexDir.listFiles()) {
                        if (file.getName().endsWith(".dex")){
                            dexFiles.add(file);
                        }
                    }
                }

                Log.i(TAG, "attachBaseContext 解密完成 dexFiles : " + dexFiles);

                for (int i = 0; i < dexFiles.size(); i++) {
                    Log.i(TAG, i + " . " + dexFiles.get(i).getAbsolutePath());
                }

                // 截止到此处 , 已经拿到了解密完毕 , 需要加载的 dex 文件
                // 加载自己解密的 dex 文件
                loadDex(dexFiles, privateDir);

                Log.i(TAG, "attachBaseContext 完成");
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
上一篇 下一篇

猜你喜欢

热点阅读