安卓app加固的简单实现

1.app加固原理

我们在用360加固的时候,会发现目录结构变成了这种格式
在这里插入图片描述
会发现我们自己的代码完全看不见了,而这里会多出一个Application就是StubApp,在这个StubApp中会去加载360的so文件,然后我们的应用就可以正确执行了。并且Mainfest中Application名会修改为StubApp

 <application
        android:name="com.stub.StubApp"
        android:allowBackup="true"
        android:appComponentFactory="androidx.core.app.CoreComponentFactory"
        android:debuggable="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        ......

其实这里的StubApp就是加固的壳入口,我们知道,app加载的主dex文件是classes.dex,那么我们只要在这里处理我们的不加密的壳代码,然后在这个壳里去解密我们加密后的核心代码,然后通过ClassLoader插入类加载器加载数组代码前面了,那么久可以实现基本的加固了。当然360里实现的比较复杂,比如会去处理清单文件的配置信息,加解密的so和加密的文件处理等。

那么我们要实现简单的加固方式的步骤可以是:
1.定义一个Application去处理dex加密的相关操作,这就是我们的壳文件,我们可以定义为一个library库,然后app清单文件中去注册这个Application,但不把这个library库的代码也打进app中
2.解压缩原apk文件,把其中的所有代码的dex文件进行加密,然后重新命名,因为主dex需要是我们的壳,所以这些文件都会按其他格式进行命名,名称随意,只要解密时候可以查找到相应的加密的文件即可
3.把定义的library库的代码文件进行dx处理为dex文件,然后放到加压缩的文件目录中,作为壳的主dex文件,其中这里的libary库中的Application中会去查找相应的加密的文件,进行解密后并进行ClassLoaderpathList的插入处理
4.使用zip流对解密的文件夹进行压缩,注意使用CRC32的压缩格式,压缩后的文件以apk进行命名
5.对apk文件进行签名

2.简单的加固处理

1.定义加解密算法

public class AES {
    public static final String DEFAULT_PWD = "abcdefghijklmnop";
    private static final String algorithmStr = "AES/ECB/PKCS5Padding";
    private static Cipher enctyCiper;
    private static Cipher decryCiper;
    public static void init() {
        try {
            enctyCiper = Cipher.getInstance(algorithmStr);
            decryCiper = Cipher.getInstance(algorithmStr);
            byte[] keyBytes = DEFAULT_PWD.getBytes();
            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
            enctyCiper.init(Cipher.ENCRYPT_MODE, keySpec);
            decryCiper.init(Cipher.DECRYPT_MODE, keySpec);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static byte[] encry(byte[] data) throws Exception {
        return enctyCiper.doFinal(data);
    }

    public static byte[] decry(byte[] data) throws Exception {
        return decryCiper.doFinal(data);
    }

    public static void encryFile(File srcFile, File descFile) throws Exception {
        byte[] buffer = getFileByte(srcFile);
        byte[] encryBuffer = encry(buffer);
        FileOutputStream fos = new FileOutputStream(descFile);
        fos.write(encryBuffer);
        fos.flush();
        fos.close();
    }

    public static void decryFile(File srcFile, File descFile) throws Exception {
        byte[] buffer = getFileByte(srcFile);
        byte[] decryBuffer = decry(buffer);
        FileOutputStream fos = new FileOutputStream(descFile);
        fos.write(decryBuffer);
        fos.flush();
        fos.close();
    }
    
    public static byte[] getFileByte(File file) throws Exception {
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
        byte[] data = new byte[(int) randomAccessFile.length()];
        randomAccessFile.read(data);
        randomAccessFile.close();
        return data;
    }
}

这里使用简单的AES对称加密进行加密和解密,并定义了原文件和加解密后写入到文件位置

2.定义压缩和解压缩的处理方式

public static void unzipBasePathFile(File fromFile, String targetFilName, File targetFile) throws Exception {
        ZipFile zipFile = new ZipFile(fromFile);

        Enumeration<? extends ZipEntry> enumeration = zipFile.entries();
        while (enumeration.hasMoreElements()) {
            ZipEntry zipEntry = enumeration.nextElement();
            if (zipEntry.getName().equals(targetFilName)) {
                FileOutputStream fos = new FileOutputStream(targetFile);
                InputStream fis = zipFile.getInputStream(zipEntry);
                byte[] data = new byte[1024];
                int length;
                while ((length = fis.read(data)) != -1) {
                    fos.write(data, 0, length);
                }
                fos.close();
                break;
            }
        }
        zipFile.close();
    }

    public static void unzip(File fromFile, File toFileDir) throws Exception {
        ZipFile zipFile = new ZipFile(fromFile);
        Enumeration<? extends ZipEntry> enumerations = zipFile.entries();
        while (enumerations.hasMoreElements()) {
            ZipEntry zipEntry = enumerations.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 targetFile = new File(toFileDir, zipEntry.getName());
                if (targetFile.getParentFile() == null || !targetFile.getParentFile().exists())
                    targetFile.getParentFile().mkdirs();
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(targetFile));
                BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(zipEntry));
                byte[] data = new byte[1024];
                int length;
                while ((length = bis.read(data)) != -1) {
                    bos.write(data, 0, length);
                    bos.flush();
                }
                bis.close();
                bos.close();
            }
        }
        zipFile.close();
    }

    public static void zip(File dir, File zipFile) throws Exception {
        if (zipFile.exists()) zipFile.delete();
        if (zipFile.getParentFile() == null || !zipFile.getParentFile().exists())
            zipFile.getParentFile().mkdirs();
        CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
        ZipOutputStream zos = new ZipOutputStream(cos);
        int bashPathLength = dir.getPath().length();
        compress(dir, zos, bashPathLength);
        zos.flush();
        zos.close();
    }

    private static void compress(File srcFile, ZipOutputStream zos, int bashPathLength) throws Exception {
        String targetPath = srcFile.getPath().substring(bashPathLength);
        if (targetPath.startsWith(File.separator))
            targetPath = targetPath.substring(1);

        if (srcFile.isDirectory()) {
            File files[] = srcFile.listFiles();
            if (files != null && files.length > 0) {
                for (File file : files) {
                    compress(file, zos, bashPathLength);
                }
            } else {
                ZipEntry zipEntry = new ZipEntry(targetPath + File.separator);
                zos.putNextEntry(zipEntry);
                zos.closeEntry();
            }
        } else {
            targetPath = targetPath.replaceAll("\\\\", "/");
            ZipEntry zipEntry = new ZipEntry(targetPath);
            zos.putNextEntry(zipEntry);
            byte data[] = new byte[1024];
            int length;
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
            while ((length = bis.read(data)) != -1) {
                zos.write(data, 0, length);
            }
            bis.close();
            zos.closeEntry();
        }
    }

我这里定义了3个方法
unzipBasePathFile 查找和名称匹配的文件,直接写入目标目录,这个是用来把library库中的aar文件的class.jar输出到解压缩的目录中去的
unzip 把压缩包文件解压到指定目录中,这个主要是用来解压apk文件的,因为apk本身就是一个压缩文件
zip 把指定目录的全部文件压缩成一个文件,这个是最终生成apk文件的方法。这里有几点需要注意。
1. 需要使用CheckOutputStream进行流校验,并指定CRC32的校验方式
2. 压缩操作是在电脑上进行处理的,解压操作是在apk运行后处理的
3. 压缩注意文件目录分隔符的操作处理,不能使用File.separator这个处理,因为电脑上的分割的可能和这个斜杠是相反的方向的
4. 压缩完毕需要关闭zip流,否则是无法完成压缩的

3.定义Dex文件加载的插入方式

public class DexInstall {
    public static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                               File optimizedDirectory) throws IllegalArgumentException,
            IllegalAccessException, NoSuchFieldException, InvocationTargetException,
            NoSuchMethodException {
        Field pathListField = findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList suppressedExceptions = new ArrayList();
        if (Build.VERSION.SDK_INT >= 23) {
            expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList, new
                            ArrayList(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions));
        } else {
            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new
                            ArrayList(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions));
        }

        if (suppressedExceptions.size() > 0) {
            Field suppressedExceptionsField1 = findField(loader,
                    "dexElementsSuppressedExceptions");
            IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[])
                    suppressedExceptionsField1.get(loader));
            if (dexElementsSuppressedExceptions1 == null) {
                dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions
                        .toArray(new IOException[suppressedExceptions.size()]);
            } else {
                IOException[] combined = new IOException[suppressedExceptions.size() +
                        dexElementsSuppressedExceptions1.length];
                suppressedExceptions.toArray(combined);
                System.arraycopy(dexElementsSuppressedExceptions1, 0, combined,
                        suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
                dexElementsSuppressedExceptions1 = combined;
            }

            suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
        }
    }

    public static Object[] makeDexElements(Object dexPathList,
                                           ArrayList<File> files, File
                                                   optimizedDirectory,
                                           ArrayList<IOException> suppressedExceptions) throws
            IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        Method makeDexElements = findMethod(dexPathList, "makeDexElements", new
                Class[]{ArrayList.class, File.class, ArrayList.class});
        return ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files,
                optimizedDirectory, suppressedExceptions}));
    }

    private static Object[] makePathElements(
            Object dexPathList, ArrayList<File> files, File optimizedDirectory,
            ArrayList<IOException> suppressedExceptions)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        Method makePathElements;
        try {
            makePathElements = findMethod(dexPathList, "makePathElements", List.class, File.class,
                    List.class);
        } catch (NoSuchMethodException e) {
            try {
                makePathElements = findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
            } catch (NoSuchMethodException e1) {
                try {
                    return makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
                } catch (NoSuchMethodException e2) {
                    throw e2;
                }
            }
        }
        return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
    }

    private static Field findField(Object instance, String name) throws NoSuchFieldException {
        Class clazz = instance.getClass();
        while (clazz != null) {
            try {
                Field e = clazz.getDeclaredField(name);
                if (!e.isAccessible()) {
                    e.setAccessible(true);
                }
                return e;
            } catch (NoSuchFieldException var4) {
                clazz = clazz.getSuperclass();
            }
        }
        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

    private static Method findMethod(Object instance, String name, Class... parameterTypes)
            throws NoSuchMethodException {
        Class clazz = instance.getClass();
        while (clazz != null) {
            try {
                Method e = clazz.getDeclaredMethod(name, parameterTypes);
                if (!e.isAccessible()) {
                    e.setAccessible(true);
                }
                return e;
            } catch (NoSuchMethodException var5) {
                clazz = clazz.getSuperclass();
            }
        }
        throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList
                (parameterTypes) + " not found in " + instance.getClass());
    }

    private static void expandFieldArray(Object instance, String fieldName, Object[]
            extraElements) throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field jlrField = findField(instance, fieldName);
        Object[] original = (Object[]) ((Object[]) jlrField.get(instance));
        Object[] combined = (Object[]) ((Object[]) Array.newInstance(original.getClass()
                .getComponentType(), original.length + extraElements.length));
        System.arraycopy(original, 0, combined, 0, original.length);
        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
        jlrField.set(instance, combined);
    }
}

这个方法是比较通用的方式,主要是把我们需要加载的dex插入到ClassloaderpathList的前列,这样在Classloder的双亲委托机制下就会保证这些代码的正常执行。在加载机制中,如果连续加载两个相同的类信息,那么第一次加载的会生效,后续加载的因为判断已经存在了,就不会再去添加到类信息列表中去了,热修复也是基于这个原理实现的。

4.定义壳的Application

public class ShellApplication extends Application {
    private static final String TAG = "ShellApplication";

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            AES.init();
            List<File> files = new ArrayList<>();
            File apkFile = new File(getApplicationInfo().sourceDir);
            File unzipFile = getDir("hook_dir", MODE_PRIVATE);
            File app = new File(unzipFile, "app");
            if (!app.exists()) {
                Zip.unzip(apkFile, app);
                File file[] = app.listFiles();
                for (File itemFile : file) {
                    String name = itemFile.getName();
                    if (name.equals("classes.dex")) {
                        files.add(itemFile);
                    } else if (name.endsWith(".dex")) {
                        AES.decryFile(itemFile, itemFile);
                        files.add(itemFile);
                    }
                }
            }
            DexInstall.install(getClassLoader(), files, unzipFile);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里主要是把apk解压缩到一个目录下,然后遍历我们所加密的dex的文件,然后解密后重新写回去,最终调用上面的方法插入到类加载的列表中去

5.定义签名和dex操作指令

public class Command {
    
    public static void dxJar(File fileFromFile, File toFile) throws Exception {
        Runtime runtime = Runtime.getRuntime();
        Process process = runtime.exec("cmd /C dx --dex --output=" + toFile.getAbsolutePath() + " " +
                fileFromFile.getAbsolutePath());
        process.waitFor();
        process.destroy();
    }

    public static void signApk(String keystorePath, String alias, String pwd, String srcPath, String desPath) throws Exception {
        String[] commends = new String[]{"cmd", "/C", "jarsigner", "-verbose", "-sigalg", "MD5WithRSA",
                "-digestalg", "SHA1",
                "-keystore", keystorePath,
                "-storepass", pwd,
                "-keypass", pwd,
                "-signedjar", desPath,
                srcPath, alias
        };
        Process process = Runtime.getRuntime().exec(commends);
        process.waitFor();
        process.destroy();
    }
}

这里有两个方法
dxJar 是把jar文件转换成dex文件
signApk 是对指定的apk进行签名,这个方法可能不太好使,可以直接用命令行签名也行

6.定义apk解压和添加壳后重新编译成apk并签名的方法

 public static void main(String[] args) throws Exception {
        String srcPath = "source/testapp-debug.apk";
        String unzipPath = "source/unzip";
        String zipPath = "target/rezip.apk";
        File srcFile = new File(srcPath);
        File unzipFileDir = new File(unzipPath);
        if (unzipFileDir.exists()) unzipFileDir.delete();
		// 步骤1
        Zip.unzip(srcFile, unzipFileDir);
        AES.init();
        String aarFilePath = "source/mylibrary-release.aar";
        String targetAarName = "classes.jar";
        String targetFilePath = "source/unzip/classes.jar";
       	//步骤2
        Zip.unzipBasePathFile(new File(aarFilePath), targetAarName, new File(targetFilePath));
        //步骤3
        Utils.encryApkDexFiles(unzipFileDir);

        String targetJarDexFilePath = "source/unzip/classes.dex";
        File aarFile = new File(targetFilePath);
        File dexFile = new File(targetJarDexFilePath);
		//步骤4
		System.out.println("aarFile existes ->>>> "+aarFile.exists());
        Command.dxJar(aarFile,dexFile);
        aarFile.delete();
        System.out.println("aarFile existes ->>>> "+aarFile.exists());
		//步骤5
        File zipFile = new File(zipPath);
        Zip.zip(unzipFileDir,zipFile);
        String descPath = "target/rezip-signed.apk";
        Command.signApk("keystore/key.jks","demo","android",zipPath,descPath);
    }
    
public class Utils {
    public static void encryApkDexFiles(File dir) throws Exception {
        File[] files = dir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".dex");
            }
        });

        for (int i = 0; i < files.length; i++) {
            File file = files[i];
            AES.encryFile(file, file);
            String name = file.getName();
            String targetName = name.split("[.]")[0] + "_" + i + ".dex";
            file.renameTo(new File(file.getPath().replace(name,targetName)));
        }
    }
}

操作有点多,这里分段解释一下
首先在项目中创建一个source的文件夹,然后把打包完成的apk和我们创建的library所编译生成的aar都添加进去
步骤1. 把apk文件解压缩到unzip文件里
步骤2. 把aar文件中的classes.jar文件拷贝到unzip文件夹中
步骤3. 遍历unzip文件夹中的dex后缀的文件,加密后写回去覆盖原文件,然后用一定格式重命名,这个命名规则可以随便定义,只要能找到即可,这个主要是预留出我们存放壳的classes.dex的文件位置,也就是apk加载的入口
步骤4. 把上面aar拷贝过来的classes.jar文件用dx命令生成classes.dex这个文件,然后删除原文件
步骤5.unzip文件夹重新打包成apk,并用自己的签名文件重新签名

执行完后的文件夹格式大概是这样的
在这里插入图片描述
其中rezip-signed.apk就是我们的目标apk了
用jadu查看的文件格式是
在这里插入图片描述
实际在模拟器上跑这个apk也是没有问题的。

可以看出这里只有解密相关的代码,没有核心的应用代码,应用代码这里是在classes_0.dex这里,当然这个命名规则是随意的,可以随便命名,只要能找到并解密即可。

真正的加固方案比这个要复杂的多,这里只是抛砖引玉实现一个最简单的加固方式,其他的加固方式都是在这个基础上进行更深层次的处理的。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值