在apk安全上,最基本的是通过混淆来对apk进行保护,但这只是加大了对源码的阅读难度,并不能真正的保护你的源码,反编译是可以轻松拿到apk的源码的,我们可以通过将非核心的dex文件暴露来达到保护核心dex文件的目的;

加固的整体思想如下图

aes加密 ios开发 aes加密app_sed


准备工作

处理存放apk的文件夹

/**
		 * 准备工作
		 */
       //存储源核心apk中的解压后的文件
		File tempFileApk = new File("app/source/apk/temp");
		if (tempFileApk.exists()) {
			File[]files = tempFileApk.listFiles();
			for(File file: files){
				if (file.isFile()) {
					file.delete();//先清空文件夹
				}
			}
		}

		//存储壳arr中的解压后的文件
		File tempFileAar = new File("app/source/aar/temp");
		if (tempFileAar.exists()) {
			File[]files = tempFileAar.listFiles();
			for(File file: files){
				if (file.isFile()) {
					file.delete();//先清空文件夹
				}
			}
		}

第一步 处理原始apk 加密核心dex

/**
		 * 第一步 处理原始apk 加密dex
		 *
		 */
		AES.init(AES.DEFAULT_PWD);
		//这样一个最简单的AES加解密就完成了,
		// 但有一个缺点,密码的长度必须为128位,也就是16个字节,否则会报错; 
		//解压apk
		File apkFile = new File("app/source/apk/app-debug.apk");
		File newApkFile = new File(apkFile.getParent() + File.separator + "temp");
		if(!newApkFile.exists()) {
			newApkFile.mkdirs();
		}
		File mainDexFile = AES.encryptAPKFile(apkFile,newApkFile);
		if (newApkFile.isDirectory()) {
			File[] listFiles = newApkFile.listFiles();
			for (File file : listFiles) {
				if (file.isFile()) {
					if (file.getName().endsWith(".dex")) {
						String name = file.getName();
						System.out.println(" name :"+name);
						int cursor = name.indexOf(".dex");
						String newName = file.getParent()+ File.separator + name.substring(0, cursor) + "_" + ".dex";
						System.out.println("newName:"+newName);
						file.renameTo(new File(newName));
					}
				}
			}
		}

第二步 处理aar 获得壳dex

/**
		 * 第二步 处理aar 获得壳dex
		 */
		File aarFile = new File("app/source/aar/protectapp-debug.aar");
		File aarDex  = Dx.jar2Dex(aarFile);
//        aarData = Utils.getBytes(aarDex);   //将dex文件读到byte 数组


		File tempMainDex = new File(newApkFile.getPath() + File.separator + "classes.dex");
		if (!tempMainDex.exists()) {
			tempMainDex.createNewFile();
		}
        System.out.println("MyMain" + tempMainDex.getAbsolutePath());
		FileOutputStream fos = new FileOutputStream(tempMainDex);
		byte[] fbytes = Utils.getBytes(aarDex);
		fos.write(fbytes);
		fos.flush();
		fos.close();

第3步 打包签名

/**
		 * 第3步 打包签名
		 */
		File unsignedApk = new File("app/result/apk-unsigned.apk");
		unsignedApk.getParentFile().mkdirs();
//        File disFile = new File(apkFile.getAbsolutePath() + File.separator+ "temp");
		Zip.zip(newApkFile, unsignedApk);
		//不用插件就不能自动使用原apk的签名...
		File signedApk = new File("app/result/apk-signed.apk");
		Signature.signature(unsignedApk, signedApk);

上面的就实现了apk对核心dex的加密后重新打包签名了,加密后的apk组成结构如下

aes加密 ios开发 aes加密app_aes加密 ios开发_02

其中classes_.dex就是核心dex,无法通过反编译工具直接反编译查看,当然壳classes.dex是可以反编译查看的,接下来介绍一下,代码中使用到的Runtime 调用cmd命令进行转dex和签名的使用方法

(1)使用dx命令转dex

注意使用sdk自带的工具dx.bat 目前存在的目录已经更改,添加path环境变量 E:\Users\Sdk\build-tools\28.0.3,使用JAVA代码调用cmd命令如下

Runtime runtime = Runtime.getRuntime();
        Process process = runtime.exec("cmd.exe /C dx --dex --output=" + aarDex.getAbsolutePath() + " " +
                classes_jar.getAbsolutePath());
        System.out.println("dxCommand 1 " );
        try {
            process.waitFor();
            System.out.println("dxCommand 2  " );
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("dxCommand 3  " );
            throw e;
        }
        System.out.println("process.exitValue() = " +process.exitValue());
            //检测程序是否执行成功,为成功
        if (process.exitValue() != 0) {
            InputStream inputStream = process.getErrorStream();
            int len;
            byte[] buffer = new byte[2048];
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            while((len=inputStream.read(buffer)) != -1){
                bos.write(buffer,0,len);
            }
            System.out.println(new String(bos.toByteArray(),"GBK"));
            throw new RuntimeException("dx run failed");
        }
        process.destroy();
        System.out.println("process: process.destroy() " );

(2)使用java代码调用window下的cmd命令,代码如下

String cmd[] = {"cmd.exe", "/C ","jarsigner",  "-sigalg", "MD5withRSA",
                "-digestalg", "SHA1",
                "-keystore", "D:\\Documents\\ReinforceApk\\app\\shunplus.jks",
                "-storepass", "123456",
                "-keypass", "123456",
                "-signedjar", signedApk.getAbsolutePath(),
                unsignedApk.getAbsolutePath(),
                "shun"};
        Process process = Runtime.getRuntime().exec(cmd);
        System.out.println("start sign");
//        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
//        String line;
//        while ((line = reader.readLine()) != null)
//            System.out.println("tasklist: " + line);
        try {
            int waitResult = process.waitFor();
            System.out.println("waitResult: " + waitResult);
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw e;
        }
        System.out.println("process.exitValue() " + process.exitValue() );
        //检测程序是否执行成功
        if (process.exitValue() != 0) {
            InputStream inputStream = process.getErrorStream();
            int len;
            byte[] buffer = new byte[2048];
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            while((len=inputStream.read(buffer)) != -1){
                bos.write(buffer,0,len);
            }
            System.out.println(new String(bos.toByteArray(),"GBK"));
            throw new RuntimeException("签名执行失败");
        }
        System.out.println("finish signed");
        process.destroy();

下面奉上Github项目地址处理项目加密代码 项目代码地址核心apk及解密代码

通过上面的代码加密后还需要一个解密的过程
在壳的ShellApplication的attachBaseContext方法中,此方法运行之前核心dex还处于加密状态,需要解密后,通过ClassLoader将dex加载到虚拟机中,程序才能正常运行;具体代码如下:

解密过程

AES.init(getPassword());
        File apkFile = new File(getApplicationInfo().sourceDir);
        //data/data/包名/files/fake_apk/
        File unZipFile = getDir("fake_apk", MODE_PRIVATE);
        File app = new File(unZipFile, "app");
        if (!app.exists()) {
            Zip.unZip(apkFile, app);
            File[] files = app.listFiles();
            for (File file : files) {
                String name = file.getName();
                if (name.equals("classes.dex")) {

                } else if (name.endsWith(".dex")) {
                    try {
                        byte[] bytes = getBytes(file);
                        FileOutputStream fos = new FileOutputStream(file);
                        byte[] decrypt = AES.decrypt(bytes);
//                        fos.write(bytes);
                        fos.write(decrypt);
                        fos.flush();
                        fos.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        List list = new ArrayList<>();
        Log.d("FAKE", Arrays.toString(app.listFiles()));
        for (File file : app.listFiles()) {
            if (file.getName().endsWith(".dex")) {
                list.add(file);
            }
        }

        Log.d("FAKE", list.toString());
        try {
            V19.install(getClassLoader(), list, unZipFile);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }

加载dex文件方法

private static final class V19 {
        private V19() {
        }

        private 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();
            Log.d(TAG, "Build.VERSION.SDK_INT " + Build.VERSION.SDK_INT);
            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) {
                Iterator suppressedExceptionsField = suppressedExceptions.iterator();

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

                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);
            }

        }

        private 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}));
        }
    }

    /**
     * A wrapper around
     * {@code private static final dalvik.system.DexPathList#makePathElements}.
     */
    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) {
            Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
            try {
                makePathElements = findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
            } catch (NoSuchMethodException e1) {
                Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
                try {
                    Log.e(TAG, "NoSuchMethodException: try use v19 instead");
                    return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
                } catch (NoSuchMethodException e2) {
                    Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
                    throw e2;
                }
            }
        }
        return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
    }